What Is Riverpod? Here’s What I Learned from Different Resources

 

Hello everyone.

I’ve been obsessed with StateManagement in Flutter lately, because as my knowledge of Flutter increases, I’m attempting to make more comprehensive projects. However, I always get stuck when it comes to moving, storing and retrieving data. I’ve tried to use the provider package a bit before, but provider is not enough for the application I’m working on right now. So when my research pointed out that Riverpod can be used in more extensive projects than provider, I started working on it.

And now I’m here to compile the information I got first from the documentation and then from different sources. I hope I will inspire you. Let’s get started.

What is Riverpod?

I hope my first note about Riverpod will be a kind of icebreaker. Riverpod is an anagram of provider, a different arrangement of the same letters. :)

Let’s see what it is. Riverpod is a modern and flexible state management solution in Flutter. It’s more secure, independent and testable than traditional approaches, and with it we can manage state in our application more effectively.

So why do we need StateManagement? I would like to talk a little bit about this before going into the details of Riverpod.

Why State Management?

Imagine we are building an application with many different screens and widgets. And some of them may need to access the same state, such as the productState. For example, in the tree diagram below, the Home Screen and the Shopping Cart widget may both need the same productState.

Once a state is defined in the Home Screen, to update it we need to create a function or state within that widget and then pass that function or state up to the shopping cart widget. When we do this, the widget tree is rebuilt to reflect this state change. This is a relatively simple and common scenario up to a point. But another common scenario is having to use the operation in more than one place, like this one, where we define the state at the vertex of the tree and then pass it through the widgets to other places where it is needed. When the latter scenario becomes more common in practice, it can lead to a bit messy and hard to maintain code, I know because it happened to me :). This is where a state management solution simplifies things for us.

As I show in the diagram below, once we define the function or state with the provider at the top, we can call it wherever we want without going through the entire hierarchy in the widget tree.

Now that we have talked about what riverpod is and why State Management is necessary, we can start using riverpod.

Installation

First of all, as in every package usage, we install our package in our project.

flutter pub add flutter_riverpod

And then it is added to the pubspec.yaml file with whatever the current version is as follows.

dependencies:
flutter_riverpod: ^2.5.3

We import it to the page we will use as follows.

import 'package:flutter_riverpod/flutter_riverpod.dart';

Usage

We wrap the runApp function in the main method of our project with ProviderScope. This is used to keep the states of all created providers.

void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}

By the way, provider in software literally means an object that covers a state and allows this state to be listened to. And it means the same thing in all StateManagement solutions and is simply used with the same logic.

At this point I want to write a simple example of a provider object. Like every function or property definition, provider also has a return type. In this example, this provider returns a String.

final helloRiverpodProvider = Provider<String>((ref) {
return 'Hello Riverpod';
});

We write this provider object as a global variable outside all methods and widgets in order to be able to access it from anywhere on the page, in all widgets we will write. Now let’s see how to use this global variable.

Using Provider Object in Widget

1- With ConsumerWidget

class HelloRiverpodWidget extends ConsumerWidget { 
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Container();
}
}

The widget we create here is extended from Consumer Widget, not from Stateless or Stateful Widget. ConsumerWidget works like Stateless Widget. In the build method, the WidgetRef parameter must be present as well as the BuildContext parameter.

We read the variable defined as a global variable with the ref.watch method and use it by assigning it to a separate variable in the widget.

class HelloRiverpodWidget extends ConsumerWidget {
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
}
}

2- With Consumer

When we wrap the whole Scaffold with ConsumerWidget, the whole Scaffold is refreshed every time there is a change in the Provider object. This can mean extra performance issues. Instead we can wrap only the Widget containing the Provider object with Consumer.

class HelloRiverpodWidget extends StatelessWidget {
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer(
builder: (context, ref, child) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
},
),
);
}
}

Here we need to configure the builder well, which is required when we create a consumer. For this reason, it is more detailed than ConsumerWidget.

Instead, if we configure our widgets to be reusable and break them into small pieces, we can continue to use ConsumerWidget and minimize the performance problem related to rebuild.

3- With ConsumerStatefulWidget

I mentioned that ConsumerWidget is the counterpart of StatelessWidget. In cases where we need to use StatefulWidget, we can use ConsumerStatefulWidget as the counterpart.

