Riverpod #

Riverpod adalah evolusi dari Provider yang mengatasi keterbatasan fundamental-nya: tidak bergantung pada BuildContext, compile-time safe, mendukung multiple instance provider, dan memiliki penanganan state async yang jauh lebih elegan. Riverpod kini menjadi salah satu pilihan state management paling direkomendasikan untuk aplikasi Flutter produksi.

Instalasi #

# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1   # opsional, untuk code generation

dev_dependencies:
  build_runner: ^2.4.13
  riverpod_generator: ^2.6.3    # opsional, untuk code generation

Setup ProviderScope #

void main() {
  runApp(
    // ProviderScope WAJIB -- membungkus seluruh aplikasi
    // menyimpan state semua provider
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

Jenis-jenis Provider #

Provider — Nilai yang Tidak Berubah #

Untuk dependency atau service yang tidak berubah sepanjang lifetime aplikasi:

// Provider sederhana
final apiServiceProvider = Provider<ApiService>((ref) {
  return ApiService(baseUrl: 'https://api.contoh.com');
});

// Provider yang bergantung pada provider lain
final produkRepositoryProvider = Provider<ProdukRepository>((ref) {
  final api = ref.watch(apiServiceProvider);
  return ProdukRepository(api: api);
});

// Akses di widget
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final repo = ref.watch(produkRepositoryProvider);
    return Text(repo.toString());
  }
}

Notifier — State yang Bisa Diubah #

Notifier menggantikan StateNotifier lama. Ini adalah cara modern untuk membuat provider dengan state yang bisa diubah melalui method:

// State class
@immutable
class KeranjangState {
  final List<CartItem> items;
  const KeranjangState({this.items = const []});

  double get total => items.fold(0, (sum, item) => sum + item.subtotal);
  int get jumlah => items.fold(0, (sum, item) => sum + item.jumlah);

  KeranjangState copyWith({List<CartItem>? items}) =>
      KeranjangState(items: items ?? this.items);
}

// Notifier
class KeranjangNotifier extends Notifier<KeranjangState> {
  @override
  KeranjangState build() => const KeranjangState();  // initial state

  void tambah(Produk produk) {
    final items = [...state.items];
    final idx = items.indexWhere((i) => i.produk.id == produk.id);
    if (idx >= 0) {
      items[idx] = items[idx].copyWith(jumlah: items[idx].jumlah + 1);
    } else {
      items.add(CartItem(produk: produk, jumlah: 1));
    }
    state = state.copyWith(items: items);
  }

  void hapus(String produkId) {
    state = state.copyWith(
      items: state.items.where((i) => i.produk.id != produkId).toList(),
    );
  }

  void kosongkan() => state = const KeranjangState();
}

// Provider-nya
final keranjangProvider = NotifierProvider<KeranjangNotifier, KeranjangState>(
  KeranjangNotifier.new,
);

AsyncNotifier — State Async #

AsyncNotifier adalah Notifier untuk state yang membutuhkan operasi async. Ia mengembalikan AsyncValue<T> yang merepresentasikan loading, data, atau error:

class ProdukNotifier extends AsyncNotifier<List<Produk>> {
  @override
  Future<List<Produk>> build() async {
    // Dipanggil sekali saat provider pertama kali dibaca
    // ref.watch di sini membuat provider re-build saat dependency berubah
    final repo = ref.watch(produkRepositoryProvider);
    return repo.getAll();
  }

  Future<void> refresh() async {
    // Invalidate dan reload
    ref.invalidateSelf();
    await future;  // tunggu selesai
  }

  Future<void> tambahProduk(Produk produk) async {
    final repo = ref.read(produkRepositoryProvider);
    // Optimistic update: langsung update UI, kemudian sync ke server
    state = AsyncData([...state.valueOrNull ?? [], produk]);
    try {
      await repo.tambah(produk);
    } catch (e) {
      // Rollback jika gagal
      state = AsyncError(e, StackTrace.current);
      ref.invalidateSelf();
    }
  }
}

final produkProvider = AsyncNotifierProvider<ProdukNotifier, List<Produk>>(
  ProdukNotifier.new,
);

FutureProvider — Data Async Sederhana #

