Pemisahan Masalah dalam Aplikasi Flutter
Engineering at ClickUp

Pemisahan Masalah dalam Aplikasi Flutter

Baru-baru ini, saya harus menerapkan panduan orientasi untuk /href/ https://clickup.com/ ClickUp /%href/ pendatang baru! Ini adalah tugas yang sangat penting karena banyak pengguna baru yang akan menemukan platform dengan /href/ https://www.youtube.com/watch?v=AirnL1NxOAw iklan yang sangat lucu yang kami tayangkan di Super Bowl /%href/ ! ✨ melalui ClickUp Panduan ini memungkinkan banyak pengguna baru kami, yang mungkin belum mengenal ClickUp, untuk dengan cepat memahami cara menggunakan beberapa fungsi dari aplikasi ini. Ini adalah upaya yang berkelanjutan, seperti halnya /href/ https://university.clickup.com/ Universitas ClickUp /%href/ sumber daya yang kami kejar! 🚀 Untungnya, arsitektur perangkat lunak di balik aplikasi seluler ClickUp Flutter memungkinkan saya untuk mengimplementasikan fungsionalitas ini dengan cukup cepat, bahkan dengan menggunakan kembali widget asli dari aplikasi! Ini berarti panduannya dinamis, responsif, dan sama persis dengan layar aplikasi yang sebenarnya - dan akan terus berlanjut, bahkan ketika widgetnya akan berkembang. Saya juga dapat mengimplementasikan fungsionalitasnya karena pemisahan masalah yang tepat. Mari kita lihat apa yang saya maksud di sini. 🤔

Pemisahan masalah

Merancang arsitektur perangkat lunak adalah salah satu topik yang paling kompleks bagi tim teknik. Di antara semua tanggung jawab, selalu sulit untuk mengantisipasi evolusi perangkat lunak di masa depan. Itulah mengapa membuat arsitektur yang berlapis-lapis dan terpisah-pisah dapat membantu Anda dan rekan tim Anda dalam banyak hal! Manfaat utama dari membuat sistem kecil yang terpisah-pisah tidak diragukan lagi adalah ketangguhan! Dan inilah yang membantu saya membuat alternatif demo dari layar yang ada dari aplikasi!

Panduan langkah demi langkah

Sekarang, bagaimana kita dapat menerapkan prinsip-prinsip tersebut pada aplikasi Flutter? Kami akan membagikan beberapa teknik yang kami gunakan untuk membangun ClickUp dengan contoh panduan sederhana. Contoh ini sangat sederhana sehingga mungkin tidak menjelaskan semua keuntungan di baliknya, tetapi percayalah, ini akan membantu Anda membuat aplikasi Flutter yang lebih mudah dipelihara dengan basis kode yang kompleks. 💡

Aplikasi

Sebagai contoh, kita akan membuat aplikasi yang menampilkan jumlah penduduk Amerika Serikat untuk setiap tahun. melalui ClickUp Kita memiliki dua layar di sini : * `Layar Utama`: hanya mencantumkan semua tahun dari 2000 hingga sekarang. Ketika pengguna mengetuk ubin tahun, mereka akan menavigasi ke `DetailScreen` dengan argumen navigasi yang disetel ke tahun yang dipilih. * `DetailScreen` : mendapatkan tahun dari argumen navigasi, memanggil /href/ https://datausa.io/api/data?drilldowns=Nation&measures=Population&year=2018 aPI datausa.io /%href/ untuk tahun ini, dan mem-parsing data JSON untuk mengekstrak nilai populasi terkait. Jika data tersedia, sebuah label akan ditampilkan dengan jumlah populasi. Kita akan fokus pada implementasi `DetailScreen` karena ini adalah yang paling menarik dengan pemanggilan asinkron.

Langkah 1\. Pendekatan Naif

/img/ https://clickup.com/blog/wp-content/uploads/2022/03/image-5-1400x607.png Pendekatan naif Stateful Widget /%img/ melalui ClickUp Implementasi yang paling jelas untuk aplikasi kita, adalah dengan menggunakan satu `StatefulWidget` untuk seluruh logika. /href/ https://dartpad.dev/embed-flutter.html?id=b37071f4386ecb27c29332e788999612&split=80&theme=dark Kode Sumber & Demo Dart /%href/