Here we have two methods to call the provider object. The first is to call it in initState().

@override
void initState() {
super.initState();
final helloRiverpod = ref.read(helloRiverpodProvider);
print(helloRiverpod);
}

The other method is to call it in the classic widget tree.

class HelloRiverpodStful extends ConsumerStatefulWidget {
const HelloRiverpodStful({super.key});

@override
ConsumerState<ConsumerStatefulWidget> createState() => _HelloRiverpodStfulState();
}

class _HelloRiverpodStfulState extends ConsumerState<HelloRiverpodStful> {

@override
Widget build(BuildContext context) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
}
}

If we use the WidgetRef ref in the build method, we call it with watch(), if we use it in one of the other methods, we call it with read(). Both cases return a value. I will compare these two methods in more detail below.

WidgetRef ref is evaluated as an argument in Consumer or ConsumerWidget and as a property in ConsumerState.

WidgetRef is the object that allows widgets like BuildContext to interact with provider objects. BuildContext allows to access ancestor widgets in the widget tree. Like Theme.of(context) or MediaQuery.of(context). WidgetRef allows accessing any provider in the application. Because all Riverpod providers are global.

ref.watch() vs ref.read()

In the build method, we use ref.watch() to observe the state of a provider object and rebuild it if it changes.

To read the state of a provider object only once (like initState) we use ref.read().

In the onPressed callback method of a button we use ref.read(), not ref.watch.

Sometimes we want to show SnackBar or alertDialog when a provider state changes. We can do this by calling the ref.listen() method inside the build method.

ref.listen() provides a callback when the provider object changes, not when build is called. So it can also be used to run asynchronous code.

In my article, I shared my Riverpod notes that I researched and learned from different sources. Before I conclude, I would like to mention a plugin for vscode: Flutter Riverpod Snippets.

This plugin speeds up writing code by automatically completing frequently used Riverpod code snippets. For example, you can quickly add basic code for common Riverpod constructs like Provider, StateNotifier, ConsumerWidget, etc. so you don’t have to write from scratch every time. It also helps you minimize bugs.

I continue to learn about Riverpod, because I can see that it is a vast subject, like the general software world description. I will write about what I learn in a second article in the future.

Error-free code to all of us.

Thank you for reading.

I would appreciate if you subscribe to be informed about my new articles.

Selin.

Riverpod Nedir? İşte Farklı Kaynaklardan Öğrendiğim En Önemli Bilgiler

 

Herkese merhaba.

Bu ara Flutter’da StateManagement konusuna kafayı takmış durumdayım. Çünkü Flutter konusunda bildiklerim arttıkça daha kapsamlı projeler yapma girişiminde bulunuyorum. Ancak her defasında veri taşıma, saklama, çağırma konusu geldiğinde tıkanıp kalıyorum. Daha önce provider paketini biraz kullanmayı denedim ancak şu an üzerinde çalıştığım uygulamam için provider yeterli olmuyor. Bunun için araştırmalarım Riverpod’un provider’dan daha kapsamlı projelerde kullanılabileceğine işaret edince başladım üzerinde çalışmaya.

Ve şimdi buraya da önce dökümantasyondan sonra da farklı kaynaklardan edindiğim bilgileri derlemeye geldim. Umarım ilham olurum. Haydi başlayalım.

Riverpod nedir?

Riverpod ile ilgili ilk notumun bir çeşit “icebreaker” olmasını umuyorum. Riverpod, provider’ın anagramıymış, aynı harflerin farklı dizilimi :)

Gelelim ne olduğuna. Riverpod, Flutter’da modern ve esnek bir durum yönetimi (state management) çözümü. Geleneksel yaklaşımlara göre daha güvenli, bağımsız ve test edilebilir bir yapı sunuyor ve bizde onunla uygulamamızdaki durumu daha etkili bir şekilde yönetebilir oluyoruz.

Peki neden StateManagement’a ihtiyacımız var? Riverpod’un detaylarına geçmeden önce biraz bundan bahsetmek istiyorum.

Neden Durum Yönetimi(State Management)?

Birçok farklı ekran ve widget içeren bir uygulama yaptığımızı düşünün. Ve bunlardan bazılarının, aynı state’e -productState gibi- erişmesi gerekebilir. Örneğin aşağıdaki ağaç diyagramında Ana Ekran ve Alışveriş Sepeti widget’ının her ikisi de aynı productState’e ihtiyaç duyabilir.

