Esta es la cuarta parte de mi serie de arquitectura Flutter:
Aunque las 2 partes anteriores claramente no estaban relacionadas con el patrón RxVMS, fueron necesarias para una comprensión clara de este enfoque. Ahora pasamos a los paquetes más importantes que necesitará para usar RxVMS en su aplicación.
GetIt: Fast ServiceLocator
Cuando recuerda un diagrama que muestra los diversos elementos RxVMS en una aplicación ...

... tal vez se pregunte cómo se conocen entre sí las distintas asignaciones, gerentes y servicios. Más importante aún, es posible que se pregunte cómo un elemento puede acceder a las funciones de otro.
Con tantos enfoques diferentes (como Widgets heredados, contenedores IoC, DI ...), personalmente prefiero el Localizador de servicios. A este respecto, tengo un artículo especial sobre GetIt : mi implementación de este enfoque, pero aquí abordaré un poco este tema. En general, registra objetos en este servicio una vez y luego tiene acceso a ellos en toda la aplicación. Es una especie de singleton ... pero con más flexibilidad.
Uso
Usar GetIt es bastante obvio. Al comienzo de la aplicación, registra los servicios y / o gerentes que planea usar posteriormente. En el futuro, solo llame a los métodos GetIt para acceder a instancias de clases registradas.
Una buena característica es que puede registrar interfaces o clases abstractas como implementaciones concretas. Al acceder a una instancia, simplemente use interfaces / abstracciones, reemplazando fácilmente las implementaciones necesarias durante el registro. Esto le permite cambiar fácilmente el Servicio real a MockService.
Un poco de practica
Por lo general, inicializo mi SeviceLocator en un archivo llamado service_locator.dart a través de una variable global. Por lo tanto, se obtiene una variable global para todo el proyecto.
Cuando quiera acceder, solo llame
RegistrationType object = sl.get<RegistrationType>();
Nota extremadamente importante: cuando use GetIt, SIEMPRE use el mismo estilo al importar archivos, ya sea paquetes (recomendados) o rutas relativas, pero no ambos enfoques a la vez. Esto se debe a que Dart trata dichos archivos como diferentes, a pesar de su identidad.
Si esto es difícil para usted, vaya a mi blog para más detalles.
Rxcommand
Ahora que usamos GetIt para acceder a nuestros objetos en todas partes (incluida la interfaz de usuario), quiero describir cómo podemos implementar funciones de controlador para eventos de IU. La forma más sencilla sería agregar funciones a los administradores y llamarlas en widgets:
class SearchManager { void lookUpZip(String zip); }
y luego en la interfaz de usuario
TextField(onChanged: sl.get<SearchManager>().lookUpZip,)
Esto lookUpZip
en cada cambio en TextField
. Pero, ¿cómo nos pasamos el resultado en el futuro? Como queremos ser reactivos, agregaríamos un StreamController
a nuestro 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); } }
y en la interfaz de usuario:
StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().nameOfCity, builder: (context, snapshot) => Text(snapShot.data);
Aunque este enfoque funciona, no es óptimo. Aquí están los problemas:
- código redundante: siempre tenemos que crear un método, StreamController y un captador para su flujo, si no queremos mostrarlo explícitamente en público
- estado ocupado: ¿qué sucede si nos gustaría mostrar Spinner mientras la función está haciendo su trabajo?
- manejo de errores: ¿qué sucede si una función arroja una excepción?
Por supuesto, podríamos agregar más StreamControllers para manejar estados y errores ... pero pronto se vuelve tedioso, y el paquete rx_command es útil aquí .
RxCommand
resuelve todos los problemas anteriores y mucho más. RxCommand
encapsula una función (síncrona o asíncrona) y publica automáticamente sus resultados en la secuencia.
Usando RxCommand, podríamos reescribir a nuestro gerente así:
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)); } }
y en la interfaz de usuario:
TextField(onChanged: sl.get<SearchManager>().lookUpZipCommand,) ... StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().lookUpZipCommand, builder: (context, snapshot) => Text(snapShot.data);
lo cual es mucho más conciso y legible.
RxCommand en detalle

