> ## Content Index
> Fetch the complete content index at: https://arkar.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Day 29: State Management with Provider
- URL: https://arkar.dev/day-29-state-management-with-provider/
- Published: 2026-11-10T01:30:00.000Z
- Updated: 2026-11-10T01:30:00.000Z
- Description: Connect Pixly's repository to the UI with Provider and ChangeNotifier. Model loading, error and data states, choose between watch, read and select, and wire everything up with MultiProvider.
- Author: Arkar Min Tun
- Tags: 100 days of flutter, flutter

Day 28 မှာ Pexels API ကို ခေါ်ပေးမယ့် `PhotoRepository` ကို ရေးခဲ့ပြီးပါပြီ။ ဒါပေမယ့် screen ပေါ်မှာ ဘာမှ မပေါ်သေးပါဘူး။ ဒီနေ့မှာတော့ repository က ယူလာတဲ့ data ကို UI ဆီ ပို့ပေးမယ့် **state management** အပိုင်းကို ဆက်လုပ်ကြပါမယ်။ ဒီနေ့ ပြီးသွားရင် app ဖွင့်လိုက်တာနဲ့ Pexels က ဓာတ်ပုံ ၂၀ ရဲ့ အချက်အလက်တွေကို list နဲ့ ပြပေးနိုင်ပြီ ဖြစ်ပါတယ်။

## ဘာလို့ State Management လိုတာလဲ?

Day 17 မှာ `StatefulWidget` နဲ့ `setState` ကို လေ့လာခဲ့ပါတယ်။ Widget တခုတည်းအတွက်ပဲ လိုတဲ့ state (ဥပမာ - checkbox အမှန်ခြစ်ထား/မထား၊ TextField ထဲက စာ) အတွက်ဆို `setState` က လုံလောက်ပါတယ်။

ဒါပေမယ့် Pixly မှာ ဓာတ်ပုံ list က

- Home screen မှာ ပြရမယ်
- Detail screen ကို ပို့ပေးရမယ် (Day 32)
- Search screen, Bookmarks screen တွေကလဲ ဆင်တူ data ကို သုံးမယ် (Day 34, 35)

ဆိုတော့ widget တခုထဲမှာပဲ သိမ်းထားလို့ မရတော့ပါဘူး။ Widget tree ရဲ့ အပေါ်ပိုင်းမှာ တနေရာထဲ ထားပြီး လိုအပ်တဲ့ widget တွေက ယူသုံး၊ data ပြောင်းရင် အဲ့ဒီ widget တွေကိုပဲ rebuild လုပ်ပေးတဲ့ နည်းလမ်း လိုလာပါတယ်။ ဒါကို state management လို့ ခေါ်ပါတယ်။

Flutter မှာ state management နည်းလမ်း အများကြီး ရှိပါတယ်။ Pixly မှာတော့ Flutter ရဲ့ built-in `ChangeNotifier` နဲ့ `provider` package ကို သုံးပါမယ်။ နားလည်ရ လွယ်ပြီး ဒီနောက်ပိုင်း သုံးမယ့် Riverpod (Day 41) လို package တွေရဲ့ အခြေခံ သဘောတရားတွေကို နားလည်ဖို့ အကောင်းဆုံး အစပဲ ဖြစ်ပါတယ်။

```bash
fvm flutter pub add provider
```

## ChangeNotifier

`ChangeNotifier` က Flutter မှာ ပါပြီးသား class တခု ဖြစ်ပါတယ်။ သူ့ကို extend လုပ်ထားတဲ့ class က data ပြောင်းတိုင်း `notifyListeners()` ကို ခေါ်ပြီး "ငါ့ data ပြောင်းသွားပြီ" လို့ နားထောင်နေသူ (listener) တွေကို အသိပေးပါတယ်။

