Ini adalah bagian keempat dari seri arsitektur Flutter saya:
Meskipun 2 bagian sebelumnya jelas tidak terkait dengan pola RxVMS, mereka diperlukan untuk pemahaman yang jelas tentang pendekatan ini. Sekarang kita beralih ke paket paling penting yang Anda perlukan untuk menggunakan RxVMS dalam aplikasi Anda.
GetIt: ServiceLocator Cepat
Saat Anda mengingat diagram yang menunjukkan berbagai elemen RxVMS dalam aplikasi ...

... mungkin Anda bertanya-tanya bagaimana berbagai pemetaan, manajer, dan layanan saling mengetahui. Lebih penting lagi, Anda mungkin bertanya-tanya bagaimana satu elemen dapat mengakses fungsi yang lain.
Dengan begitu banyak pendekatan berbeda (seperti Widget yang Diwarisi, wadah IoC, DI ...), saya pribadi lebih suka Service Locator. Dalam hal ini, saya memiliki artikel khusus tentang GetIt - implementasi saya dari pendekatan ini, tetapi di sini saya akan sedikit menyentuh topik ini. Secara umum, Anda mendaftarkan objek dalam layanan ini satu kali, dan kemudian Anda memiliki akses ke sana di seluruh aplikasi. Ini semacam singleton ... tetapi dengan lebih banyak fleksibilitas.
Gunakan
Menggunakan GetIt cukup jelas. Di awal aplikasi, Anda mendaftarkan layanan dan / atau manajer yang Anda rencanakan untuk digunakan selanjutnya. Di masa depan, panggil saja metode GetIt untuk mengakses instance kelas terdaftar.
Fitur yang bagus adalah Anda dapat mendaftarkan antarmuka atau kelas abstrak seperti implementasi konkret. Saat mengakses sebuah instance, cukup gunakan antarmuka / abstraksi, dengan mudah mengganti implementasi yang diperlukan selama pendaftaran. Ini memungkinkan Anda untuk dengan mudah mengganti Layanan asli ke MockService.
Sedikit latihan
Saya biasanya menginisialisasi SeviceLocator saya dalam file bernama service_locator.dart melalui variabel global. Dengan demikian satu variabel global diperoleh untuk seluruh proyek.
Kapan pun Anda ingin mengakses, panggil saja
RegistrationType object = sl.get<RegistrationType>();
Catatan yang sangat penting: Saat menggunakan GetIt, SELALU menggunakan gaya yang sama saat mengimpor file - baik paket (disarankan) atau jalur relatif, tetapi tidak keduanya mendekati sekaligus. Ini karena Dart memperlakukan file-file tersebut sebagai berbeda, terlepas dari identitasnya.
Jika ini sulit bagi Anda, silakan kunjungi blog saya untuk detailnya.
Perintah
Sekarang kita menggunakan GetIt untuk mengakses objek kita di mana saja (termasuk antarmuka pengguna), saya ingin menjelaskan bagaimana kita dapat mengimplementasikan fungsi-fungsi handler untuk acara-acara UI. Cara paling sederhana adalah menambahkan fungsi ke manajer dan memanggilnya di widget:
class SearchManager { void lookUpZip(String zip); }
dan kemudian di UI
TextField(onChanged: sl.get<SearchManager>().lookUpZip,)
Ini akan lookUpZip
pada setiap perubahan di TextField
. Tetapi bagaimana kita menyampaikan hasilnya kepada kita? Karena kami ingin menjadi reaktif, kami akan menambahkan StreamController
ke SearchManager
kami:
abstract class SearchManager { Stream<String> get nameOfCity; void lookUpZip(String zip); } class SearchManagerImplementation implements SearchManager{ @override Stream<String> get nameOfCity => cityController.stream; StreamController<String> cityController = new StreamController(); @override Future<void> lookUpZip(String zip) async { var cityName = await sl.get<ZipApiService>().lookUpZip(zip); cityController.add(cityName); } }
dan di UI:
StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().nameOfCity, builder: (context, snapshot) => Text(snapShot.data);
Meskipun pendekatan ini berhasil, itu tidak optimal. Inilah masalahnya:
- kode redundan - kita selalu harus membuat metode, StreamController, dan pengambil untuk alirannya, jika kita tidak ingin menampilkannya secara eksplisit di domain publik
- status sibuk - bagaimana jika kita ingin menampilkan Spinner saat fungsi melakukan tugasnya?
- penanganan kesalahan - apa yang terjadi jika suatu fungsi melempar pengecualian?
Tentu saja, kita bisa menambahkan lebih banyak StreamControllers untuk menangani status dan kesalahan ... tetapi segera itu membosankan, dan paket rx_command berguna di sini .
RxCommand
menyelesaikan semua masalah di atas dan banyak lagi. RxCommand
merangkum fungsi (sinkron atau asinkron) dan secara otomatis memposting hasilnya ke aliran.
Menggunakan RxCommand, kita dapat menulis ulang manajer kita seperti ini:
abstract class SearchManager { RxCommand<String,String> lookUpZipCommand; } class SearchManagerImplementation implements SearchManager{ @override RxCommand<String,String> lookUpZipCommand; SearchManagerImplementation() { lookUpZipCommand = RxCommand.createAsync((zip) => sl.get<ZipApiService>().lookUpZip(zip)); } }
dan di UI:
TextField(onChanged: sl.get<SearchManager>().lookUpZipCommand,) ... StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().lookUpZipCommand, builder: (context, snapshot) => Text(snapShot.data);
yang jauh lebih ringkas dan mudah dibaca.
Perintah Rx secara detail

