Terakhir diperbarui: 07 November 2024
Penulis: Habibie Ed Dien
Pada codelab ini, Anda akan mempelajari tentang dasar manajemen state di Flutter beserta contoh penggunaannya. Cara kerja, manfaat, dan cara mengelola state di multi screen.
Video berikut menjelaskan tentang apa itu state dan bagaimana manfaatnya. Silakan simak dan pahami!
Setelah menyelesaikan codelab ini Anda akan mampu untuk:
Berikut merupakan sumber daya yang diperlukan untuk menyelesaikan praktikum ini:
Seiring berkembangnya aplikasi, pengelolaan aliran data melalui aplikasi tersebut menjadi lebih kompleks dan penting. Komunitas Flutter telah merancang beberapa solusi untuk menangani pengelolaan state. Semua solusi ini memiliki satu aspek yang sama: pemisahan UI dan logika bisnis.
Sebelum mendalami bagaimana manajemen state itu bekerja, pada codelab ini Anda akan praktik menggunakan state ke tingkat yang lebih tinggi.
Dalam codelab ini, Anda akan membuat aplikasi pencatatan tugas. Dalam aplikasi ini, pengguna
akan dapat membuat daftar tugas yang berisi banyak tugas. Pengguna akan dapat menambah dan melengkapi tugasnya.
Beberapa topik pembahasan di codelab ini terdiri dari:
InheritedWidget
dan InheritedNotifier
Model dan View merupakan konsep yang sangat penting dalam arsitektur aplikasi flutter. Model adalah kelas yang menangani dengan data untuk suatu aplikasi, sedangkan View adalah kelas yang menampilkan data tersebut di layar.
Di Flutter, View dibuat dari widget
. Selanjutnya, model akan menjadi kelas dasar Dart itu yang tidak mewarisi apa pun dari Framework Flutter. Masing-masing kelas ini bertanggung jawab
untuk satu dan hanya satu pekerjaan. Model berkaitan dengan penanganan data untuk aplikasi Anda. View fokus untuk menampilkan UI di layar HP. Ketika Anda konsisten dengan konsep arsitektur model dan view, kode Anda akan menjadi lebih sederhana dan mudah dibaca.
Dalam codelab ini, kita akan membangun aplikasi master_plan dengan membuat kelas model dan view.
Selesaikan langkah-langkah praktikum berikut ini menggunakan editor Visual Studio Code (VS Code) atau Android Studio atau code editor lain kesukaan Anda.
Buatlah sebuah project flutter baru dengan nama master_plan di folder src week-10 repository GitHub Anda atau sesuai style laporan praktikum yang telah disepakati. Lalu buatlah susunan folder dalam project seperti gambar berikut ini.
task.dart
Praktik terbaik untuk memulai adalah pada lapisan data (data layer). Ini akan memberi Anda gambaran yang jelas tentang aplikasi Anda, tanpa masuk ke detail antarmuka pengguna Anda. Di folder model, buat file bernama task.dart
dan buat class Task
. Class ini memiliki atribut description
dengan tipe data String dan complete
dengan tipe data Boolean, serta ada konstruktor. Kelas ini akan menyimpan data tugas untuk aplikasi kita. Tambahkan kode berikut:
class Task {
final String description;
final bool complete;
const Task({
this.complete = false,
this.description = '',
});
}
plan.dart
Kita juga perlu sebuah List untuk menyimpan daftar rencana dalam aplikasi to-do ini. Buat file plan.dart
di dalam folder models dan isi kode seperti berikut.
import './task.dart';
class Plan {
final String name;
final List<Task> tasks;
const Plan({this.name = '', this.tasks = const []});
}
data_layer.dart
Kita dapat membungkus beberapa data layer ke dalam sebuah file yang nanti akan mengekspor kedua model tersebut. Dengan begitu, proses impor akan lebih ringkas seiring berkembangnya aplikasi. Buat file bernama data_layer.dart
di folder models. Kodenya hanya berisi export
seperti berikut.
export 'plan.dart';
export 'task.dart';
main.dart
Ubah isi kode main.dart
sebagai berikut.
import 'package:flutter/material.dart';
import './views/plan_screen.dart';
void main() => runApp(MasterPlanApp());
class MasterPlanApp extends StatelessWidget {
const MasterPlanApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.purple),
home: PlanScreen(),
);
}
}
plan_screen.dart
Pada folder views
, buatlah sebuah file plan_screen.dart
dan gunakan templat StatefulWidget
untuk membuat class PlanScreen
. Isi kodenya adalah sebagai berikut. Gantilah teks ‘Namaku' dengan nama panggilan Anda pada title AppBar
.
import '../models/data_layer.dart';
import 'package:flutter/material.dart';
class PlanScreen extends StatefulWidget {
const PlanScreen({super.key});
@override
State createState() => _PlanScreenState();
}
class _PlanScreenState extends State<PlanScreen> {
Plan plan = const Plan();
@override
Widget build(BuildContext context) {
return Scaffold(
// ganti ‘Namaku' dengan Nama panggilan Anda
appBar: AppBar(title: const Text('Master Plan Namaku')),
body: _buildList(),
floatingActionButton: _buildAddTaskButton(),
);
}
}
_buildAddTaskButton()
Anda akan melihat beberapa error di langkah 6, karena method yang belum dibuat. Ayo kita buat mulai dari yang paling mudah yaitu tombol Tambah Rencana. Tambah kode berikut di bawah method build
di dalam class _PlanScreenState
.
Widget _buildAddTaskButton() {
return FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
setState(() {
plan = Plan(
name: plan.name,
tasks: List<Task>.from(plan.tasks)
..add(const Task()),
);
});
},
);
}
_buildList()
Kita akan buat widget berupa List
yang dapat dilakukan scroll, yaitu ListView.builder
. Buat widget ListView
seperti kode berikut ini.
Widget _buildList() {
return ListView.builder(
itemCount: plan.tasks.length,
itemBuilder: (context, index) =>
_buildTaskTile(plan.tasks[index], index),
);
}
_buildTaskTile
Dari langkah 8, kita butuh ListTile
untuk menampilkan setiap nilai dari plan.tasks
. Kita buat dinamis untuk setiap index
data, sehingga membuat view
menjadi lebih mudah. Tambahkan kode berikut ini.
Widget _buildTaskTile(Task task, int index) {
return ListTile(
leading: Checkbox(
value: task.complete,
onChanged: (selected) {
setState(() {
plan = Plan(
name: plan.name,
tasks: List<Task>.from(plan.tasks)
..[index] = Task(
description: task.description,
complete: selected ?? false,
),
);
});
}),
title: TextFormField(
initialValue: task.description,
onChanged: (text) {
setState(() {
plan = Plan(
name: plan.name,
tasks: List<Task>.from(plan.tasks)
..[index] = Task(
description: text,
complete: task.complete,
),
);
});
},
),
);
}
Run atau tekan F5 untuk melihat hasil aplikasi yang Anda telah buat. Capture hasilnya untuk soal praktikum nomor 4.
Anda dapat menambah tugas sebanyak-banyaknya, menandainya jika sudah beres, dan melakukan scroll jika sudah semakin banyak isinya. Namun, ada salah satu fitur tertentu di iOS perlu kita tambahkan. Ketika keyboard tampil, Anda akan kesulitan untuk mengisi yang paling bawah. Untuk mengatasi itu, Anda dapat menggunakan ScrollController
untuk menghapus focus dari semua TextField
selama event scroll dilakukan. Pada file plan_screen.dart
, tambahkan variabel scroll controller di class State tepat setelah variabel plan
.
late ScrollController scrollController;
Tambahkan method initState()
setelah deklarasi variabel scrollController
seperti kode berikut.
@override
void initState() {
super.initState();
scrollController = ScrollController()
..addListener(() {
FocusScope.of(context).requestFocus(FocusNode());
});
}
Tambahkan controller dan keyboard behavior pada ListView di method _buildList
seperti kode berikut ini.
return ListView.builder(
controller: scrollController,
keyboardDismissBehavior: Theme.of(context).platform ==
TargetPlatform.iOS
? ScrollViewKeyboardDismissBehavior.onDrag
: ScrollViewKeyboardDismissBehavior.manual,
Terakhir, tambahkan method dispose()
berguna ketika widget sudah tidak digunakan lagi.
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
Lakukan Hot restart (bukan hot reload) pada aplikasi Flutter Anda. Anda akan melihat tampilan akhir seperti gambar berikut. Jika masih terdapat error, silakan diperbaiki hingga bisa running.
README.md
! Jika Anda menemukan ada yang error atau tidak berjalan dengan baik, silakan diperbaiki.Bagaimana seharusnya Anda mengakses data pada aplikasi?
Beberapa pilihan yang bisa dilakukan adalah meletakkan data dalam satu kelas yang sama sehingga menjadi bagian dari life cycle aplikasi Anda.
Kemudian muncul pertanyaan, bagaimana meletakkan model dalam pohon widget ? sedangkan model bukanlah widget, sehingga tidak akan tampil pada screen.
Solusi yang memungkinkan adalah menggunakan InheritedWidget
. Sejauh ini kita hanya menggunakan dua jenis widget, yaitu StatelessWidget
dan StatefulWidget
. Kedua widget tersebut digunakan untuk layouting UI di screen. Di mana satu bersifat statis dan dinamis. Sedangkan InheritedWidget
itu berbeda, ia dapat meneruskan data ke sub-widget turunannya (biasanya ketika Anda menerapkan decomposition widget). Jika dilihat dari perspektif user, itu tidak akan terlihat prosesnya (invisible). InheritedWidget
dapat digunakan sebagai pintu untuk komunikasi antara view dan data layers.
Pada codelab ini, kita akan memperbarui kode dari aplikasi Master Plan dengan memisahkan data todo list ke luar class view-nya.
Setelah Anda menyelesaikan praktikum 1, Anda dapat melanjutkan praktikum 2 ini. Selesaikan langkah-langkah praktikum berikut ini menggunakan editor Visual Studio Code (VS Code) atau Android Studio atau code editor lain kesukaan Anda.
plan_provider.dart
Buat folder baru provider
di dalam folder lib
, lalu buat file baru dengan nama plan_provider.dart
berisi kode seperti berikut.
import 'package:flutter/material.dart';
import '../models/data_layer.dart';
class PlanProvider extends InheritedNotifier<ValueNotifier<Plan>> {
const PlanProvider({super.key, required Widget child, required
ValueNotifier<Plan> notifier})
: super(child: child, notifier: notifier);
static ValueNotifier<Plan> of(BuildContext context) {
return context.
dependOnInheritedWidgetOfExactType<PlanProvider>()!.notifier!;
}
}
main.dart
Gantilah pada bagian atribut home
dengan PlanProvider
seperti berikut. Jangan lupa sesuaikan bagian impor jika dibutuhkan.
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.purple),
home: PlanProvider(
notifier: ValueNotifier<Plan>(const Plan()),
child: const PlanScreen(),
),
);
plan.dart
Tambahkan dua method di dalam model class Plan
seperti kode berikut.
int get completedCount => tasks
.where((task) => task.complete)
.length;
String get completenessMessage =>
'$completedCount out of ${tasks.length} tasks';
Edit PlanScreen
agar menggunakan data dari PlanProvider
. Hapus deklarasi variabel plan
(ini akan membuat error). Kita akan perbaiki pada langkah 5 berikut ini.
_buildAddTaskButton
Tambahkan BuildContext
sebagai parameter dan gunakan PlanProvider
sebagai sumber datanya. Edit bagian kode seperti berikut.
Widget _buildAddTaskButton(BuildContext context) {
ValueNotifier<Plan> planNotifier = PlanProvider.of(context);
return FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
Plan currentPlan = planNotifier.value;
planNotifier.value = Plan(
name: currentPlan.name,
tasks: List<Task>.from(currentPlan.tasks)..add(const Task()),
);
},
);
}
_buildTaskTile
Tambahkan parameter BuildContext
, gunakan PlanProvider
sebagai sumber data. Ganti TextField
menjadi TextFormField
untuk membuat inisial data provider
menjadi lebih mudah.
Widget _buildTaskTile(Task task, int index, BuildContext context) {
ValueNotifier<Plan> planNotifier = PlanProvider.of(context);
return ListTile(
leading: Checkbox(
value: task.complete,
onChanged: (selected) {
Plan currentPlan = planNotifier.value;
planNotifier.value = Plan(
name: currentPlan.name,
tasks: List<Task>.from(currentPlan.tasks)
..[index] = Task(
description: task.description,
complete: selected ?? false,
),
);
}),
title: TextFormField(
initialValue: task.description,
onChanged: (text) {
Plan currentPlan = planNotifier.value;
planNotifier.value = Plan(
name: currentPlan.name,
tasks: List<Task>.from(currentPlan.tasks)
..[index] = Task(
description: text,
complete: task.complete,
),
);
},
),
);
}
_buildList
Sesuaikan parameter pada bagian _buildTaskTile
seperti kode berikut.
Widget _buildList(Plan plan) {
return ListView.builder(
controller: scrollController,
itemCount: plan.tasks.length,
itemBuilder: (context, index) =>
_buildTaskTile(plan.tasks[index], index, context),
);
}
class PlanScreen
Edit method build sehingga bisa tampil progress pada bagian bawah (footer). Caranya, bungkus (wrap) _buildList dengan widget Expanded dan masukkan ke dalam widget Column seperti kode pada Langkah 9.
SafeArea
Terakhir, tambahkan widget SafeArea
dengan berisi completenessMessage
pada akhir widget Column
. Perhatikan kode berikut ini.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Master Plan')),
body: ValueListenableBuilder<Plan>(
valueListenable: PlanProvider.of(context),
builder: (context, plan, child) {
return Column(
children: [
Expanded(child: _buildList(plan)),
SafeArea(child: Text(plan.completenessMessage))
],
);
},
),
floatingActionButton: _buildAddTaskButton(context),
);
}
Akhirnya, run atau tekan F5 jika aplikasi belum running. Tidak akan terlihat perubahan pada UI, namun dengan melakukan langkah-langkah di atas, Anda telah menerapkan cara memisahkan dengan baik antara view dan model. Ini merupakan hal terpenting dalam mengelola state di aplikasi Anda.
README.md
! Jika Anda menemukan ada yang error atau tidak berjalan dengan baik, silakan diperbaiki sesuai dengan tujuan aplikasi tersebut dibuat.InheritedWidget
pada langkah 1 tersebut! Mengapa yang digunakan InheritedNotifier
?Satu kalimat populer atau viral yang beredar dalam komunitas Flutter adalah "Lift State Up". Mantra ini merujuk ke sebuah ide di mana objek State seharusnya berada lebih tinggi dari pada widget yang membutuhkannya di dalam sebuah widget tree. InheritedWidget
yang telah kita buat sebelumnya bekerja dengan sempurna pada satu screen, tapi apa yang akan terjadi jika kita tambah screen kedua ?
Pada codelab ini, Anda akan menambah screen lain pada aplikasi Master Plan sehingga bisa membuat kelompok daftar plan lebih dari satu.
Selesaikan langkah-langkah praktikum berikut ini menggunakan editor Visual Studio Code (VS Code) atau Android Studio atau code editor lain kesukaan Anda.
PlanProvider
Perhatikan kode berikut, edit class PlanProvider
sehingga dapat menangani List Plan.
class PlanProvider extends
InheritedNotifier<ValueNotifier<List<Plan>>> {
const PlanProvider({super.key, required Widget child, required
ValueNotifier<List<Plan>> notifier})
: super(child: child, notifier: notifier);
static ValueNotifier<List<Plan>> of(BuildContext context) {
return context.
dependOnInheritedWidgetOfExactType<PlanProvider>()!.notifier!;
}
}
main.dart
Langkah sebelumnya dapat menyebabkan error pada main.dart
dan plan_screen.dart
. Pada method build
, gantilah menjadi kode seperti ini.
@override
Widget build(BuildContext context) {
return PlanProvider(
notifier: ValueNotifier<List<Plan>>(const []),
child: MaterialApp(
title: 'State management app',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const PlanScreen(),
),
);
}
Tambahkan variabel plan
dan atribut pada constructor-nya seperti berikut.
final Plan plan;
const PlanScreen({super.key, required this.plan});
Itu akan terjadi error setiap kali memanggil PlanProvider.of(context)
. Itu terjadi karena screen saat ini hanya menerima tugas-tugas untuk satu kelompok Plan
, tapi sekarang PlanProvider
menjadi list dari objek plan tersebut.
getter Plan
Tambahkan getter pada _PlanScreenState
seperti kode berikut.
class _PlanScreenState extends State<PlanScreen> {
late ScrollController scrollController;
Plan get plan => widget.plan;
initState()
Pada bagian ini kode tetap seperti berikut.
@override
void initState() {
super.initState();
scrollController = ScrollController()
..addListener(() {
FocusScope.of(context).requestFocus(FocusNode());
});
}
build
Pastikan Anda telah merubah ke List
dan mengubah nilai pada currentPlan
seperti kode berikut ini.
@override
Widget build(BuildContext context) {
ValueNotifier<List<Plan>> plansNotifier = PlanProvider.of(context);
return Scaffold(
appBar: AppBar(title: Text(_plan.name)),
body: ValueListenableBuilder<List<Plan>>(
valueListenable: plansNotifier,
builder: (context, plans, child) {
Plan currentPlan = plans.firstWhere((p) => p.name == plan.
name);
return Column(
children: [
Expanded(child: _buildList(currentPlan)),
SafeArea(child: Text(currentPlan.
completenessMessage)),
],);},),
floatingActionButton: _buildAddTaskButton(context,)
,);
}
Widget _buildAddTaskButton(BuildContext context) {
ValueNotifier<List<Plan>> planNotifier = PlanProvider.
of(context);
return FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
Plan currentPlan = plan;
int planIndex =
planNotifier.value.indexWhere((p) => p.name == currentPlan.name);
List<Task> updatedTasks = List<Task>.from(currentPlan.tasks)
..add(const Task());
planNotifier.value = List<Plan>.from(planNotifier.value)
..[planIndex] = Plan(
name: currentPlan.name,
tasks: updatedTasks,
);
plan = Plan(
name: currentPlan.name,
tasks: updatedTasks,
);},);
}
_buildTaskTile
Pastikan ubah ke List
dan variabel planNotifier
seperti kode berikut ini.
Widget _buildTaskTile(Task task, int index, BuildContext context)
{
ValueNotifier<List<Plan>> planNotifier = PlanProvider.
of(context);
return ListTile(
leading: Checkbox(
value: task.complete,
onChanged: (selected) {
Plan currentPlan = plan;
int planIndex = planNotifier.value
.indexWhere((p) => p.name == currentPlan.name);
planNotifier.value = List<Plan>.from(planNotifier.value)
..[planIndex] = Plan(
name: currentPlan.name,
tasks: List<Task>.from(currentPlan.tasks)
..[index] = Task(
description: task.description,
complete: selected ?? false,
),);
}),
title: TextFormField(
initialValue: task.description,
onChanged: (text) {
Plan currentPlan = plan;
int planIndex =
planNotifier.value.indexWhere((p) => p.name ==
currentPlan.name);
planNotifier.value = List<Plan>.from(planNotifier.value)
..[planIndex] = Plan(
name: currentPlan.name,
tasks: List<Task>.from(currentPlan.tasks)
..[index] = Task(
description: text,
complete: task.complete,
),
);
},),);}
Pada folder view, buatlah file baru dengan nama plan_creator_screen.dart
dan deklarasikan dengan StatefulWidget
bernama PlanCreatorScreen
. Gantilah di main.dart
pada atribut home menjadi seperti berikut.
home: const PlanCreatorScreen(),
Kita perlu tambahkan variabel TextEditingController
sehingga bisa membuat TextField
sederhana untuk menambah Plan baru. Jangan lupa tambahkan dispose ketika widget unmounted seperti kode berikut.
final textController = TextEditingController();
@override
void dispose() {
textController.dispose();
super.dispose();
}
Letakkan method Widget build
berikut di atas void dispose
. Gantilah ‘Namaku' dengan nama panggilan Anda.
@override
Widget build(BuildContext context) {
return Scaffold(
// ganti ‘Namaku' dengan nama panggilan Anda
appBar: AppBar(title: const Text('Master Plans Namaku')),
body: Column(children: [
_buildListCreator(),
Expanded(child: _buildMasterPlans())
]),
);
}
_buildListCreator
Buatlah widget berikut setelah widget build.
Widget _buildListCreator() {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Material(
color: Theme.of(context).cardColor,
elevation: 10,
child: TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'Add a plan',
contentPadding: EdgeInsets.all(20)),
onEditingComplete: addPlan),
));
}
void addPlan()
Tambahkan method berikut untuk menerima inputan dari user berupa text plan.
void addPlan() {
final text = textController.text;
if (text.isEmpty) {
return;
}
final plan = Plan(name: text, tasks: []);
ValueNotifier<List<Plan>> planNotifier =
PlanProvider.of(context);
planNotifier.value = List<Plan>.from(planNotifier.value)..
add(plan);
textController.clear();
FocusScope.of(context).requestFocus(FocusNode());
setState(() {});
}
widget _buildMasterPlans()
Tambahkan widget seperti kode berikut.
Widget _buildMasterPlans() {
ValueNotifier<List<Plan>> planNotifier = PlanProvider.of(context);
List<Plan> plans = planNotifier.value;
if (plans.isEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(Icons.note, size: 100, color: Colors.grey),
Text('Anda belum memiliki rencana apapun.',
style: Theme.of(context).textTheme.headlineSmall)
]);
}
return ListView.builder(
itemCount: plans.length,
itemBuilder: (context, index) {
final plan = plans[index];
return ListTile(
title: Text(plan.name),
subtitle: Text(plan.completenessMessage),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) =>
PlanScreen(plan: plan,)));
});
});
}
Terakhir, run atau tekan F5 untuk melihat hasilnya jika memang belum running. Bisa juga lakukan hot restart jika aplikasi sudah running. Maka hasilnya akan seperti gambar berikut ini.
README.md
! Jika Anda menemukan ada yang error atau tidak berjalan dengan baik, silakan diperbaiki sesuai dengan tujuan aplikasi tersebut dibuat.Selamat Anda telah menyelesaikan Codelab ini. Anda telah mempelajari terkait state management dan contoh penggunaannya.
Pada codelab berikutnya, Anda akan mempelajari tentang Pemrograman Asynchronous di Flutter.
Jangan sungkan jika Anda menemukan kesalahan pada codelab ini untuk merevisi atau sekedar melaporkan issue melalui tautan di pojok kiri bawah (Report a mistake).
Silakan cek beberapa sumber belajar lainnya...