Flutter.dev: Manajemen status aplikasi sederhana

Halo. Pada bulan September, OTUS meluncurkan kursus baru , Flutter Mobile Developer . Menjelang permulaan kursus, kami secara tradisional telah menyiapkan terjemahan yang berguna untuk Anda.








Sekarang setelah Anda mengetahui tentang pemrograman UI deklaratif dan perbedaan antara status ephemeral dan status aplikasi , Anda siap mempelajari cara mengelola status aplikasi dengan mudah.



Kami akan menggunakan paketnya provider. Jika Anda baru mengenal Flutter dan tidak memiliki alasan kuat untuk memilih pendekatan yang berbeda (Redux, Rx, hooks, dll.), Ini mungkin pendekatan terbaik untuk memulai. Paket provider ini mudah dipelajari dan tidak memerlukan banyak kode. Dia juga beroperasi dengan konsep yang dapat diterapkan di semua pendekatan lainnya.



Namun, jika Anda sudah memiliki banyak pengalaman dalam mengelola status dari kerangka kerja reaktif lainnya, Anda dapat mencari paket dan tutorial lain yang tercantum di halaman opsi .



Contoh







Sebagai contoh, perhatikan aplikasi sederhana berikut.



Aplikasi ini memiliki dua layar terpisah: katalog dan keranjang belanja (diwakili oleh widget MyCatalogdan MyCartmasing - masing). Dalam hal ini, ini adalah aplikasi belanja, tetapi Anda dapat membayangkan struktur yang sama dalam aplikasi jejaring sosial sederhana (ganti katalog dengan "dinding" dan keranjang dengan "favorit").



Layar katalog menyertakan bilah aplikasi yang dapat disesuaikan ( MyAppBar) dan tampilan bergulir dari beberapa item daftar ( MyListItems).



Berikut adalah aplikasi dalam bentuk pohon widget:







Jadi, kami memiliki minimal 5 subclass Widget. Banyak dari mereka membutuhkan akses untuk menyatakan bahwa mereka tidak memiliki. Misalnya, masing-masingMyListItemharus dapat menambahkan diri Anda ke keranjang. Mereka mungkin juga perlu memeriksa apakah item yang sedang ditampilkan ada di keranjang.



Ini membawa kita ke pertanyaan pertama kita: di mana kita harus meletakkan keadaan bucket saat ini?



Kondisi meningkat



Di Flutter, masuk akal untuk menempatkan status di atas widget yang menggunakannya.



Untuk apa? Dalam kerangka kerja deklaratif seperti Flutter, jika Anda ingin mengubah antarmuka pengguna, Anda harus membangunnya kembali. Anda tidak bisa pergi dan menulis MyCart.updateWith(somethingNew). Dengan kata lain, sulit untuk memaksa mengubah widget dari luar dengan memanggil metode di atasnya. Dan bahkan jika Anda bisa membuatnya berfungsi, Anda akan melawan kerangka kerja alih-alih membiarkannya membantu Anda.



// :   
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}




Bahkan jika Anda membuat kode di atas berfungsi, Anda harus berurusan MyCartdengan yang berikut di widget :



// :   
Widget build(BuildContext context) {
  return SomeWidget(
//   .
  );
}

void updateWith(Item item) {
// -      UI.
}




Anda perlu mempertimbangkan status UI saat ini dan menerapkan data baru ke dalamnya. Sulit untuk menghindari kesalahan di sini.



Di Flutter, Anda membuat widget baru setiap kali kontennya berubah. Alih-alih MyCart.updateWith(somethingNew)(pemanggilan metode), Anda menggunakan MyCart(contents)(konstruktor). Karena Anda hanya dapat membuat widget baru di metode build induknya, jika ingin mengubahnya contentsharus di dalam induk MyCartatau lebih tinggi.



// 
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}




Sekarang MyCarthanya memiliki satu jalur eksekusi kode untuk membuat versi antarmuka pengguna apa pun.



// 
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    //     ,    .
    // ยทยทยท
  );
}




Dalam contoh kita, contentsseharusnya dalam MyApp. Setiap kali berubah, itu membangun kembali MyCart di atas (lebih lanjut tentang itu nanti). MyCartDengan cara ini Anda tidak perlu khawatir tentang siklus proses - ini hanya mendeklarasikan apa yang harus ditampilkan untuk konten tertentu. Jika berubah, widget lama MyCartakan hilang dan digantikan sepenuhnya oleh yang baru.







