Takip et

Flutter’da Provider ile Durum Yönetimi: Kapsamlı Bir Rehber

Flutter’da Provider ile Durum Yönetimi: Kapsamlı Bir Rehber Flutter, hızlı ve güzel mobil uygulamalar geliştirmek için harika bir UI toolkit’i

Flutter’da Provider ile Durum Yönetimi: Kapsamlı Bir Rehber

Flutter, hızlı ve güzel mobil uygulamalar geliştirmek için harika bir UI toolkit’idir. Ancak, her modern uygulamanın karşılaştığı temel zorluklardan biri, uygulamanın durumunu (state) etkili bir şekilde yönetmektir. Durum yönetimi, bir uygulamanın kullanıcı arayüzünün (UI) ve iş mantığının senkronize kalmasını, verilerin tutarlı olmasını ve uygulamanın karmaşıklaştıkça bile bakımı kolay olmasını sağlar. Flutter ekosisteminde birçok durum yönetimi çözümü bulunsa da, provider paketi, basitliği, performansı ve esnekliği sayesinde en popüler ve önerilen çözümlerden biri haline gelmiştir. Bu makalede, provider paketini derinlemesine inceleyecek, temel kavramlarını açıklayacak, kullanım senaryolarını örneklerle gösterecek ve iyi uygulama yöntemlerini tartışacağız.

Durum Yönetimi Neden Önemli?

Bir Flutter uygulamasında “durum”, kullanıcı etkileşimleri, ağ istekleri veya diğer olaylar sonucunda değişebilen herhangi bir veridir. Örneğin, bir sayaç uygulamasındaki mevcut sayı, bir e-ticaret uygulamasındaki alışveriş sepeti içeriği veya bir kullanıcının oturum açma durumu, birer durum örneğidir.

Durum yönetimi olmadan, uygulamalar hızla karmaşıklaşabilir ve bakımı zor hale gelebilir:

* setState Cehennemi (setState Hell): Küçük, lokal durum değişiklikleri için setState kullanmak yeterli olsa da, uygulamanın genel durumunu yönetmek için birden fazla setState çağrısı yapmak, kodun okunabilirliğini ve yönetilebilirliğini ciddi şekilde azaltır.
* Prop Drilling: Verileri, aslında ihtiyacı olmayan birçok ara widget üzerinden “delmek” zorunda kalmak, kodun esnekliğini azaltır ve hatalara yol açabilir.
* Yeniden Çizim Performansı: Durum değişikliklerinde tüm widget ağacının gereksiz yere yeniden çizilmesi, performans sorunlarına yol açabilir.
* Test Edilebilirlik: İş mantığı ile UI’ın iç içe geçtiği durumlarda, kodun birim testlerini yazmak zorlaşır.

Durum, genellikle iki ana kategoriye ayrılabilir:

* Geçici (Ephemeral) Durum: Yalnızca tek bir widget içinde yaşayan ve genellikle setState ile yönetilebilen lokal durumdur. Örneğin, bir Checkbox widget’ının işaretli olup olmadığı.
* Uygulama (App) Durumu: Uygulamanın birden fazla bölümünde paylaşılan ve yaşam döngüsü boyunca devam eden durumdur. Örneğin, kullanıcı kimlik bilgileri, veritabanı içeriği, tema ayarları. provider gibi çözümler genellikle uygulama durumunu yönetmek için kullanılır.

Etkili durum yönetimi, bu sorunları çözerek kodun daha düzenli, performanslı, test edilebilir ve ölçeklenebilir olmasını sağlar.

Provider Nedir?

provider, Google tarafından önerilen ve Flutter ekibinin de aktif olarak desteklediği bir durum yönetimi çözümüdür. Temelde, Flutter’ın kendi InheritedWidget mekanizmasının etrafına inşa edilmiş, kullanımı daha kolay ve daha esnek bir sarmalayıcıdır. InheritedWidget‘lar, widget ağacında yukarıdan aşağıya doğru veri geçişini sağlamak için kullanılan güçlü bir yapıdır. provider, bu gücü daha erişilebilir bir API ile sunar.