Untuk data async yang tidak perlu method tambahan:

// FutureProvider sederhana -- load data sekali
final profilProvider = FutureProvider<Profil>((ref) async {
  final api = ref.watch(apiServiceProvider);
  return api.getProfil();
});

// Dengan autoDispose -- provider dihapus saat tidak ada listener
final detailProdukProvider = FutureProvider.autoDispose.family<Produk, String>(
  (ref, id) async {
    final repo = ref.watch(produkRepositoryProvider);
    return repo.getById(id);
  },
);

StreamProvider — Data Real-time #

// StreamProvider untuk data real-time (Firebase, WebSocket)
final chatProvider = StreamProvider.autoDispose.family<List<Pesan>, String>(
  (ref, roomId) {
    final service = ref.watch(chatServiceProvider);
    return service.streamPesan(roomId);
  },
);

ref.watch, ref.read, ref.listen #

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch -- subscribe, rebuild saat berubah
    // GUNAKAN DI BUILD METHOD
    final keranjang = ref.watch(keranjangProvider);
    final produk = ref.watch(produkProvider);

    return Column(
      children: [
        Text('${keranjang.jumlah} item'),
        ElevatedButton(
          onPressed: () {
            // ref.read -- baca tanpa subscribe
            // GUNAKAN DI CALLBACK / EVENT HANDLER
            ref.read(keranjangProvider.notifier).tambah(produk);
          },
          child: const Text('Tambah'),
        ),
      ],
    );
  }
}

// ref.listen -- untuk side effect (navigasi, snackbar)
// GUNAKAN DI BUILD METHOD TAPI UNTUK SIDE EFFECT, BUKAN REBUILD
class LoginWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<AuthState>(authProvider, (previous, next) {
      if (next.isLoggedIn && !(previous?.isLoggedIn ?? false)) {
        // Navigasi setelah login berhasil
        context.go('/home');
      }
      if (next.error != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(next.error!)),
        );
      }
    });

    return const LoginForm();
  }
}

AsyncValue — Menangani State Async dengan Elegan #

AsyncValue<T> adalah sealed class yang merepresentasikan tiga kemungkinan state dari operasi async:

// Di widget -- gunakan .when() untuk handle semua state
class ProdukList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final produkAsync = ref.watch(produkProvider);

    return produkAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(
        child: Column(
          children: [
            Text('Error: $error'),
            ElevatedButton(
              onPressed: () => ref.invalidate(produkProvider),
              child: const Text('Coba Lagi'),
            ),
          ],
        ),
      ),
      data: (produk) => ListView.builder(
        itemCount: produk.length,
        itemBuilder: (context, index) => ProdukTile(produk: produk[index]),
      ),
    );
  }
}

// Varian .when() yang berguna:
produkAsync.whenData((data) => data.length);         // hanya handle data
produkAsync.valueOrNull;                              // nilai atau null
produkAsync.hasValue;                                 // true jika ada data
produkAsync.isLoading;                               // true jika loading
produkAsync.hasError;                                // true jika error

// Tampilkan data lama saat refresh (tidak blank saat reload)
produkAsync.when(
  skipLoadingOnReload: true,   // jangan tampilkan loading saat refresh
  loading: () => const LinearProgressIndicator(),
  error: (e, _) => Text('Error: $e'),
  data: (data) => ProdukGrid(produk: data),
)

family — Provider dengan Parameter #

family memungkinkan membuat provider yang berbeda untuk setiap nilai parameter:

// Provider berbeda untuk setiap ID produk
final detailProdukProvider = FutureProvider.autoDispose
    .family<Produk, String>((ref, String id) async {
  final repo = ref.watch(produkRepositoryProvider);
  return repo.getById(id);
});

// Penggunaan -- Flutter membuat provider terpisah untuk setiap ID
final produkA = ref.watch(detailProdukProvider('produk-123'));
final produkB = ref.watch(detailProdukProvider('produk-456'));

// Dengan Notifier + family
class PesananNotifier extends FamilyAsyncNotifier<Pesanan, String> {
  @override
  Future<Pesanan> build(String pesananId) async {
    final repo = ref.watch(pesananRepositoryProvider);
    return repo.getById(pesananId);
  }

