Detail tentang paket Penyedia untuk Flutter

Halo, Habr!

Rencana jangka menengah kami meliputi rilis buku Flutter. Mengenai bahasa Dart sebagai topik, kami masih mengambil posisi yang lebih hati-hati, jadi kami akan mencoba mengevaluasi relevansinya sesuai dengan hasil artikel ini. Ini akan fokus pada paket Penyedia dan, karenanya, pada manajemen negara di Flutter.

Penyedia adalah paket manajemen negara yang ditulis oleh Remy Rusle dan diadopsi oleh Google dan komunitas Flutter. Tapi apa itu manajemen negara? Sebagai permulaan, apa syaratnya? Biarkan saya mengingatkan Anda bahwa negara hanyalah data untuk mewakili UI di aplikasi Anda. Manajemen negara adalah pendekatan untuk membuat data ini, mengakses, menangani, dan membuangnya. Untuk lebih memahami paket Penyedia, kami secara singkat menguraikan sejarah manajemen negara di Flutter.

1. StatefulWidget


StatelessWidget adalah komponen UI sederhana yang hanya menampilkan ketika memiliki data. Tidak StatelessWidgetada "memori"; itu dibuat dan dihancurkan seperlunya. Flutter juga memiliki StatefulWidget , di mana ada memori, berkat satelit yang berumur panjang - objek State . Kelas ini memiliki metode setState(), ketika dipanggil, widget diluncurkan yang membangun kembali negara dan menampilkannya dalam bentuk baru. Ini adalah bentuk paling sederhana manajemen negara Flutter yang disediakan di luar kotak. Ini adalah contoh dengan tombol yang selalu menampilkan waktu terakhir kali ditekan:

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

Jadi apa masalahnya dengan pendekatan ini? Misalkan aplikasi Anda memiliki beberapa status global yang disimpan di root StatefulWidget. Ini berisi data yang dimaksudkan untuk digunakan di berbagai bagian UI. Data ini dibagikan dan diteruskan ke setiap widget anak dalam bentuk parameter. Setiap peristiwa yang direncanakan untuk mengubah data ini kemudian muncul dalam bentuk panggilan balik. Dengan demikian, melalui semua widget perantara, banyak parameter dan panggilan balik ditransmisikan, yang segera dapat menyebabkan kebingungan. Lebih buruk lagi, setiap pembaruan ke root yang disebutkan di atas akan menyebabkan pembangunan kembali seluruh pohon widget, yang tidak efisien.

2. Warisan Warisan


InheritedWidget adalah widget khusus yang turunannya dapat mengaksesnya tanpa tautan langsung. Hanya dengan beralih ke InheritedWidget, widget yang digunakan dapat mendaftar untuk membangun kembali otomatis, yang akan terjadi ketika membangun kembali widget leluhur. Teknik ini memungkinkan Anda untuk mengatur pembaruan UI secara lebih efisien. Alih-alih membangun kembali aplikasi dalam jumlah besar sebagai respons terhadap perubahan kecil dalam kondisi, Anda dapat memilih hanya widget tertentu yang perlu dibangun kembali. Anda sudah bekerja dengan InheritedWidgetsetiap kali Anda menggunakan MediaQuery.of(context)atau Theme.of(context). Benar, kecil kemungkinannya Anda telah mengimplementasikan InheritedWidget Anda sendiri dengan pelestarian negara. Faktanya adalah bahwa mengimplementasikannya dengan benar tidaklah mudah.

3. ScopedModel


ScopedModel adalah paket yang dibuat pada tahun 2017 oleh Brian Egan, yang membuatnya mudah digunakan InheritedWidgetuntuk menyimpan status aplikasi. Pertama, Anda perlu membuat objek keadaan yang mewarisi dari Model , dan kemudian menyebutnya notifyListeners()ketika propertinya berubah. Situasi ini mengingatkan pada implementasi antarmuka PropertyChangeListener di Jawa.

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Untuk memberikan objek keadaan kita, kita membungkus objek ini dalam widget ScopedModeldi root aplikasi kita:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

Sekarang setiap widget turunan akan dapat diakses MyModelmenggunakan widget ScopedModelDescendant . Contoh model diteruskan ke parameter builder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