RxCommand tiene una Observable de entrada y cinco de Salida:
canExecuteInput es un Observable<bool>
opcional que puede pasar a la función de fábrica al crear el RxCommand. Le indica a RxCommand si se puede ejecutar, dependiendo del último valor que recibió.
isExecuting es un Observable<bool>
que indica si el comando está realizando actualmente su función. Cuando un equipo está ocupado, no se puede volver a ejecutar. Si desea mostrar Spinner en tiempo de ejecución, escuche isExecuting
canExecute es un Observable<bool>
que indica la capacidad de ejecutar un comando. Esto, por ejemplo, va bien con StreamBuilder para cambiar la apariencia de un botón entre el estado activado / desactivado.
su significado es el siguiente:
Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) => canExecuteInput && !isExecuting).distinct.
lo que significa
- será
false
si isExecuting devuelve true
- se devolverá
true
solo si isExecuting devuelve false
Y canExecuteInput no devuelve false
.
thrownExceptions es una Observable<Exception>
. Todas las excepciones que puede lanzar una función empaquetada serán capturadas y enviadas a este Observable. Es conveniente escucharlo y mostrar un cuadro de diálogo si se produce un error.
(el equipo en sí) es en realidad también un Observable. Los valores devueltos por la función de trabajo se transmitirán en este canal, por lo que puede pasar directamente RxCommand a StreamBuilder como parámetro de flujo
los resultados contienen todos los estados de comando en un Observable<CommandResult>
, donde CommandResult
define como
.results
Observable es especialmente útil si desea pasar el resultado de un comando directamente a StreamBuilder. Esto mostrará contenido diferente dependiendo del estado del comando, y funciona muy bien con RxLoader
del paquete rx_widgets . Aquí hay un widget de ejemplo de RxLoader que usa .results
Observable:
Expanded(
Crear comandos Rx
RxCommands puede usar funciones síncronas y asíncronas que:
- No tienen un parámetro y no devuelven un resultado;
- Tienen un parámetro y no devuelven un resultado;
- No tienen un parámetro y devuelven un resultado;
- Tienen un parámetro y devuelven un resultado;
Para todas las opciones, RxCommand ofrece varios métodos de fábrica, teniendo en cuenta los controladores síncronos y así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,...
Incluso si su función empaquetada no devuelve un valor, RxCommand devolverá un valor vacío después de ejecutar la función. Por lo tanto, puede configurar el oyente para tal comando para que responda a la finalización de la función.
Acceso al último resultado.
RxCommand.lastResult
le da acceso al último valor exitoso del resultado de la ejecución del comando, que puede usarse como initialData
en StreamBuilder.
Si desea obtener el último resultado incluido en los eventos CommandResult en tiempo de ejecución o en caso de error, puede pasar emitInitialCommandResult = true
al crear el comando.
Si desea asignar un valor inicial a .lastResult
, por ejemplo, si lo usa como initialData
en StreamBuilder, puede pasarlo con el parámetro initialLastResult
al crear el comando.
Ejemplo: hacer que Flutter sea reactivo
La última versión del ejemplo se ha reorganizado para RxVMS , por lo que ahora debería tener una buena opción sobre cómo usarlo.
Como se trata de una aplicación muy simple, solo necesitamos un administrador:
class AppManager { RxCommand<String, List<WeatherEntry>> updateWeatherCommand; RxCommand<bool, bool> switchChangedCommand; RxCommand<String, String> textChangedCommand; AppManager() {
Puede combinar diferentes comandos Rx juntos. Tenga en cuenta que switchingChangedCommand es en realidad un observable canExecute para updateWeatherCommand
.
Ahora veamos cómo se usa el Administrador en la interfaz de usuario:
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, ),
Patrones de uso típicos
Ya hemos visto una forma de responder a diferentes estados de un comando usando CommandResults . En los casos en que queremos mostrar si el comando fue exitoso (pero no mostrar el resultado), un patrón común es escuchar el comando Observables en la función initState
. Aquí hay un ejemplo de un proyecto real.
Definición para createEventCommand
:
RxCommand<Event, void> createEventCommand;
Esto creará un objeto Evento en la base de datos y no devolverá ningún valor real. Pero, como aprendimos anteriormente, incluso un RxCommand con un tipo de retorno de void
devolverá un solo elemento de datos al finalizar. Por lo tanto, podemos usar este comportamiento para desencadenar una acción en nuestra aplicación tan pronto como se complete el comando:
@override void initState() {
Importante : no olvide completar las suscripciones cuando ya no las necesitemos:
@override void dispose() { _eventCommandSubscription?.cancel(); _errorSubscription?.cancel(); super.dispose(); }
Además, si desea utilizar la pantalla de contador ocupado, puede:
- escuche los comandos
isExecuting
Observable en la función initState
; - mostrar / ocultar el contador en la suscripción; también
- use el comando en sí como fuente de datos para StreamBuilder
Hacer la vida más fácil con RxCommandListeners
Si desea usar Observable múltiple, probablemente tenga que administrar múltiples suscripciones. El control directo de escuchar y liberar un grupo de suscripciones puede ser difícil, hace que el código sea menos legible y lo pone en riesgo de errores (por ejemplo, olvidarse de cancel
en el proceso de finalización).
La última versión de rx_command agrega la clase auxiliar RxCommandListener
, que está diseñada para simplificar este procesamiento. Su constructor acepta comandos y controladores para varios cambios de estado:
class RxCommandListener<TParam, TResult> { final RxCommand<TParam, TResult> command;
No necesita pasar todas las funciones del controlador. Todos son opcionales, por lo que simplemente puede transferir los que necesita. Solo necesita llamar a dispose
para su RxCommandListener
en su función de dispose
, y cancelará todo lo utilizado dentro de la suscripción.
Comparemos el mismo código con y sin RxCommandListener
en otro ejemplo real. El comando selectAndUploadImageCommand
se usa aquí en la pantalla de chat donde el usuario puede cargar imágenes. Cuando se llama al comando:
- Se muestra el cuadro de diálogo ImagePicker.
- Después de seleccionar la imagen se carga
- Una vez completada la descarga, el comando devuelve la dirección de almacenamiento de imágenes para que pueda crear una nueva entrada de chat.
Sin RxCommandListener :
_selectImageCommandSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .listen((imageLocation) async { if (imageLocation == null) return;
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"));
En general, siempre usaría un RxCommandListener si hay más de un Observable.
Pruebe RxCommands y vea cómo puede facilitarle la vida.
Por cierto, no necesita usar RxVMS para aprovechar RxCommands .
Para obtener más información sobre RxCommand
lea el paquete léame RxCommand
.