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 StatelessWidget
ada "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 InheritedWidget
setiap 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 InheritedWidget
untuk 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 ScopedModel
di root aplikasi kita:ScopedModel<MyModel>(
model: MyModel(),
child: MyApp(...)
)
Sekarang setiap widget turunan akan dapat diakses MyModel
menggunakan 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';
},
);
}
}
ScopedModel
mendapatkan popularitas di Flutter sebagai alat untuk manajemen negara, tetapi penggunaannya terbatas pada penyediaan objek yang mewarisi kelas Model
dan 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 StatelessWidget
dan 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 myBloc
dalam contoh di atas? Bagaimana kita menelepon dispose()
untuk menyingkirkannya? Streaming memerlukan penggunaan StreamController
, yang harus dilakukan closed
segera setelah tidak diperlukan - ini dilakukan untuk mencegah kebocoran memori. (Tidak ada yang namanya destruktor kelas di Dart; hanya kelas State
di yang StatefulWidget
memiliki 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
Provider
Adalah 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 Provider
akan menjadi paket pilihan untuk mengelola negara. Tentu saja, paket lain juga diperbolehkan, tetapi jika Anda ragu, Google merekomendasikan untuk berhenti Provider
.Provider
dibangun "dengan widget, untuk widget."Provider
memungkinkan Anda untuk menempatkan objek apa pun dengan status di pohon widget dan membuka akses ke sana untuk widget lainnya (anak). Ini juga Provider
membantu mengelola masa objek negara dengan menginisialisasi mereka dengan data dan melakukan pembersihan setelah dihapus dari pohon widget. Oleh karena itu, Provider
sangat 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,Provider
hadir 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 Provider
tempat yang dibutuhkan:import 'package:provider/provider.dart';
Penyedia basisMembuat basis Provide
r di root aplikasi kami; ini akan berisi contoh dari model kami:Provider<MyModel>(
builder: (context) => MyModel(),
child: MyApp(...),
)
Parameter builder
menciptakan 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 MyWidget
mendapat instance MyModel
menggunakan widget Konsumen . Widget ini memberi kami builder
berisi 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 Consumer
berguna dalam kasus-kasus ketika kode tidak dapat dengan mudah mendapatkan tautan BuildContext
.Menurut Anda apa yang akan terjadi dengan widget asli MyWidget
yang 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, Provider
tidak akan dapat "melihat" bahwa kami telah memperbarui properti dengan benar foo
dan memerintahkan widget untuk MyWidget
diperbarui sebagai tanggapan.ChangeNotifierProviderTapi ada harapan! Anda dapat membuat kelas kami MyModel
menerapkan 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 foo
dengan getter
dan 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 OtherWidget
memperbarui properti foo
dalam instance MyModel
, itu MyWidget
akan secara otomatis memperbarui untuk mencerminkan perubahan ini. Keren kan?Ngomong-ngomong. Anda mungkin memperhatikan penangan tombol OtherWidget
yang dengannya kami menggunakan sintaks berikut:final model = Provider.of<MyModel>(context);
Secara default, sintaks ini secara otomatis akan menyebabkan pembangunan kembali instance OtherWidget
segera setelah model berubah MyModel
. Mungkin kita tidak membutuhkan ini. Pada akhirnya, itu OtherWidget
hanya 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 Provider
begitu saja.StreamProviderSekilas, tidak jelas mengapa itu diperlukan StreamProvider
. Pada akhirnya, Anda bisa menggunakan yang biasa saja StreamBuilder
jika Anda perlu mengonsumsi streaming di Flutter. Misalnya, di sini kita mendengarkan aliran yang onAuthStateChanged
disediakan 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 StreamProvider
akar 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.FutureProviderMirip dengan contoh di atas, ini FutureProvider
adalah alternatif dari standar FutureBuilder
saat 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 Consumer
seperti pada contoh di StreamProvider
atas.ValueListenableProviderValueListenable 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 ValueNotifier
menggunakan 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 Consumer
bersarang :ValueListenableProvider
counter
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 counter
dari 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 counter
dari 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++;
},
);
}
}
MultiProviderJika 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(...)
)
)
)
MultiProvider
Memungkinkan 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(...),
)
ProxyProviderProxyProvider
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) {
},
);
Kesimpulan
Saat digunakan, InheritedWidget
Provider
ini 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 Provider
telah menjadi layak untuk dicoba tanpa penundaan!