State Ana Ekran widget’ında tanımlandığında, bunu güncellemek için bu widget içinde bir fonksiyon veya state oluşturmamız ve ardından bu fonksiyon veya state’i alışveriş sepeti widget’ına kadar iletmemiz gerekir. Bunu yaptığımızda, widget ağacı bu durum değişikliğini yansıtacak şekilde yeniden oluşturulur. Bu bir aşamaya kadar nispeten basit ve yaygın bir senaryo. Ancak yaygın olan bir başka senaryo da bunun gibi durumu ağacın tepe noktasında tanımladığımız ve daha sonra widget’lar aracılığıyla ihtiyaç duyulan diğer yerlere aktardığımız operasyonu birden fazla yerde kullanmak durumunda kalmak. İkinci senaryo uygulamada daha yaygın hale geldiğinde, biraz dağınık ve bakımı zorlaşan kodlara yol açabiliyor, başıma geldiği için biliyorum :). İşte bu noktada bir durum yönetimi çözümü bizim için işleri basitleştiriyor.

Aşağıdaki diagramda gösterdiğim gibi provider ile fonksiyon ya da state’i bir kez en tepe noktasında tanımladıktan sonra widget ağacında tüm hiyerarşiyi gezmeden istediğimiz yerde çağırabiliyoruz.

Riverpod’un ne olduğu ve State Management’ın neden gerekli olduğunu konuştuğumuza göre riverpod’u kullanmaya başlayabiliriz.

Kurulumu

Öncelikle her paket kullanımında olduğu gibi paketimizi projemize install ediyoruz.

flutter pub add flutter_riverpod

Ve ardından pubspec.yaml dosyasına aşağıdaki gibi güncel versiyonu ne ise onunla ekleniyor.

dependencies:
flutter_riverpod: ^2.5.3

Kullanacağımız sayfaya ise aşağıdaki gibi import ediyoruz.

import 'package:flutter_riverpod/flutter_riverpod.dart';

Kullanımı

Projemizin main metodu içindeki runApp fonksiyonunu ProviderScope ile sarıyoruz. Bu oluşturulan tüm provider’ların state’lerini tutmaya yarar.

void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}

Bu arada provider yazılımda kelime anlamı olarak bir state’i kapsayan ve bu state’in dinlenmesini sağlayan nesne demek. Ve tüm StateManagement çözümlerinde aynı anlama geliyor ve basitçe aynı mantıkla kullanılıyor.

Bu noktada basit bir provider nesnesi örneği yazmak istiyorum. Her fonksion ya da property tanımlaması gibi provider’ın da bir dönüş türü oluyor. Vereceğim örnekte bu provider bir String döndürüyor.

final helloRiverpodProvider = Provider<String>((ref) {
return 'Hello Riverpod';
});

Bu provider nesnesine sayfanın her yerinden, yazacağımız tüm widget’lar içerisinden ulaşabilmek için global bir değişken olarak tüm metot ve widget’ların dışına yazıyoruz. Şimdi bu tanımladığımız global değişkeni nasıl kullanacağımıza bakalım.

Provider Nesnesinin Widget İçerisinde Kullanımı

1- ConsumerWidget ile

class HelloRiverpodWidget extends ConsumerWidget { 
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Container();
}
}

Burada oluşturduğumuz widget Stateless veya Stateful Widget’tan değil, Consumer Widget’tan extend olur. ConsumerWidget Stateless Widget gibi çalışır. build metodu içerisinde BuildContext parametresinin yanısıra WidgetRef parametresi de bulunmak zorundadır.

ref.watch metoduyla global variable olarak tanımlanan değişkeni okuyup widget içerisinde ayrı bir değişkene atayarak kullanıyoruz.

class HelloRiverpodWidget extends ConsumerWidget {
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
}
}

2- Consumer ile

ConsumerWidget ile tüm Scaffold’u sarmaladığımızda Provider nesnesinde her değişiklik olduğunda tüm Scaffold yenilenir. Bu da ekstra performans sorunu demek olabilir. Bunun yerine yalnızca Provider nesnesi içeren Widget’ı Consumer ile sarabiliriz.