Setiap widget turunan juga akan dapat memperbarui model, yang secara otomatis akan memprovokasi pembangunan kembali apa pun ScopedModelDescendants(asalkan model kami memanggil dengan benar notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModelmendapatkan popularitas di Flutter sebagai alat untuk manajemen negara, tetapi penggunaannya terbatas pada penyediaan objek yang mewarisi kelas Modeldan menggunakan pola pemberitahuan perubahan ini.

4. BLoC


Pada konferensi Google I / O '18 , pola Komponen Logika Bisnis (BLoC) diperkenalkan , yang berfungsi sebagai alat lain untuk menarik status dari widget. Kelas BLoC adalah komponen non-UI berumur panjang yang mempertahankan status dan mengeksposnya sebagai aliran dan penerima. Mengambil logika negara dan bisnis di luar UI, Anda dapat mengimplementasikan widget sesederhana StatelessWidgetdan menggunakan StreamBuilder untuk pembangunan kembali otomatis. Akibatnya, widget "menjadi bodoh", dan menjadi lebih mudah untuk diuji.

Contoh kelas BLoC:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

Masalah dengan pola BLoC adalah tidak jelas bagaimana membuat dan menghancurkan objek BLoC. Bagaimana instance dibuat myBlocdalam contoh di atas? Bagaimana kita menelepon dispose()untuk menyingkirkannya? Streaming memerlukan penggunaan StreamController, yang harus dilakukan closedsegera setelah tidak diperlukan - ini dilakukan untuk mencegah kebocoran memori. (Tidak ada yang namanya destruktor kelas di Dart; hanya kelas Statedi yang StatefulWidgetmemiliki metode dispose()). Selain itu, tidak jelas cara membagikan BLoC ini di antara banyak widget. Seringkali sulit bagi pengembang untuk menguasai BLoC. Ada beberapa paket yang berusaha menyederhanakan ini.

5. Penyedia


ProviderAdalah paket yang ditulis pada tahun 2018 oleh Remy Rusle, mirip dengan ScopedModel, tetapi yang fungsinya tidak terbatas, menyediakan subkelas Model. Ini juga pembungkus yang menyimpulkan InheritedWidget, tetapi penyedia dapat menyediakan objek negara apa pun, termasuk BLoC, stream, futures, dan lainnya. Karena penyedia ini sangat sederhana dan fleksibel, Google mengumumkan pada konferensi Google I / O '19 bahwa di masa depan itu Providerakan menjadi paket pilihan untuk mengelola negara. Tentu saja, paket lain juga diperbolehkan, tetapi jika Anda ragu, Google merekomendasikan untuk berhenti Provider.

Providerdibangun "dengan widget, untuk widget."Providermemungkinkan Anda untuk menempatkan objek apa pun dengan status di pohon widget dan membuka akses ke sana untuk widget lainnya (anak). Ini juga Providermembantu mengelola masa objek negara dengan menginisialisasi mereka dengan data dan melakukan pembersihan setelah dihapus dari pohon widget. Oleh karena itu, Providersangat cocok bahkan untuk menerapkan komponen BLoC atau dapat berfungsi sebagai dasar untuk solusi manajemen negara lainnya! Atau hanya digunakan untuk mengimplementasikan dependensi - istilah mewah yang berarti mentransfer data ke widget dengan cara yang memungkinkan Anda untuk melonggarkan koneksi dan meningkatkan testabilitas kode. Akhirnya,Providerhadir dengan sekumpulan kelas khusus, berkat yang bahkan lebih nyaman digunakan. Selanjutnya, kita akan melihat lebih dekat pada masing-masing kelas ini.

  • Penyedia Dasar
  • ChangeNotifierProvider
  • StreamProvider
  • Futureprovider
  • ValueListenableProvider
  • MultiProvider
  • Proxyprovider

Instalasi


Untuk menggunakannya Provider, pertama-tama tambahkan dependensi ke file kami pubspec.yaml:

provider: ^3.0.0

Kemudian kami mengimpor paket di Providertempat yang dibutuhkan:

import 'package:provider/provider.dart';

Penyedia basis

Membuat basis Provider di root aplikasi kami; ini akan berisi contoh dari model kami:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

Parameter buildermenciptakan instance MyModel. Jika Anda ingin memberikan contoh yang ada padanya, gunakan konstruktor di sini Provider.value.

Kemudian Anda dapat menggunakan instance model ini di mana saja MyApp, menggunakan widget Consumer:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

Dalam contoh di atas, kelas MyWidgetmendapat instance MyModelmenggunakan widget Konsumen . Widget ini memberi kami builderberisi objek kami di parameter value.

Sekarang, apa yang harus kita lakukan jika kita ingin memperbarui data dalam model kita? Katakanlah kita memiliki widget lain di mana, ketika sebuah tombol diklik, properti harus diperbarui foo:

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

Perhatikan sintaksis khusus yang digunakan untuk mengakses instance MyModel. Secara fungsional, ini setara dengan mengakses widget Consumer. Widget Consumerberguna dalam kasus-kasus ketika kode tidak dapat dengan mudah mendapatkan tautan BuildContext.

Menurut Anda apa yang akan terjadi dengan widget asli MyWidgetyang kami buat sebelumnya? Akankah makna baru ditampilkan di dalamnya bar? Sayangnya tidak . Tidak dimungkinkan untuk mendengarkan perubahan pada objek Dart tradisional lama (setidaknya tanpa refleksi, yang tidak disediakan dalam Flutter). Dengan demikian, Providertidak akan dapat "melihat" bahwa kami telah memperbarui properti dengan benar foodan memerintahkan widget untuk MyWidgetdiperbarui sebagai tanggapan.

ChangeNotifierProvider

Tapi ada harapan! Anda dapat membuat kelas kami MyModelmenerapkan kenajisan ChangeNotifier. Dibutuhkan sedikit waktu untuk mengubah implementasi model kami dan memanggil metode khusus notifyListeners()setiap kali salah satu properti kami berubah. Ini bekerja dengan cara yang kira-kira sama ScopedModel, tetapi dalam hal ini bagus bahwa Anda tidak perlu mewarisi dari kelas model tertentu. Sudah cukup untuk mewujudkan pencampuran ChangeNotifier. Begini tampilannya:

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

Seperti yang Anda lihat, kami mengganti properti kami foodengan getterdan setter, didukung oleh variabel pribadi _foo. Dengan cara ini kita dapat "mencegat" setiap perubahan yang dilakukan pada properti foo dan membiarkan pendengar kita tahu bahwa objek kita telah berubah.

Sekarang, dari luar Provider, kita dapat mengubah implementasi kita sehingga menggunakan kelas yang berbeda yang disebut ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

Seperti ini! Sekarang, ketika kami OtherWidgetmemperbarui properti foodalam instance MyModel, itu MyWidgetakan secara otomatis memperbarui untuk mencerminkan perubahan ini. Keren kan?

Ngomong-ngomong. Anda mungkin memperhatikan penangan tombol OtherWidgetyang dengannya kami menggunakan sintaks berikut:

final model = Provider.of<MyModel>(context);

Secara default, sintaks ini secara otomatis akan menyebabkan pembangunan kembali instance OtherWidgetsegera setelah model berubah MyModel. Mungkin kita tidak membutuhkan ini. Pada akhirnya, itu OtherWidgethanya berisi tombol yang tidak berubah sama sekali ketika nilainya berubah MyModel. Untuk menghindari pembangunan kembali, Anda dapat menggunakan sintaks berikut untuk mengakses model kami tanpa mendaftar untuk membangun kembali:

final model = Provider.of<MyModel>(context, listen: false);

Ini adalah pesona lain yang disediakan dalam paket Providerbegitu saja.

StreamProvider

Sekilas, tidak jelas mengapa itu diperlukan StreamProvider. Pada akhirnya, Anda bisa menggunakan yang biasa saja StreamBuilderjika Anda perlu mengonsumsi streaming di Flutter. Misalnya, di sini kita mendengarkan aliran yang onAuthStateChangeddisediakan oleh FirebaseAuth:

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

Untuk melakukan hal yang sama dengan bantuan Provider, kami dapat menyediakan streaming melalui StreamProviderakar dari aplikasi kami:

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

Kemudian konsumsilah widget anak, seperti yang biasa dilakukan dengan Provider:

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

Kode widget kami tidak hanya menjadi lebih bersih, tetapi juga mengabstraksi fakta bahwa data berasal dari aliran. Jika kita memutuskan untuk mengubah implementasi basis, misalnya, menjadi FutureProvider, maka tidak diperlukan perubahan pada kode widget. Seperti yang akan Anda lihat, ini berlaku untuk semua penyedia lain yang ditunjukkan di bawah ini.

FutureProvider

Mirip dengan contoh di atas, ini FutureProvideradalah alternatif dari standar FutureBuildersaat bekerja dengan widget. Berikut ini sebuah contoh:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

Untuk menggunakan nilai ini di widget anak, kami menggunakan implementasi yang sama Consumerseperti pada contoh di StreamProvideratas.

ValueListenableProvider

ValueListenable adalah antarmuka Dart yang diimplementasikan oleh kelas ValueNotifier yang mengambil nilai dan memberi tahu pendengar ketika ia berubah ke nilai lain. Sebagai contoh, dimungkinkan untuk membungkus bilangan bulat bilangan dalam kelas model sederhana:

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

Saat bekerja dengan tipe kompleks, ia ValueNotifiermenggunakan operator dari ==objek yang disimpan di dalamnya untuk menentukan apakah nilainya telah berubah.
Mari kita buat yang paling sederhana Provider, yang akan berisi model utama kita, dan itu akan diikuti oleh properti mendengarkan Consumerbersarang :ValueListenableProvidercounter

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

Harap dicatat bahwa penyedia bersarang ini bertipe int. Mungkin ada yang lain. Jika Anda memiliki beberapa penyedia dari jenis yang sama terdaftar, Penyedia akan mengembalikan "terdekat" (leluhur terdekat).

Berikut cara mendengarkan properti counterdari widget anak apa pun:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

Tapi di sini cara memperbarui properti counterdari widget lain. Harap dicatat: kami membutuhkan akses ke salinan asli MyModel.

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

Jika Anda menggunakan banyak widgetProvider, maka di root aplikasi Anda mendapatkan struktur jelek dari banyak lampiran:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProviderMemungkinkan Anda mendeklarasikan semuanya pada tingkat yang sama. Ini hanya gula sintaksis: pada level intra-sistem, mereka semua tetap bersarang.

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider adalah kelas menarik yang ditambahkan dalam rilis paket ketigaProvider. Hal ini memungkinkan Anda untuk menyatakan penyedia bahwa mereka sendiri tergantung pada penyedia lain, hingga enam pada satu. Dalam contoh ini, kelas Bar spesifik-instanceFoo. Ini berguna ketika mengkompilasi kumpulan root dari layanan yang saling bergantung satu sama lain.

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

Argumen tipe generik pertama adalah tipe yang menjadi sandaran Anda ProxyProvider, dan yang kedua adalah tipe yang dikembalikan.

Cara mendengarkan banyak penyedia sekaligus


Bagaimana jika kita ingin satu widget untuk mendengarkan banyak penyedia dan membangun kembali ketika salah satu dari mereka berubah? Anda dapat mendengarkan hingga 6 penyedia sekaligus menggunakan opsi widget Consumer. Kami akan menerima contoh sebagai parameter metode tambahan builder.

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

Kesimpulan


Saat digunakan, InheritedWidget Providerini memungkinkan Anda untuk mengelola negara seperti biasa di Flutter. Ini memungkinkan widget untuk mengakses objek negara dan mendengarkannya sedemikian rupa sehingga mekanisme notifikasi yang mendasarinya diabstraksikan. Lebih mudah untuk mengatur masa pakai objek negara dengan membuat titik jangkar untuk membuat objek ini sesuai kebutuhan dan menyingkirkannya saat diperlukan. Mekanisme ini dapat digunakan untuk dengan mudah mengimplementasikan dependensi dan bahkan sebagai dasar untuk opsi manajemen negara yang lebih maju. Dengan restu Google dan dukungan yang semakin besar di komunitas Flutter, paket ini Providertelah menjadi layak untuk dicoba tanpa penundaan!

All Articles