Baru-baru ini, saya harus menerapkan panduan orientasi untuk ClickUp pendatang baru! Ini adalah tugas yang sangat penting karena banyak pengguna baru yang akan menemukan platform dengan iklan yang sangat lucu yang kami tayangkan di Super Bowl ! ✨
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 Universitas ClickUp 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 keDetailScreen
dengan argumen navigasi yang disetel ke tahun yang dipilih.DetailScreen
: mendapatkan tahun dari argumen navigasi, memanggilaPI datausa.io 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
melalui ClickUp
Implementasi yang paling jelas untuk aplikasi kita, adalah dengan menggunakan satu StatefulWidget
untuk seluruh logika.
Kode Sumber & Demo Dart
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
aPI datausa.io
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<Map<dinamis, dinamis>?> _masa depan;
void didChangeDependencies() {
super.didChangeDependencies();
tahun akhir = ModalRoute.of(context)!.settings.arguments as int;
if (_tahun != tahun) {
_masa depan = _loadMeasure(tahun);
}
}
Future<Map<dinamis, dinamis>?> _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<dynamic>;
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<Map<dinamis, dinamis>?>(
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
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! 👀 Kode Sumber & Demo Dart
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<Measure?> 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<dynamic>;
if (data.isNotEmpty) {
return Measure.fromJson(data.first as Map<String, Object?>);
}
return null;
}
Menyediakan klien API
Untuk contoh kita, kita menggunakan
penyedia
untuk menyuntikkan sebuah instance DataUsaApiClient
di akar pohon kita.
Penyedia <DataUsaApiClient>(
create: (konteks) => const DataUsaApiClient(),
child: const MaterialApp(
home: HomePage(),
),
)
Menggunakan klien API
Penyedia mengizinkan widget turunan apa pun ( seperti kami _DetailScreen_)_ untuk membaca
DataUsaApiClientterdekat yang ada di bagian atas dalam pohon. Kita kemudian dapat menggunakan metode
getMeasureuntuk 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<DataUsaApiClient>();
_masa depan = api.getMeasure(tahun);
}
}
Klien API Demo
Sekarang kita bisa memanfaatkannya!
Jika Anda tidak tahu: kelas apa saja
di dart juga secara implisit mendefinisikan antarmuka yang terkait
. 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<Ukuran?> 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<DataUsaApiKlien>(
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
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
bergetar_blok
atau
riverpod
(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! Kode Sumber & Demo Dart
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 setara atau dibekukan paket-paket tersebut merupakan pilihan yang bagus untuk membantu Anda mengimplementasikan metode
hashCode
danoperator ==
!
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 dimulaiLoadingDetailState
: data sedang dimuatLoadedDetailState
: data telah berhasil dimuat denganmeasure
terkaitNoDataDetailState
: data telah dimuat, tetapi tidak ada data yang tersediaUnknownErrorDetailState
: operasi gagal karenaerror
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 Jenis serikat dari paket yang dibekukan untuk merepresentasikan kondisi logika Anda! Ini menambahkan banyak utilitas seperti metode
copyWith
ataumap
.
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<DetailState> {
DetailNotifier({
diperlukan int tahun,
required this.api,
}) : super(DetailState.notLoaded(tahun));
final DataUsaApiClient api;
int get tahun => nilai.tahun;
Future<void> 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<DetailNotifier>(
create: (context) {
notifier akhir = DetailNotifier(
tahun: tahun,
api: context.read<DataUsaApiClient>(),
);
notifier.refresh();
kembalikan notifier;
},
anak Consumer<DetailNotifier>(
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
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 mengubahDetailState
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.
Kode Sumber & Demo Dart
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<DetailNotifier>(
create: (context) {
notifier akhir = DetailNotifier(
tahun: tahun,
api: context.read<DataUsaApiClient>(),
);
notifier.refresh();
mengembalikan notifier;
},
child: anak: ProxyProvider<DetailNotifier, DetailState>(
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<DetailState>(
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 meningkatkan kinerja 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<DetailState>
untuk mensimulasikan keadaan layout.
ListTile(
title: const Text('Buka demo yang telah dimuat'),
onTap: () {
Navigator.push(
konteks,
MaterialPageRoute(
pembangun: (konteks) {
return Penyedia<DetailState>.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<DetailState>.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!