Pemisahan Masalah dalam Aplikasi Flutter
Engineering at ClickUp

Pemisahan Masalah dalam Aplikasi Flutter

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 ke DetailScreen 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

Pendekatan naif Stateful Widget

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

Pembalikan kontrol ke Penyedia dan Klien api

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 membacaDataUsaApiClientterdekat yang ada di bagian atas dalam pohon. Kita kemudian dapat menggunakan metodegetMeasureuntuk memulaiFuture` 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

Manajemen pernyataan dalam aplikasi flutter

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 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 Jenis serikat dari paket yang dibekukan 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<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

Widget tata letak khusus visual dalam aplikasi flutter

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. 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!