Inilah yang kami maksud ketika kami mengatakan bahwa widget tidak dapat diubah. Mereka tidak berubah - mereka diganti.



Sekarang kita tahu di mana harus meletakkan status bucket, mari kita lihat cara mengaksesnya.



Akses negara



Ketika pengguna mengklik salah satu item di katalog, itu ditambahkan ke keranjang. Tapi karena gerobak sudah habis MyListItem, bagaimana kita melakukan ini?



Opsi sederhana adalah menyediakan panggilan balik yang MyListItemdapat dipanggil saat diklik. Fungsi Dart adalah objek kelas satu, jadi Anda bisa meneruskannya sesuka Anda. Jadi, secara internal, MyCatalogAnda dapat menentukan yang berikut:



@override
Widget build(BuildContext context) {
  return SomeWidget(
   //  ,      .
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}




Ini berfungsi dengan baik, tetapi untuk status aplikasi yang perlu Anda ubah dari banyak tempat berbeda, Anda harus meneruskan banyak callback, yang menjadi membosankan dengan cukup cepat.



Untungnya, Flutter memiliki mekanisme yang memungkinkan widget menyediakan data dan layanan untuk turunannya (dengan kata lain, tidak hanya untuk turunannya, tetapi juga widget hilir). Seperti yang Anda harapkan dari Flutter, di mana Semuanya adalah Widget , mekanisme ini adalah jenis hanya khusus widget: InheritedWidget, InheritedNotifier, InheritedModeldan lain-lain. Kami tidak akan menjelaskannya di sini karena mereka sedikit keluar dari garis dengan apa yang kami coba lakukan.



Sebagai gantinya, kami akan menggunakan paket yang berfungsi dengan widget tingkat rendah tetapi mudah digunakan. Ini namanya provider.



Dengan, providerAnda tidak perlu khawatir tentang panggilan balik atau InheritedWidgets. Tetapi Anda perlu memahami 3 konsep:



  • ChangeNotifier
  • ChangeNotifierProvider
  • Konsumen




ChangeNotifier



ChangeNotifierAdalah kelas sederhana yang termasuk dalam Flutter SDK yang memberikan pemberitahuan perubahan status kepada pendengarnya. Dengan kata lain, jika ada ChangeNotifier, Anda dapat berlangganan perubahannya. (Ini adalah bentuk Observable - bagi mereka yang tidak terbiasa dengan istilah tersebut.)



ChangeNotifierIn provideradalah salah satu cara untuk merangkum status aplikasi. Untuk aplikasi yang sangat sederhana, Anda bisa melakukannya dengan satu ChangeNotifier. Dalam model yang lebih kompleks, Anda akan memiliki beberapa model dan karenanya beberapa ChangeNotifiers. (Anda tidak perlu menggunakan ChangeNotifiersama sekali provider, tetapi kelas ini mudah digunakan.)



Dalam aplikasi contoh belanja, kami ingin mengelola status keranjang dalam ChangeNotifier. Kami membuat kelas baru yang memperluasnya, misalnya:



class CartModel extends ChangeNotifier {
///    .
  final List<Item> _items = [];

  ///     .
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  ///      ( ,      42 ).
  int get totalPrice => _items.length * 42;

  ///  [item]  .   [removeAll] -     .
  void add(Item item) {
    _items.add(item);
    //    ,    ,   .
    notifyListeners();
  }

  ///     .
  void removeAll() {
    _items.clear();
    //    ,    ,   .
    notifyListeners();
  }
}




Satu-satunya bagian kode khusus ChangeNotifieradalah panggilan notifyListeners(). Panggil metode ini setiap kali model berubah sedemikian rupa sehingga dapat tercermin di UI aplikasi Anda. Segala sesuatu yang lain CartModeladalah model itu sendiri dan logika bisnisnya.



ChangeNotifieradalah bagian dari flutter:foundationdan tidak bergantung pada kelas level yang lebih tinggi di Flutter. Ini mudah untuk diuji (Anda bahkan tidak perlu menggunakan pengujian widget untuk itu). Misalnya, inilah unit test sederhana CartModel:



test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});




ChangeNotifierProvider



ChangeNotifierProviderAdalah widget yang menyediakan instance untuk ChangeNotifieranak-anaknya. Itu datang dalam satu paket provider.



Kami sudah tahu di mana harus meletakkannya ChangeNotifierProvider: di atas widget yang membutuhkan akses ke sana. Dalam hal CartModelini menyiratkan sesuatu di atas MyCartdan MyCatalog.