provider‘ın temel prensibi basittir: bir veriyi (veya bir veri modelini) widget ağacının belirli bir noktasında sağlar ve bu veriye ihtiyaç duyan alt widget’ların bu veriyi kolayca erişmesini ve değişikliklerini dinlemesini sağlar. Bir veri değiştiğinde, sadece ilgili widget’lar yeniden çizilir, bu da performans açısından büyük bir avantajdır.

provider paketini kullanmaya başlamak için pubspec.yaml dosyanıza eklemeniz yeterlidir:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.5 # Mevcut en son sürümü kullanın

Ardından terminalinizde flutter pub get komutunu çalıştırın.

Provider’ın Temel Kavramları

provider paketini anlamak için birkaç temel bileşeni kavramak önemlidir.

ChangeNotifier

ChangeNotifier, Flutter SDK’sının bir parçası olan, durum değişikliklerini dinleyicilere bildirmek için kullanılan basit bir sınıftır. Kendi özel durum modellerimizi oluştururken genellikle ChangeNotifier sınıfını extend ederiz. Bir modeldeki veri değiştiğinde, notifyListeners() metodunu çağırarak bu modeli dinleyen tüm widget’lara değişiklik olduğunu bildiririz.

Örnek:

import 'package:flutter/foundation.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Dinleyicilere değişiklik olduğunu bildir
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

Bu CounterModel sınıfı, _count adında özel bir tamsayı tutar. increment ve decrement metotları _count değerini değiştirir ve ardından notifyListeners() çağırarak bu modeli dinleyen tüm widget’ları yeniden çizime teşvik eder.

ChangeNotifierProvider

ChangeNotifierProvider, bir ChangeNotifier örneğini widget ağacına sağlamak için kullanılır. Bu, alt widget’ların bu ChangeNotifier‘a erişmesini sağlar. Genellikle uygulamanın en üst seviyelerinde veya belirli bir ekranın kökünde kullanılır.

ChangeNotifierProvider‘ın create parametresi, provider’ın ilk kez oluşturulduğunda nasıl bir örnek sağlayacağını tanımlar. Bu, provider’ın yaşam döngüsünü kontrol etmenizi sağlar.

Örnek:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_model.dart'; // Yukarıdaki CounterModel dosyanız

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(), // CounterModel'i sağlar
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

Burada ChangeNotifierProvider, CounterModel‘in bir örneğini oluşturur ve MyApp widget’ı ile onun tüm alt widget’larına bu modeli erişilebilir kılar.

Consumer

Consumer widget’ı, bir Provider tarafından sağlanan veriyi dinlemek ve bu veri değiştiğinde sadece kendi builder metodunu yeniden çizmek için kullanılır. Bu, uygulamanızın performansını artırmak için önemlidir, çünkü yalnızca ilgili UI parçası yeniden oluşturulur.

Consumer‘ın builder metodu üç argüman alır: context, value (sağlanan veri modeli) ve child (isteğe bağlı, performans optimizasyonu için).