```dart
// lib/providers/photos_provider.dart
import 'package:flutter/foundation.dart';
import 'package:pixly/core/result.dart';
import 'package:pixly/models/photo.dart';
import 'package:pixly/repositories/photo_repository.dart';

/// Holds the curated photos and tells listeners when they change.
class PhotosProvider extends ChangeNotifier {
  PhotosProvider(this._repository);

  final PhotoRepository _repository;

  List<Photo> _photos = [];
  bool _isLoading = false;
  String? _error;
  bool _disposed = false;

  List<Photo> get photos => List.unmodifiable(_photos);
  bool get isLoading => _isLoading;
  String? get error => _error;

  Future<void> loadCurated() async {
    if (_isLoading) return; // already loading, ignore extra taps
    _isLoading = true;
    _error = null;
    notifyListeners();

    final result = await _repository.curated();
    if (_disposed) return; // the screen went away while we were waiting

    switch (result) {
      case Success(:final data):
        _photos = data.photos;
      case Failure(:final message):
        _error = message;
    }

    _isLoading = false;
    notifyListeners();
  }

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }
}
```

### အဖြစ်များတဲ့ State တွေ

Network ကနေ data ယူတဲ့ screen အများစုမှာ အောက်က state တွေကို ကိုင်တွယ်ဖို့ လိုပါတယ်။

| State   | စစ်တဲ့ပုံ                      | UI မှာ ပြမယ့်ပုံ                 |
| ------- | ------------------------------ | -------------------------------- |
| Loading | isLoading == true              | Progress indicator               |
| Error   | error != null                  | Error message + Try again button |
| Empty   | အောင်မြင်ပေမယ့် photos.isEmpty | "No photos yet" message          |
| Data    | photos ထဲမှာ ပုံ ရှိ           | ဓာတ်ပုံ list                     |

Request အောင်မြင်သွားပေမယ့် ပုံ တပုံမှ ပြန်မလာတာ (Empty) ကို မေ့တတ်ကြပါတယ်။ ဒီ case ကို မကိုင်တွယ်ရင် user က screen အလွတ်ကြီးပဲ မြင်ရမှာ ဖြစ်ပါတယ်။

### Private fields + Public getters

`_photos`, `_isLoading`, `_error` တွေကို `_` နဲ့ private ထားပြီး getter တွေကနေပဲ ဖတ်ခွင့်ပေးထားပါတယ်။ ဒါမှ UI ကနေ `provider._isLoading = false` လို state ကို တိုက်ရိုက် ပြင်လို့ မရတော့ဘဲ state ပြောင်းတာ အကုန်လုံး `loadCurated()` လို method တွေကနေပဲ ဖြစ်မှာပါ။ `photos` getter ကလဲ `List.unmodifiable` နဲ့ ပြန်ပေးတဲ့အတွက် အပြင်ကနေ `photos.clear()` လို့ ခေါ်ရင် error တက်ပါလိမ့်မယ်။ State ကို ဘယ်နေရာကနေ ပြောင်းလဲဆိုတာ ရှင်းရှင်းလင်းလင်း ဖြစ်နေတာက bug ရှာတဲ့အခါ အရမ်း အရေးကြီးပါတယ်။

### notifyListeners() ကို ၂ ခါ ခေါ်ထားတာ

1. Loading စတဲ့အခါ - UI က progress indicator ပြနိုင်ဖို့
2. ပြီးသွားတဲ့အခါ - data ဒါမှမဟုတ် error ကို ပြနိုင်ဖို့

`notifyListeners()` မခေါ်ရင် data ပြောင်းသွားပေမယ့် UI က မသိတဲ့အတွက် screen ပေါ်မှာ ဘာမှ ပြောင်းမှာ မဟုတ်ပါဘူး။ ChangeNotifier သုံးတဲ့အခါ အမှားအများဆုံးက `notifyListeners()` ခေါ်ဖို့ မေ့တာပါ။

### Loading guard