Mengakses argumen navigasi `tahun`

Untuk mengakses tahun yang diminta, kita membaca `RouteSettings` dari widget bawaan `ModalRoute`. ``` void didChangeDependencies() { super.didChangeDependencies(); tahun akhir = ModalRoute.of(context)!.settings.arguments as int; // ... } ```

Panggilan HTTP

Contoh ini memanggil fungsi `get` dari paket `http` untuk mendapatkan data dari /href/ https://datausa.io/api/data?drilldowns=Nation&measures=Population&year=2018 aPI datausa.io /%href/ mem-parsing JSON yang dihasilkan dengan metode `jsonDecode` dari pustaka `dart:convert`, dan menyimpan `Future` sebagai bagian dari state dengan properti bernama `_future`. ``` akhir Masa Depan?> _masa depan; void didChangeDependencies() { super.didChangeDependencies(); tahun akhir = ModalRoute.of(context)!.settings.arguments as int; if (_tahun != tahun) { _masa depan = _loadMeasure(tahun); } } Future?> _loadMeasure(int tahun) async { _year = tahun; final uri = Uri.parse( 'https://datausa.io/api/data?drilldowns=Nation&measures=Population&year=$year'); final hasil = menunggu get(uri); final body = jsonDecode(hasil.body); final data = body['data'] as List; if (data.isNotEmpty) { kembalikan data.pertama; } return null; ```

Rendering

Untuk membuat pohon widget, kita menggunakan `FutureBuilder`, yang membangun ulang dirinya sendiri terkait kondisi saat ini dari pemanggilan asinkron `_future`. ``` @menimpa Widget build(konteks BuildContext context) { return Perancah( appBar: AppBar( title: Text('Tahun $_tahun'), ), tubuh: FutureBuilder?>( masa depan _masa depan, builder: (konteks, snapshot) { switch (snapshot.connectionState) { case ConnectionState.done: kesalahan akhir = snapshot.error; if (error != null) { // kembalikan pohon "error". } data akhir = snapshot.data; if (data != null) { // mengembalikan pohon "hasil". } // kembalikan "kosong" data tree.case ConnectionState.none: case ConnectionState.waiting: case ConnectionState.active: // mengembalikan pohon data "memuat". } }, ), ); } ```

Ulasan

Oke, implementasinya singkat dan hanya menggunakan widget bawaan, tetapi sekarang pikirkan tujuan awal kita: membuat alternatif demo (atau tes) untuk layar ini. Sangat sulit untuk mengontrol hasil dari panggilan HTTP untuk memaksa aplikasi melakukan rendering pada kondisi tertentu. Di situlah konsep inversi kontrol akan membantu kita. 🧐

Langkah 2\. Pembalikan kendali

/img/ https://clickup.com/blog/wp-content/uploads/2022/03/image-6-1400x607.png Pembalikan kontrol ke Penyedia dan Klien api /%img/ melalui ClickUp Prinsip ini mungkin sulit dipahami oleh pengembang baru (dan juga sulit dijelaskan), tetapi ide utamanya adalah untuk mengekstrak masalah di luar komponen kita - sehingga mereka tidak bertanggung jawab untuk memilih perilaku - dan mendelegasikannya sebagai gantinya. Dalam situasi yang lebih umum, ini hanya terdiri dari membuat abstraksi dan menyuntikkan implementasi ke dalam komponen kita sehingga implementasinya dapat diubah di kemudian hari jika diperlukan. Tapi jangan khawatir, hal ini akan lebih masuk akal setelah contoh berikutnya! 👀 /href/ https://dartpad.dev/embed-flutter.html?id=d68322aa5a6e6bdd4d1fbbf8f86aebd3&split=80&theme=dark Kode Sumber & Demo Dart /%href/

Membuat objek klien API

Untuk mengontrol panggilan HTTP ke API kita, kita telah mengisolasi implementasi ke dalam kelas `DataUsaApiClient` khusus. Kami juga telah membuat kelas `Measure` untuk membuat data lebih mudah dimanipulasi dan dipelihara. ``` class DataUsaApiClient { const DataUsaApiClient({ this.endpoint = 'https://datausa.io/api/data', }); final String titik akhir; Future getMeasure(int tahun) async { final uri = Uri.parse('$endpoint?drilldowns=Nation&measures=Population&year=$year'); hasil akhir = menunggu get(uri); final body = jsonDecode(hasil.body); final data = body['data'] as List; if (data.isNotEmpty) { return Measure.fromJson(data.first as Map); } return null; } ```

