state etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
state etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

Flutter State Management’ta En Sık Yapılan 10 Hata ve Çözüm Yolları

 


Herkese merhaba. Bugün sizlere çoğumuz için karmaşık gelen StateManagement konusu ile geldim. Hemen başlayalım.

Flutter projeniz ilk başta minik bir “merhaba dünya” uygulaması gibi masum görünebilir. Ama işler büyüyüp ekranlar, API çağrıları, kullanıcı etkileşimleri ve veri yönetimi devreye girdiğinde, state management konusu bir anda korku filmine dönüşebilir.

Yanlış state yönetimi:

  1. Uygulamanızı yavaşlatır.
  2. Kodun okunabilirliğini bitirir.
  3. Takım içi çalışmayı zorlaştırır.
  4. Ve en kötüsü, gelecekte hata ayıklamayı imkânsız hale getirir.

Bu yazıda, Flutter’da state yönetimi yaparken en sık karşılaşılan 10 hatayıgerçek kod örnekleri ve çözüm yollarıyla inceleyeceğiz.

Ayrıca, bu hataları nasıl önceden fark edebileceğinizi ve daha performanslı, sürdürülebilir kod yazabileceğinizi konuşacağız.

1. Tüm State’i Tek Yere Toplamak

Hata: Tek bir Provider, Bloc veya State sınıfı içine tüm uygulamanın state’ini koymak.

class AppState with ChangeNotifier {
String userName = '';
int cartItemCount = 0;
bool isDarkMode = false;
List<String> notifications = [];

void updateUserName(String name) {
userName = name;
notifyListeners();
}

void addNotification(String message) {
notifications.add(message);
notifyListeners();
}
}

Bu yapı küçük projede masum görünür. Ancak proje büyüdükçe:

  • Gereksiz rebuild’ler olur (tek alan değişse bile tüm dinleyiciler tetiklenir).
  • Kod karmaşıklaşır.
  • Test etmek zorlaşır.

Çözüm:
Modüler state yönetimi → Her feature için ayrı state sınıfları veya Bloc’lar.

class UserState with ChangeNotifier {
String userName = '';
void updateUserName(String name) {
userName = name;
notifyListeners();
}
}

class CartState with ChangeNotifier {
int itemCount = 0;
void addItem() {
itemCount++;
notifyListeners();
}
}

2. Gereksiz Rebuild’ler

Hata: Consumer veya BlocBuilder ile tüm ekranı sarmak.

Consumer<CartState>(
builder: (context, cart, child) {
return Scaffold(
appBar: AppBar(title: Text('Cart (${cart.itemCount})')),
body: ProductList(),
);
},
);

itemCount değiştiğinde tüm ekran yeniden çizilir.

Çözüm:
Sadece gerekli kısmı rebuild etmek için Selector veya küçük widget’lar kullanın.

AppBar(
title: Selector<CartState, int>(
selector: (context, cart) => cart.itemCount,
builder: (context, count, child) => Text('Cart ($count)'),
),
)

3. Local ve Global State’i Karıştırmak

Hata: UI’ye özgü verileri global state’te tutmak.

Örnek: Modal açık/kapalı durumu globalde tutuluyor.

class AppState with ChangeNotifier {
bool isModalOpen = false;
}

Çözüm:
Ekrana özel state’leri StatefulWidget içinde veya local provider’da tutun.

class MyScreen extends StatefulWidget {
@override
State<MyScreen> createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
bool isModalOpen = false;
}

4. Business Logic ile UI’nin Karışması

Hata: API çağrıları veya veri işleme kodunun doğrudan widget içinde olması.

ElevatedButton(
onPressed: () async {
final data = await fetchData();
setState(() {
result = data;
});
},
child: Text("Load"),
)

Çözüm:
İş mantığını state management katmanına taşıyın.

class DataState with ChangeNotifier {
String result = '';
Future<void> loadData() async {
result = await fetchData();
notifyListeners();
}
}

5. Immutable Olmayan State Kullanımı

Hata: State objesini doğrudan değiştirmek.

state.user.name = 'John'; // False

Çözüm:
State’i immutable tutmak, copyWith kullanmak.

class User {
final String name;
User({required this.name});

User copyWith({String? name}) {
return User(name: name ?? this.name);
}
}

6. dispose() ve Memory Management İhmali

Hata: Stream, Controller veya Timer’ları dispose etmemek.

class MyNotifier with ChangeNotifier {
final controller = StreamController();
}

Çözüm:
dispose() metodunu eklemek.

@override
void dispose() {
controller.close();
super.dispose();
}

7. Provider’da notifyListeners()’ın Fazla Kullanımı

Hata: Tek bir değişiklik için tüm dinleyicileri tetiklemek.

void updateCart() {
itemCount++;
notifyListeners();
}

Çözüm:
Veriyi parçalara ayırmak veya context.select() ile hedef rebuild yapmak.

8. Bloc’ta Tek Event ile Her Şeyi Yönetmek

Hata: Tüm işlemleri tek event’te toplamak.

class AppEvent {}

Çözüm:
Her işlem için ayrı event tanımlayın.

class LoadUser extends AppEvent {}
class UpdateCart extends AppEvent {}

9. GetX’te Gereksiz Reactive Variable Tanımlamak

Hata: Değişmeyecek verileri bile .obs yapmak.

final appName = 'MyApp'.obs; // Not useful

Çözüm:
Sadece değişen ve UI’yi etkileyen değerleri reactive yapın.

10. Yanlış Kütüphane Seçimi

Hata: Proje ihtiyacını analiz etmeden popüler olduğu için kütüphane seçmek.

Çözüm:

  • Küçük projeler → Provider / Riverpod
  • Orta projeler → Riverpod / Bloc
  • Büyük ekip projeleri → Bloc / Clean Architecture

Sonuç ve Öneriler

  1. State yönetimini proje başında doğru planlayın.
  2. Modüler yaklaşım kullanın.
  3. Gereksiz rebuild’leri azaltın.
  4. Immutable veri yapıları tercih edin.
  5. Test edilebilirliği göz ardı etmeyin.

Buraya kadar okuduğun için teşekkür ederim.

Bu yazıyı beğendiysen alkış butonuna tıklamayı unutma. Diğer içeriklerimden haberdar olmak için abone olabilirsin.

Teşekkür ederim.

Selin.

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.