`if (_isLoading) return;` က user က "Try again" ကို ဆက်တိုက် နှိပ်လိုက်ရင် request တွေ အများကြီး တပြိုင်နက် ထွက်မသွားအောင် ကာကွယ်ပေးပါတယ်။ Pexels မှာ rate limit ရှိတဲ့အတွက် request အပိုတွေက တကယ် ကုန်ကျပါတယ်။ Day 31 မှာ infinite scroll လုပ်တဲ့အခါ ဒီ guard က ပိုတောင် အရေးကြီးလာပါလိမ့်မယ်။

### Dispose ပြီးမှ ပြန်လာတဲ့ response

`await _repository.curated()` က network ကို စောင့်နေတုန်း provider ကို ပိုင်ဆိုင်တဲ့ `ChangeNotifierProvider` ကို widget tree ကနေ ဖယ်လိုက်ရင် provider က `dispose()` ဖြစ်သွားပါပြီ။ (Pixly မှာတော့ provider ကို `MaterialApp` ရဲ့ အပေါ်မှာ ထားထားတဲ့အတွက် screen ပိတ်ရုံနဲ့ dispose မဖြစ်ပါဘူး။ Screen တခုအတွက်ပဲ provider ထားတဲ့ app တွေမှာတော့ screen ပိတ်တာနဲ့ dispose ဖြစ်ပါတယ်။) ပြီးမှ response ပြန်ရောက်လာပြီး `notifyListeners()` ကို ခေါ်ရင် debug mode မှာ "used after being disposed" ဆိုတဲ့ assertion error တက်ပါလိမ့်မယ်။

ဒါကြောင့် `_disposed` flag လေး ထားပြီး `dispose()` ကို override လုပ်ထားပါတယ်။ `await` ပြီးတဲ့နောက် provider dispose ဖြစ်သွားပြီဆိုရင် state ကို မပြင်တော့ဘဲ ချက်ချင်း `return` လုပ်လိုက်ပါတယ်။ `await` စောင့်နေတုန်း သက်တမ်း ကုန်သွားနိုင်တဲ့ object (disposed provider, unmounted widget စတာတွေ) ကို `await` ပြီးမှ ဆက်သုံးမယ်ဆိုရင် ဒီလို စစ်ဖို့ လိုတယ်ဆိုတာ မှတ်ထားသင့်ပါတယ်။

### Result ကို switch နဲ့ ကိုင်တွယ်ခြင်း

`switch (result)` ထဲမှာ `Success(:final data)` လို့ ရေးထားတာက Day 9 မှာ ပြောခဲ့တဲ့ object pattern ပါ။ `result` က `Success` ဖြစ်ခဲ့ရင် သူ့ရဲ့ `data` field ကို `data` ဆိုတဲ့ variable ထဲ ထုတ်ပေးပါတယ်။ `Result` က `sealed` class ဖြစ်တဲ့အတွက် `Failure` case ကို မေ့ပြီး မရေးမိရင် compiler က error ပြပေးပါတယ်။

## Provider ကို App ထဲ ထည့်ခြင်း

`PhotosProvider` ကို widget တွေက ယူသုံးနိုင်အောင် widget tree ရဲ့ အပေါ်ဆုံးမှာ ထားပေးရပါမယ်။

```dart
// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pixly/config/constants.dart';
import 'package:pixly/config/theme.dart';
import 'package:pixly/core/api/api_client.dart';
import 'package:pixly/providers/photos_provider.dart';
import 'package:pixly/repositories/photo_repository.dart';
import 'package:pixly/screens/home_screen.dart';
import 'package:provider/provider.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await dotenv.load(fileName: '.env');

  // Show the Poppins font license on Flutter's licenses page.
  LicenseRegistry.addLicense(() async* {
    final license = await rootBundle.loadString('assets/fonts/OFL.txt');
    yield LicenseEntryWithLineBreaks(['Poppins'], license);
  });

  runApp(
    MultiProvider(
      providers: [
        Provider(create: (_) => PhotoRepository(ApiClient())),
        ChangeNotifierProvider(
          create: (context) =>
              PhotosProvider(context.read<PhotoRepository>())..loadCurated(),
        ),
      ],
      child: const PixlyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: AppConstants.appName,
      debugShowCheckedModeBanner: false,
      theme: AppTheme.lightTheme,
      darkTheme: AppTheme.darkTheme,
      themeMode: ThemeMode.system,
      home: const HomeScreen(),
    );
  }
}
```

