Flutter merupakan Android SDK yang sedang terkenal di tahun ini, bahkan lonjakan developer yang mengadopsi untuk ke ranah produksi juga semakin banyak. Meski belum sepenuhnya pada versi stabil, Flutter sudah berhasil menggaet banyak developer untuk menggunakannya, apalagi ada aplikasi-aplikasi dari Flutter di lapak playstore. Hal tersebut sanggup menjadi potensi besar bagi developer untuk menjajal menciptakan aplikasi mobile alternative selain native development.
Perlu diketahui bahwa Flutter ini berbeda dengan framework yang sudah pernah-pernah sahabat jumpai menyerupai Hybrid App, PWA, AMP, Cordova, dan PhoneGap. Apabila dibandingkan dengan React native hampir sama hanya flow mekanismenya berbeda. Baik HybridApp ataupun React native menggunakan bahasa Java Script sedangkan Flutter menggunakan bahasa yang tidak mengecewakan absurd (bahkan opsi pada goresan pena ini untuk editor yang digunakan malah belum ada), yakni Dart. Apabila teman-teman ingin tahu kenapa Flutter menggunakan Dart teman-teman sanggup kunjungi di laman ini.
Saya cukupkan untuk perkenalan Flutter, mari kita bahas lebih jau mengenai penggunaan Flutter, case kali ini kita akan membangun Todo List menggunakan Flutter, Todo List ini dari tampilan UI kita sanggup mengadopsi UI miliki Tasks App Google. Supaya lebih yummy dan nyaman untuk pengembangan selanjutnya kita akan membangun dengan menggunakan application architecture Redux. Apabila teman-teman sudah pernah menggunakan React native, niscaya teman-teman tidak absurd dengan aplication architecture ini, oke aku akan tekankan kepada teman-teman kegunaan Redux di Flutter ini.
- Di Setiap kondisi terntu Flutter menggunakan state untuk memantai perubahan, baik perubahan data atau perubahan ui dengan Redux kita tahu kapan state berubah, dan tentunya pengelolaan state jadi lebih simpel sehingga timbulnya duplikasi sanggup dihindari.
- Fungsi Reducer mempermudah kita untuk melaksanakan unit testing, kita sanggup melewati state dan mengubah state.
- Tentunya aplikasi kita akan terstruktur, sebab beberapa kepingan kita bagi dengan layer untuk melaksanakan proses tertentu, dari segi action, models, business logic, dll. sehingga apabila ada fitur gres teman-teman sanggup mengimplementaisnya lebih mudah.
Todo List dengan Redux
Fitur-fitur yang sanggup kita bangkit untuk aplikasi Todolist ialah sebagai berikut
- Create task and save task persistance
- Read task
- Remove task
- Toogle task remover
Library yang kita akan pergunakan
- Redux – Redux for Dart using generics for typed State.
- Flutter Redux – A set of utilities that allow you to easily consume a Redux Store.
- Shared Preferences – Providing a persistent store for simple data.
- Sqflite – SQLite plugin for Flutter. Supports both iOS and Android.
- flutter_redux_dev_tools – A Widget you can use to show a Redux Time Travel UI.
Berikut teladan jadi Aplikasi Todo List, menyerupai Task App Google bukan? 🤣
Mari bersiap koding 👇
Mari kita buat User Interface(UI) terlebih dahulu, UI yang akan kita buat menyerupai yang sudah diterangkan diatas kita mengadopsi UI Google tasks. Yap akomodasi menciptakan material desain terang sekali didukung oleh Flutter, jadi konsep Material desain sanggup diimplementasikan lebih simpel menggunakan Flutter,
Membuat BottomAppbar Widget
Contoh menyerupai gambar diatas, kita akan menciptakan BottomAppbar widget,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /** * Create Floating with notch */ class FloatingTask extends StatelessWidget { String label; Icon icon; FloatingTask(this.label, this.icon); @override Widget build(BuildContext context){ return new Scaffold( appBar: AppBar(title: new Text(label)), floatingActionButton: FloatingActionButton.extended( elevation: 3.0, icon: icon, label: new Text(label, style: TextStyle( fontWeight: FontWeight.bold),), onPressed: (){}, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, bottomNavigationBar: new PreferredSize( preferredSize: const Size.fromHeight(100.0), child: new Container( height: 55.0, child: BottomAppBar( hasNotch: false, child: new Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ IconButton( icon: Icon(Icons.menu), onPressed: () {}, ), IconButton( icon: Icon(Icons.more_vert), onPressed: (){}, ) ], ), ), ) ) ); } } |
Berdasarkan aba-aba diatas kita menciptakan kelas yang meng-extends stateless Widget, artinya widget ini immutable/data tidak berubah. Pada parameter bottomNavigationbar kita menginstasiasi PreferedSize salah satu widget yang sanggup memilih Height pada kafe dengan nested child BottomBar didalam container. Parameter dengan nama hasNotch merupakan param yang memilih lekukan pada kafe apabila terdapat floating actionbar yang overlapping. Kemudian di param Row terdapat MainAxisSize.Max artinya Row memperoleh space maksimal menurut space layar. Param SpaceBetween mempunyai kegunaan untuk memisahkan Widget secara berseberangan.
Baiklah kita akan mengintegrasikan library Redux terlebih dahulu kedalam package project kita menggunakan file pubspec.yaml. Taruh dibawah flutter dan indent harus sejajar dengan kata flutter di atasnya. kemudian save otomatis package akan did0wnl0ad, apabila terdapat kesalahan maka diharuskan untuk mengetik di terminal menggunakan
Runs-MBP:redux_task_apps ran$ flutter packages get
1 2 3 4 5 6 7 8 9 10 11 | name: redux_task_apps description: A new Flutter project. dependencies: flutter: sdk: flutter redux: "^3.0.0" flutter_redux: "^0.5.1" shared_preferences: "^0.4.2" flutter_redux_dev_tools: "^0.3.0" redux_dev_tools: "^0.4.0" |
Nah kemudian teman-teman diharuskan menciptakan struktur project sebagai berikut,
Struktur project diatas terdiri atas item, list, model, redux, util, dan widget. Masing-masing mempunyai fungsi yang berbeda, item sebagai file untuk action setiap click pada item khususnya item list. kemudian list yakni widget khusus untuk menampilkan list builder beserta itemnya, selanjutnya model berisi data model atau POJO yang nantinya akan digunakan dan dimuat, berikutnya ada Util berisi file pendukung menyerupai file untuk mengumpulkan semua package import. terakhir widget sebagai penyimpan file-file widget.
Model Data
Langkah pertama sebelum memulai project ialah menciptakan model kelas terlebih dahulu, kenapa? sebab model kelas atau POJO tidak terikat oleh kelas manapun alias berdiri sendiri, mari kita tuliskan kode, sebelumnya teman-teman menciptakan file terlebih dahulu dengan nama “data_model.dart” di dalam model folder. Satu lagi, di dalam file dart ini tidak diharuskan untuk menulis nama kelas dengan nama file yang sama.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Data { String tugas; bool checked; Data(this.tugas, this.checked); Data.fromJson(Map<String, dynamic> json) : tugas = json['tugas'], checked = json['checked']; Map<String, dynamic> toJson() => {'tugas': tugas, 'checked': checked}; @override String toString() { return "$tugas: $checked"; } } |
Berikut aba-aba yang telah kita tuliskan, dari aba-aba ditas kita menciptakan kelas dengan nama Data. Bila teman-teman perhatikan diatas yang terdapat pada Data.fromJson() nah pada baris tersebut di Dart Lang dikenal sebagai Initializer list yakni meng-invoke superclass konstruktor untuk meng-init terlebih dahulu sebelum menjalan/mengeksekusi didalam body konstruktor. Nah Data.fromJson tersebut berfungsi sebagai parsing Json, param json bertipe Map ini diolah untuk di parsing ke masing-masing objek. Artinya json[‘tugas’] berisi String kiprah di simpan pada objek tugas, sedangkan json[‘checked’] berisi boolean true/false di simpan pada object boolean checked. kemudian fungi toJson() berfungsi men-transform objek kiprah dan objek checked kedalam Map<String, dynamic> atau Json.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class ListData { static var empty = ListData(new List()); final List<Data> data; ListData(this.data); ListData.fromJson(Map<String, dynamic> json) : data = (json['data'] as List) .map((i) => new Data.fromJson(i as Map<String, dynamic>)) .toList(); Map<String, dynamic> toJson() => {'data': data}; @override String toString() => "$data"; } |
Selanjutnya kita menciptakan file dengan nama list_data.dart di dalam folder model. ListData ini sebagai list yang memuat model data dari kelas Data. LisData.fromJson merupaka initializer list konstruktor hampir sama dengan fungsi kelas di Data, perbedaannya kita akan mengolah list data. Data json[‘data’] yang bertipe List akan diiterasi dengan map yang setiap iterasi didalamnya yakni “i” akan dimasukkan kedalam instansiasi objek gres Data.fromJson sebagai dynamic. Nah model ini sebetulnya digunakan untuk menyimpan data menggunakan Json, JSON kita simpan menggunakan shared preferences dengan tipe String. untuk strukturnya kurang lebih menyerupai dibawah ini.
Integrasi Komponen Redux
Langkah selanjutnya kita akan menciptakan file action di dalam folder redux action ini mempunyai kegunaan sebagai intent yang sanggup memanggil perubahan state aplikasi, dalam aplikasi yang ingin kita kembangkan ada empat yakni menambahkan, mengecek perubahan aksi. memperoleh list item, meremove item.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class AddTaskAction { final Data data; AddTaskAction(this.data); } class ToogleItemStateAction { final Data data; ToogleItemStateAction(this.data); } class FetchItemAction { final List<Data> list; FetchItemAction(this.list); } class LoadItemAction {} class RemoveItemAction { final Data data; RemoveItemAction(this.data); } |
Setelah kita mendefinisikan kelas action, kita akan menciptakan fungsi untuk mengecek action, dan kemudian berpotensi untuk men-transformasi action sebelum melalui proses reducer. Secara konsep menyerupai presentasion layer yang disebut sebagai middleware atau jembatan antar view ui dengan model data. Teman-teman sanggup menciptakan file gres dengan nama middleware.dart. Proses didalamnya yakni mengecek kondisi action state untuk di sanksi lebih lanjut dan proses penyimpanan json yang telah kita transform tadi ke dalam Shared preferences berbentuk String menyerupai dibawah ini.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | const String APP_STATE_KEY = "APP_STATE"; void storeDataItemMiddleware( Store<ListData> task, action, NextDispatcher next) { next(action); if (action is AddTaskAction || action is ToogleItemStateAction || action is RemoveItemAction) { saveStateToPref(task.state); } if (action is LoadItemAction) { loadStateFromPref().then((state) { task.dispatch(new FetchItemAction(state.data)); }); } } void saveStateToPref(ListData data) async { SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); var stateString = json.encode(data.toJson()); await sharedPreferences.setString(APP_STATE_KEY, stateString); } Future<ListData> loadStateFromPref() async { SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); var stateString = sharedPreferences.getString(APP_STATE_KEY); Map stateMap = json.decode(stateString); return new ListData.fromJson(stateMap); } |
Nah dua file sudah kita buat di dalam folder redux, untuk selanjutny kita akan menciptakan reducer, apa itu reducer? reducer merupakan kepingan pada state redux yang berfungsi sebagai akseptor state dan action aplikasi yang sedang berjalan dan mengembalikan state yang baru. Nah kali ini kita menciptakan empat buah reducer untuk lebih jelasnya mari kita lihat aba-aba berikut,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ListData listDataReducer(ListData stateList, dynamic action) { if (action is AddTaskAction) { return addItem(stateList.data, action); } else if (action is ToogleItemStateAction) { return toogleItemState(stateList.data, action); } else if (action is RemoveItemAction) { return removeItem(stateList.data, action); } else if (action is FetchItemAction) { return loadItem(action); } return stateList; } ListData addItem(List<Data> data, AddTaskAction action) { return new ListData(List.from(data)..add(action.data)); } ListData toogleItemState(List<Data> data, ToogleItemStateAction action) { return new ListData(data .map((item) => item.tugas == action.data.tugas ? action.data : item) .toList()); } ListData removeItem(List<Data> data, RemoveItemAction action) { return new ListData( List.from(data)..removeWhere((item) => item.tugas == action.data.tugas)); } ListData loadItem(FetchItemAction action) { return new ListData(action.list); } |
Empat reducer yang sudah kita tuliskan diatas mempunyai beberapa fungsi, sama menyerupai fungsi di action, method addItem digunakan untuk menambahkan data pada json apabila ada komplemen data baru. method toogleItemState mempunyai kegunaan untuk meamntau perubahan state. Method removeItem mempunyai kegunaan untuk mengahapus data menurut string “tugas”. Terakhir method load item digunakan untuk mengambil data dari shared preferences yang berbentuk json. Jika teman-teman melihat ada titik titik artinya method tersebut menggunakan cascade notations.
Membuat List Item dan List Builder
Setelah kita menuliskan semua komponen redux kita akan menciptakan widget khusus untuk menangani Listdata. Buat satu file dengan nama list.dart di folder widget, di dalam file tersebut kita menaruh dua kelas yakni ListDataWidget dan ListDataItem. Kelas ListDataWidget digunakan sebagai builder atau adapter untuk menyusun list, sedangkan ListDataItem digunakan untuk menciptakan satu list item. Nah kita sanggup mengimplementasikan fitur swipe remove laiknya task google sendiri dengan Dismissible. Dismissible merupakan widget khusus untuk menangani swipe remover, atribute yang sanggup gunakan background, onDismissed, dan child. Store Connector merupakan sebuah widget yang memperoleh Store dari storeProvider, dikonversi dari store ke viewmodel menggunakan fungsi konverter. Artinya setiap waktu store berubah data atau event maka Widget akan segera otomatis me-rebuild kembali konsepnya sama menyerupai setState(). No need manage subscripstions 😎
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | typedef OnStateChanged = Function(Data data); typedef onRemoveIconClicked = Function(Data data); class ListDataItem extends StatelessWidget { final Data data; ListDataItem(this.data); @override Widget build(BuildContext context) { return new StoreConnector<ListData, OnStateChanged>( converter: (store) { return (item) => store.dispatch(RemoveItemAction(item)); }, builder: (context, callback) { return Dismissible( key: Key(data.tugas), background: Container( color: Colors.blue, ), onDismissed: (direction) { callback(Data(data.tugas, data.checked)); print("menghapus"); Scaffold.of(context).showSnackBar( SnackBar(content: Text("${data.tugas} dihapus"))); }, child: new ListTile( title: new Text(data.tugas), leading: new StoreConnector<ListData, OnStateChanged>( converter: (store) { return (item) => store.dispatch(ToogleItemStateAction(item)); }, builder: (context, callback) { return new Checkbox( value: data.checked, onChanged: (bool value) { callback(Data(data.tugas, value)); }, ); }, ), )); }, ); } } class ListDataWidget extends StatelessWidget { //Build widget base state of store @override Widget build(BuildContext context) { return new StoreConnector<ListData, List<Data>>( //Convert store into viewmodel kesudahannya akan dimasukkan kedalam widget converter: (store) => store.state.data, builder: (context, list) { return new ListView.builder( itemCount: list.length, itemBuilder: (context, position) => new ListDataItem(list[position]), ); }, ); } } |
Mengintegrasikan semua komponen ke dalam Widget
Selanjutnya kita akan menambahkan fitur pada Floating Action Button (FAB), sehingga kita akan memperbaiki kodingan widget FAB diawal tulisan. Buat file gres dengan nama widget.dart, di dalam file tersebut berisi kelas FAB widget, kelas Bottom sheet widget, dan dua kelas khusus menangani Stateful widget. berikut cuplikan koding untuk FAB widget. Karena widget ListData meruapakan widget yang membutuhkan perubahan data maka kita akan menggunakan stategull widget. Kalau teman-teman menggunakan text editor/IDE visual code terkadang kesulitan kita ialah menggunakan import, artinya teman-teman akan menggunakan import manual. biar kodingan kita lebih rapi teman-teman sanggup menggunakan satu buah file untuk menyimpan semua import. Buatlah satu buah file gres dengan nama all_library.dart di dalam folder util.
1 2 3 4 5 6 7 8 9 10 11 12 13 | library allLibrary; export 'package:redux_task_apps/widget/widget.dart'; export 'package:redux_task_apps/widget/list.dart'; export 'package:flutter/material.dart'; export 'package:flutter_redux/flutter_redux.dart'; export 'package:redux/redux.dart'; export 'package:redux_task_apps/model/data_model.dart'; export 'package:redux_task_apps/model/list_data.dart'; export 'package:redux_task_apps/redux/action.dart'; export 'package:redux_task_apps/redux/middleware.dart'; export 'package:redux_task_apps/redux/reducer.dart'; export 'package:redux_dev_tools/redux_dev_tools.dart'; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | /** * Create Floating with notch */ class FloatingTask extends StatelessWidget { String label; Icon icon; FloatingTask(this.label, this.icon); @override Widget build(BuildContext context) { final GlobalKey<ScaffoldState> _scaffoldState = new GlobalKey(); return new Scaffold( appBar: AppBar(title: new Text(label)), body: new ListDataWidget(), floatingActionButton: FloatingActionButton.extended( elevation: 3.0, icon: icon, label: new Text( label, style: TextStyle(fontWeight: FontWeight.bold), ), onPressed: () { showModalBottomSheet( context: context, builder: (context) => new AddItemBottomSheet()); }, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, bottomNavigationBar: new PreferredSize( preferredSize: const Size.fromHeight(100.0), child: new Container( height: 55.0, child: BottomAppBar( hasNotch: false, child: new Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ IconButton( icon: Icon(Icons.menu), onPressed: () {}, ), IconButton( icon: Icon(Icons.more_vert), onPressed: () {}, ) ], ), ), ))); } } /** * Membuat Bottomsheet yang mempunyai textfield */ typedef OnAddCallback = Function(String dataTugas); class AddItemBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { return new StoreConnector<ListData, OnAddCallback>( converter: (store) { return (dataTugas) => store.dispatch(AddTaskAction(Data(dataTugas, false))); }, builder: (context, callback) { return new AddItemBottomSheetWidget(callback); }, ); } } class AddItemBottomSheetWidget extends StatefulWidget { final OnAddCallback callback; AddItemBottomSheetWidget(this.callback); @override State<StatefulWidget> createState() => new AddItemBottomSheetState(callback); } class AddItemBottomSheetState extends State<AddItemBottomSheetWidget> { String dataTugas; final OnAddCallback callback; final myController = TextEditingController(); //New Code @override void dispose() { myController.dispose(); super.dispose(); } AddItemBottomSheetState(this.callback); @override Widget build(BuildContext context) { return new Container( padding: EdgeInsets.all(15.0), child: new Column( children: <Widget>[ new Row( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ new Expanded( child: new TextField( autofocus: true, controller: myController, decoration: new InputDecoration( labelText: 'Tugas', hintText: 'contoh koding'), //lost ), ) ], ), new Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ new IconButton( icon: Icon( Icons.add_circle, color: Colors.blue, ), ), new GestureDetector( onTap: () { setState(() { // dataTugas = myController.text; callback(myController.text); }); Navigator.pop(context); // callback(dataTugas); }, child: new Text( "Save", style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold), ), ) ], ) ], )); } } |
Baiklah, tampaknya cukup sekian dari aku kurang lebihnya mohon maaf dan semangat untuk berkarya. apabila teman-teman menemui hambatan teman-teman sanggup komen di bawah atau kontak saya. Kodingan selengkapnya sanggup teman-teman dapatkan di sini. Akhir kata dari saya, aku ucapkan terimakasih. 😎😎
Sumber aciknadzirah.blogspot.com
EmoticonEmoticon