RxVMS foundations: RxCommand and GetIt

Welcome to part 4 of my series on Flutter Architecture:

While the last two posts weren’t specifically aimed at RxVMS, they are a necessary requirement to understand the RxVMS architecture. Today, we turn to the important packages you will need to use RxVMS in your own App.

GetIt: The Fast ServiceLocator

When you recall the diagram picturing the different elements of an RxVMS app…

RxVMS

…you might have wondered how the various views, managers, and services are able to know of each other. More importantly, you may be wondering how one element is able to access another one’s functions.

While there are many different ways to achieve this (e.g. Inherited Widgets, IoC containers, dependency injections…), I personally prefer the use of a service locator. I wrote a whole blog post on the use of my Service Locator GetIt, but I’ll give a short introduction here as well. Basically, you register types or objects in the ServiceLocator (SL) once, and then you can access these objects from anywhere in your app. This is similar to how you’d use a Singleton…but with more flexibility.

Usage

GetIt’s usage is pretty straightforward. At the start of your app, you register the Services or Managers that you will want to access later in other parts of your app. After that, you call the SL to access instances of the registered types.

The nice thing is that you can register an interface or abstract class together with a concrete implementation. When accessing the instance, you always ask for the interface/abstract class type. This makes it easy to switch the implementation by just switching the concrete type at registration time. This allows you to easily switch a real Service for a MockService.

Getting practical

I typically initialize my SL in a file called service_locator.dart, which also contains the global (ambient) variable for the SL. This will be the only global variable in your app.

// This is an example of a current project
GetIt sl = new GetIt();

void setUpServiceLocator(ErrorReporter reporter) {

// Services

  // [registerSingleton] registers an instance of an object with the type or a derived-type 
  // of the one you pass as a generic Parameter
  // sl.get<ErrorReporter>.get() will always return that instance
  sl.registerSingleton<ErrorReporter>(reporter);

  // [registerLazySingleton] gets passed a factory function that returns the type or a derived-type 
  // of the one you pass as a generic Parameter
  // sl.get<ImageService>.get() will call the registered factory function on the first call and store
  // the returned instance. On any subsequent calls, the stored instance is returned
  sl.registerLazySingleton<ImageService>(() => new ImageServiceImplementation());

  sl.registerLazySingleton<MapService>(() => new MapServiceImplementation());

// Managers

  sl.registerSingleton<UserManager>(new UserManagerImplementation());

  sl.registerLazySingleton<EventManager>(() => new EvenManagerImplementation());

  sl.registerLazySingleton<ImageManager>(() => new ImageManagerImplementation());

  sl.registerLazySingleton<AppManager>(() => new AppManagerImplementation());
}

Whenever you want to access one of these objects, simply call

  RegistrationType object = sl.get<RegistrationType>();

//because GetIt is a callable class, you can write this even shorter:
  RegistrationType object2 = sl<RegistrationType>();

Extremely important: If you use GetIt, ALWAYS use the same style when you import your project files. Either use packages (recommended) or use relative paths, but never both. Again, DON’T mix relative paths with packages! This is because Dart will treat them as two different files, even though both are referencing the same file.

If this was a bit too quick, please read my blog post for more details.

RxCommand

Now that we are using GetIt to access our Objects from anywhere (including the UI), I want to look at how we can implement handler functions for UI events. The simplest approach would be to add functions to our Manager objects and use them in our Widgets:

class SearchManager {
  void lookUpZip(String zip);
}

Then in the UI:

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

This would call lookUpZip on every change in the TextField. But how do we display the result? As we try to be reactive, we could add a StreamController to the 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);
  }
}

Then in the UI we can use:

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

While this approach does work, it isn’t optimal. Issues include:

  • boilerplate – We must always create a function, a StreamController, and a getter for the Stream if we don’t want to expose the whole StreamController.
  • busy states – While the function is running, what if we want our UI to show a Spinner?
  • error handling – What happens if lookUpZip throws an exception?

