Noções básicas do RxVMS: RxCommand e GetIt

Esta é a quarta parte da minha série de arquitetura Flutter:



Embora as duas partes anteriores não estivessem claramente relacionadas ao padrão RxVMS, elas eram necessárias para um entendimento claro dessa abordagem. Agora vamos aos pacotes mais importantes que você precisará usar o RxVMS em seu aplicativo.


GetIt: Fast ServiceLocator


Quando você lembra de um diagrama que mostra os vários elementos RxVMS em um aplicativo ...


imagem


... talvez você esteja se perguntando como os vários mapeamentos, gerentes e serviços se descobrem. Mais importante, você pode estar se perguntando como um elemento pode acessar as funções de outro.


Com tantas abordagens diferentes (como Widgets Herdados, contêineres IoC, DI ...), eu pessoalmente prefiro o Localizador de Serviços. Nesse sentido, tenho um artigo especial sobre o GetIt - minha implementação dessa abordagem, mas aqui abordarei um pouco esse tópico. Em geral, você registra objetos neste serviço uma vez e depois tem acesso a eles em todo o aplicativo. É meio singleton ... mas com mais flexibilidade.


Use


Usar o GetIt é bastante óbvio. No início do aplicativo, você registra serviços e / ou gerentes que planeja usar posteriormente. No futuro, basta chamar os métodos GetIt para acessar instâncias de classes registradas.


Um recurso interessante é que você pode registrar interfaces ou classes abstratas, como implementações concretas. Ao acessar uma instância, basta usar interfaces / abstrações, substituindo facilmente as implementações necessárias durante o registro. Isso permite que você alterne facilmente o Serviço real para o MockService.


Um pouco de prática


Normalmente, inicializo meu SeviceLocator em um arquivo chamado service_locator.dart por meio de uma variável global. Assim, uma variável global é obtida para todo o projeto.


//    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()); 

Sempre que quiser acessar, basta ligar para


 RegistrationType object = sl.get<RegistrationType>(); //  GetIt  `callable`,   : RegistrationType object2 = sl<RegistrationType>(); 

Nota extremamente importante: Ao usar o GetIt, SEMPRE use o mesmo estilo ao importar arquivos - pacotes (recomendado) ou caminhos relativos, mas não as duas abordagens ao mesmo tempo. Isso ocorre porque o Dart trata esses arquivos como diferentes, apesar de sua identidade.


Se isso for difícil para você, acesse o meu blog para obter detalhes.


Rxcommand


Agora que usamos o GetIt para acessar nossos objetos em qualquer lugar (incluindo a interface do usuário), quero descrever como podemos implementar funções de manipulador para eventos da interface do usuário. A maneira mais simples seria adicionar funções aos gerentes e chamá-las em widgets:


 class SearchManager { void lookUpZip(String zip); } 

e depois na interface do usuário


 TextField(onChanged: sl.get<SearchManager>().lookUpZip,) 

Isso lookUpZip em todas as alterações no TextField . Mas como transmitimos o resultado para nós? Como queremos ser reativos, adicionaríamos um StreamController ao nosso SearchManager :


 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); } } 

e na interface do usuário:


 StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().nameOfCity, builder: (context, snapshot) => Text(snapShot.data); 

Embora essa abordagem funcione, ela não é ótima. Aqui estão os problemas:


  • código redundante - sempre precisamos criar um método, StreamController e um getter para seu fluxo, se não quisermos exibi-lo explicitamente no domínio público
  • estado ocupado - e se gostaríamos de exibir o Spinner enquanto a função está realizando seu trabalho?
  • tratamento de erros - o que acontece se uma função lança uma exceção?

Obviamente, poderíamos adicionar mais StreamControllers para lidar com estados e erros ... mas logo fica entediante e o pacote rx_command é útil aqui .


RxCommand resolve todos os problemas acima e muito mais. RxCommand encapsula uma função (síncrona ou assíncrona) e RxCommand automaticamente seus resultados no fluxo.


Usando o RxCommand, poderíamos reescrever nosso gerente assim:


 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)); } } 

e na interface do usuário:


 TextField(onChanged: sl.get<SearchManager>().lookUpZipCommand,) ... StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().lookUpZipCommand, builder: (context, snapshot) => Text(snapShot.data); 

o que é muito mais conciso e legível.


RxCommand em detalhes



O RxCommand possui uma entrada e cinco Observables de saída:


  • canExecuteInput é um Observable<bool> opcional que você pode passar para a função de fábrica ao criar o RxCommand. Sinaliza ao RxCommand se ele pode ser executado, dependendo do último valor recebido.


  • isExecuting é um Observable<bool> que sinaliza se o comando está atualmente executando sua função. Quando uma equipe está ocupada, não pode ser executada novamente. Se você deseja exibir o Spinner em tempo de execução, ouça isExecuting


  • canExecute é um Observable<bool> que sinaliza a capacidade de executar um comando. Isso, por exemplo, combina bem com o StreamBuilder para alterar a aparência de um botão entre o estado ativado / desativado.
    seu significado é o seguinte:


     Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) => canExecuteInput && !isExecuting).distinct. 

    o que significa


    • será false se isExecuting retornar true
    • ele será retornado true somente se isExecuting retornar false E canExecuteInput não retornar false .

  • thrownExceptions é uma Observable<Exception> . Todas as exceções que uma função empacotada pode lançar serão capturadas e enviadas para este Observável. É conveniente ouvi-lo e exibir uma caixa de diálogo se ocorrer um erro.


  • (a própria equipe) também é, na verdade, um Observável. Os valores retornados pela função de trabalho serão transmitidos neste canal, para que você possa passar diretamente o RxCommand para o StreamBuilder como um parâmetro de fluxo


  • Os resultados contêm todos os estados de comando em um Observable<CommandResult> , em que CommandResult definido como



 /// 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 é especialmente útil se você deseja passar o resultado de um comando diretamente para o StreamBuilder. Isso exibirá conteúdo diferente, dependendo do status do comando, e funciona muito bem com o RxLoader do pacote rx_widgets . Aqui está um exemplo de widget RxLoader que usa .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()}")), ), ), 

