CINXE.COM
rx_command | Dart package
<!DOCTYPE html> <html lang="en-us"><head><script src="https://www.googletagmanager.com/gtm.js?id=GTM-MX6DBN9" async="async"></script><script src="/static/hash-j60jq2j3/js/gtm.js" async="async"></script><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1"/><meta name="robots" content="noindex"/><meta name="twitter:card" content="summary"/><meta name="twitter:site" content="@dart_lang"/><meta name="twitter:description" content="Reactive event handler wrapper class inspired by ReactiveUI."/><meta name="twitter:image" content="https://pub.dev/static/hash-j60jq2j3/img/pub-dev-icon-cover-image.png"/><meta property="og:type" content="website"/><meta property="og:site_name" content="Dart packages"/><meta property="og:title" content="rx_command | Dart package"/><meta property="og:description" content="Reactive event handler wrapper class inspired by ReactiveUI."/><meta property="og:image" content="https://pub.dev/static/hash-j60jq2j3/img/pub-dev-icon-cover-image.png"/><title>rx_command | Dart package</title><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&family=Google+Sans+Display:wght@400&family=Google+Sans+Text:wght@400;500;700&family=Google+Sans+Mono:wght@400;700&display=swap"/><link rel="shortcut icon" href="/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8"/><meta rel="apple-touch-icon" href="/static/hash-j60jq2j3/img/apple-touch-icon.png"/><meta rel="apple-touch-icon-precomposed" href="/static/hash-j60jq2j3/img/apple-touch-icon.png"/><link rel="stylesheet" href="https://www.gstatic.com/glue/v25_0/ccb.min.css"/><link rel="search" type="application/opensearchdescription+xml" title="Dart packages" href="/osd.xml"/><link rel="canonical" href="https://pub.dev/packages/rx_command"/><meta name="description" content="Reactive event handler wrapper class inspired by ReactiveUI."/><link rel="alternate" type="application/atom+xml" title="Updated Packages Feed for Pub" href="/feed.atom"/><link rel="stylesheet" type="text/css" href="/static/hash-j60jq2j3/material/bundle/styles.css"/><link rel="stylesheet" type="text/css" href="/static/hash-j60jq2j3/css/style.css"/><script src="/static/hash-j60jq2j3/material/bundle/script.min.js" defer="defer"></script><script src="/static/hash-j60jq2j3/js/script.dart.js" defer="defer"></script><script src="https://www.gstatic.com/brandstudio/kato/cookie_choice_component/cookie_consent_bar.v3.js" defer="defer" data-autoload-cookie-consent-bar="true"></script><meta name="pub-page-data" content="eyJwa2dEYXRhIjp7InBhY2thZ2UiOiJyeF9jb21tYW5kIiwidmVyc2lvbiI6IjYuMC4xIiwibGlrZXMiOjQ4LCJwdWJsaXNoZXJJZCI6ImZsdXR0ZXJjb21tdW5pdHkuZGV2IiwiaXNEaXNjb250aW51ZWQiOmZhbHNlLCJpc0xhdGVzdCI6dHJ1ZX0sInNlc3Npb25Bd2FyZSI6ZmFsc2V9"/><link rel="preload" href="/static/hash-j60jq2j3/highlight/highlight-with-init.js" as="script"/></head><body class="light-theme"><script src="/static/hash-j60jq2j3/js/dark-init.js"></script><noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MX6DBN9" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript><div class="site-header"><button class="hamburger" aria-label="menu toggle"></button><a class="logo" href="/" aria-label="Go to the landing page of pub.dev"><img class="site-logo" src="/static/hash-j60jq2j3/img/pub-dev-logo.svg" alt="" width="140" height="30" role="presentation"/></a><div class="site-header-space"></div><div class="site-header-mask"></div><div class="site-header-search"><form action="/packages" method="GET"><input class="site-header-search-input" name="q" placeholder="New search..." autocomplete="on" title="Search"/></form></div><nav class="site-header-nav scroll-container"><div class="nav-login-container"><button id="-account-login" class="nav-main-button link">Sign in</button></div><div class="nav-container nav-help-container hoverable"><button class="nav-main-button">Help</button><div class="nav-hover-popup"><div class="nav-table-columns"><div class="nav-table-column"><h3>pub.dev</h3><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div><div class="nav-table-column"><h3>Flutter</h3><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div><div class="nav-table-column"><h3>Dart</h3><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">pub.dev <img class="foldable-icon" src="/static/hash-j60jq2j3/img/nav-mobile-foldable-icon.svg" alt="toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="/help/search" rel="noopener" target="_blank">Searching for packages</a><a class="nav-link" href="/help/scoring" rel="noopener" target="_blank">Package scoring and pub points</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Flutter <img class="foldable-icon" src="/static/hash-j60jq2j3/img/nav-mobile-foldable-icon.svg" alt="toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://flutter.dev/using-packages/" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://flutter.dev/developing-packages/" rel="noopener" target="_blank">Developing packages and plugins</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div><div class="nav-container nav-help-container-mobile foldable"><h3 class="foldable-button">Dart <img class="foldable-icon" src="/static/hash-j60jq2j3/img/nav-mobile-foldable-icon.svg" alt="toggle folding of the section" width="13" height="6"/></h3><div class="foldable-content"><a class="nav-link" href="https://dart.dev/guides/packages" rel="noopener" target="_blank">Using packages</a><a class="nav-link" href="https://dart.dev/tools/pub/publishing" rel="noopener" target="_blank">Publishing a package</a></div></div></nav><button class="-pub-theme-toggle" aria-label="light/dark theme toggle"></button></div><div id="banner-container"></div><main class="container"><div class="detail-wrapper -active -has-info-box"><div class="detail-header -is-loose"><div class="detail-container"><div class="detail-header-outer-block"><div class="detail-header-content-block"><h1 class="title pub-monochrome-icon-hoverable">rx_command 6.0.1 <span class="pkg-page-title-copy"><img class="pub-monochrome-icon pkg-page-title-copy-icon filter-invert-on-dark" src="/static/hash-j60jq2j3/img/content-copy-icon.svg" alt="copy "rx_command: ^6.0.1" to clipboard" width="18" height="18" title="Copy "rx_command: ^6.0.1" to clipboard" data-copy-content="rx_command: ^6.0.1" data-ga-click-event="copy-package-version"/><div class="pkg-page-title-copy-feedback"><span class="code">rx_command: ^6.0.1</span> copied to clipboard</div></span></h1><div class="metadata">Published <span><a class="-x-ago" href="" title="Jul 13, 2021" role="button" data-timestamp="1626178107252">3 years ago</a></span> • <a class="-pub-publisher" href="/publishers/fluttercommunity.dev"><img class="-pub-publisher-shield filter-invert-on-dark" src="/static/hash-j60jq2j3/img/material-icon-verified.svg" alt="verified publisher" width="14" height="14" title="Published by a pub.dev verified publisher"/>fluttercommunity.dev</a><span class="package-badge" title="Package is compatible with Dart 3.">Dart 3 compatible</span></div><div class="detail-tags-and-like"><div class="detail-tags"><div class="-pub-tag-badge"><span class="tag-badge-main">SDK</span><a class="tag-badge-sub" href="/packages?q=sdk%3Adart" rel="nofollow" title="Packages compatible with Dart SDK">Dart</a><a class="tag-badge-sub" href="/packages?q=sdk%3Aflutter" rel="nofollow" title="Packages compatible with Flutter SDK">Flutter</a></div><div class="-pub-tag-badge"><span class="tag-badge-main">Platform</span><a class="tag-badge-sub" href="/packages?q=platform%3Aandroid" rel="nofollow" title="Packages compatible with Android platform">Android</a><a class="tag-badge-sub" href="/packages?q=platform%3Aios" rel="nofollow" title="Packages compatible with iOS platform">iOS</a><a class="tag-badge-sub" href="/packages?q=platform%3Alinux" rel="nofollow" title="Packages compatible with Linux platform">Linux</a><a class="tag-badge-sub" href="/packages?q=platform%3Amacos" rel="nofollow" title="Packages compatible with macOS platform">macOS</a><a class="tag-badge-sub" href="/packages?q=platform%3Aweb" rel="nofollow" title="Packages compatible with Web platform">web</a><a class="tag-badge-sub" href="/packages?q=platform%3Awindows" rel="nofollow" title="Packages compatible with Windows platform">Windows</a></div></div><div class="detail-like"><button id="-pub-like-icon-button" class="mdc-icon-button" data-ga-click-event="toggle-like" aria-pressed="false" title="Like this package"><img class="mdc-icon-button__icon" src="/static/hash-j60jq2j3/img/like-inactive.svg" alt="liked status: inactive" width="18" height="18"/><img class="mdc-icon-button__icon mdc-icon-button__icon--on" src="/static/hash-j60jq2j3/img/like-active.svg" alt="liked status: active" width="18" height="18"/></button><span class="likes-count"><span id="likes-count">48</span></span></div></div></div></div></div></div><div class="detail-container"><div class="detail-lead"><div class="detail-metadata-toggle"><div class="detail-metadata-toggle-icon">→</div><h3 class="detail-lead-title">Metadata</h3></div><p class="detail-lead-text">Reactive event handler wrapper class inspired by ReactiveUI.</p><p class="detail-lead-more"><a class="detail-metadata-toggle">More...</a></p></div></div><div class="detail-body"><div class="detail-tabs"><div class="detail-tabs-wide-header"><div class="detail-container"><ul class="detail-tabs-header"><li class="detail-tab tab-button detail-tab-readme-title -active">Readme</li><li class="detail-tab tab-link detail-tab-changelog-title"><a href="/packages/rx_command/changelog" role="button">Changelog</a></li><li class="detail-tab tab-link detail-tab-example-title"><a href="/packages/rx_command/example" role="button">Example</a></li><li class="detail-tab tab-link detail-tab-installing-title"><a href="/packages/rx_command/install" role="button">Installing</a></li><li class="detail-tab tab-link detail-tab-versions-title"><a href="/packages/rx_command/versions" role="button">Versions</a></li><li class="detail-tab tab-link detail-tab-analysis-title"><a href="/packages/rx_command/score" role="button">Scores</a></li></ul></div></div><div class="detail-container detail-body-main"><div class="detail-tabs-content"><section class="tab-content detail-tab-readme-content -active markdown-body"><p><a href="https://github.com/fluttercommunity/community" rel="ugc"><img src="https://fluttercommunity.dev/_github/header/rx_command" alt="Flutter Community: rx_command"></a></p> <h1 id="rxcommand" class="hash-header">RxCommand <a href="#rxcommand" class="hash-link">#</a></h1> <blockquote> <p><strong>BREAKING CHANGE with V5.0</strong> RxCommand no longer works with Observables but with plain Dart Streams because the latest RxDart version now uses extension methods on Streams instead of Observables:</p> </blockquote> <pre><code class="language-Dart">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,... </code></pre> <blockquote> <p>IMPORTANT: As of V3.0 <code>CommandResult</code> objects are now emitted on the <code>.results</code> property and the pure results of the wrapped function on the RxCommand itself. So I switched the two because while working on RxVMS it turned out that I use the pure result much more often. Also the name of <code>.results</code> matches much better with <code>CommandResult</code>. If you don't want to change your code you can just stay on 2.06 if you don't need any of V 3.0 features.</p> </blockquote> <p>You can find a tutorial on how to use <code>RxCommands</code> in this blog post <a href="https://www.burkharts.net/apps/blog/making-flutter-more-reactive/" rel="ugc">Making Flutter more Reactive</a></p> <p><code>RxCommand</code> is an <a href="http://reactivex.io/" rel="ugc"><em>Reactive Extensions</em> (Rx)</a> based abstraction for event handlers. It is based on <code>ReactiveCommand</code> for the <a href="https://reactiveui.net/" rel="ugc">ReactiveUI</a> framework. It makes heavy use of the <a href="https://github.com/ReactiveX/rxdart" rel="ugc">RxDart</a> package.</p> <blockquote> <p>PRs are always welcome ;-)</p> </blockquote> <blockquote> <p>MAYBE BREAKING CHANGE in 2.0.0: Till now the <code>results</code> Stream and the <code>RxCommand</code> itself behaved like a <code>BehaviourSubjects</code>. This can lead to problems when using with Flutter. From now on the default is <code>PublishSubject</code>. If you need <code>BehaviourSubject</code> behaviour, meaning every new listener gets the last received value, you can set <code>emitsLastValueToNewSubscriptions = true</code> when creating <code>RxCommand</code>.</p> </blockquote> <p>If you don't know Rx think of it as Dart <code>Streams</code> on steroids. <code>RxCommand</code> capsules a given handler function that can then be executed by its <code>execute</code> 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 Stream interface. Additionally it offers Streams for it's current execution state, if the command can be executed and for all possibly thrown exceptions during command execution.</p> <p>A very simple example</p> <pre><code class="language-Dart">final command = RxCommand.createSync<int, String>((myInt) => "$myInt"); command.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" </code></pre> <p>Getting a bit more impressive:</p> <pre><code class="language-Dart">// This command will be executed everytime the text in a TextField changes final textChangedCommand = RxCommand.createSync((s) => s); // handler for results textChangedCommand .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 }); </code></pre> <h2 id="getting-started" class="hash-header">Getting Started <a href="#getting-started" class="hash-link">#</a></h2> <p>Add to your <code>pubspec.yaml</code> dependencies <code>rxdart</code> and <code>rx_command</code>.</p> <p>An <code>RxCommand</code> is a generic class of type <code>RxCommand<TParam, TResult></code> where <code>TParam</code> is the type of data that is passed when calling <code>execute</code> and <code>TResult</code> denotes the return type of the handler function. To signal that a handler doesn't take a parameter or returns a <code>null</code> value use <code>void</code> as type. Even if you create a <code>RxCommand<void,void></code> you will receive a <code>null</code> value when the wrapped function finishes so you can listen for the successful completion.</p> <p>An example of the declaration from the included sample App</p> <pre><code class="language-Dart">RxCommand<String,List<WeatherEntry>> updateWeatherCommand; RxCommand<bool,bool> switchChangedCommand; </code></pre> <p><code>updateWeatherCommand</code> expects a handler that takes a <code>String</code> as parameter and returns a <code>List<WeatherEntry></code>. <code>switchChangedCommand</code> expects and returns a <code>bool</code> value</p> <h3 id="creating-rxcommands" class="hash-header">Creating RxCommands <a href="#creating-rxcommands" class="hash-link">#</a></h3> <p>For the different variations of possible handler methods RxCommand offers several factory methods for synchronous and asynchronous handlers. They look like this.</p> <pre><code class="language-Dart"> /// Creates a RxCommand for a synchronous handler function with no parameter and no return type /// `action`: handler function /// `restriction` : Stream 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<void, void> createSyncNoParamNoResult(Action action,[Stream<bool>? restriction]) </code></pre> <p>There are these variants:</p> <pre><code class="language-Dart">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,... </code></pre> <p>Please check the API docs for detailed description of all parameters</p> <h4 id="createfromstream">createFromStream</h4> <p>Creates a RxCommand from an "one time" Stream. This is handy if used together with a Stream generator function. You can pass in an additional <code>Stream<bool></code> as <code>restriction</code> that determines if command can be executed.</p> <pre><code class="language-Dart"> /// Creates a RxCommand from an "one time" stream. This is handy if used together with a streame generator function. /// [provider]: provider function that returns a Stream that will be subscribed on the call of [execute] /// [restriction] : stream that can be used to enable/disable the command based on some other state change static RxCommand<TParam, TResult> createFromStream<TParam, TResult>(StreamProvider<TParam, TResult> provider, {Stream<bool>? restriction}) </code></pre> <h4 id="example-for-restriction">Example for <code>restriction</code></h4> <p>Every RxCommand exposes a <strong><code>canExecute</code></strong> Stream property, which sends <code>false</code> while the command is executing (like the <code>isExecuting</code> Stream property) but also if the command receives <code>false</code> from the <strong><code>restriction</code></strong> Stream passed when creating the RxCommand.</p> <p>The sample App contains a <code>Switch</code> widget that enables/disables the update command. The switch itself is bound to the <code>switchChangedCommand</code> whose result is then used as <code>restriction</code> of the <code>updateWeatherCommand</code>:</p> <pre><code class="language-Dart">switchChangedCommand = RxCommand.createSync<bool,bool>((b)=>b); // We pass the result of switchChangedCommand as restriction Stream to the updateWeatherCommand updateWeatherCommand = RxCommand.createAsync<String?, List<WeatherEntry>>(update, switchChangedCommand); </code></pre> <p>As the <em>Update</em> <code>Button</code>'s building is based on a <code>StreamBuilder</code> that listens on the <code>canExecute</code> Stream of the <code>updateWeatherCommand</code>, the button enabled/disabled state gets automatically updated when the <code>Switch's</code> state changes</p> <h4 id="error-handling-with-rxcommands">Error handling with RxCommands</h4> <p>By default all exceptions thrown by the wrapped function will be caught and swallowed. If you want to react on them, you can listen on the <code>thrownException</code> property. If you want to force RxCommand not to catch Exceptions set the <code>throwExceptions</code> <em>property</em> to <code>true</code>.</p> <h3 id="using-rxcommands-in-a-flutter-app" class="hash-header">Using RxCommands in a Flutter App <a href="#using-rxcommands-in-a-flutter-app" class="hash-link">#</a></h3> <p><code>RxCommand</code> is typically used in a ViewModel of a Page, which is made accessible to the Widgets via an <code>InheritedWidget</code> or <code>GetIt</code>.</p> <p>The <code>results</code> of the command is best used with a <code>StreamBuilder</code> or inside a <code>StatefulWidget</code>.</p> <p>By subscribing (listening) to the <code>isExecuting</code> 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.</p> <p>By subscribing to the <code>canExecute</code> property of a RxCommand you can react on any state change of the executability of the command. Like changing the appearance of a Button.</p> <p>As RxCommand is a callable class you can assign it directly to handler functions of Flutter widgets like:</p> <pre><code class="language-Dart">new TextField(onChanged: TheViewModel.of(context).textChangedCommand,) </code></pre> <h4 id="listening-for-commandresults">Listening for CommandResults</h4> <p>The original <code>ReactiveCommand</code> from <em>ReactiveUI</em> separates the state information of the command into four Streams (<code>result, thrownExceptions, isExecuting, canExecute</code>) 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 <code>StreamBuilder</code> to have all this information at one place so that you can decide what to display depending on the returned state. Therefore <code>RxCommand</code> offer the <code>.results</code> Stream emitting <code>CommandResult</code> objects:</p> <pre><code class="language-Dart">class CommandResult<TParam, TResult> { final TParam? paramData; final TResult? data; final Object? error; final bool isExecuting; const CommandResult(this.data, this.error, this.isExecuting); bool get hasData => data != null; bool get hasError => error != null; } </code></pre> <p><code>isExecuting</code> will issue a <code>bool</code> value on each state change. Even if you subscribe to a newly created command it will issue <code>false</code>. When listening for <code>CommandResult</code> this normally doesn't make sense, so by default no initial <code>CommandResult</code> will be emitted in correspondence with this initial value from <code>isExecuting</code>. However, If you want to get an initial Result with <code>isExecuting==false, data==null, error==null</code> pass <code>emitInitialCommandResult: true</code> when creating a command. Note: this initial <code>CommandResult</code> has always <code>data==null</code>, even if the <code>initialLastResult</code> parameter is not null.</p> <h3 id="accessing-the-last-result" class="hash-header">Accessing the last result <a href="#accessing-the-last-result" class="hash-link">#</a></h3> <p><code>RxCommand.lastResult</code> gives you access to the last successful result of the commands execution.</p> <p>If you want to get the last result included in the <code>CommandResult</code> events while executing or in case of an error you can pass <code>emitInitialCommandResult: true</code> when creating the command.</p> <p>If you want to assign an initialValue to <code>.lastResult</code> e.g. if you use it with a <code>StreamBuilder's</code> <code>initialData</code> you can pass it with the <code>initialLastResult</code> parameter when creating the command.</p> <h3 id="disposing-subscriptions-listeners" class="hash-header">Disposing subscriptions (listeners) <a href="#disposing-subscriptions-listeners" class="hash-link">#</a></h3> <p>When subscribing to an Stream with <code>.listen</code> you should store the returned <code>StreamSubscription</code> and call <code>.cancel</code> 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. <code>RxCommand</code> has a <code>dispose</code> function that will cancel all active subscriptions on its Streams. Calling <code>dispose</code> before a command gets out of scope is a good practise.</p> <h2 id="exploring-the-sample-app" class="hash-header">Exploring the sample App <a href="#exploring-the-sample-app" class="hash-link">#</a></h2> <p>The best way to understand how <code>RxCommand</code> is used is to look at the supplied sample app which is a simple app that queries a REST API for weather data.</p> <h3 id="the-viewmodel" class="hash-header">The ViewModel <a href="#the-viewmodel" class="hash-link">#</a></h3> <p>It follow the MVVM design pattern so all business logic is bundled in the <code>WeatherViewModel</code> class in <code>weather_viewmodel.dart</code>.</p> <p>It is made accessible to the Widgets by using an <a href="https://docs.flutter.io/flutter/widgets/InheritedWidget-class.html" rel="ugc">InheritedWidget</a> which is defined in main.dart and returns and instance of <code>WeatherViewModel</code>when used like <code>TheViewModel.of(context)</code></p> <p>The view model publishes two commands</p> <ul> <li><code>updateWeatherCommand</code> 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 a <code>StreamBuilder</code>in your View.</li> <li><code>switchChangedCommand</code> which will be bound to a <code>Switch</code> widget to enable/disable the <code>updateWeatherCommand</code>.</li> </ul> <h3 id="the-view" class="hash-header">The View <a href="#the-view" class="hash-link">#</a></h3> <p><code>main.dart</code> creates the ViewModel and places it at the very base of the app`s widget tree.</p> <p><code>homepage.dart</code> creates a <code>Column</code> with a</p> <ul> <li> <p><code>TextField</code> where you can enter a filter text which binds to the ViewModels <code>textChangedCommand</code>.</p> </li> <li> <p>a middle block which can either be a <code>ListView</code> (<code>WeatherListView</code>) or a busy spinner. It is created by a <code>StreamBuilder</code> which listens to <code>TheViewModel.of(context).updateWeatherCommand.isExecuting</code><br></p> </li> <li> <p>A row with the Update <code>Button</code> and a <code>Switch</code> that toggles if an update should be possible or not by binding to <code>TheViewModel.of(context).switchChangedCommand</code>. To change the enabled state of the button the button is build by a <code>StreamBuilder</code> that listens to the <code>TheViewModel.of(context).updateWeatherCommand.canExecute</code></p> </li> </ul> <p><code>listview.dart</code> implements <code>WeatherListView</code> which consists again of a StreamBuilder which updates automatically by listening on <code>TheViewModel.of(context).updateWeatherCommand</code>.</p> <h2 id="making-live-easier-with-rxcommandlisteners" class="hash-header">Making live easier with RxCommandListeners <a href="#making-live-easier-with-rxcommandlisteners" class="hash-link">#</a></h2> <p>If you want to react on more than one Stream of one command the listening and freeing of multiple of subscriptions makes the code less readable and you have to be careful not to forget to cancel all of them.</p> <p><code>RxCommandListener</code> makes this handling much easier. Its constructor takes a command and direct handler functions for the different state changes:</p> <pre><code class="language-Dart">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(dynamic ex)? onError; // Is called when canExecute changes final void Function(bool state)? onCanExecuteChange; // is called with the vealue of the .results Stream 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(); </code></pre> <p>You don't have to pass all handler functions. they all are optional so you can just pass the ones you need. You only have to <code>dispose</code> the <code>RxCommandListener</code> in your <code>dispose</code> function and it will cancel all internally uses subscriptions.</p> <p>Let's compare the same code with and without <code>RxCommandListener</code> in some real app code. The <code>selectAndUploadImageCommand</code> here is used in a chat screen where the user can upload images to the chat. When the command is called an <code>ImagePicker</code> dialog is shown and after successful selection of an image the image is uploaded. On completion of the upload the command returns the storage location of the image so that a new image chat entry can be created.</p> <pre><code class="language-Dart">_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")); </code></pre> <p>to</p> <pre><code class="language-Dart">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")); </code></pre> <p>As a rule of thumb I would only use an RxCommandListener if I want to listen to more than one Stream.</p> <h2 id="mocking-rxcommands" class="hash-header">Mocking RxCommands <a href="#mocking-rxcommands" class="hash-link">#</a></h2> <p>When writing UI Tests with Flutter its often better not to work with the real commands in the ViewModel but to use a <code>MockCommand</code> to have better control over the data a command receives and emits.</p> <p>For this the <code>MockCommand</code> class is for. It behaves almost like a normal <code>RxCommand</code></p> <p>It's created by</p> <pre><code class="language-Dart"> /// Factory constructor that can take an optional observable to control if the command can be executet factory MockCommand({ Stream<bool>? restriction, bool emitInitialCommandResult = false, bool emitLastResult = false, bool emitsLastValueToNewSubscriptions = false, TResult? initialLastResult, String? debugName, }) </code></pre> <p>You don't pass a handler function because this should be controlled from the outside. To control the outcome of the Command execution you can inspect these properties:</p> <pre><code class="language-Dart">/// the last value that was passed when execute or the command directly was called TParam? lastPassedValueToExecute; /// Number of times execute or the command directly was called int executionCount = 0; </code></pre> <p>To simulate a certain data output after calling the command use:</p> <pre><code class="language-Dart">/// to be able to simulate any output of the command when it is called you can here queue the output data for the next exeution call queueResultsForNextExecuteCall(List<CommandResult<TParam, TResult>> values) </code></pre> <p>To execute the command you can either call the command instance directly or call <code>execute</code></p> <pre><code class="language-Dart">/// Can either be called directly or by calling the object itself because RxCommands are callable classes /// Will increase [executionCount] and assign [lastPassedValueToExecute] the value of [param] /// If you have queued a result with [queueResultsForNextExecuteCall] it will be copies tho the output stream. /// [isExecuting], [canExceute] and [results] will work as with a real command. execute([TParam? param]) </code></pre> <p>Here an example from the <code>rx_widgets</code> example App</p> <pre><code class="language-Dart">testWidgets('Tapping update button updates the weather', (tester) async { final model = new MockModel(); // using mockito final command = new MockCommand<String,List<WeatherEntry>>(); final widget = new ModelProvider( model: model, child: new MaterialApp(home: new HomePage()), ); // to make the mocked model use the MockCommand instance. when(model.updateWeatherCommand).thenReturn(command); // if your App does not only access the command but also calls // it directly you have to register the call too: when(model.updateWeatherCommand()).thenAnswer((_)=>command()); command.queueResultsForNextExecuteCall([CommandResult<List<WeatherEntry>>( [WeatherEntry("London", 10.0, 30.0, "sunny", 12)],null, false)]); expect(command.results, emitsInOrder([ crm(null, false, false), // default value that will be emited at startup crm([WeatherEntry("London", 10.0, 30.0, "sunny", 12)], // data false, false) ])); await tester.pumpWidget(widget); // Build initial State await tester.pump(); // Build after Stream delivers value await tester.tap(find.byKey(AppKeys.updateButtonEnabled)); }); </code></pre> <p>To verify the changing states of the command e.g. to check if linked UI controls are created or in a certain state use:</p> <pre><code class="language-Dart">/// For a more fine grained control to simulate the different states of an `RxCommand` /// there are these functions /// [startExecution] will issue a [CommandResult] with /// paramData: [param] /// data: null /// error: null /// isExecuting : true void startExecution([TParam? param]) /// [endExecutionWithData] will issue a [CommandResult] with /// paramData: [lastPassedValueToExecute] /// data: [data] /// error: null /// isExecuting : false void endExecutionWithData(TResult data) /// [endExecutionWithData] will issue a `CommandResult` with /// paramData: [lastPassedValueToExecute] /// data: null /// error: Exception([message]) /// isExecuting : false void endExecutionWithError(String message) /// [endExecutionWithData] will issue a `CommandResult` with /// paramData: [lastPassedValueToExecute] /// data: null /// error: null /// isExecuting : false void endExecutionNoData() </code></pre> <p>Also an example from <code>rx_widgets</code></p> <pre><code class="language-Dart">testWidgets('Shows a loading spinner and disables the button while executing and shows the ListView on data arrival', (tester) async { final model = new MockModel(); final command = new MockCommand<String,List<WeatherEntry>>(); final widget = new ModelProvider( model: model, child: new MaterialApp(home: new HomePage()), ); // Link MockCommand instance to mocked field in model when(model.updateWeatherCommand).thenReturn(command); await tester.pumpWidget(widget);// Build initial State await tester.pump(); expect(find.byKey(AppKeys.loadingSpinner), findsNothing); expect(find.byKey(AppKeys.updateButtonDisabled), findsNothing); expect(find.byKey(AppKeys.updateButtonEnabled), findsOneWidget); expect(find.byKey(AppKeys.weatherList), findsNothing); expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsOneWidget); command.startExecution(); await tester.pump(); // because there are two streams involded it seems we have to pump // twice so that both streambuilders can work await tester.pump(); expect(find.byKey(AppKeys.loadingSpinner), findsOneWidget); expect(find.byKey(AppKeys.updateButtonDisabled), findsOneWidget); expect(find.byKey(AppKeys.updateButtonEnabled), findsNothing); expect(find.byKey(AppKeys.weatherList), findsNothing); expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsNothing); command.endExecutionWithData([new WeatherEntry("London", 10.0, 30.0, "sunny", 12)]); await tester.pump(); // Build after Stream delivers value expect(find.byKey(AppKeys.loadingSpinner), findsNothing); expect(find.byKey(AppKeys.updateButtonDisabled), findsNothing); expect(find.byKey(AppKeys.updateButtonEnabled), findsOneWidget); expect(find.byKey(AppKeys.weatherList), findsOneWidget); expect(find.byKey(AppKeys.loaderError), findsNothing); expect(find.byKey(AppKeys.loaderPlaceHolder), findsNothing); }); </code></pre> </section></div></div></div><aside class="detail-info-box"><a class="packages-scores" href="/packages/rx_command/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">48</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">135</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">points</div></div><div class="packages-score packages-score-downloads" title="Number of downloads of this package during the past 30 days"><div class="packages-score-value -has-value"><span class="packages-score-value-number">531</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">downloads</div></div></a><h3 class="title">Publisher</h3><p><a href="/publishers/fluttercommunity.dev"><img class="-pub-publisher-shield filter-invert-on-dark" src="/static/hash-j60jq2j3/img/material-icon-verified.svg" alt="verified publisher" width="14" height="14" title="Published by a pub.dev verified publisher"/>fluttercommunity.dev</a></p><h3 class="title">Weekly Downloads</h3><div id="-weekly-downloads-sparkline" class="weekly-downloads-sparkline" data-widget="weekly-sparkline" data-weekly-sparkline-points="gCzrZ3oAAABxAAAAmAAAAIIAAAAxAAAAewAAALsAAACbAAAArQAAAJ0AAACiAAAAdgAAAHUAAAA6AAAAmAAAANEAAACBAAAAegAAAIwAAABoAAAAbgAAAFIAAABTAAAA6QAAAIoAAABwAAAApgAAAHcAAACSAAAAywAAAM0AAACTAAAAnwAAALwAAAC+AAAAbwAAAHUAAADXAAAAMgEAAO0AAADuAAAAYwEAAO0AAABgAQAAGwEAALwAAAAqAQAAugAAAKEAAADSAAAAtAAAAPQAAAA="></div><h3 class="title pkg-infobox-metadata">Metadata</h3><p>Reactive event handler wrapper class inspired by ReactiveUI.</p><p><a class="link" href="https://github.com/fluttercommunity/rx_command" rel="ugc">Repository (GitHub)</a><br/></p><h3 class="title">Documentation</h3><p><a class="link" href="/documentation/rx_command/latest/">API reference</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img filter-invert-on-dark" src="/static/hash-j60jq2j3/img/material-icon-balance.svg" alt="" width="14" height="14" role="presentation"/>MIT (<a href="/packages/rx_command/license">license</a>)</p><h3 class="title">Dependencies</h3><p><a href="/packages/quiver" title="^3.0.1">quiver</a>, <a href="/packages/rxdart" title="^0.27.0">rxdart</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Arx_command" rel="nofollow">Packages that depend on rx_command</a></p></aside></div><script type="application/ld+json">{"@context":"http\u003a\u002f\u002fschema.org","@type":"SoftwareSourceCode","name":"rx\u005fcommand","version":"6.0.1","description":"rx\u005fcommand - Reactive event handler wrapper class inspired by ReactiveUI.","url":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002frx\u005fcommand","dateCreated":"2018-04-10T16\u003a03\u003a56.247298Z","dateModified":"2021-07-13T12\u003a08\u003a27.252844Z","programmingLanguage":"Dart","image":"https\u003a\u002f\u002fpub.dev\u002fstatic\u002fimg\u002fpub-dev-icon-cover-image.png","license":"https\u003a\u002f\u002fpub.dev\u002fpackages\u002frx\u005fcommand\u002flicense"}</script></div><div class="detail-metadata"><h3 class="detail-metadata-title"><span class="detail-metadata-toggle">←</span> Metadata</h3><div class="detail-info-box"><a class="packages-scores" href="/packages/rx_command/score"><div class="packages-score packages-score-like"><div class="packages-score-value -has-value"><span class="packages-score-value-number">48</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">likes</div></div><div class="packages-score packages-score-health"><div class="packages-score-value -has-value"><span class="packages-score-value-number">135</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">points</div></div><div class="packages-score packages-score-downloads" title="Number of downloads of this package during the past 30 days"><div class="packages-score-value -has-value"><span class="packages-score-value-number">531</span><span class="packages-score-value-sign"></span></div><div class="packages-score-label">downloads</div></div></a><h3 class="title">Publisher</h3><p><a href="/publishers/fluttercommunity.dev"><img class="-pub-publisher-shield filter-invert-on-dark" src="/static/hash-j60jq2j3/img/material-icon-verified.svg" alt="verified publisher" width="14" height="14" title="Published by a pub.dev verified publisher"/>fluttercommunity.dev</a></p><h3 class="title">Weekly Downloads</h3><div id="-weekly-downloads-sparkline" class="weekly-downloads-sparkline" data-widget="weekly-sparkline" data-weekly-sparkline-points="gCzrZ3oAAABxAAAAmAAAAIIAAAAxAAAAewAAALsAAACbAAAArQAAAJ0AAACiAAAAdgAAAHUAAAA6AAAAmAAAANEAAACBAAAAegAAAIwAAABoAAAAbgAAAFIAAABTAAAA6QAAAIoAAABwAAAApgAAAHcAAACSAAAAywAAAM0AAACTAAAAnwAAALwAAAC+AAAAbwAAAHUAAADXAAAAMgEAAO0AAADuAAAAYwEAAO0AAABgAQAAGwEAALwAAAAqAQAAugAAAKEAAADSAAAAtAAAAPQAAAA="></div><h3 class="title pkg-infobox-metadata">Metadata</h3><p>Reactive event handler wrapper class inspired by ReactiveUI.</p><p><a class="link" href="https://github.com/fluttercommunity/rx_command" rel="ugc">Repository (GitHub)</a><br/></p><h3 class="title">Documentation</h3><p><a class="link" href="/documentation/rx_command/latest/">API reference</a><br/></p><h3 class="title">License</h3><p><img class="inline-icon-img filter-invert-on-dark" src="/static/hash-j60jq2j3/img/material-icon-balance.svg" alt="" width="14" height="14" role="presentation"/>MIT (<a href="/packages/rx_command/license">license</a>)</p><h3 class="title">Dependencies</h3><p><a href="/packages/quiver" title="^3.0.1">quiver</a>, <a href="/packages/rxdart" title="^0.27.0">rxdart</a></p><h3 class="title">More</h3><p><a href="/packages?q=dependency%3Arx_command" rel="nofollow">Packages that depend on rx_command</a></p></div><p class="detail-lead-back"><a class="detail-metadata-toggle">Back</a></p></div><div id="-screenshot-carousel" class="carousel"><fab id="-carousel-prev" class="mdc-fab carousel-prev carousel-nav" data-mdc-auto-init="MDCRipple" title="Previous" data-ga-click-event="screenshot-carousel-prev-click" tabindex="0"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-j60jq2j3/img/keyboard_arrow_left.svg" alt="previous" width="24" height="24" aria-hidden="true"/></fab><div id="-image-container" class="image-container"></div><fab id="-carousel-next" class="mdc-fab carousel-next carousel-nav" data-mdc-auto-init="MDCRipple" title="Next" data-ga-click-event="screenshot-carousel-next-click" tabindex="0"><div class="mdc-fab__ripple"></div><img class="mdc-fab__icon" src="/static/hash-j60jq2j3/img/keyboard_arrow_right.svg" alt="next" width="24" height="24" aria-hidden="true"/></fab><p id="-screenshot-description" class="screenshot-description"></p></div></main><footer class="site-footer"><a class="link" href="https://dart.dev/">Dart language</a><a class="link sep" href="/report?subject=package%3Arx_command&url=https%3A%2F%2Fpub.dev%2Fpackages%2Frx_command">Report package</a><a class="link sep" href="/policy">Policy</a><a class="link sep" href="https://www.google.com/intl/en/policies/terms/">Terms</a><a class="link sep" href="https://developers.google.com/terms/">API Terms</a><a class="link sep" href="/security">Security</a><a class="link sep" href="https://www.google.com/intl/en/policies/privacy/">Privacy</a><a class="link sep" href="/help">Help</a><a class="link icon sep" href="/feed.atom"><img class="inline-icon" src="/static/hash-j60jq2j3/img/rss-feed-icon.svg" alt="RSS" width="20" height="20" title="RSS/atom feed"/></a><a class="link icon github_issue" href="https://github.com/dart-lang/pub-dev/issues/new"><img class="inline-icon" src="/static/hash-j60jq2j3/img/bug-report-white-96px.png" alt="bug report" width="20" height="20" title="Report an issue with this site"/></a></footer><script src="/static/hash-j60jq2j3/highlight/highlight-with-init.js" defer="defer"></script></body></html>