Örnek:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_model.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('You have pushed the button this many times:'),
            // Consumer sadece CounterModel değiştiğinde yeniden çizilir
            Consumer(
              builder: (context, counter, child) {
                return Text(
                  '${counter.count}',
                  style: Theme.of(context).textTheme.headlineMedium,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Provider.of ile modeli al ve increment metodunu çağır
          // listen: false, çünkü burada sadece metot çağırıyoruz, dinlemiyoruz.
          Provider.of(context, listen: false).increment();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Bu örnekte, Consumer sadece CounterModel‘deki count değeri değiştiğinde Text widget’ını yeniden çizer. Diğer kısımlar etkilenmez.

Provider.of(context)

Provider.of(context) metodu, widget ağacından belirli bir türdeki (T) provider’ı doğrudan almak için kullanılır. İki önemli kullanım şekli vardır:

* Provider.of(context, listen: true) (Varsayılan): Bu, provider’ı dinler ve provider’daki veri değiştiğinde widget’ı yeniden çizer. Consumer widget’ına benzer bir etki yaratır, ancak Consumer daha spesifik bir yeniden çizim kontrolü sağlar. Genellikle build metodu içinde, bir widget’ın UI’ını provider’daki veriye göre güncellemesi gerektiğinde kullanılır.
* Provider.of(context, listen: false): Bu, provider’ı dinlemez ve provider’daki veri değiştiğinde widget’ı yeniden çizmez. Genellikle bir metot çağırmak veya bir olay işleyici içinde provider’a erişmek gerektiğinde kullanılır (örneğin, bir butona tıklandığında increment() metodunu çağırmak gibi). Bu, gereksiz yeniden çizimleri önlemek için çok önemlidir.

Yukarıdaki FloatingActionButton örneğinde listen: false kullanılmasına dikkat edin. Butonun kendisinin count değerindeki değişiklikleri dinlemesine gerek yoktur; sadece increment metodunu çağırması yeterlidir.

Selector

Selector, Consumer‘a benzer, ancak daha ince taneli kontrol sağlar. Consumer bir modeli dinlerken, Selector modelin belirli bir kısmını dinlemenizi ve sadece o kısım değiştiğinde yeniden çizim yapmanızı sağlar. Bu, özellikle büyük modellerde gereksiz yeniden çizimleri daha da azaltmak için faydalıdır.

Selector‘ın iki parametresi vardır:

* selector: Modelden belirli bir değeri seçen bir fonksiyondur.
* builder: Seçilen değer değiştiğinde çağrılan bir fonksiyondur.

Örnek:

// ... HomePage içinde ...
            Selector(
              selector: (context, counter) => counter.count, // Sadece count değerini dinle
              builder: (context, count, child) {
                print('Text widget rebuilt with count: $count');
                return Text(
                  '$count',
                  style: Theme.of(context).textTheme.headlineMedium,
                );
              },
            ),
// ...

Bu örnekte, Selector sadece CounterModel‘deki count değeri değiştiğinde Text widget’ını yeniden çizer. CounterModel‘de count dışında başka bir özellik değişirse, bu Text widget’ı yeniden çizilmeyecektir.

MultiProvider

Bir uygulamada genellikle birden fazla provider’a ihtiyaç duyulur. MultiProvider widget’ı, birden fazla provider’ı tek bir noktada tanımlamanıza olanak tanır, bu da widget ağacınızı daha temiz ve düzenli hale getirir.

Örnek:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_model.dart';
// Diğer modelleriniz
// import 'auth_model.dart';
// import 'theme_model.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CounterModel()),
        // ChangeNotifierProvider(create: (context) => AuthModel()),
        // ChangeNotifierProvider(create: (context) => ThemeModel()),
      ],
      child: MyApp(),
    ),
  );
}

MultiProvider ile, tüm uygulama genelinde erişilebilir olması gereken tüm provider’ları MyApp‘ın üzerinde tanımlayabiliriz.

Provider Kullanım Senaryoları ve Örnekler

Şimdi, provider‘ın farklı kullanım senaryolarını daha detaylı örneklerle inceleyelim.

Basit Bir Sayaç Uygulaması

Yukarıdaki örnekler, basit bir sayaç uygulamasının temelini oluşturuyor. Tam bir örnekle konuyu pekiştirelim.

counter_model.dart:

import 'package:flutter/foundation.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

  void decrement() {
    _count--;
    notifyListeners();
  }
}

main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_model.dart';
import 'home_page.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Counter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

home_page.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_model.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('You have pushed the button this many times:'),
            Consumer(
              builder: (context, counter, child) {
                return Text(
                  '${counter.count}',
                  style: Theme.of(context).textTheme.headlineMedium,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              Provider.of(context, listen: false).decrement();
            },
            tooltip: 'Decrement',
            child: Icon(Icons.remove),
            heroTag: 'decrement',
          ),
          SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () {
              Provider.of(context, listen: false).increment();
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
            heroTag: 'increment',
          ),
        ],
      ),
    );
  }
}

Bu yapı, provider ile durum yönetimi için temel bir iskelet sağlar. CounterModel iş mantığını (sayacı artırma/azaltma) ve durumu (_count) kapsüllerken, HomePage UI’ı temsil eder ve Consumer aracılığıyla CounterModel‘deki değişiklikleri dinler. Butonlar ise Provider.of(context, listen: false) kullanarak iş mantığı metotlarını çağırır.

Asenkron İşlemler ve API Çağrıları