RxCommand memiliki satu input dan lima output yang dapat diobservasi:
canExecuteInput adalah Observable<bool>
opsional yang dapat Anda lewati ke fungsi pabrik saat membuat RxCommand. Ini memberi sinyal kepada RxCommand apakah dapat dijalankan, tergantung pada nilai terakhir yang diterima.
isExecuting adalah Observable<bool>
yang memberi sinyal apakah perintah saat ini menjalankan fungsinya. Ketika sebuah tim sibuk, itu tidak dapat dijalankan kembali. Jika Anda ingin menampilkan Spinner saat runtime, dengarkan isExecuting
canExecute adalah Observable<bool>
yang memberi sinyal kemampuan untuk menjalankan perintah. Ini, misalnya, berjalan baik dengan StreamBuilder untuk mengubah tampilan tombol antara kondisi on / off.
artinya adalah sebagai berikut:
Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) => canExecuteInput && !isExecuting).distinct.
yang artinya
- akan
false
jika isExecuting mengembalikan true
- itu akan dikembalikan
true
hanya jika isExecuting mengembalikan false
DAN canExecuteInput tidak mengembalikan false
.
thrownExceptions adalah Observable<Exception>
. Semua pengecualian yang dapat dibuang oleh fungsi yang dikemas akan ditangkap dan dikirim ke Observable ini. Lebih mudah untuk mendengarkannya dan menampilkan kotak dialog jika terjadi kesalahan.
(tim itu sendiri) sebenarnya juga bisa diamati. Nilai-nilai yang dikembalikan oleh fungsi kerja akan dikirim pada saluran ini, sehingga Anda bisa langsung meneruskan RxCommand ke StreamBuilder sebagai parameter aliran
hasil berisi semua status perintah dalam satu Observable<CommandResult>
, di mana CommandResult
didefinisikan sebagai
.results
Observable sangat berguna jika Anda ingin meneruskan hasil dari perintah langsung ke StreamBuilder. Ini akan menampilkan konten yang berbeda tergantung pada status perintah, dan itu berfungsi dengan sangat baik dengan RxLoader
dari paket rx_widgets . Berikut adalah contoh widget RxLoader yang menggunakan .results
Observable:
Expanded(
Membuat Perintah Rx
RxCommands dapat menggunakan fungsi sinkron dan asinkron yang:
- Mereka tidak memiliki parameter dan tidak mengembalikan hasilnya;
- Mereka memiliki parameter dan tidak mengembalikan hasilnya;
- Mereka tidak memiliki parameter dan mengembalikan hasil;
- Mereka memiliki parameter dan mengembalikan hasilnya;
Untuk semua opsi, RxCommand menawarkan beberapa metode pabrik, dengan mempertimbangkan penangan yang sinkron dan asinkron:
static RxCommand<TParam, TResult> createSync<TParam, TResult>(Func1<TParam, TResult> func,... static RxCommand<void, TResult> createSyncNoParam<TResult>(Func<TResult> func,... static RxCommand<TParam, void> createSyncNoResult<TParam>(Action1<TParam> action,... static RxCommand<void, void> createSyncNoParamNoResult(Action action,... static RxCommand<TParam, TResult> createAsync<TParam, TResult>(AsyncFunc1<TParam, TResult> func,... static RxCommand<void, TResult> createAsyncNoParam<TResult>(AsyncFunc<TResult> func,... static RxCommand<TParam, void> createAsyncNoResult<TParam>(AsyncAction1<TParam> action,... static RxCommand<void, void> createAsyncNoParamNoResult(AsyncAction action,...
Bahkan jika fungsi Anda yang dikemas tidak mengembalikan nilai, RxCommand akan mengembalikan nilai kosong setelah fungsi dijalankan. Dengan demikian, Anda dapat mengatur pendengar untuk perintah seperti itu sehingga merespon penyelesaian fungsi.
Akses ke hasil terbaru
RxCommand.lastResult
memberi Anda akses ke nilai sukses terakhir dari hasil eksekusi perintah, yang dapat digunakan sebagai initialData
di StreamBuilder.
Jika Anda ingin mendapatkan hasil terakhir yang termasuk dalam acara CommandResult pada saat run time atau jika terjadi kesalahan, Anda dapat mengirimkan emitInitialCommandResult = true
saat membuat perintah.
Jika Anda ingin menetapkan nilai awal ke .lastResult
, misalnya, jika Anda menggunakannya sebagai initialData
di StreamBuilder, Anda bisa meneruskannya dengan parameter initialLastResult
saat membuat perintah.
Contoh - membuat Flutter reaktif
Versi terbaru dari contoh telah diatur ulang untuk RxVMS , jadi sekarang Anda harus memiliki pilihan yang baik tentang cara menggunakannya.
Karena ini adalah aplikasi yang sangat sederhana, kami hanya perlu satu manajer:
class AppManager { RxCommand<String, List<WeatherEntry>> updateWeatherCommand; RxCommand<bool, bool> switchChangedCommand; RxCommand<String, String> textChangedCommand; AppManager() {
Anda dapat menggabungkan Perintah Rx yang berbeda bersama-sama. Perhatikan bahwa switchedChangedCommand
sebenarnya adalah canExecute yang dapat diobservasi untuk updateWeatherCommand
.
Sekarang mari kita lihat bagaimana Manajer digunakan dalam antarmuka pengguna:
return Scaffold( appBar: AppBar(title: Text("WeatherDemo")), resizeToAvoidBottomPadding: false, body: Column( children: <Widget>[ Padding( padding: const EdgeInsets.all(16.0), child: TextField( key: AppKeys.textField, autocorrect: false, controller: _controller, decoration: InputDecoration( hintText: "Filter cities", ), style: TextStyle( fontSize: 20.0, ),
Pola Penggunaan Khas
Kami telah melihat satu cara untuk merespons berbagai status perintah menggunakan CommandResults . Dalam kasus di mana kita ingin menampilkan apakah perintah itu berhasil (tetapi tidak menampilkan hasilnya), pola umum adalah mendengarkan perintah Observables di fungsi initState
StatefulWidget. Ini adalah contoh dari proyek nyata.
Definisi untuk createEventCommand
:
RxCommand<Event, void> createEventCommand;
Ini akan membuat objek Acara dalam database dan tidak akan mengembalikan nilai nyata apa pun. Tetapi, seperti yang telah kita pelajari sebelumnya, bahkan perintah RxC dengan tipe pengembalian void
akan mengembalikan item data tunggal saat selesai. Dengan demikian, kita dapat menggunakan perilaku ini untuk memicu tindakan dalam aplikasi kita segera setelah perintah selesai:
@override void initState() {
Penting : jangan lupa untuk menyelesaikan langganan saat kami tidak lagi membutuhkannya:
@override void dispose() { _eventCommandSubscription?.cancel(); _errorSubscription?.cancel(); super.dispose(); }
Selain itu, jika Anda ingin menggunakan layar penghitung yang sibuk, Anda dapat:
- mendengarkan perintah
isExecuting
Observable dalam fungsi initState
; - perlihatkan / sembunyikan konter dalam berlangganan; juga
- gunakan Command itu sendiri sebagai sumber data untuk StreamBuilder
Membuat hidup lebih mudah dengan RxCommandListeners
Jika Anda ingin menggunakan beberapa Observable, Anda mungkin harus mengelola beberapa langganan. Kontrol langsung mendengarkan dan melepaskan sekelompok langganan bisa sulit, membuat kode kurang dibaca dan menempatkan Anda pada risiko kesalahan (misalnya, lupa cancel
dalam proses penyelesaian).
Versi terbaru dari rx_command menambahkan helper class RxCommandListener
, yang dirancang untuk menyederhanakan pemrosesan ini. Konstruktornya menerima perintah dan penangan untuk berbagai perubahan status:
class RxCommandListener<TParam, TResult> { final RxCommand<TParam, TResult> command;
Anda tidak perlu melewati semua fungsi handler. Semuanya bersifat opsional, jadi Anda cukup mentransfer yang Anda butuhkan. Anda hanya perlu memanggil dispose
untuk RxCommandListener
Anda dalam fungsi dispose
Anda, dan itu akan membatalkan semua yang digunakan di dalam berlangganan.
Mari kita bandingkan kode yang sama dengan dan tanpa RxCommandListener
dalam contoh nyata lainnya. Perintah selectAndUploadImageCommand
digunakan di sini pada layar obrolan tempat pengguna dapat mengunggah gambar. Ketika perintah dipanggil:
- Dialog ImagePicker ditampilkan.
- Setelah memilih gambar dimuat
- Setelah unduhan selesai, perintah mengembalikan alamat penyimpanan gambar sehingga Anda dapat membuat entri obrolan baru.
Tanpa RxCommandListener :
_selectImageCommandSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .listen((imageLocation) async { if (imageLocation == null) return;
Menggunakan RxCommandListener :
selectImageListener = RxCommandListener( command: sl.get<ImageManager>().selectAndUploadImageCommand, onValue: (imageLocation) async { if (imageLocation == null) return; sl.get<EventManager>().createChatEntryCommand(new ChatEntry( event: widget.event, isImage: true, content: imageLocation.downloadUrl, )); }, onIsBusy: () => MySpinner.show(context), onNotBusy: MySpinner.hide, onError: (ex) => showMessageDialog(context, 'Upload problem', "We cannot upload your selected image at the moment. Please check your internet connection"));
Secara umum, saya akan selalu menggunakan RxCommandListener jika ada lebih dari satu yang bisa diamati.
Coba RxCommands dan lihat bagaimana hal itu dapat membuat hidup Anda lebih mudah.
Omong-omong, Anda tidak perlu menggunakan RxVMS untuk memanfaatkan RxCommands .
Untuk informasi lebih lanjut tentang RxCommand
baca paket RxCommand
readme .