Flutter’da State Yönetimi Yaklaşımları

26/05/2021 9:53

Bartu Kaan Pideci, Mobilist

Flutter ile uygulama geliştirirken şüphesiz ki en önemli konulardan biri state yönetimidir. Flutter’ın resmi İnternet sayfasında önerdiği birçok state management yaklaşımı vardır.

Bunlardan bazıları setState, Provider, Bloc, Mobx, Getx, GetIt gibi yaklaşımlardır. Bu seride sizlerle beraber farklı kullanım şekilleri olan bu yaklaşımlara bir göz atacağız.

Flutter’ın şu meşhur counter (sayaç) uygulamasını herkes bilir. Kısaca hatırlatmak gerekirse, bir flutter projesi oluşturup derlediğimiz anda ilk olarak karşımıza çıkan Flutter’ın low-level bir yaklaşım olarak gördüğü setState yaklaşımı ile geliştirilmiş örnek bir sayaç uygulamasıdır.

Bu sayaç uygulamasını farklı state management yaklaşımları ile geliştirip aralarındaki farkları inceleyeceğiz ve bu yaklaşımların bize sağlayacağı yararlardan bahsetmeye çalışacağım.

İlk olarak, setState yaklaşımını en popüler state management yaklaşımlarından olan Provider paketi ile karşılaştıracağız.

setState vs Provider

Öncelikle setState yaklaşımını bir inceleyelim.
setState metodu yalnızca Stateful Widget’lar içerisinde kullanılan bir kavramdır.

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Örnekte görüldüğü gibi _counter değişkeninin değeri _incrementCounter() methodu içerisinde artıyor ve setSate methodu sayesinde değişikliği Widget Tree’mize bildiriyoruz.

void _incrementCounter() {
setState(() {
_counter++;
});
}

Floating Action Button’ın onPressed property’sinde _incrementCounter methodu çağırılıyor. Yani butona her tıkladığımızda setState methodu çağrılıyor. setState methodunun çağırılması bulunduğu class’ın build methodunu tetikler ve içerisinde bulunan widget ağacımızın uygulama ekranına yeniden oluşturulması (rebuild) anlamına gelir ve bu durum işlem gücü açısından maliyetli olabileceği gibi state yönetimi açısından da pek istediğimiz bir yaklaşım şekli değildir.

floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);

Tüm widget ağacının yeniden çizilmesi yerine sadece _counter değişkenini tutan Text Widget’ını bu değişiklikten haberdar edip yalnızca bu widget’ın yeniden çizilmesini sağlamak çok daha uygun ve doğru bir yaklaşım olacaktır.

Peki nasıl?

Provider paketi bize bu nokta da yardımcı olabilecek güzel bir yaklaşımdır.

Öncelikle Provider paketini pubspec.yaml dosyamızdaki dependencies alanına ekliyoruz.

dependencies:
flutter:
sdk: flutter
provider: ^4.3.3

ChangeNotifier
ChangeNotifier Flutter SDK’da bulunan ve dinleyicilerine (listeners) değişiklik bildirimi (change notification) sağlayan basit bir sınıftır. Başka bir deyişle, eğer bir şey bir ChangeNotifier ise, onun değişikliklerine abone (subscribe) olabilirsiniz. Provider da ChangeNotifier uygulama state’ini kapsüllemenin bir yoludur.
Counter uygulaması örneğimizde, bir ChangeNotifier’daki counter değişkeninin state’ini yönetmek istiyoruz. Bu şekilde onu extend eden yeni bir sınıf oluşturuyoruz:

class Counter extends ChangeNotifier{
int counter = 0;

void increment(){
counter ++;
notifyListeners();
}
}

Evet Counter sınıfımız hazır şimdi bu sınıfımızı widget ağacımıza ChangeNotifierProvider ile enjekte edelim.

Change Notifier Provider
ChangeNotifierProvider
, soyundan gelenlere bir ChangeNotifier instance (örneği) sağlayan widget’tır. Change Notifier Provider’ı ona erişmesi gereken widget’ların üzerine koyuyoruz.

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter Provider',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider<Counter>(
create: (_) => Counter(), child: MyHomePage()),
);
}
}

Örnekte de görüldüğü gibi MyHomePage() widgetını ChangeNotifierProvider sınıfı ile sarmalayarak Counter sınıfını ağacımıza enjekte etmiş olduk.

Artık, MyHomePage içerisinde Counter sınıfın değişkenlerine ve metotlarına erişme imkânımız var.

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App with Provider'),
),
body: Center(
child: Consumer<Counter>(
builder: (context, counter, child) {
return Text(counter.counter.toString(),
style: Theme.of(context).textTheme.headline4);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
var counter = context.read<Counter>();
counter.increment();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Burada Consumer ile sarmaladığımız counter değişkenini tutan Text Widget’ımızı görüyoruz. Consumer kullanırken erişmek istediğimiz modelin türünü belirtmeliyiz.

Bu durumda Counter istiyoruz; bu yüzden Consumer <Counter> yazıyoruz. Eğer Counter’ı belirtmezsek, Provider paketi bize yardımcı olamaz. Provider, türlere bağlıdır ve tür olmadan ne istediğimizi bilemez.

Consumer Widget’ı mümkün olduğunca widget ağacımızın derinliklerini yerleştirmek en iyi yöntemdir (best practice). Kullanıcı arayüzünün büyük bir bölümünü yeniden oluşturmak yerine sadece ilgili widget’ın değiştirilmesini sağlar.

setState ile geliştirilmiş örnek counter uygulamasından farklı olarak Stateless Widget kullandık ve Counter değişkenini içeren Text Widget’ını Consumer<Counter> ile sarmaladık ve değişiklikte yani butona her tıkladığında tüm widget ağacımızın yeniden çizilmesi yerine ilgili Text Widget’ının yeniden oluşturulmasını sağladık.

var providerCounter = context.read<Counter>();
providerCounter.increment();

Bu sefer onPressed property’si içerinde bir providerCounter değişkeni tanımladık ve yukarıda oluşturduğumuz Counter sınıfını read ederek içerisinde bulunan incerement() metodumuza erişmiş olduk.

Eğer amacınız sadece basit bir counter uygulaması yapmak ise, Flutter’ın demo örneğinde olduğu gibi bir kullanım ihtiyacınızı karşılaması açısından uygun olabilir.

Ancak, iş daha karmaşık ve büyük projelere geldiğinde, bu tarz bir yaklaşım performans kayıplarına neden olacağı gibi, çeşitli problemleri de beraberinde getirir.

Ve son olarak belirtmeliyiz ki, en iyi state management yöntemi diye bir şey yoktur.

Proje ihtiyaçlarına, projede çalışacak kişilerin yetkinliklerine göre seçilebilecek optimum çözüm vardır. Hiçbiri diğerinden daha iyidir diyemeyiz. Her biri projemizdeki ihtiyaçlarımızı karşılama noktasında önem kazanır. Kısaca en doğru yöntem, ekibe ve projeye en uygun olan yöntemdir.

Size bu yazımda çeşitli kullanım şekilleri olan Provider paketinden ve setState yöntemi ile arasındaki farklardan kısaca bahsetmeye çalıştım.

Bu serinin devamında yukarıda bahsettiğim diğer state management yöntemlerini de anlatmaya çalışacağım.

Sonraki yazılarımda görüşmek üzere…


Flutter’da State Yönetimi Yaklaşımları was originally published in mobilist_labs on Medium, where people are continuing the conversation by highlighting and responding to this story.