  Future<void> batalkan() async {
    final repo = ref.read(pesananRepositoryProvider);
    await repo.batalkan(arg);  // arg = pesananId yang diteruskan ke build
    ref.invalidateSelf();
  }
}

final pesananProvider = AsyncNotifierProvider.autoDispose
    .family<PesananNotifier, Pesanan, String>(PesananNotifier.new);

autoDispose — Provider yang Otomatis Dihapus #

// autoDispose: provider dihapus dari memory saat tidak ada lagi listener
// Sangat berguna untuk data yang hanya relevan di satu screen
final searchProvider = FutureProvider.autoDispose.family<List<Produk>, String>(
  (ref, query) async {
    // Debounce: batalkan request jika query berubah sebelum selesai
    ref.onDispose(() => print('Search provider untuk "$query" di-dispose'));

    final repo = ref.watch(produkRepositoryProvider);
    return repo.search(query);
  },
);

// Jika ingin provider tetap hidup meskipun tidak ada listener:
final cacheProvider = FutureProvider.autoDispose<Data>((ref) async {
  final link = ref.keepAlive();  // cegah dispose
  ref.onDispose(() => link.close());  // tutup keepAlive jika diperlukan
  return fetchData();
});

ConsumerWidget dan ConsumerStatefulWidget #

// ConsumerWidget -- pengganti StatelessWidget
class ProdukScreen extends ConsumerWidget {
  const ProdukScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref tersedia langsung di parameter build
    final produk = ref.watch(produkProvider);
    return ProdukList(produk: produk);
  }
}

// ConsumerStatefulWidget -- pengganti StatefulWidget
class SearchScreen extends ConsumerStatefulWidget {
  const SearchScreen({super.key});

  @override
  ConsumerState<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends ConsumerState<SearchScreen> {
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    // ref tersedia sebagai field -- tidak perlu di parameter
    final hasil = ref.watch(searchProvider(_controller.text));
    return Column(
      children: [
        TextField(controller: _controller, onChanged: (_) => setState(() {})),
        hasil.when(
          loading: () => const CircularProgressIndicator(),
          error: (e, _) => Text('Error: $e'),
          data: (data) => ProdukList(produk: data),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Struktur Folder yang Direkomendasikan #

lib/
  ├── main.dart
  ├── features/
  │   ├── produk/
  │   │   ├── data/
  │   │   │   └── produk_repository.dart
  │   │   ├── domain/
  │   │   │   └── produk.dart
  │   │   └── presentation/
  │   │       ├── produk_screen.dart
  │   │       └── produk_provider.dart   ← providers di sini
  │   └── keranjang/
  │       ├── data/
  │       ├── domain/
  │       └── presentation/
  │           ├── keranjang_screen.dart
  │           └── keranjang_provider.dart
  └── core/
      ├── network/
      │   └── api_service_provider.dart
      └── storage/
          └── storage_provider.dart

Ringkasan #

  • Riverpod mengatasi keterbatasan Provider — tidak bergantung BuildContext, compile-time safe, mendukung multiple instance, dan penanganan async yang elegan.
  • Selalu bungkus aplikasi dengan ProviderScope di main().
  • Gunakan Notifier untuk state yang bisa diubah dengan method, AsyncNotifier untuk state yang membutuhkan operasi async.
  • FutureProvider untuk data async sederhana, StreamProvider untuk data real-time.
  • Gunakan ref.watch() di build() untuk subscribe, ref.read() di callback untuk aksi tanpa subscribe, dan ref.listen() untuk side effect seperti navigasi atau snackbar.
  • AsyncValue.when() menangani loading, error, dan data dengan elegan dalam satu metode.
  • family membuat provider terpisah per parameter — gunakan untuk data per-ID seperti detail produk.
  • autoDispose otomatis menghapus provider dari memory saat tidak ada listener — gunakan untuk screen-specific data.
  • Gunakan ConsumerWidget sebagai pengganti StatelessWidget dan ConsumerStatefulWidget sebagai pengganti StatefulWidget.

← Sebelumnya: Provider   Berikutnya: Bloc & Cubit →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact