Flutter

Riverpod untuk Pemula

Panduan belajar Riverpod di Flutter: ProviderScope, ConsumerWidget, WidgetRef, StateProvider, NotifierProvider, FutureProvider, AsyncValue, dan contoh Task Manager.

9 dari 57 materi State Management flutterriverpodstate-managementasyncvalue

Riverpod untuk Pemula

Riverpod adalah state management modern untuk Flutter/Dart. Secara konsep, Riverpod mirip dengan Provider karena sama-sama membantu membagikan state ke widget. Bedanya, Riverpod tidak bergantung langsung pada BuildContext, lebih mudah dites, dan punya dukungan yang rapi untuk state async seperti loading, data, dan error.

Materi ini cocok dibaca setelah:

  • setState
  • ValueNotifier
  • Provider
  • CRUD lokal
  • Task Manager sederhana

Target materi:

  • Memahami kenapa Riverpod dipakai.
  • Memasang flutter_riverpod.
  • Memahami ProviderScope.
  • Membaca provider dengan ref.watch.
  • Memanggil action dengan ref.read.
  • Membuat state sederhana dengan StateProvider.
  • Membuat logic state dengan NotifierProvider.
  • Membaca data async dengan FutureProvider.
  • Memahami AsyncValue.

Cara belajar materi ini:

Pahami Provider dulu
-> pahami bahwa Riverpod membaca state lewat ref
-> mulai dari StateProvider
-> lanjut ke Provider untuk data turunan
-> naik ke NotifierProvider untuk logic
-> gunakan FutureProvider untuk data async

Riverpod bisa terasa membingungkan jika langsung masuk ke banyak jenis provider. Jadi, jangan hafalkan semuanya di awal. Pahami peran masing-masing:

JenisKapan Dipakai
StateProviderstate kecil seperti counter, filter, search query
Providerdata turunan atau computed state
NotifierProviderstate yang punya banyak action/logic
FutureProviderdata async dari API, storage, atau Firebase
AsyncValuemembedakan loading, error, dan data

Prinsip utama Riverpod:

Widget membaca state dengan ref.watch.
Widget memanggil aksi dengan ref.read.
Logic state disimpan di provider/notifier.

Perbedaan besar dari Provider biasa:

Provider package -> context.watch, context.read
Riverpod -> ref.watch, ref.read

Hal yang harus dipahami setelah materi ini:

  • kenapa ProviderScope wajib ada
  • apa itu WidgetRef
  • kenapa ref.watch membuat UI rebuild
  • kenapa action sebaiknya memakai ref.read
  • kapan StateProvider cukup
  • kapan harus pindah ke NotifierProvider
  • kenapa AsyncValue sangat membantu untuk API/Firebase

1. Kenapa Belajar Riverpod

Provider bagus untuk belajar state management pertama. Namun saat project semakin besar, biasanya muncul kebutuhan seperti:

  • state lebih mudah dites
  • tidak bergantung pada BuildContext
  • async state lebih rapi
  • dependency antar state lebih jelas
  • logic aplikasi bisa dipisahkan lebih bersih

Riverpod membantu kebutuhan itu.

Perbandingan sederhana:

ProviderRiverpod
Membaca state lewat contextMembaca state lewat ref
Butuh BuildContextTidak bergantung pada BuildContext
Umum memakai ChangeNotifierBisa memakai StateProvider, NotifierProvider, FutureProvider, dan lainnya
Async state sering dibuat manualAsync state didukung lewat AsyncValue

Riverpod bukan wajib untuk semua aplikasi. Tapi jika ingin naik level dari Provider, Riverpod adalah pilihan yang bagus.


2. Install Riverpod

Tambahkan package:

flutter pub add flutter_riverpod

Lalu jalankan:

flutter pub get

Import:

import 'package:flutter_riverpod/flutter_riverpod.dart';

3. ProviderScope

Riverpod membutuhkan ProviderScope di root aplikasi.

Contoh main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'pages/home_page.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

Penjelasan:

  • ProviderScope menyimpan container Riverpod.
  • Semua provider hidup di dalam scope ini.
  • Biasanya ProviderScope diletakkan paling atas di runApp.

Jika lupa menambahkan ProviderScope, provider tidak bisa dibaca oleh widget.


4. ConsumerWidget dan WidgetRef

Di Riverpod, widget yang membaca provider biasanya memakai ConsumerWidget.

Contoh:

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return const Scaffold(
      body: Center(
        child: Text('Hello Riverpod'),
      ),
    );
  }
}

Perhatikan perbedaannya:

Widget build(BuildContext context, WidgetRef ref)

ref dipakai untuk:

  • membaca provider
  • memanggil notifier
  • mendengarkan perubahan state

Aturan sederhana:

ref.watch -> membaca state dan rebuild saat berubah
ref.read -> membaca sekali atau memanggil action

5. StateProvider untuk State Sederhana

StateProvider cocok untuk state sederhana seperti:

  • counter
  • selected tab
  • selected filter
  • toggle dark mode sederhana
  • search query

Contoh counter:

final counterProvider = StateProvider<int>((ref) {
  return 0;
});

Membaca state:

final counter = ref.watch(counterProvider);

Mengubah state:

ref.read(counterProvider.notifier).state++;

Contoh halaman lengkap:

final counterProvider = StateProvider<int>((ref) => 0);

class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Counter'),
      ),
      body: Center(
        child: Text(
          '$counter',
          style: Theme.of(context).textTheme.displayLarge,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Penjelasan:

  • ref.watch(counterProvider) membuat UI rebuild saat counter berubah.
  • ref.read(counterProvider.notifier).state++ mengubah nilai counter.

Gunakan StateProvider untuk state kecil. Jika logic mulai banyak, pindah ke NotifierProvider.


6. StateProvider untuk Filter

Contoh filter task:

enum TaskStatus {
  todo,
  progress,
  done,
}

final selectedStatusProvider = StateProvider<TaskStatus?>((ref) {
  return null;
});

Membaca filter:

final selectedStatus = ref.watch(selectedStatusProvider);

Mengubah filter:

ref.read(selectedStatusProvider.notifier).state = TaskStatus.done;

Menghapus filter:

ref.read(selectedStatusProvider.notifier).state = null;

Contoh UI:

ChoiceChip(
  label: const Text('Semua'),
  selected: selectedStatus == null,
  onSelected: (_) {
    ref.read(selectedStatusProvider.notifier).state = null;
  },
)

Ini cocok karena selected filter adalah state sederhana.


7. Provider untuk Computed State

Riverpod punya Provider untuk data turunan atau computed state.

Misalnya ada list task dan filter status.

final tasksProvider = StateProvider<List<Task>>((ref) {
  return [];
});

final selectedStatusProvider = StateProvider<TaskStatus?>((ref) {
  return null;
});

final filteredTasksProvider = Provider<List<Task>>((ref) {
  final tasks = ref.watch(tasksProvider);
  final selectedStatus = ref.watch(selectedStatusProvider);

  if (selectedStatus == null) {
    return tasks;
  }

  return tasks.where((task) => task.status == selectedStatus).toList();
});

Penjelasan:

  • tasksProvider menyimpan data utama.
  • selectedStatusProvider menyimpan filter.
  • filteredTasksProvider menghitung hasil akhir.

Kelebihan pola ini:

  • logic filter tidak ditulis di widget
  • widget cukup membaca filteredTasksProvider
  • jika task atau filter berubah, hasil ikut update

8. NotifierProvider untuk Logic Lebih Rapi

Jika state punya banyak action, gunakan NotifierProvider.

Contoh:

class CounterNotifier extends Notifier<int> {
  @override
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }

  void decrement() {
    if (state == 0) return;
    state--;
  }

  void reset() {
    state = 0;
  }
}

final counterNotifierProvider =
    NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

Membaca state:

final counter = ref.watch(counterNotifierProvider);

Memanggil action:

ref.read(counterNotifierProvider.notifier).increment();

Contoh UI:

class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('NotifierProvider Counter'),
      ),
      body: Center(
        child: Text('$counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterNotifierProvider.notifier).increment();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Kapan memakai NotifierProvider?

  • saat state punya banyak function
  • saat logic tidak cocok ditaruh di widget
  • saat ingin state lebih mudah dites
  • saat membuat fitur seperti task manager, cart, atau auth sederhana

9. Task Manager dengan NotifierProvider

Model:

enum TaskStatus {
  todo,
  progress,
  done,
}

class Task {
  final int id;
  final String title;
  final String description;
  final TaskStatus status;

  const Task({
    required this.id,
    required this.title,
    required this.description,
    required this.status,
  });

  Task copyWith({
    String? title,
    String? description,
    TaskStatus? status,
  }) {
    return Task(
      id: id,
      title: title ?? this.title,
      description: description ?? this.description,
      status: status ?? this.status,
    );
  }
}

Notifier:

class TaskNotifier extends Notifier<List<Task>> {
  @override
  List<Task> build() {
    return [];
  }

  void addTask({
    required String title,
    required String description,
  }) {
    final task = Task(
      id: DateTime.now().millisecondsSinceEpoch,
      title: title,
      description: description,
      status: TaskStatus.todo,
    );

    state = [...state, task];
  }

  void updateTask(Task updatedTask) {
    state = [
      for (final task in state)
        if (task.id == updatedTask.id) updatedTask else task,
    ];
  }

  void updateStatus(int id, TaskStatus status) {
    state = [
      for (final task in state)
        if (task.id == id) task.copyWith(status: status) else task,
    ];
  }

  void deleteTask(int id) {
    state = state.where((task) => task.id != id).toList();
  }
}

final taskNotifierProvider =
    NotifierProvider<TaskNotifier, List<Task>>(TaskNotifier.new);

Penjelasan:

  • state adalah list task saat ini.
  • Saat menambah data, kita membuat list baru dengan state = [...state, task].
  • Saat update, kita membuat list baru menggunakan for.
  • Saat delete, kita membuat list baru dengan .where().

Riverpod lebih cocok dengan pola immutable seperti ini.


10. Filter dan Search dengan Riverpod

Provider filter:

final selectedStatusProvider = StateProvider<TaskStatus?>((ref) => null);
final searchQueryProvider = StateProvider<String>((ref) => '');

Provider hasil akhir:

final visibleTasksProvider = Provider<List<Task>>((ref) {
  final tasks = ref.watch(taskNotifierProvider);
  final selectedStatus = ref.watch(selectedStatusProvider);
  final searchQuery = ref.watch(searchQueryProvider).toLowerCase();

  final filteredByStatus = selectedStatus == null
      ? tasks
      : tasks.where((task) => task.status == selectedStatus).toList();

  if (searchQuery.isEmpty) {
    return filteredByStatus;
  }

  return filteredByStatus.where((task) {
    final title = task.title.toLowerCase();
    final description = task.description.toLowerCase();

    return title.contains(searchQuery) || description.contains(searchQuery);
  }).toList();
});

Widget cukup membaca:

final visibleTasks = ref.watch(visibleTasksProvider);

Keuntungannya:

  • UI tidak perlu tahu cara filter dan search dihitung.
  • Jika task, filter, atau search berubah, visibleTasksProvider otomatis menghitung ulang.

11. FutureProvider untuk Data Async

FutureProvider cocok untuk data async seperti:

  • request API
  • membaca file
  • mengambil data remote
  • membaca data awal dari storage

Contoh:

final postsProvider = FutureProvider<List<String>>((ref) async {
  await Future.delayed(const Duration(seconds: 1));

  return [
    'Belajar Flutter',
    'Belajar Riverpod',
    'Belajar Firebase',
  ];
});

Membaca di UI:

class PostsPage extends ConsumerWidget {
  const PostsPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final postsAsync = ref.watch(postsProvider);

    return postsAsync.when(
      loading: () {
        return const Center(
          child: CircularProgressIndicator(),
        );
      },
      error: (error, stackTrace) {
        return Center(
          child: Text('Error: $error'),
        );
      },
      data: (posts) {
        return ListView.builder(
          itemCount: posts.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(posts[index]),
            );
          },
        );
      },
    );
  }
}

Penjelasan:

  • postsAsync bertipe AsyncValue<List<String>>.
  • .when() memisahkan UI loading, error, dan data.

Ini membuat async state lebih rapi daripada membuat banyak variable manual seperti isLoading, errorMessage, dan data.


12. Memahami AsyncValue

AsyncValue merepresentasikan tiga kondisi:

loading
error
data

Contoh:

final userAsync = ref.watch(userProvider);

Lalu:

userAsync.when(
  loading: () => const CircularProgressIndicator(),
  error: (error, stackTrace) => Text('Error: $error'),
  data: (user) => Text(user.name),
)

Kelebihan AsyncValue:

  • UI loading lebih jelas.
  • Error lebih mudah ditangani.
  • Data sukses punya jalur sendiri.
  • Cocok untuk API dan Firebase.

Nanti saat belajar Firebase atau API CRUD, AsyncValue akan sangat berguna.


13. ref.watch vs ref.read

Aturan sederhana:

ref.watch -> untuk UI yang harus rebuild saat state berubah
ref.read -> untuk action seperti tombol, submit form, delete data

Contoh watch:

final tasks = ref.watch(visibleTasksProvider);

Contoh read:

ref.read(taskNotifierProvider.notifier).deleteTask(task.id);

Kesalahan umum:

final notifier = ref.watch(taskNotifierProvider.notifier);

Untuk memanggil function, gunakan read:

final notifier = ref.read(taskNotifierProvider.notifier);

14. ConsumerStatefulWidget