Sure, we could add more StreamControllers to handle busy states and errors…but this gets tedious if we have to do that manually for every event handler. That’s where the rx_command package comes in handy.

RxCommand solves all of the above issues and more. An RxCommand encapsulates a function (sync or async) and automatically publishes its results on a Stream.

With RxCommand, we could rewrite our manager to:

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

In the UI:

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

    ...

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

Which is much more concise and readable.

RxCommand in Detail

rxcommand

RxCommand has one input and five output Observables:

  • canExecuteInput is an optional Observable<bool> which you can pass to the factory function when creating an RxCommand. It signals the RxCommand if it can be executed, depending on the last value it received over this Observable.
  • isExecuting is an Observable<bool> that signals if the command is currently executing its wrapped function. When a command is busy, it cannot be triggered a second time. If you want to display a Spinner while the wrapped function is running, listen to isExecuting.
  • canExecute is an Observable<bool> that signals if the command can be executed. This synergizes well with a StreamBuilder to change the appearance of a button between enabled/disabled.
    • Its value is actually:
Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) 
        => canExecuteInput && !isExecuting).distinct.
  • Which means:
    • It will emit false if isExecuting emits true
    • It will emit true only if isExecuting emits false AND canExecuteInput hasn’t emitted false.
  • thrownExceptions is an Observable<Exeption>. All thrown exceptions that the wrapped function may throw will be caught and emitted on this Observable. It is ideal to listen to it and display a dialog if an error occurs.

  • (the command itself) is actually an Observable. Values returned by the wrapped function will be emitted on that channel, so you can directly pass an RxCommand to a StreamBuilder as a stream parameter.

  • results contain all command states in one Observable<CommandResult>, where CommandResult is defined as
/// 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';
  }
}

The .results Observable is especially useful if you want to feed the result of a Command directly to a StreamBuilder. This will display different contents depending on the state of the command’s execution, and it works very well with RxLoader from the rx_widgets package. Here is an example of an RxLoader widget that uses the .results Observable:

Expanded(
  /// RxLoader executes different builders based on the 
  /// state of the Stream<CommandResult>
  child: RxLoader<List<WeatherEntry>>(
    spinnerKey: AppKeys.loadingSpinner,
    radius: 25.0,
    commandResults: sl.get<AppManager>().updateWeatherCommand.results,
    /// executed if .hasData == true
    dataBuilder: (context, data) =>
        WeatherListView(data, key: AppKeys.weatherList),
    /// executed if .isExceuting == true
    placeHolderBuilder: (context) => Center(
        key: AppKeys.loaderPlaceHolder, child: Text("No Data")),
    /// executed if .hasError == true
    errorBuilder: (context, ex) => Center(
        key: AppKeys.loaderError,
        child: Text("Error: ${ex.toString()}")),
  ),
),

Creating RxCommands

RxCommands can wrap synchronous and asynchronous functions. Functions that:

  • Take no parameter and don’t return any value
  • Take a Parameter and don’t return a result value
  • Take no Parameter but return a value
  • Take a parameter and return a value

For the different variations of possible handler functions RxCommand offers several factory methods for synchronous and asynchronous handlers. They 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<void, void> createSyncNoParamNoResult(Action action,[Observable<bool> canExecute])

There are these variants:

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

Even if your wrapped function does not return a value, RxCommand will emit a void value when the function has executed. So you can listen to such a command to react when the function has finished

Accessing the last result

RxCommand.lastResult gives you access to the last successful result value of the commands execution. Which can be uses as initialData of a StreamBuilder.
If you want to get the last result included in the CommandResult events while executing or in case of an error you can pass emitInitialCommandResult=true when creating the command.
If you want to assign an initialValue to .lastResult e.g. if you use it with a StreamBuilder's initialData you can pass it with the initialLastResult parameter when creating the command.

Example – Making Flutter Reactive

The latest version of the https://github.com/escamoteur/making_flutter_reactive- repository was refactored to RxVMS, so now you should have a good example on how to use it.

Because it’s a very simple app, we only need one Manager:

class AppManager {
  RxCommand<String, List<WeatherEntry>> updateWeatherCommand;
  RxCommand<bool, bool> switchChangedCommand;
  RxCommand<String, String> textChangedCommand;

  AppManager() {
    // This Command expects a bool value when executed and passes it through on it's
    // result Observable (stream)
    switchChangedCommand = RxCommand.createSync<bool, bool>((b) => b);

    // We pass the result of switchChangedCommand as canExecute Observable to
    // the updateWeatherCommand
    updateWeatherCommand = RxCommand.createAsync<String, List<WeatherEntry>>(
      sl.get<WeatherService>().getWeatherEntriesForCity,
      canExecute: switchChangedCommand,
    );

    // Will be called on every change of the search field
    textChangedCommand = RxCommand.createSync<String, String>((s) => s);

    // When the user starts typing
    textChangedCommand
        // Wait for the user to stop typing for 500ms
        .debounce(new Duration(milliseconds: 500))
        // Then call the updateWeatherCommand
        .listen(updateWeatherCommand);

    // Update data on startup
    updateWeatherCommand('');
  }
}

You can combine different RxCommands together. Notice how switchedChangedCommand is actually the canExecute Observable for updateWeatherCommand.

Now observe how the Manager is used in the UI:

    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,
              ),
              // Here we use the textChangedCommand!
              onChanged: sl.get<AppManager>().textChangedCommand,
            ),
          ),
          Expanded(
            /// RxLoader executes different builders based on the
            /// state of the 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>[
                /// Building the Updatebutton depending on updateWeatherCommand.canExecute
                Expanded(
                  // This might be solved with a Streambuilder too but it should show `WidgetSelector`
                  child: WidgetSelector(
                    buildEvents: sl
                        .get<AppManager>()
                        .updateWeatherCommand
                        .canExecute, //We access our ViewModel through the inherited Widget
                    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,
                    ),
                  ),
                ),
                // This Switch toggles the canExecuteInput of the updadateWeatherCommand
                StateFullSwitch(
                  state: true,
                  onChanged: sl.get<AppManager>().switchChangedCommand,
                ),
              ],
            ),
          ),
        ],
      ),
    );

Typical usage patterns

We have already seen one way to react to the different states of a command using CommandResults. In cases where we want to display if the command has succeeded (but not display the result), a common pattern is to listen to the command’s Observables in the initState function of a StatefulWidget. Here is an example of a real project.

createEventCommand is defined as:

  RxCommand<Event, void> createEventCommand;

It will create an Event object in the database and return no real value. But as we have learned before, even an RxCommand with return type void will emit one data item on completion. So we can use this behaviour to trigger an action in our App as soon as the command completes:

@override
void initState() {
  // this subscription just waits for the completion of the command and then pops the page and shows a Toast
  _eventCommandSubscription = _createCommand.listen((_) async {
    Navigator.pop(context);
    await showToast('Event saved');
  });

  // react on any error while creating an event
  _errorSubscription = _createEventCommand.thrownExceptions.listen((ex) async {
    await sl.get<ErrorReporter>().logException(ex);
    await showMessageDialog(context, 'There was a problem saving event', ex.toString());
  });
}

Important: we have to cancel this subscriptions when we don’t need them anymore:

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

Also, if you want to use an overlay to display a busy spinner, you could:

  • listen to the isExceuting Observable of a command in the initState function;
  • show/hide the spinner in the subscription; and
  • use the Command itself as the data source for a StreamBuilder

Making life easier with RxCommandListeners

If you want to react to more than one Observable, you may have to manage multiple subscriptions. Directly managing the listening and freeing of multiple subscriptions can be challenging, makes the code less readable, and puts you at risk for making mistakes (e.g. forgetting to cancel during dispose).

The latest version of rx_command added a helper class called RxCommandListener, which is designed to makes this handling easier. Its constructor takes a command and direct handler functions for the different state changes:

class RxCommandListener<TParam, TResult> {
  final RxCommand<TParam, TResult> command;