- **`MultiProvider`** \- provider တွေ အများကြီးကို တပြိုင်နက် ထည့်ချင်တဲ့အခါ အထပ်ထပ် nest မလုပ်ရအောင် list အနေနဲ့ ထည့်ခွင့်ပေးပါတယ်။
- `**Provider**` \- ပြောင်းလဲမှု မရှိတဲ့ object (ဒီမှာ `PhotoRepository`) ကို ပေးဖို့ပါ။ `PhotoRepository` ကို app တခုလုံးမှာ တခုတည်း ဆောက်ပြီး မျှသုံးသွားမှာ ဖြစ်ပါတယ်။
- `**ChangeNotifierProvider**` \- `ChangeNotifier` ကို ပေးဖို့ပါ။ `notifyListeners()` ခေါ်တိုင်း သူ့ကို နားထောင်နေတဲ့ widget တွေကို rebuild လုပ်ပေးပြီး ဒီ provider ကို widget tree ကနေ ဖယ်လိုက်တဲ့အခါ `dispose()` ကိုလဲ အလိုအလျောက် ခေါ်ပေးပါတယ်။
- `**context.read<PhotoRepository>()**` \- provider ၂ ခုကြားမှာ `PhotosProvider` က `PhotoRepository` ကို လိုတဲ့အတွက် အပေါ်က provider ကို `read` နဲ့ ယူထားပါတယ်။ ဒီလိုလုပ်ဖို့ `PhotoRepository` provider ကို list ထဲမှာ အရင် ထည့်ထားရပါမယ်။
- `**..loadCurated()**` \- Dart ရဲ့ cascade operator (`..`) ပါ။ object ကို ဆောက်ပြီး method ကို ခေါ်ပြီးမှ object ကိုယ်တိုင်ကို ပြန်ပေးပါတယ်။ ဒီနေရာမှာ `loadCurated()` က `Future` ကို ပြန်ပေးပေမယ့် cascade က အဲ့ဒီ Future ကို **await မလုပ်ပါဘူး**။ ဒါကြောင့် provider ကို ချက်ချင်း ပြန်ပေးပြီး loading က နောက်ကွယ်မှာ ဆက်လုပ်နေမှာပါ။ UI က အရင်ဆုံး loading state ကို မြင်ရပြီး data ရောက်တဲ့အခါ `notifyListeners()` ကနေ rebuild ဖြစ်သွားပါတယ်။
- **Lazy creation** \- `Provider` နဲ့ `ChangeNotifierProvider` ရဲ့ `create` က default အနေနဲ့ lazy ဖြစ်ပါတယ်။ App စဖွင့်တာနဲ့ ဆောက်တာ မဟုတ်ဘဲ widget တခုခုက ပထမဆုံး အကြိမ် `watch`/`read` လုပ်မှ ဆောက်ပါတယ်။ Pixly မှာတော့ `HomeScreen` က ချက်ချင်း `watch` လုပ်တဲ့အတွက် app ပွင့်ပြီး ချက်ချင်းနီးပါး loading စသွားမှာ ဖြစ်ပါတယ်။

## watch, read, select

Widget တွေကနေ provider ကို ယူသုံးဖို့ `BuildContext` extension method ၃ ခု ရှိပါတယ်။