Anda tidak ingin memposting ChangeNotifierProviderlebih tinggi dari yang diperlukan (karena Anda tidak ingin mencemari ruang lingkup). Tapi dalam kasus kami, satu-satunya widget yang berakhir MyCartdan MyCatalog- itu MyApp.



void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: MyApp(),
    ),
  );
}




Perhatikan bahwa kami mendefinisikan konstruktor yang membuat instance baru CartModel. ChangeNotifierProvidercukup pintar untuk tidak dibangun kembali CartModelkecuali benar-benar diperlukan. Ini juga secara otomatis memanggil dispose () pada CartModel ketika instance tidak lagi diperlukan.



Jika Anda ingin menyediakan lebih dari satu kelas, Anda dapat menggunakan MultiProvider:



void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}




Konsumen



Sekarang ini CartModeldisediakan untuk widget dalam aplikasi kita melalui deklarasi ChangeNotifierProviderdi atas, kita dapat mulai menggunakannya.



Ini dilakukan melalui widget Consumer.



return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);




Kita harus menentukan jenis model yang ingin kita akses. Dalam hal ini, kami membutuhkannya CartModel, jadi kami menulis Consumer<CartModel>. Jika Anda tidak menetapkan generic ( <CartModel>), paket providertidak akan dapat membantu Anda. provideradalah berbasis tipe dan tanpa tipe itu tidak akan mengerti apa yang Anda inginkan.



Satu-satunya argumen yang diperlukan untuk widget Consumerini adalah builder. Builder adalah fungsi yang dipanggil saat ada perubahan ChangeNotifier. (Dengan kata lain, saat Anda memanggil notifyListeners()model Anda, semua metode pembangun dari semua widget yang relevan Consumerdipanggil.)



Konstruktor dipanggil dengan tiga argumen. Yang pertama adalah context, yang juga Anda dapatkan di setiap metode build.

Argumen kedua untuk fungsi pembangun adalah sebuah instanceChangeNotifier... Inilah yang kami minta sejak awal. Anda dapat menggunakan data model untuk menentukan bagaimana tampilan antarmuka pengguna pada suatu titik tertentu.



Argumen ketiga adalah child, diperlukan untuk optimasi. Jika Anda memiliki subtree widget besar di bawah Anda Consumeryang tidak berubah ketika model berubah, Anda dapat membuatnya sekali dan mendapatkannya melalui builder.



return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
        children: [
          //   SomeExhibitedWidget,    .
          child,
          Text("Total price: ${cart.totalPrice}"),
        ],
      ),
  //    .
  child: SomeExpensiveWidget(),
);




Yang terbaik adalah menempatkan widget Konsumen Anda sedalam mungkin di pohon. Anda tidak ingin membangun kembali sebagian besar antarmuka pengguna hanya karena beberapa detail telah berubah di suatu tempat.



//   
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);




Daripada ini:



//  
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);




Provider.of



Terkadang Anda tidak benar-benar membutuhkan data dalam model untuk mengubah antarmuka pengguna, tetapi Anda masih memerlukan akses ke sana. Misalnya, tombol ClearCartmemungkinkan pengguna untuk menghapus semuanya dari keranjang. Tidak perlu menampilkan isi gerobak, cukup panggil clear().



Kita bisa menggunakannya Consumer<CartModel>untuk itu, tapi itu akan sia-sia. Kami akan meminta kerangka kerja untuk membangun kembali widget, yang tidak perlu dibangun kembali.



Untuk kasus penggunaan ini, kita dapat menggunakan Provider.ofparameter yang listendisetel ke false.



Provider.of<CartModel>(context, listen: false).removeAll();




Menggunakan baris di atas dalam metode build tidak akan membangun kembali widget ini saat dipanggil notifyListeners .



Menyatukan semuanya



Anda dapat melihat contoh yang dibahas di artikel ini. Jika Anda mencari sesuatu yang sedikit lebih sederhana, lihat seperti apa aplikasi Penghitung sederhana yang dibangun dengan penyedia .



Saat Anda siap untuk bermain dengan providerdiri sendiri, ingatlah untuk menambahkan ketergantungannya pada Anda terlebih dahulu pubspec.yaml.



name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^3.0.0

dev_dependencies:
  # ...




Sekarang Anda bisa 'package:provider/provider.dart'; dan mulai membangun ...






All Articles