Gerçek dünya uygulamalarında sıkça karşılaşılan bir senaryo, ağdan veri çekme gibi asenkron işlemlerdir. provider bu tür durumları yönetmek için de çok uygundur. Genellikle, verinin yüklenme durumunu (loading), hata durumunu (error) ve başarılı veri durumunu (data) modelinizde tutarsınız.

post_model.dart:

class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(Map json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

post_provider.dart:

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'post_model.dart';

enum PostStatus { initial, loading, loaded, error }

class PostProvider extends ChangeNotifier {
  List _posts = [];
  PostStatus _status = PostStatus.initial;
  String? _errorMessage;

  List get posts => _posts;
  PostStatus get status => _status;
  String? get errorMessage => _errorMessage;

  Future fetchPosts() async {
    _status = PostStatus.loading;
    notifyListeners();

    try {
      final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
      if (response.statusCode == 200) {
        final List data = json.decode(response.body);
        _posts = data.map((json) => Post.fromJson(json)).toList();
        _status = PostStatus.loaded;
      } else {
        _errorMessage = 'Failed to load posts: ${response.statusCode}';
        _status = PostStatus.error;
      }
    } catch (e) {
      _errorMessage = 'An error occurred: $e';
      _status = PostStatus.error;
    } finally {
      notifyListeners();
    }
  }
}

main.dart (Provider ekleme):

// ...
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CounterModel()),
        ChangeNotifierProvider(create: (context) => PostProvider()), // PostProvider'ı ekle
      ],
      child: MyApp(),
    ),
  );
}
// ...

posts_page.dart (UI):

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'post_provider.dart';

class PostsPage extends StatefulWidget {
  @override
  State createState() => _PostsPageState();
}

class _PostsPageState extends State {
  @override
  void initState() {
    super.initState();
    // Sayfa yüklendiğinde postları çek
    Provider.of(context, listen: false).fetchPosts();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Posts')),
      body: Consumer(
        builder: (context, postProvider, child) {
          if (postProvider.status == PostStatus.loading) {
            return Center(child: CircularProgressIndicator());
          } else if (postProvider.status == PostStatus.error) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: ${postProvider.errorMessage}'),
                  ElevatedButton(
                    onPressed: () => postProvider.fetchPosts(),
                    child: Text('Retry'),
                  ),
                ],
              ),
            );
          } else if (postProvider.status == PostStatus.loaded) {
            return ListView.builder(
              itemCount: postProvider.posts.length,
              itemBuilder: (context, index) {
                final post = postProvider.posts[index];
                return Card(
                  margin: EdgeInsets.all(8.0),
                  child: ListTile(
                    title: Text(post.title),
                    subtitle: Text(post.body),
                  ),
                );
              },
            );
          }
          return Center(child: Text('Press button to load posts'));
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Provider.of(context, listen: false).fetchPosts(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}

Bu örnek, PostProvider‘ın _status ve _errorMessage gibi durum değişkenlerini kullanarak ağ isteklerinin farklı aşamalarını nasıl yönettiğini gösterir. UI, Consumer aracılığıyla bu durumu dinler ve buna göre yükleme göstergesi, hata mesajı veya veri listesini görüntüler. initState içinde fetchPosts() çağrılırken listen: false kullanılması, widget’ın provider’ı dinlemesini engeller, çünkü initState içinde UI’ı doğrudan yeniden çizmek istemeyiz.

Kullanıcı Kimlik Doğrulama

Kullanıcı kimlik doğrulama, uygulamanın farklı bölümlerinin (örneğin, giriş ekranı ve ana ekran) kullanıcının oturum açma durumuna göre değiştiği yaygın bir senaryodur.

auth_provider.dart:

import 'package:flutter/foundation.dart';

enum AuthStatus { authenticated, unauthenticated, authenticating }

class AuthProvider extends ChangeNotifier {
  AuthStatus _status = AuthStatus.unauthenticated;
  String? _userToken; // Gerçek uygulamada daha detaylı User nesnesi olabilir

  AuthStatus get status => _status;
  bool get isAuthenticated => _status == AuthStatus.authenticated;
  String? get userToken => _userToken;