| Method                    | ဘယ်အချိန် သုံးမလဲ                             | Rebuild ဖြစ်လား                      |
| ------------------------- | --------------------------------------------- | ------------------------------------ |
| context.watch<T>()        | build ထဲမှာ data ကို ပြဖို့                   | notifyListeners() ခေါ်တိုင်း rebuild |
| context.read<T>()         | Button နှိပ်တာလို event ထဲမှာ method ခေါ်ဖို့ | Rebuild မဖြစ်                        |
| context.select<T, R>(...) | Data ရဲ့ အစိတ်အပိုင်းတခုပဲ လိုတဲ့အခါ          | ရွေးထားတဲ့ value ပြောင်းမှပဲ rebuild |

```dart
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pixly/config/assets.dart';
import 'package:pixly/config/constants.dart';
import 'package:pixly/providers/photos_provider.dart';
import 'package:pixly/utils/color_utils.dart';
import 'package:pixly/widgets/empty_state.dart';
import 'package:provider/provider.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          spacing: 12,
          children: [
            SvgPicture.asset(AppAssets.logo, width: 32, height: 32),
            const Text(AppConstants.appName),
          ],
        ),
      ),
      body: const _PhotosBody(),
    );
  }
}

class _PhotosBody extends StatelessWidget {
  const _PhotosBody();

  @override
  Widget build(BuildContext context) {
    final provider = context.watch<PhotosProvider>();
    final photos = provider.photos;

    if (provider.isLoading && photos.isEmpty) {
      return const Center(child: CircularProgressIndicator());
    }

    if (provider.error != null && photos.isEmpty) {
      return EmptyState(
        title: 'Something went wrong',
        message: provider.error!,
        action: FilledButton.icon(
          onPressed: () => context.read<PhotosProvider>().loadCurated(),
          icon: const Icon(Icons.refresh),
          label: const Text('Try again'),
        ),
      );
    }

    if (photos.isEmpty) {
      return const EmptyState(
        title: 'No photos yet',
        message: 'Beautiful photos from Pexels will show up here soon.',
      );
    }

    return ListView.builder(
      itemCount: photos.length,
      itemBuilder: (context, index) {
        final photo = photos[index];
        return ListTile(
          leading: CircleAvatar(backgroundColor: colorFromHex(photo.avgColor)),
          title: Text(
            photo.alt.isEmpty ? 'Untitled' : photo.alt,
            maxLines: 1,
            overflow: .ellipsis,
          ),
          subtitle: Text('by ${photo.photographer}'),
        );
      },
    );
  }
}
```

- `build` ထဲမှာ `context.watch<PhotosProvider>()` ကို သုံးထားတဲ့အတွက် `PhotosProvider` က `notifyListeners()` ခေါ်တိုင်း ဒီ widget က အသစ် build ဖြစ်ပြီး loading → list ပြောင်းသွားမှာပါ။
- "Try again" button ရဲ့ `onPressed` ထဲမှာတော့ `context.read` ကို သုံးထားပါတယ်။ Button နှိပ်တဲ့အချိန်မှာ method ခေါ်ရုံပဲ လိုပြီး rebuild နဲ့ မဆိုင်လို့ပါ။ `onPressed` လို callback ထဲမှာ `watch` ကို သုံးရင် provider package က debug mode မှာ assertion error ပြပါလိမ့်မယ်။
- `HomeScreen` တခုလုံးမှာ `watch` မလုပ်ဘဲ body အပိုင်းကိုပဲ `_PhotosBody` ဆိုပြီး widget သီးသန့် ခွဲထားပါတယ်။ ဒါမှ data ပြောင်းတဲ့အခါ AppBar က rebuild မဖြစ်တော့ဘဲ body ပဲ rebuild ဖြစ်မှာ ဖြစ်ပါတယ်။

`select` ကို ဥပမာ ပြရရင် AppBar မှာ ပုံ အရေအတွက်ပဲ ပြချင်တယ်ဆိုပါစို့။ `watch` လိုပဲ `select` ကိုလဲ widget ရဲ့ `build` method ထဲမှာ ခေါ်ရပါတယ်။

```dart
class PhotoCountTitle extends StatelessWidget {
  const PhotoCountTitle({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select<PhotosProvider, int>((p) => p.photos.length);
    return Text('$count photos');
  }
}
```

ဒီလိုဆိုရင် `PhotosProvider` ရဲ့ loading state ပြောင်းတာ၊ error ပြောင်းတာတွေကြောင့် rebuild မဖြစ်ဘဲ ပုံ အရေအတွက် ပြောင်းမှပဲ rebuild ဖြစ်မှာပါ။ (Theme ပြောင်းတာ၊ parent rebuild ဖြစ်တာ စတဲ့ provider နဲ့ မဆိုင်တဲ့ အကြောင်းတွေကြောင့်တော့ rebuild ဖြစ်နိုင်ပါသေးတယ်။)

`avgColor` ကိုတော့ `CircleAvatar` ရဲ့ background အဖြစ် ပြဖို့ hex string ကို `Color` ပြောင်းပေးတဲ့ function လေး တခု ရေးထားပါတယ်။

```dart
// lib/utils/color_utils.dart
import 'package:flutter/painting.dart';

/// Turns a hex string like "#7A8C6E" into a [Color]. Falls back to grey.
Color colorFromHex(String hex) {
  final value = int.tryParse(hex.replaceFirst('#', ''), radix: 16);
  if (value == null || hex.replaceFirst('#', '').length != 6) {
    return const Color(0xFF9E9E9E);
  }
  return Color(0xFF000000 | value);
}
```

`#7A8C6E` လို hex မှာ alpha (opacity) မပါတဲ့အတွက် `0xFF000000` နဲ့ OR (`|`) လုပ်ပြီး အပြည့် မြင်ရအောင် alpha ကို `FF` ထည့်ပေးတာ ဖြစ်ပါတယ်။

## ရလဒ်

![Loading state](https://arkar.dev/content/images/2026/10/100dof-day29-day29_loading.png)

Loading state

![Photos loaded](https://arkar.dev/content/images/2026/10/100dof-day29-day29_list.png)

Photos loaded

![Error state with a retry button](https://arkar.dev/content/images/2026/10/100dof-day29-day29_error.png)

Error state with a retry button

(Screenshot တွေမှာ ပြထားတဲ့ ဓာတ်ပုံ အချက်အလက်တွေက Pexels response ပုံစံအတိုင်း ပြင်ဆင်ထားတဲ့ sample data ဖြစ်ပါတယ်။ ကိုယ့် API key နဲ့ run ရင်တော့ Pexels က curated photo အစစ်တွေကို မြင်ရမှာပါ။)

ပုံတွေ ကိုယ်တိုင်ကိုတော့ မပြရသေးပါဘူး။ မနက်ဖြန် Day 30 မှာ ဒီ list ကို Day 24 က GridView နဲ့ ပြောင်းပြီး ဓာတ်ပုံတွေကို cache လုပ်ပြီး ပြပေးမှာ ဖြစ်ပြီး loading state ကိုလဲ shimmer effect နဲ့ ပိုလှအောင် လုပ်သွားမှာ ဖြစ်ပါတယ်။

**Source code:** ဒီနေ့ရဲ့ code အပြည့်အစုံကို [GitHub](https://github.com/arkarmintun1/100-days-of-flutter/tree/main/day29/pixly?ref=arkar.dev) မှာ run ကြည့်လို့ ရပါတယ်။

---

ဒီနေ့ Day 29 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။

- Youtube: [https://www.youtube.com/@arkarmintun](https://www.youtube.com/@arkarmintun?ref=arkar.dev)
- Newsletter: <https://arkar.dev/>
- Discord: [https://discord.gg/3xUJ6k6dkH](https://discord.gg/3xUJ6k6dkH?ref=arkar.dev)