  // Is called on every emitted value of the command
  final void Function(TResult value) onValue;
  // Is called when isExceuting changes 
  final void Function(bool isBusy) onIsBusyChange;
  // Is called on exceptions in the wrapped command function
  final void Function(Exception ex) onError;
  // Is called when canExecute changes
  final void Function(bool state) onCanExecuteChange;
  // is called with the vealue of the .results Observable of the command
  final void Function(CommandResult<TResult> result) onResult;

  // to make the handling of busy states even easier these are called on their respective states 
  final void Function() onIsBusy;
  final void Function() onNotBusy;

  // optional you can directly pass in a debounce duration for the values of the command
  final Duration debounceDuration;

RxCommandListener(this.command,{    
  this.onValue,
  this.onIsBusyChange,
  this.onIsBusy,
  this.onNotBusy,
  this.onError,
  this.onCanExecuteChange,
  this.onResult,
  this.debounceDuration,}
)

 void dispose(); 

You don’t have to pass all of the handler functions. They all are optional, so you can just pass the ones you need. You only have to dispose the RxCommandListener in your dispose function, and it will cancel all of the internally-used subscriptions.

Let’s compare the same code with and without RxCommandListener in another real app example. The selectAndUploadImageCommand here is used in a chat screen where the user can upload images to the chat. When the command is called:

  • An ImagePicker dialog is shown.
  • After an image has been selected, the image is uploaded.
  • Once upload is complete, the command returns the storage location of the image so that a new image chat entry can be created.

Without RxCommandListener:

_selectImageCommandSubscription = sl
      .get<ImageManager>()
      .selectAndUploadImageCommand
      .listen((imageLocation) async {
    if (imageLocation == null) return;
    // this calls the execute method of the command
    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"));

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

As a rule of thumb, I would only use an RxCommandListener if I wanted to listen to more than one observable.

Give RxCommands a try and see how it can make your live easier. You don’t need to use RxVMS to benefit from the power of RxCommands.

For additional information about RxCommand read the readme of the rx_command package.

Unfortunately I’m pretty busy in a client project at the moment so that I can’t say when the next post will be ready, so please have a bit patience.

Contact me:

26 thoughts on “RxVMS foundations: RxCommand and GetIt

  1. aytunc says:

    Hi Thomas,

    Thanks for the great blog.
    I am a beginner to Flutter and I am trying to grasp your architecture. It will take some time I believe:)

    One quick question: Is GetIt an alternative to BLOC method? Or is it mainly a cool singleton architecture?

    I was started with BLOC(sink-stream) pattern and i was getting used to it. I read your rxdart blogs which were really helpful and was planning on using rxdart in BLOC. Then I saw about GetIt. Now I am a little confused about where GetIt stands? If it is an alternative to BLOC/inherited widget/scoped model/etc, what are the main pros/cons?

    Thank you very much.

    • Thomas Burkhart says:

      GetIt is not an replacement for BLOC but it can be used to access your BLOCs from the UI in this regard it is a replacement for en InheritedWidget/ScopedModel. It also has the advantage that it is completely separated from any UI code

  2. Bachar says:

    Yeaaah!!! that’s what I called have fun on the belt . waiting for more

  3. johnq says:

    is there an equivalent of RxCommand in Javascript?
    i’d like to use RxVMS paradigm with React too

  4. Kelvin Ji says:

    Great post! Looking forward your next post and more examples.

    Thanks very much.

  5. Kangmicin says:

    Cool this what I waiting for so long, I already read your previous series in I am in love with. so excited for the upcoming post! 🙂

  6. Russell says:

    Hi Thomas!

    Glad to see you continue with this excellent series. Looking forward to more.

    One question about RxCommand. In some cases my manager wants to expose a command that is only callable from within the manager but its result stream is exposed for clients to listen to.

    Before RxCommand I would make my trio of subject, private sink and public stream. How to model that with RxCommand?

    Sure I can just have the public RxCommand but this will allow code outside the manager to call the command and I want to disallow this.

    Any advice?

    • Thomas Burkhart says:

      Thanks!!

      What you want is actually pretty easy, just expose the Observable interface of the command by adding a public getter getter like Observable get publicStream => myCommand;

  7. nhwilly says:

    Just coming from Xamarin and ReactiveUI as a beginner.

    Can’t believe I might be able to get the best of both worlds. Great post, great package and great expectations!

  8. Riddick4Ever says:

    k, lol really easy to read code and App flow If I use RxVMS. I hav been using normal Streambuilders in my current app(using Blocs) with using Navigate inside build(keep getting error) Do u think Rxloader will let me use Navigator inside its builder??? Either way I ll use this pattern for my App cause of higher Modularity.

    • Thomas Burkhart says:

      Hi,
      no you won’t be able to use Navigator inside, but I recommend using an RxCommand and listening in initstate where you can use the Navigator

  9. Natwar Singh says:

    After reading all of your posts about RxVAMS, RxVMS and watching your video I am sure its the architecture to go with flutter. It is easy to learn, read the code and understand whats going on and why.

    Thanks for your blogs and great packages (RxCommand, RxWidget and GetIt).

  10. Igor Pietraszko says:

    Where do you hold state data like current user. For example, an app stores the user locally. User is authenticated through some authentication service which is fine. But how does one get access to that state data over and over again? Rather then through a command, should the user be accessed as a variable? On the Manager (e.g. UserManager).

    I also found that I have a UserService (to retrieve additional info about user), SecurityService (to authenticate the user) and “Data”Service (to retrieve business data for the app based on the logged in user). No I could have three different Managers but then I have to switch between then depending on what data I need to access. So I rather went with an AppManager which in turn calls these three services. Would that be the suggested pattern?

    • Thomas Burkhart says:

      To your first question, I typically use have a currentUser property in my UserManager.

      Sure you could use one single AppManager but I prefer to split them into separate Managers to keep them at a reasonable code size.

  11. Valery says:

    Hi Thomas!
    I have translated all of 4 posts of this series. All with link back of course.
    https://habr.com/ru/post/448776/
    https://habr.com/ru/post/450950/
    https://habr.com/ru/post/451292/
    https://habr.com/ru/post/449872/
    Is there any chance to see rest of planned ones? ))

    Best regards!

  12. Max says:

    I just started reading your posts an am really interested in the approach you’re taking. Thanks for sharing this with everyone.

    I actually started using redux as I love ngrx for angular and ionic apps. Ngrx is built around rxjs, and you get so many cool built-in features. I don’t totally agree with your opinion on redux, but I’m also not happy with it’s current implementation in Flutter.

    Because of my disappointing first steps in Flutter redux, I’m going to look to switch to your approach, so keep the awesome content coming!

    Max

    • Thomas Burkhart says:

      Hi Max, I guess when you are already comfortable with redux then redux is ok. I personally just dont like it.
      Thanks for the nice words.
      Thomas

  13. hary says:

    Hi.. I’m interesting to implement RxVMS for my very first project, but i’m new in flutter and android. I’ve no idea how to create auth/login page with php and mysql as my backend. Would you like to give me some tutorial, please?

    Thank you in advance for any help you can provide

    • Thomas Burkhart says:

      Hi, I’m sorry but I don’t have the time to guide you through the development of your backend. Also I recommend to use FirebaseAuth as backend instead of rolling your on in PHP.

  14. Valery says:

    Hi again, Thomas!

    It’s really pity that you have no time to continue this series..

    it was started very promisingly and i hope you will continue…

    Best regards!

  15. Saman says:

    Hi sir… I tried to implement your great code architecture in my project, But Ive faced a question since I'm new to reactive programming. Ive posted it on StackOverFlow.
    I would be glad if you can make time and answer it since it’s based on your posts.
    https://stackoverflow.com/questions/57978973/dart-processing-rxcommand-result-before-send-it-to-rxloader

Leave a Reply

Your email address will not be published. Required fields are marked *