  Future login(String username, String password) async {
    _status = AuthStatus.authenticating;
    notifyListeners();
    await Future.delayed(Duration(seconds: 2)); // Simulate API call

    if (username == 'test' && password == 'password') {
      _userToken = 'some_jwt_token';
      _status = AuthStatus.authenticated;
    } else {
      _status = AuthStatus.unauthenticated;
      _userToken = null;
    }
    notifyListeners();
  }

  Future logout() async {
    _status = AuthStatus.authenticating; // Opsiyonel: Çıkış yaparken bir durum göstergesi
    notifyListeners();
    await Future.delayed(Duration(seconds: 1)); // Simulate API call
    _userToken = null;
    _status = AuthStatus.unauthenticated;
    notifyListeners();
  }
}

main.dart (AuthProvider ekleme ve yönlendirme):

// ...
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CounterModel()),
        ChangeNotifierProvider(create: (context) => PostProvider()),
        ChangeNotifierProvider(create: (context) => AuthProvider()), // AuthProvider'ı ekle
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Auth Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Consumer(
        builder: (context, authProvider, _) {
          if (authProvider.status == AuthStatus.authenticating) {
            return Scaffold(
              body: Center(child: CircularProgressIndicator()),
            );
          } else if (authProvider.isAuthenticated) {
            return HomePage(); // Oturum açıkken ana sayfaya git
          } else {
            return LoginPage(); // Oturum kapalıyken giriş sayfasına git
          }
        },
      ),
    );
  }
}

Bu örnekte, MyApp widget’ı, AuthProvider‘ın durumunu dinleyerek uygulamanın hangi ana ekranı göstereceğine karar verir (giriş, yükleniyor veya ana sayfa). Bu, uygulamanın kök seviyesinde yönlendirme mantığını yönetmek için güçlü bir yaklaşımdır.

Provider’ın Gelişmiş Kullanımı

provider paketi, temel ChangeNotifierProvider ve Consumer‘ın ötesinde daha gelişmiş senaryolar için de çözümler sunar.

ProxyProvider

Bazen bir provider’ın değeri, başka bir provider’ın değerine bağlı olabilir. Örneğin, bir UserProfileProvider‘ın, AuthProvider‘dan gelen kullanıcı kimlik bilgilerine ihtiyacı olabilir. İşte bu noktada ProxyProvider devreye girer.

ProxyProvider, birden fazla değeri alır ve bu değerlere dayanarak yeni bir değer oluşturur. ProxyProvider‘ın farklı varyantları vardır (ProxyProvider, ProxyProvider2, ProxyProvider3 vb.), aldığı bağımlılık sayısına göre.

Örnek (AuthUser’a bağlı UserProfile):

// auth_provider.dart'tan gelen AuthProvider'ı kullanıyoruz
// UserProfileProvider.dart
import 'package:flutter/foundation.dart';
import 'auth_provider.dart';

class UserProfile {
  final String userId;
  final String email;
  final String? bio;

  UserProfile({required this.userId, required this.email, this.bio});

  // Profil verilerini API'den çekme simülasyonu
  static Future fetch(String userId) async {
    await Future.delayed(Duration(seconds: 1));
    return UserProfile(userId: userId, email: '$userId@example.com', bio: 'This is my bio for $userId');
  }
}

class UserProfileProvider extends ChangeNotifier {
  final AuthProvider _authProvider;
  UserProfile? _userProfile;
  bool _isLoading = false;

  UserProfile? get userProfile => _userProfile;
  bool get isLoading => _isLoading;

  UserProfileProvider(this._authProvider) {
    _authProvider.addListener(_onAuthChanged);
    _onAuthChanged(); // İlk durumu ayarla
  }

  void _onAuthChanged() async {
    if (_authProvider.isAuthenticated && _authProvider.userToken != null) {
      _isLoading = true;
      notifyListeners();
      // Gerçek uygulamada userToken'dan userId'yi almalısınız
      _userProfile = await UserProfile.fetch(_authProvider.userToken!);
      _isLoading = false;
    } else {
      _userProfile = null;
    }
    notifyListeners();
  }

  @override
  void dispose() {
    _authProvider.removeListener(_onAuthChanged);
    super.dispose();
  }
}

main.dart (MultiProvider içinde ProxyProvider kullanımı):