Menyediakan klien API

Untuk contoh kita, kita menggunakan /href/ https://file+.vscode-resource.vscode-webview.net/Users/alois/Desktop/article/clickup-separation\_of\_concern.md penyedia /%href/ untuk menyuntikkan sebuah instance `DataUsaApiClient` di akar pohon kita. ``` Penyedia ( create: (konteks) => const DataUsaApiClient(), child: const MaterialApp( home: HomePage(), ), ) ```

Menggunakan klien API

Penyedia mengizinkan widget turunan apa pun _(_ _seperti_ _kami_ __DetailScreen_`_)_ untuk membaca `DataUsaApiClient` terdekat yang ada di bagian atas dalam pohon. Kita kemudian dapat menggunakan metode `getMeasure` untuk memulai `Future` kita, sebagai pengganti implementasi HTTP yang sebenarnya. ``` @overridevoid didChangeDependencies() { super.didChangeDependencies(); tahun akhir = ModalRoute.of(context)!.settings.arguments as int; if (_tahun != tahun) { _tahun = tahun; final api = context.read(); _masa depan = api.getMeasure(tahun); } } ```

Klien API Demo

Sekarang kita bisa memanfaatkannya! Jika Anda tidak tahu: kelas apa saja /href/ https://dart.dev/samples#interfaces-and-abstract-classes di dart juga secara implisit mendefinisikan antarmuka yang terkait /%href/ . Hal ini memungkinkan kita untuk menyediakan implementasi alternatif dari `DataUsaApiClient` yang selalu mengembalikan instance yang sama dari pemanggilan metode `getMeasure`. > Metode ini ``` class DemoDataUsaApiClient mengimplementasikan DataUsaApiClient { const DemoDataUsaApiClient(this.measure); ukuran Measure akhir; @overrideString get endpoint => ''; @override Masa Depan getUkuran(int tahun) { return Future.value(measure); } ```

Menampilkan halaman demo

Kita sekarang memiliki semua tombol untuk menampilkan contoh demo dari `DetailPage`! Kita cukup mengganti instance `DataUsaApiClient` yang saat ini disediakan dengan membungkus `DetailScreen` kita dengan penyedia yang membuat instance `DemoDataUsaApiClient` sebagai gantinya! Dan itu saja - `DetailScreen` kita membaca instance demo ini, dan menggunakan data `demoMeasure` kita sebagai pengganti panggilan HTTP. ``` ListTile( title: const Text('Buka demo'), onTap: () { const demoMeasure = Measure( tahun 2022, populasi: 425484, bangsa: 'Amerika Serikat', ); Navigator.push( konteks, MaterialPageRoute( pengaturan: RouteSettings(argumen: demoMeasure.year), pembangun: (konteks) { return Penyedia( create: (konteks) => const DemoDataUsaApiClient(demoMeasure), child: const DetailScreen(), ); }, ), ); }, ) ```

Ulasan

Ini adalah contoh yang bagus dari _Inversion of control._ Widget `DetailScreen` kita tidak bertanggung jawab atas logika mendapatkan data lagi, tetapi mendelegasikannya ke objek klien khusus. Dan kita sekarang dapat membuat contoh demo layar, atau mengimplementasikan tes widget untuk layar kita! Luar biasa! 👏 Tapi kita bisa melakukan yang lebih baik lagi! Karena kita tidak dapat mensimulasikan status pemuatan, misalnya, kita tidak memiliki kontrol penuh atas perubahan status pada tingkat widget kita.

Langkah 3\. Manajemen status

/img/ https://clickup.com/blog/wp-content/uploads/2022/03/image-7-1400x607.png Manajemen pernyataan dalam aplikasi flutter /%img/ melalui ClickUp Ini adalah topik yang sedang hangat di Flutter! Saya yakin Anda sudah membaca banyak sekali thread panjang tentang orang-orang yang mencoba memilih solusi _manajemen state_ terbaik untuk Flutter. Dan untuk memperjelas, bukan itu yang akan kita bahas dalam artikel ini. Menurut kami, selama Anda memisahkan logika bisnis dari logika visual, Anda akan baik-baik saja! Membuat lapisan-lapisan ini sangat penting untuk pemeliharaan. Contoh kita sederhana, tetapi dalam aplikasi nyata, logika dapat dengan cepat menjadi kompleks dan pemisahan seperti itu membuatnya jauh lebih mudah untuk menemukan algoritma logika murni Anda. Subjek ini sering diringkas sebagai _manajemen state_. Dalam contoh ini, kita menggunakan `ValueNotifier` dasar bersama dengan `Provider`. Tetapi kita juga bisa menggunakan /href/ https://pub.dev/packages/flutter\_blok bergetar\_blok /%href/ atau /href/ https://pub.dev/packages/riverpod riverpod /%href/ (atau solusi lain), dan itu akan bekerja dengan baik juga. Prinsip-prinsipnya tetap sama, dan selama Anda memisahkan state dan logika Anda, bahkan memungkinkan untuk mem-porting basis kode Anda dari salah satu solusi lain. Pemisahan ini juga membantu kita untuk mengontrol setiap state dari widget kita sehingga kita dapat mengotak-atiknya dengan segala cara yang memungkinkan! /href/ https://dartpad.dev/embed-flutter.html?id=e148980534dae261ef1174a15f7ac5cf&split=80&theme=dark Kode Sumber & Demo Dart /%href/

Membuat state khusus

Alih-alih mengandalkan `AsyncSnapshot` dari kerangka kerja, kita sekarang merepresentasikan status layar sebagai objek `DetailState`. Penting juga untuk mengimplementasikan metode `hashCode` dan `operator ==` untuk membuat objek kita dapat dibandingkan berdasarkan nilai. Hal ini memungkinkan kita untuk mendeteksi jika dua instance harus dianggap berbeda. > 💡 Kode > > /href/ > https://pub.dev/packages/equatable > setara > /%href/ > > atau > > /href/ > https://pub.dev/packages/freezed > dibekukan > /%href/ > > paket-paket tersebut merupakan pilihan yang bagus untuk membantu Anda mengimplementasikan metode `hashCode` dan `operator ==`! ``` abstract class DetailState { const DetailState(this.year); int tahun terakhir; operator @overridebool == (Object other) => identik(this, other) || (other adalah DetailState && runtimeType == other.runtimeType && year == other.year); @overrideint get hashCode => runtimeType.hashCode ^ year; ``` Status kita selalu dikaitkan dengan `tahun`, tetapi kita juga memiliki empat kemungkinan status yang berbeda mengenai apa yang ingin kita tampilkan kepada pengguna: * `NotLoadedDetailState`: pembaruan data belum dimulai * `LoadingDetailState`: data sedang dimuat * `LoadedDetailState`: data telah berhasil dimuat dengan `measure` terkait * `NoDataDetailState`: data telah dimuat, tetapi tidak ada data yang tersedia * `UnknownErrorDetailState`: operasi gagal karena `error` yang tidak diketahui ``` class NotLoadedDetailState extends DetailState { const NotLoadedDetailState(int tahun) : super(tahun); } class LoadedDetailState extends DetailState { const LoadedDetailState({ required int tahun, required this.measure, }) : super(tahun); ukuran ukuran akhir Measure; operator @overridebool == (Objek lain) => identik (ini, lainnya) || (other adalah LoadedDetailState && measure == other.measure); @overrideint get hashCode => runtimeType.hashCode ^ measure.hashCode; } class NoDataDetailState extends DetailState { const NoDataDetailState(int tahun) : super(tahun); } class LoadingDetailState extends DetailState { const LoadingDetailState(int tahun) : super(tahun); } class UnknownErrorDetailState extends DetailState { const UnknownErrorDetailState({ required int tahun, required this.error, }) : super(tahun); kesalahan dinamis akhir; operator @overridebool == (Objek lain) => identik (ini, lainnya) || (other adalah UnknownErrorDetailState && tahun == other.tahun && error == other.error); @overrideint get hashCode => Object.hash(super.hashCode, error.has ``` State-state tersebut lebih jelas daripada `AsyncSnapshot`, karena state tersebut benar-benar merepresentasikan kasus penggunaan kita. Dan sekali lagi, hal ini membuat kode kita lebih mudah dipelihara! > 💡 Kami sangat merekomendasikan > > /href/ > https://pub.dev/packages/freezed#unionssealed-classes > Jenis serikat dari paket yang dibekukan > /%href/ > > untuk merepresentasikan kondisi logika Anda! Ini menambahkan banyak utilitas seperti metode `copyWith` atau `map`.

Menempatkan logika dalam sebuah Notifier

Sekarang kita memiliki representasi dari state kita, kita perlu menyimpan instance dari state tersebut di suatu tempat - itulah tujuan dari `DetailNotifier`. Ini akan menyimpan instance `DetailState` saat ini dalam properti `value` dan akan menyediakan metode untuk memperbarui state. Kami menyediakan state awal `NotLoadedDetailState`, dan metode `refresh` untuk memuat data dari `api` dan memperbarui `value` saat ini. ``` class DetailNotifier extends ValueNotifier { DetailNotifier({ diperlukan int tahun, required this.api, }) : super(DetailState.notLoaded(tahun)); final DataUsaApiClient api; int get tahun => nilai.tahun; Future refresh() async { if (value is! LoadingDetailState) { value = DetailState.loading(tahun); try { hasil akhir = await api.getMeasure(tahun); if (hasil != null) { nilai = DetailState.dimuat( tahun: tahun, ukuran: hasil, ); } else { nilai = DetailState.noData(tahun); } } catch (error) { nilai = DetailState.unknownError( tahun: tahun, kesalahan: kesalahan, ); } } } ```

Memberikan status untuk tampilan

Untuk menginstansiasi dan mengamati keadaan layar kita, kita juga bergantung pada penyedia dan `ChangeNotifierProvider`. Penyedia semacam ini secara otomatis mencari `ChangeListener` yang dibuat dan akan memicu pembuatan ulang dari `Consumer` setiap kali diberitahu adanya perubahan (ketika nilai notifier kita berbeda dengan nilai sebelumnya) ``` class DetailScreen extends StatelessWidget { const DetailScreen({ Kunci? kunci, }) : super(key: key); @menimpa Widget membangun (konteks BuildContext context) { tahun akhir = ModalRoute.of(context)!.settings.arguments as int; return ChangeNotifierProvider( create: (context) { notifier akhir = DetailNotifier( tahun: tahun, api: context.read(), ); notifier.refresh(); kembalikan notifier; }, anak Consumer( pembangun: (konteks, notifier, anak) { keadaan akhir = notifier.value; // ... }, ), ); } } ```

Ulasan

Bagus! Arsitektur aplikasi kami mulai terlihat cukup bagus. Semuanya dipisahkan ke dalam lapisan-lapisan yang terdefinisi dengan baik, dengan perhatian khusus! 🤗 Satu hal yang masih kurang untuk uji coba, kita ingin mendefinisikan `DetailState` saat ini untuk mengontrol keadaan `DetailScreen` yang terkait.

Langkah 4\. Widget tata letak khusus visual

/img/ https://clickup.com/blog/wp-content/uploads/2022/03/image-8-1400x607.png Widget tata letak khusus visual dalam aplikasi flutter /%img/ melalui ClickUp Pada langkah terakhir, kita memberikan tanggung jawab yang terlalu besar pada widget `DetailScreen`: widget ini bertanggung jawab untuk menginstansiasi `DetailNotifier`. Dan seperti yang telah kita lihat sebelumnya, kita mencoba menghindari tanggung jawab logika apa pun pada lapisan tampilan! Kita dapat dengan mudah mengatasi hal ini dengan membuat layer lain untuk widget layar kita: kita akan membagi widget `DetailScreen` menjadi dua: * `DetailScreen` bertanggung jawab untuk mengatur berbagai ketergantungan layar kita dari kondisi aplikasi saat ini (navigasi, pemberi notifikasi, status, layanan, ...), * `DetailLayout` hanya mengubah `DetailState` menjadi sebuah pohon widget khusus. Dengan menggabungkan keduanya, kita dapat dengan mudah membuat contoh demo/test `DetailLayout`, tetapi memiliki `DetailScreen` untuk kasus penggunaan yang sebenarnya dalam aplikasi kita. /href/ https://dartpad.dev/embed-flutter.html?id=6e4075480df18bfe42b932f419a231ac&split=80&theme=dark Kode Sumber & Demo Dart /%href/

Tata letak khusus

Untuk mencapai pemisahan yang lebih baik, kami memindahkan semua yang ada di bawah widget `Konsumen` ke widget `Tata Letak Detail` khusus. Widget baru ini hanya mengonsumsi data dan tidak bertanggung jawab atas instansiasi apa pun. Widget ini hanya mengubah status baca ke pohon widget tertentu. Panggilan `ModalRoute.of` dan instance `ChangeNotifierProvider` tetap berada di `DetailScreen`, dan widget ini hanya mengembalikan `DetailLayout` dengan pohon ketergantungan yang telah dikonfigurasi sebelumnya! Perbaikan kecil ini khusus untuk penggunaan provider, tetapi Anda akan melihat bahwa kami juga telah menambahkan `ProxyProvider` sehingga setiap widget turunan dapat secara langsung menggunakan `DetailState`. Hal ini akan memudahkan untuk meniru data. ``` class DetailScreen extends StatelessWidget { const DetailScreen({ Kunci? kunci, }) : super(key: key); @menimpa Widget membangun (konteks BuildContext context) { tahun akhir = ModalRoute.of(context)!.settings.arguments as int; return ChangeNotifierProvider( create: (context) { notifier akhir = DetailNotifier( tahun: tahun, api: context.read(), ); notifier.refresh(); mengembalikan notifier; }, child: anak: ProxyProvider( update: (konteks, nilai, sebelumnya) => nilai.nilai, child: const DetailLayout(), ), ); } } class DetailLayout extends StatelessWidget { const DetailLayout({ Kunci? kunci, }) : super(key: key); @menimpa Widget build(konteks BuildContext) { return Consumer( pembangun: (konteks, state, anak) { return Perancah( appBar: Bilah aplikasi ( title: Text('Tahun ${state.year}'), ), body: () { // ... }(), ); }, ); } ```

Mengekstrak widget sebagai kelas khusus

Jangan pernah ragu untuk mengekstrak pohon widget menjadi kelas khusus! Ini akan /href/ /blog?p=56354 meningkatkan kinerja /%href/ dan membuat kode lebih mudah dipelihara. Dalam contoh kita, kita membuat satu widget tata letak visual untuk setiap jenis state yang terkait: ``` if (state adalah NotLoadedDetailState || state adalah LoadingDetailState) { kembalikan const LoadingDetailLayout(); } if (state is LoadedDetailState) { return LoadedDetailLayout(state: state); } if (state is UnknownErrorDetailState) { return UnknownErrorDetailLayout(state: state); } return const NoDataDetailLayout(); ```

Contoh demo

Sekarang kita memiliki kontrol penuh atas apa yang dapat kita tiru dan tampilkan di layar kita! Kita hanya perlu membungkus `DetailLayout` dengan `Provider` untuk mensimulasikan keadaan layout. ``` ListTile( title: const Text('Buka demo yang telah dimuat'), onTap: () { Navigator.push( konteks, MaterialPageRoute( pembangun: (konteks) { return Penyedia.value( value: const DetailState.loaded( tahun 2022, measure: Measure( tahun: 2022, populasi: 425484, bangsa: 'Amerika Serikat', ), ), child: const DetailLayout(), ); }, ), ); }, ), ListTile( title: const Text('Buka demo "loading"'), onTap: () { Navigator.push( konteks, MaterialPageRoute( pembangun: (konteks) { return Penyedia.value( value: const DetailState.loading(2022), child: const DetailLayout(), ); }, ), ); }, ), ```

Kesimpulan

Menciptakan arsitektur perangkat lunak yang dapat dipelihara secara pasti tidaklah mudah! Mengantisipasi skenario masa depan dapat menuntut banyak usaha, tetapi saya harap beberapa tips yang saya bagikan dapat membantu Anda di masa depan! Contoh-contoh di atas mungkin terlihat sederhana - bahkan mungkin terlihat seperti rekayasa yang berlebihan - tetapi seiring dengan bertambahnya kompleksitas aplikasi Anda, memiliki standar-standar tersebut akan sangat membantu Anda! 💪 Selamat bersenang-senang dengan Flutter, dan ikuti blog ini untuk mendapatkan lebih banyak artikel teknis seperti ini! Tetap disini!