Jika butuh lifecycle seperti initState, gunakan ConsumerStatefulWidget.

Contoh:

class SearchPage extends ConsumerStatefulWidget {
  const SearchPage({super.key});

  @override
  ConsumerState<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends ConsumerState<SearchPage> {
  final controller = TextEditingController();

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

  @override
  Widget build(BuildContext context) {
    final query = ref.watch(searchQueryProvider);

    return TextField(
      controller: controller,
      decoration: InputDecoration(
        labelText: 'Cari: $query',
      ),
      onChanged: (value) {
        ref.read(searchQueryProvider.notifier).state = value;
      },
    );
  }
}

Gunakan ConsumerStatefulWidget saat:

  • butuh TextEditingController
  • butuh initState
  • butuh dispose
  • butuh animation controller

Jika tidak butuh lifecycle, gunakan ConsumerWidget.


15. Kesalahan Umum Riverpod

Lupa ProviderScope

Jika tidak ada ProviderScope, provider tidak bisa dipakai.

Solusi:

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

Memakai read untuk UI yang harus update

Kurang tepat:

final count = ref.read(counterProvider);

Jika UI harus update, gunakan:

final count = ref.watch(counterProvider);

Logic terlalu banyak di widget

Jika widget masih menyimpan semua logic, manfaat Riverpod jadi kecil.

Lebih baik:

ref.read(taskNotifierProvider.notifier).addTask(
  title: title,
  description: description,
);

State mutable langsung diubah

Kurang aman:

state.add(task);

Lebih baik:

state = [...state, task];

Tujuannya agar Riverpod tahu state berubah dan UI bisa update dengan benar.


16. Kapan Memakai Riverpod

Riverpod cocok untuk:

  • aplikasi menengah sampai besar
  • state yang dipakai banyak halaman
  • API dan Firebase
  • auth state
  • cart, task, profile, dan settings
  • project yang butuh testing lebih rapi

Untuk pemula:

setState -> Provider -> Riverpod

Jangan terburu-buru memakai semua fitur Riverpod. Mulai dari:

  1. ProviderScope
  2. ConsumerWidget
  3. StateProvider
  4. NotifierProvider
  5. FutureProvider
  6. AsyncValue

Latihan Riverpod

Kerjakan latihan ini bertahap. Jangan langsung lompat ke NotifierProvider kalau StateProvider belum terasa nyaman.

Latihan 1: Counter dengan StateProvider

Buat counter dengan:

  • StateProvider<int>
  • tombol tambah
  • tombol kurang
  • tombol reset

Pertanyaan untuk dicek:

  • Apakah app sudah dibungkus ProviderScope?
  • Apakah angka dibaca dengan ref.watch?
  • Apakah tombol mengubah state dengan ref.read(...notifier)?

Latihan 2: Filter Task dengan StateProvider

Buat:

  • selectedStatusProvider
  • searchQueryProvider
  • visibleTasksProvider

Target:

  • filter status tidak mengubah list asli
  • search tidak mengubah list asli
  • UI cukup membaca visibleTasksProvider

Latihan 3: TaskNotifier

Pindahkan CRUD task ke NotifierProvider.

Target:

  • addTask
  • updateTask
  • updateStatus
  • deleteTask

Pertanyaan untuk dicek:

  • Apakah list diperbarui dengan membuat list baru?
  • Apakah UI memanggil action dengan ref.read?
  • Apakah UI membaca data dengan ref.watch?

Latihan 4: FutureProvider

Buat simulasi data async:

final sampleDataProvider = FutureProvider<List<String>>((ref) async {
  await Future.delayed(const Duration(seconds: 1));
  return ['Dart', 'Flutter', 'Riverpod'];
});

Tampilkan dengan AsyncValue.when:

  • loading
  • error
  • data

17. Checklist Riverpod

Pastikan sudah paham:

  • Riverpod membaca state dengan ref, bukan context.
  • ProviderScope wajib dipasang di root aplikasi.
  • ConsumerWidget dipakai untuk membaca provider.
  • ref.watch dipakai agar UI rebuild.
  • ref.read dipakai untuk action.
  • StateProvider cocok untuk state sederhana.
  • Provider cocok untuk computed state.
  • NotifierProvider cocok untuk state dengan banyak logic.
  • FutureProvider cocok untuk data async.
  • AsyncValue memisahkan loading, error, dan data.
  • State list sebaiknya diubah dengan membuat list baru.

Jika checklist ini sudah aman, lanjutkan ke Upgrade Project Task Manager dengan Riverpod atau masuk ke Cubit/Bloc Dasar.


Referensi Resmi