// ...
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CounterModel()),
        ChangeNotifierProvider(create: (context) => PostProvider()),
        ChangeNotifierProvider(create: (context) => AuthProvider()),
        // AuthProvider'a bağlı UserProfileProvider
        ChangeNotifierProxyProvider(
          create: (context) => UserProfileProvider(
            Provider.of(context, listen: false),
          ),
          update: (context, authProvider, userProfileProvider) {
            // AuthProvider değiştiğinde UserProfileProvider'ı güncelle
            // Genellikle bu, UserProfileProvider'ın kendi içinde yönetilir.
            // Bu örnekte, UserProfileProvider'ın constructor'ında listener ekleyerek hallettik.
            // update metodu, daha çok yeni bir UserProfileProvider örneği oluşturmadan
            // mevcut örneği güncellemek için kullanılır.
            return userProfileProvider!;
          },
        ),
      ],
      child: MyApp(),
    ),
  );
}
// ...

Yukarıdaki örnekte ChangeNotifierProxyProvider kullanıldı. create metodu, AuthProvider‘ın ilk değerini kullanarak UserProfileProvider‘ı oluşturur. update metodu ise AuthProvider her değiştiğinde çağrılır ve UserProfileProvider‘ın güncellenmesini sağlar. Bu, bağımlılıkları yönetmek için çok esnek bir yoldur.

StreamProvider & FutureProvider

provider paketi, asenkron verilerle (Future veya Stream) çalışmak için özel provider’lar da sunar:

* FutureProvider: Tek bir asenkron değeri (bir Future) sağlamak için kullanılır. Future tamamlandığında değeri sağlar ve dinleyicileri bilgilendirir. Genellikle bir API’den tek seferlik veri çekmek için kullanılır.

// main.dart içinde
    FutureProvider(
      create: (context) => Future.delayed(Duration(seconds: 2), () => 'Hello from Future!'),
      initialData: 'Loading...', // Future tamamlanana kadar gösterilecek veri
      child: MyApp(),
    ),
    // UI'da kullanım:
    Consumer(
      builder: (context, message, child) => Text(message),
    ),

* StreamProvider: Bir akıştan (Stream) gelen değerleri sağlamak için kullanılır. Akış yeni bir değer yayınladığında, provider bu değeri günceller ve dinleyicileri bilgilendirir. Gerçek zamanlı veriler (örneğin, WebSocket bağlantıları, Firestore dinleyicileri) için idealdir.

// main.dart içinde
    StreamProvider(
      create: (context) => Stream.periodic(Duration(seconds: 1), (i) => i).take(10),
      initialData: 0,
      child: MyApp(),
    ),
    // UI'da kullanım:
    Consumer(
      builder: (context, count, child) => Text('Stream count: $count'),
    ),

Bu provider’lar, FutureBuilder ve StreamBuilder‘ın sağladığı işlevselliği, provider‘ın kolay erişim ve widget ağacında yayılım avantajlarıyla birleştirir.

Provider’ı Test Etmek

provider ile yazılan kodun test edilmesi oldukça kolaydır, çünkü iş mantığı (ChangeNotifier modelleri) UI’dan ayrılmıştır.

* Unit Testler: ChangeNotifier modellerinizi bağımsız olarak test edebilirsiniz. Sadece modeli oluşturun, metotlarını çağırın ve durumun beklendiği gibi değişip değişmediğini kontrol edin. notifyListeners() çağrılarının yapıldığını doğrulamak için addListener metodunu kullanarak dinleyicilerin çağrıldığını kontrol edebilirsiniz.

import 'package:flutter_test/flutter_test.dart';
    import 'package:your_app/counter_model.dart';

    void main() {
      group('CounterModel', () {
        test('initial count should be 0', () {
          final counter = CounterModel();
          expect(counter.count, 0);
        });

        test('count should increment', () {
          final counter = CounterModel();
          counter.increment();
          expect(counter.count, 1);
        });

        test('count should decrement', () {
          final counter = CounterModel();
          counter.decrement();
          expect(counter.count, -1);
        });

        test('calling increment should notify listeners', () {
          final counter = CounterModel();
          var listenerCalled = false;
          counter.addListener(() => listenerCalled = true);
          counter.increment();
          expect(listenerCalled, isTrue);
        });
      });
    }

* Widget Testler: Provider’a bağımlı widget’ları test ederken, widget’ı bir ChangeNotifierProvider veya MultiProvider ile sarmalamanız gerekir. Bu, widget’ın test ortamında provider’a erişebilmesini sağlar.

import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:provider/provider.dart';
    import 'package:your_app/counter_model.dart';
    import 'package:your_app/home_page.dart'; // HomePage'i içe aktarın

    void main() {
      testWidgets('Counter increments correctly', (WidgetTester tester) async {
        await tester.pumpWidget(
          ChangeNotifierProvider(
            create: (context) => CounterModel(),
            child: MaterialApp(home: HomePage()),
          ),
        );

        // Initial count is 0
        expect(find.text('0'), findsOneWidget);
        expect(find.text('1'), findsNothing);

        // Tap the '+' button
        await tester.tap(find.byIcon(Icons.add));
        await tester.pump(); // Rebuild the widget after state change

        // Count should be 1
        expect(find.text('0'), findsNothing);
        expect(find.text('1'), findsOneWidget);
      });
    }

Provider’ın Avantajları ve Dezavantajları

Her araç gibi, provider‘ın da kendi güçlü ve zayıf yönleri vardır.

Avantajları

* Basitlik ve Öğrenme Kolaylığı: provider, diğer bazı çözümlere kıyasla öğrenmesi ve uygulaması oldukça kolaydır. Temel kavramları hızlıca kavranabilir.
* Performans: InheritedWidget üzerine kurulu olması sayesinde, provider oldukça performanslıdır. Consumer ve Selector gibi widget’lar, sadece ilgili UI parçalarının yeniden çizilmesini sağlayarak gereksiz iş yükünü azaltır.
* Test Edilebilirlik: İş mantığının UI’dan ayrılması, unit testlerin yazılmasını kolaylaştırır.
* Topluluk Desteği ve Popülerlik: provider, Flutter topluluğunda çok yaygın olarak kullanılır ve geniş bir desteğe sahiptir. Bol miktarda örnek, dokümantasyon ve çözüm bulmak kolaydır.
* Esneklik: Basit sayaçlardan karmaşık asenkron işlemlere ve bağımlılık enjeksiyonuna kadar çeşitli senaryolarda kullanılabilir.
* Minimum Boilerplate: Diğer bazı çözümlere göre daha az “boilerplate” (tekrarlayan kod) gerektirir.

Dezavantajları

* context Bağımlılığı: Provider.of(context) kullanırken her zaman BuildContext‘e erişiminiz olması gerekir. Bu, özellikle build metotları dışında (örneğin, bir servis sınıfında) provider’lara erişmek istediğinizde bazen zorlayıcı olabilir.
* Derin Widget Ağaçlarında Karmaşıklık: Çok sayıda iç içe Provider kullanıldığında widget ağacı karmaşıklaşabilir ve okunabilirliği azalabilir. MultiProvider bu durumu bir nebze hafifletir.
* Durum Değişikliklerinin Takibi: Bazen bir modeldeki hangi değişikliğin hangi Consumer‘ı tetiklediğini takip etmek zor olabilir, özellikle model büyüdüğünde. Selector bu sorunu hafifletmeye yardımcı olur.
* Boilerplate (Bazı Durumlarda): Basit uygulamalar için az boilerplate olsa da, çok sayıda ChangeNotifier ve Provider tanımlamanız gereken büyük uygulamalarda yine de bir miktar tekrarlayan kod oluşabilir.

Diğer Durum Yönetimi Çözümleriyle Karşılaştırma

Flutter ekosisteminde provider dışında birçok başka durum yönetimi çözümü de bulunmaktadır:

* BLoC/Cubit: Daha büyük ve karmaşık uygulamalar için güçlü ve ölçeklenebilir bir mimari sunar. Olaylar ve durumlar arasında net bir ayrım yapar. provider ile birlikte de kullanılabilir (örneğin, BlocProvider ve BlocBuilder provider paketinin bir parçasıdır).
* Riverpod: provider‘ın geliştiricisi tarafından oluşturulan ve provider‘ın birçok dezavantajını (özellikle context bağımlılığı ve compile-time safety eksikliği) gidermeyi amaçlayan bir çözümdür. Daha tip güvenli ve bağımsız provider’lar sunar.
* GetX: All-in-one bir çözüm olarak bilinir; durum yönetimi, bağımlılık enjeksiyonu ve yönlendirme gibi birçok özelliği tek bir pakette sunar. Hızlı geliştirme için popülerdir ancak bazen aşırı kapsamlı olduğu eleştirilir.
* Redux: JavaScript dünyasından esinlenilmiş, tek bir durum ağacı ve öngörülebilir durum değişiklikleri prensibine dayanan bir çözümdür. Flutter için de uygulamaları mevcuttur.

provider, genellikle Flutter’a yeni başlayanlar ve orta ölçekli uygulamalar için iyi bir başlangıç noktasıdır. Basitliği ve esnekliği, çoğu projenin ihtiyaçlarını karşılayabilir. Daha büyük ve daha katı bir mimariye ihtiyaç duyulduğunda BLoC/Cubit veya Riverpod gibi çözümler düşünülebilir.

İyi Uygulama Yöntemleri

provider ile etkili durum yönetimi için bazı iyi uygulama yöntemleri şunlardır:

1. Durumu Ayırın: İş mantığını (model) UI’dan kesinlikle ayırın. ChangeNotifier sınıflarınız sadece veriyi ve bu veriyi manipüle eden metotları içermelidir. UI widget’ları ise sadece bu modeli dinlemeli ve görüntülemelidir.
2. Küçük ve Odaklanmış Modeller Kullanın: Büyük, her şeyi içeren bir ChangeNotifier yerine, her biri belirli bir sorumluluğa sahip daha küçük modeller oluşturun. Bu, kodun daha yönetilebilir ve test edilebilir olmasını sağlar.
3. Consumer ve Selector ile Gereksiz Yeniden Çizimleri Önleyin: Uygulamanızın performansını optimize etmek için, sadece durumun ilgili kısmının değiştiği zaman yeniden çizilmesi gereken yerlerde Consumer veya Selector kullanın.
4. listen: false Kullanmayı Unutmayın: Bir provider’dan sadece bir metot çağırmak veya bir değeri okumak istediğinizde, ancak o değerin değişikliklerini dinlemek istemediğinizde mutlaka Provider.of(context, listen: false) kullanın. Bu, gereksiz yeniden çizimleri önler.
5. Hata Yönetimi ve Yükleme Durumlarını Ele Alın: Asenkron işlemlerde (API çağrıları gibi), modelinizde yükleme durumu (isLoading), hata durumu (errorMessage) ve başarılı veri durumu (data) gibi durumları tutun. UI’ınız bu durumlara göre uygun geri bildirimleri göstermelidir.
6. Provider Hiyerarşisini Düşünerek Oluşturun: Provider’ları widget ağacında ihtiyaç duyulan en yüksek seviyede tanımlayın. MultiProvider kullanarak birden fazla provider’ı tek bir noktada gruplayın.
7. Test Edilebilir Kod Yazın: Modellerinizi ve servislerinizi bağımsız olarak test edilebilir şekilde tasarlayın.

Sonuç

Flutter’da provider paketi, durum yönetimi için basit, performanslı ve esnek bir çözümdür. InheritedWidget‘ın gücünü daha erişilebilir bir API ile sunarak, geliştiricilerin karmaşık durumları kolayca yönetmesine olanak tanır. ChangeNotifier, ChangeNotifierProvider, Consumer, Selector ve ProxyProvider gibi temel kavramlarını anlayarak, uygulamalarınızın daha düzenli, ölçeklenebilir ve bakımı kolay olmasını sağlayabilirsiniz.

İster yeni başlayan ister deneyimli bir Flutter geliştiricisi olun, provider paketi çoğu uygulamanın ihtiyaçlarını karşılayabilecek güçlü bir araçtır. İyi uygulama yöntemlerini takip ederek ve uygulamanızın özel gereksinimlerine göre diğer çözümlerle karşılaştırma yaparak, projeniz için en uygun durum yönetimi stratejisini belirleyebilirsiniz. provider ile durum yönetimi, Flutter geliştirme yolculuğunuzda size büyük kolaylıklar sağlayacaktır.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Exit mobile version