rx_command 1.0.2 rx_command: ^1.0.2 copied to clipboard
Reactive event handler wrapper class inspired by ReactiveUI.
RxCommand #
RxCommand
is an Reactive Extensions (Rx) based abstraction for event handlers. It is based on ReactiveCommand
for the ReactiveUI framework. It makes heavy use of the RxDart package.
PRs are always welcome ;-)
If you don't know Rx think of it as Dart Streams
on steroids. RxCommand
capsules a given handler function that can then be executed by its execute
method or directly assigned to a widget's handler because it's a callable class. The result of this method is then published through its results
Observable (Observable wrap Dart Streams). Additionally it offers Observables for it's current execution state, if the command can be executed and for all possibly thrown exceptions during command execution.
A very simple example
final command = RxCommand.createSync3<int, String>((myInt) => "$myInt");
command.results.listen((s) => print(s)); // Setup the listener that now waits for events, not doing anything
// Somwhere else
command.execute(10); // the listener will print "10"
Getting a bit more impressive:
// This command will be executed everytime the text in a TextField changes
final textChangedCommand = RxCommand.createSync3((s) => s);
// handler for results
textChangedCommand.results
.debounce( new Duration(milliseconds: 500)) // Rx magic: make sure we start processing
// only if the user make a short pause typing
.listen( (filterText)
{
updateWeatherCommand.execute( filterText); // I could omit he execute because RxCommand is a callable class but here it
// makes the intention clearer
});
Getting Started #
Add to your pubspec.yaml
dependencies to rxdart
and rx_command
.
An RxCommand
is a generic class of type RxCommand<TParam, TRESULT>
where TPARAM
is the type of data that is passed when calling execute
and TResult
denotes the return type of the handler function. To signal that a handler doesn't take a parameter or returns a Null
value.
An example of the declaration from the included sample App
RxCommand<String,List<WeatherEntry>> updateWeatherCommand;
RxCommand<bool,bool> switchChangedCommand;
updateWeatherCommand
expects a handler that takes a String
as parameter and returns a List<WeatherEntry>
. switchChangedCommand
expects and returns a bool
value
Creating RxCommands #
For the different variations of possible handler methods RxCommand offers several factory methods for synchronous and asynchronous handlers. Due to the limitation that Dart doesn't allow method overloading they are numbered and look like this.
/// Creates a RxCommand for a synchronous handler function with no parameter and no return type
/// `action`: handler function
/// `canExecute` : observable that can bve used to enable/diable the command based on some other state change
/// if omitted the command can be executed always except it's already executing
static RxCommand<Null, Null> createSync(Action action,[Observable<bool> canExecute])
createFromStream
Creates a RxCommand from an "one time" observable. This is handy if used together with a Stream generator function.
provider
: provider function that returns a new Stream
that will be subscribed on the call of [execute]
canExecute
: observable that can be used to enable/disable the command based on some other state change
If omitted the command can be executed always except it's already executing
static RxCommand<TParam, TResult> createFromStream<TParam, TResult>(StreamProvider<TParam, TResult> provider, [Observable<bool> canExecute])
An RxCommand
created with createFromStream
will emit one more CommandResult
item after the last data Item was received. Not sure if this is an ideal solution, will have play with it
Example #
The sample App contains a Switch
widget that enables/disables the update command. The switch itself is bound to the switchChangedCommand
that's result is then used as canExcecute
of the updateWeatherCommand
:
switchChangedCommand = RxCommand.createSync3<bool,bool>((b)=>b);
// We pass the result of switchChangedCommand as canExecute Observable to the upDateWeatherCommand
updateWeatherCommand = RxCommand.createAsync3<String,List<WeatherEntry>>(update,switchChangedCommand.results);
As the Update Button
's building is based on a StreamBuilder
that listens on the canExecute
Observable of the updateWeatherCommand
the buttons enabled/disabled state gets automatically updated when the Switch's
state changes
Using RxCommands #
RxCommand
is typically used in a ViewModel of a Page, which is made accessible to the Widgets via an InheritedWidget
. Its execute
method can then directly be assigned as event handler of the Widgets.
The result
of the command is best used with a StreamBuilder
or inside a StatefulWidget.
By subscribing (listening) to the isExecuting
property of a RxCommand you can react on any execution state change of the command. E.g. show a spinner while the command is running.
By subscribing to the canExecute
property of a RxCommand you can react on any state change of the executability of the command.
As RxCommand is a callable class you can assign it directly to handler functions of Flutter widgets like:
new TextField(onChanged: TheViewModel.of(context).textChangedCommand,)
Listening for CommandResults
The original ReactiveCommand
from ReactiveUI separates the state information of the command into four Observables (result, thrownExceptions, isExecuting, canExecute
) this works great in an environment that doesn't rebuild the whole screen on state change. Flutter it's often desirable when working with a StreamBuilder
to have all this information at one place so that you can decide what to display depending on the returned state. Therefore RxCommand
itself is an Observable emitting CommandResult
objects:
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;
}
Disposing subscriptions (listeners) #
When subscribing to an Observable with .listen
you should store the returned StreamSubscription
and call .cancel
on it if you want to cancel this subscription to a later point or if the object where the subscription is made is getting destroyed to avoid memory leaks.
RxCommand
has a dispose
function that will cancel all active subscriptions on its observables. Calling dispose
before a command gets out of scope is a good practise.
Exploring the sample App #
The best way to understand how RxCommand
is used is to look at the supplied sample app which is a simple app that queries a REST API for weather data.
The ViewModel #
It follow the MVVM design pattern so all business logic is bundled in the WeatherViewModel
class in weather_viewmodel.dart.
It is made accessible to the Widgets by using an InheritedWidget which is defined in main.dart and returns and instance of WeatherViewModel
when used like TheViewModel.of(context)
The view model publishes two commands
updateWeatherCommand
which makes a call to the weather API and filters the result based on a string that is passed to execute. Its result will be bound to aStreamBuilder
in your View.switchChangedCommand
which will be bound to aSwitch
widget to enable/disable the `updateWeatherCommand.
The View #
main.dart
creates the ViewModel and places it at the very base of the app`s widget tree.
homepage.dart
creates a Column
with a
-
TextField
where you can enter a filter text which binds to the ViewModelstextChangedCommand
. -
a middle block which can either be a
ListView
(WeatherListView
) or a busy spinner. It is created by aStreamBuilder
which listens to
TheViewModel.of(context).updateWeatherCommand.isExecuting
-
A row with the Update
Button
and aSwitch
that toggles if an update should be possible or not by binding toTheViewModel.of(context).switchChangedCommand)
. To change the enabled state of the button the button is build by aStreamBuilder
that listens to theTheViewModel.of(context).updateWeatherCommand.canExecute
listview.dart
implements WeatherListView
which consists again of a StreamBuilder which updates automatically by listening on TheViewModel.of(context).updateWeatherCommand.results