class HelloRiverpodWidget extends StatelessWidget {
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer(
builder: (context, ref, child) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
},
),
);
}
}

Burada consumer oluşturduğumuzda required olan builder’ı iyi yapılandırmak gerekir. Bu sebeple ConsumerWidget’tan daha detaylıdır.

Bunun yerine widget’larımızı yeniden kullanılabilecek şekilde yapılandırır, küçük parçalara ayırırsak ConsumerWidget’ı kullanmaya devam edebilir, rebuild ile ilgili performans sorununu en aza indirebiliriz.

3- ConsumerStatefulWidget ile

ConsumerWidget’ın StatelessWidget’ın karşılığı olduğunu söylemiştim. StatefulWidget kullanmamız gereken durumlarda karşılık olarak ConsumerStatefulWidget’ı kullanabiliriz.

Burada provider nesnesini çağırmak için iki yöntemimiz var. Birincisi initState() içerisinde çağırmak.

 @override
void initState() {
super.initState();
final helloRiverpod = ref.read(helloRiverpodProvider);
print(helloRiverpod);
}

Diğer yöntem ise klasik widget ağacı içerisinde çağırmak.

class HelloRiverpodStful extends ConsumerStatefulWidget {
const HelloRiverpodStful({super.key});

@override
ConsumerState<ConsumerStatefulWidget> createState() => _HelloRiverpodStfulState();
}

class _HelloRiverpodStfulState extends ConsumerState<HelloRiverpodStful> {

@override
Widget build(BuildContext context) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
}
}

WidgetRef ref’i build metodu içerisinde kullanıyorsak watch() ile, diğer metotlardan birinde kullanıyorsak read() ile çağırırız. Her iki durum da değer döndürüyor. aşağıda bu iki metodun karşılaştırmasına daha detaylı şekilde yer vereceğim.

WidgetRef ref, Consumer veya ConsumerWidget içerisinde bir argüman olarak, ConsumerState içerisinde ise property olarak değerlendirilir.

WidgetRef BuildContext gibi widget’ların provider nesneleri ile etkileşime girmesini sağlayan nesne. BuildContext widget ağacındaki ata widget’lara ulaşmayı sağlar. Theme.of(context) veya MediaQuery.of(context) gibi. WidgetRef ise uygulama içindeki herhangi bir provider’a erişmeyi sağlar. Çünkü tüm Riverpod provider’ları globaldir.

ref.watch() vs ref.read()

Build metodu içerisinde bir provider nesnesinin durumunu gözlemlemek ve değişirse yeniden build etmek için ref.watch kullanıyoruz.

Bir provider nesnesinin durumunu yalnızca bir kez okumak için (initState gibi) ref.read kullanıyoruz.

Bir butonun onPressed callback metodu içinde ref.watch değil ref.read kullanıyoruz.

Bazen bir provider durumu değiştiğinde SnackBar veya alertDialog göstermek isteriz. Bunu build metodu içinde ref.listen metodunu çağırarak yapabiliriz.

ref.listen build çağrıldığında değil, provider nesnesi değiştiğinde bir callback sağlar. Bu yüzden asenkron kodu çalıştırmak için de kullanılabilir.

Makalemde farklı kaynaklardan araştırdığım ve öğrendiğim Riverpod notlarımı aktardım. Sonlandırmadan önce vscode için bir eklentiden bahsetmek istiyorum: Flutter Riverpod Snippets.

Bu eklenti, sık kullanılan Riverpod kod parçacıklarını (snippets) otomatik olarak tamamlayıp kod yazmayı hızlandırıyor. Örneğin, ProviderStateNotifierConsumerWidget gibi yaygın Riverpod yapılarının temel kodlarını hızlıca ekleyebiliyorsunuz, böylece her seferinde sıfırdan yazmak zorunda kalmıyorsunuz. Hataları da minimize etmenize destek oluyor.

Ben Riverpod’u öğrenmeye devam ediyorum, zira genel yazılım dünyası tanımlaması gibi, derya deniz bir konu olduğunu görebiliyorum. İlerleyen zamanlarda yeni öğrendiklerimi de ikinci bir makalede yazarım.

Hepimize hatasız kodlar.

Okuduğunuz için teşekkürler.

Yeni makalelerimden haberdar olmak için abone olursanız memnun olurum.

Selin.