Criando RxCommands


Os RxCommands podem usar funções síncronas e assíncronas que:


  • Eles não têm um parâmetro e não retornam um resultado;
  • Eles têm um parâmetro e não retornam um resultado;
  • Eles não têm um parâmetro e retornam um resultado;
  • Eles têm um parâmetro e retornam um resultado;

Para todas as opções, o RxCommand oferece vários métodos de fábrica, levando em consideração manipuladores síncronos e assíncronos:


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

Mesmo que sua função compactada não retorne um valor, o RxCommand retornará um valor vazio após a execução da função. Assim, você pode definir o ouvinte para um comando desse tipo, para que ele responda à conclusão da função.


Acesso ao resultado mais recente


RxCommand.lastResult fornece acesso ao último valor bem-sucedido do resultado da execução do comando, que pode ser usado como dados iniciais no StreamBuilder.


Se você deseja obter o último resultado incluído nos eventos CommandResult em tempo de execução ou em caso de erro, você pode passar emitInitialCommandResult = true ao criar o comando.


Se você deseja atribuir um valor inicial a .lastResult , por exemplo, se você o usar como initialData no StreamBuilder, poderá transmiti-lo com o parâmetro initialLastResult ao criar o comando.


Exemplo - tornando Flutter reativo


A versão mais recente do exemplo foi reorganizada para o RxVMS ; portanto, agora você deve ter uma boa opção de como usá-lo.


Como esse é um aplicativo muito simples, precisamos apenas de um gerente:


 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(''); } } 

Você pode combinar diferentes RxCommands juntos. Observe que switchedChangedCommand é realmente um Observable canExecute para updateWeatherCommand .


Agora vamos ver como o Manager é usado na interface do usuário:


  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, ), ], ), ), ], ), ); 

Padrões típicos de uso


Já vimos uma maneira de responder a diferentes estados de um comando usando CommandResults . Nos casos em que queremos exibir se o comando foi bem-sucedido (mas não exibir o resultado), um padrão comum é ouvir o comando Observables na função initState StatefulWidget. Aqui está um exemplo de um projeto real.


Definição para createEventCommand :


  RxCommand<Event, void> createEventCommand; 

Isso criará um objeto Event no banco de dados e não retornará nenhum valor real. Mas, como aprendemos anteriormente, mesmo um RxCommand com um tipo de retorno void retornará um único item de dados na conclusão. Portanto, podemos usar esse comportamento para disparar uma ação em nosso aplicativo assim que o comando for concluído:


 @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()); }); } 

Importante : não se esqueça de concluir as assinaturas quando não precisar mais delas:


 @override void dispose() { _eventCommandSubscription?.cancel(); _errorSubscription?.cancel(); super.dispose(); } 

Além disso, se você quiser usar a exibição do contador de ocupado, poderá:


  • ouça os comandos isExecuting Observable na função initState ;
  • mostrar / ocultar o contador na assinatura; também
  • use o próprio comando como fonte de dados para o StreamBuilder

Facilitando a vida com o RxCommandListeners


Se você deseja usar várias Observable, provavelmente precisará gerenciar várias assinaturas. O controle direto de ouvir e liberar um grupo de assinaturas pode ser difícil, torna o código menos legível e coloca você em risco de erros (por exemplo, esquecendo de cancel no processo de conclusão).


A versão mais recente do rx_command adiciona a classe auxiliar RxCommandListener , projetada para simplificar esse processamento. Seu construtor aceita comandos e manipuladores para várias alterações de estado:


 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(); 

Você não precisa passar todas as funções do manipulador. Todos eles são opcionais, para que você possa simplesmente transferir os necessários. Você só precisa chamar o dispose do seu RxCommandListener na sua função de dispose , e ele cancelará todos os usados ​​na assinatura.


Vamos comparar o mesmo código com e sem RxCommandListener em outro exemplo real. O comando selectAndUploadImageCommand é usado aqui na tela de bate-papo em que o usuário pode fazer upload de imagens. Quando o comando é chamado:


  • A caixa de diálogo ImagePicker é exibida.
  • Depois de selecionar a imagem carregada
  • Após a conclusão do download, o comando retorna o endereço de armazenamento de imagens para que você possa criar uma nova entrada de bate-papo.

Sem 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")); 

Usando 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")); 

Geralmente, eu sempre usaria um RxCommandListener se houver mais de um Observable.


Experimente o RxCommands e veja como isso pode facilitar sua vida.
A propósito, você não precisa usar o RxVMS para tirar proveito dos RxCommands .


Para obter mais informações sobre o RxCommand leia o pacote leia-me RxCommand .

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


All Articles