Dasar-Dasar RxVMS: RxCommand dan GetIt

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


gambar


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


//    GetIt sl = new GetIt(); void setUpServiceLocator(ErrorReporter reporter) { //  // [registerSingleton]  -  . //   . // sl.get<ErrorReporter>.get()    . sl.registerSingleton<ErrorReporter>(reporter); // [registerLazySingleton]   ,       // sl.get<ImageService>.get()            . sl.registerLazySingleton<ImageService>(() => new ImageServiceImplementation()); sl.registerLazySingleton<MapService>(() => new MapServiceImplementation()); //  sl.registerSingleton<UserManager>(new UserManagerImplementation()); sl.registerLazySingleton<EventManager>(() => new EvenManagerImplementation()); sl.registerLazySingleton<ImageManager>(() => new ImageManagerImplementation()); sl.registerLazySingleton<AppManager>(() => new AppManagerImplementation()); 

Kapan pun Anda ingin mengakses, panggil saja


 RegistrationType object = sl.get<RegistrationType>(); //  GetIt  `callable`,   : RegistrationType object2 = sl<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



 /// Combined execution state of an `RxCommand` /// Will be issued for any state change of any of the fields /// During normal command execution, you will get this item's listening at the command's [.results] observable. /// 1. If the command was just newly created, you will get `null, false, false` (data, error, isExecuting) /// 2. When calling execute: `null, false, true` /// 3. When exceution finishes: `result, false, false` class CommandResult<T> { final T data; final Exception error; final bool isExecuting; const CommandResult(this.data, this.error, this.isExecuting); bool get hasData => data != null; bool get hasError => error != null; @override bool operator ==(Object other) => other is CommandResult<T> && other.data == data && other.error == error && other.isExecuting == isExecuting; @override int get hashCode => hash3(data.hashCode, error.hashCode, isExecuting.hashCode); @override String toString() { return 'Data: $data - HasError: $hasError - IsExecuting: $isExecuting'; } } 

.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( /// RxLoader      ///    Stream<CommandResult> child: RxLoader<List<WeatherEntry>>( spinnerKey: AppKeys.loadingSpinner, radius: 25.0, commandResults: sl.get<AppManager>().updateWeatherCommand.results, /// ,  .hasData == true dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), /// ,  .isExceuting == true placeHolderBuilder: (context) => Center( key: AppKeys.loaderPlaceHolder, child: Text("No Data")), /// ,  .hasError == true errorBuilder: (context, ex) => Center( key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), ), ), 

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() { //    bool         //   Observable switchChangedCommand = RxCommand.createSync<bool, bool>((b) => b); //    switchChangedCommand  canExecute Observable  // updateWeatherCommand updateWeatherCommand = RxCommand.createAsync<String, List<WeatherEntry>>( sl.get<WeatherService>().getWeatherEntriesForCity, canExecute: switchChangedCommand, ); //         textChangedCommand = RxCommand.createSync<String, String>((s) => s); //    ... textChangedCommand //     500ms... .debounce(new Duration(milliseconds: 500)) // ...   updateWeatherCommand .listen(updateWeatherCommand); //     updateWeatherCommand(''); } } 

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, ), //    textChangedCommand! onChanged: sl.get<AppManager>().textChangedCommand, ), ), Expanded( /// RxLoader   builders   ///   Stream<CommandResult> child: RxLoader<List<WeatherEntry>>( spinnerKey: AppKeys.loadingSpinner, radius: 25.0, commandResults: sl.get<AppManager>().updateWeatherCommand.results, dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), placeHolderBuilder: (context) => Center(key: AppKeys.loaderPlaceHolder, child: Text("No Data")), errorBuilder: (context, ex) => Center(key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: <Widget>[ ///  Updatebutton    updateWeatherCommand.canExecute Expanded( //        Streambuilder, //      WidgetSelector child: WidgetSelector( buildEvents: sl .get<AppManager>() .updateWeatherCommand .canExecute, onTrue: RaisedButton( key: AppKeys.updateButtonEnabled, child: Text("Update"), onPressed: () { _controller.clear(); sl.get<AppManager>().updateWeatherCommand(); }, ), onFalse: RaisedButton( key: AppKeys.updateButtonDisabled, child: Text("Please Wait"), onPressed: null, ), ), ), //    canExecuteInput StateFullSwitch( state: true, onChanged: sl.get<AppManager>().switchChangedCommand, ), ], ), ), ], ), ); 

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() { //      ,        _eventCommandSubscription = _createCommand.listen((_) async { Navigator.pop(context); await showToast('Event saved'); }); //        _errorSubscription = _createEventCommand.thrownExceptions.listen((ex) async { await sl.get<ErrorReporter>().logException(ex); await showMessageDialog(context, 'There was a problem saving event', ex.toString()); }); } 

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; //       final void Function(TResult value) onValue; //    isExecuting final void Function(bool isBusy) onIsBusyChange; //       final void Function(Exception ex) onError; //    canExecute final void Function(bool state) onCanExecuteChange; //    .results Observable  final void Function(CommandResult<TResult> result) onResult; //     /  final void Function() onIsBusy; final void Function() onNotBusy; //     final Duration debounceDuration; RxCommandListener(this.command,{ this.onValue, this.onIsBusyChange, this.onIsBusy, this.onNotBusy, this.onError, this.onCanExecuteChange, this.onResult, this.debounceDuration,} ) void dispose(); 

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; //    sl.get<EventManager>().createChatEntryCommand(new ChatEntry( event: widget.event, isImage: true, content: imageLocation.downloadUrl, )); }); _selectImageIsExecutingSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .isExecuting .listen((busy) { if (busy) { MySpinner.show(context); } else { MySpinner.hide(); } }); _selectImageErrorSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .thrownExceptions .listen((ex) => showMessageDialog(context, 'Upload problem', "We cannot upload your selected image at the moment. Please check your internet connection")); 

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 .

Source: https://habr.com/ru/post/id449872/


All Articles