Education
Software Development Executive - II
Last updated on Nov 1, 2023
Last updated on Aug 7, 2023
Reactive Programming is a paradigm that has gained substantial recognition for managing asynchronous data streams and handling reactive user interfaces, among other things. Think of user interactions, API responses, or even animations - Reactive Programming handles them all.
Enter RxDart - a reactive streaming extension library built on top of Dart Streams. It’s not an attempt to replace Dart Streams but embellishes them with the goodness of reactive programming. RxDart in Flutter has become increasingly popular due to its seamless capabilities in managing asynchronous data stream challenges.
So, how does RxDart connect with Dart Streams? Dart comes equipped with a powerful Streams API out-of-the-box. However, RxDart adds that extra sparkle with more functionalities based on reactive extensions for Dart Streams, bringing in more flexibility and control.
Flutter applications usually have to deal with asynchronous data streams extensively - be it user interactions, network responses, or data changes. By leveraging RxDart, we can handle these data streams more lively, responsive, and controlled. Especially when dealing with complex functionalities, Flutter RxDart provides ease of use and maintains the flow of data in a precise reactive way.
Before diving into the usage of RxDart in Flutter, we need to set things up.
Setting up RxDart in your Flutter project is a breeze. All you need to do is include it as a dependency in your pubspec.yaml file.
Here is how you add RxDart:
1 dependencies: 2 flutter: 3 sdk: flutter 4 rxdart: ^0.27.2 5
Remember to replace the package version with the latest version of RxDart available. Don’t forget to run flutter pub get in your terminal to ensure all dependencies are fetched.
Now that we've added RxDart to our project, let's see a simple example that depicts the usage of RxDart and Dart Streams. RxDart does not provide its Observable class as a replacement for Dart Streams, but it offers several additional Stream classes, operators (extension methods on the Stream class), and Subjects.
Below is a simple example of how we can use RxDart's capabilities:
1 import 'package:rxdart/rxdart.dart'; 2 3 void main() { 4 final subject = BehaviorSubject<int>(); 5 6 // observer 7 subject.stream.listen(print); // prints 1, 2, 3 8 9 // producer 10 subject.sink.add(1); 11 subject.sink.add(2); 12 subject.sink.add(3); 13 14 subject.close(); 15 } 16
In the above Flutter RxDart example, we're creating a BehaviorSubject that deals with integers. We listen to the stream of data and print whatever data is added into the BehaviorSubject.
After setting up RxDart and understanding its basics, we now venture into more exciting functionalities that the Flutter RxDart combo provides. Let's break it down into Stream Classes, Extension Methods, and Subjects.
In RxDart, Stream Classes allow us to create Streams with specific capabilities, such as combining or merging many Streams. Dart itself provides a Stream class with ways to create a Stream, like Stream.fromIterable or Stream.periodic. RxDart, however, takes it up a notch and extends some cool Stream classes for different use-cases.
Here's how you can merge two streams using RxDart's MergeStream:
1 final myFirstStream = Stream.fromIterable([1, 2, 3]); 2 final mySecondStream = Stream.fromIterable([4, 5, 6]); 3 4 final mergedStream = MergeStream([myFirstStream, mySecondStream]); 5 6 mergedStream.listen(print); // prints 1, 2, 3, 4, 5, 6 7
In this code, we create two Streams and merge them using the MergeStream class which results in a single Stream that merges the events from both input Streams.
RxDart extension methods are just methods that may be applied to any Stream. They empower an existing Stream and change it into a new Stream with enhanced capabilities. Throttling or buffering events, for example.
Let's see an example where we buffer a Stream of integers to groups of two:
1 Stream.fromIterable([1, 2, 3, 4, 5]) 2 .bufferCount(2) 3 .listen(print); // prints [1, 2], [3, 4], [5] 4
The bufferCount is an extension method provided by RxDart that buffers a Stream into a specified count.
Another powerful feature of RxDart is Subjects - a type of StreamController with added powers! Dart's StreamController creates and manages a Stream, while RxDart offers two additional types, BehaviorSubject and ReplaySubject.
The BehaviorSubject is a type of StreamController that caches the latest added value or error. So, when a new listener subscribes to the Stream, the latest value or error will be emitted to the listener. This can be extremely useful in scenarios where you want to share a single value (or its latest status) with multiple components in your Flutter application.
An example of a BehaviorSubject would be:
1 final behaviorSubject = BehaviorSubject<int>(); 2 3 // Adding data to the stream 4 behaviorSubject.add(1); 5 behaviorSubject.add(2); 6 behaviorSubject.add(3); 7 8 // This will print 3, as BehaviorSubject always returns the last added item 9 behaviorSubject.stream.listen(print); 10 11 behaviorSubject.close(); 12
RxDart provides a gamut of functionalities that play a significant role in enhancing the way we work with reactive programming in Flutter. Streams, Extension methods, and Subjects are the core elements to making RxDart one of the most efficient means to handle complex asynchronous data streams in Flutter.
When starting with RxDart, developers coming from other Rx libraries might question the differences between Observables that they used previously and Dart Streams. While they often work similarly, there are a few variations worth mentioning.
RxDart Observables vs Flutter Streams
Dart Streams can be thought of as asynchronous Iterables spread out over time, and when an error occurs in a Stream, the Stream emits an error event and then is finished. On the other hand, Observables, as in the standard Rx scenarios, terminate when an error occurs.
Dart Streams can have a single subscriber (Single-subscription streams) or multiple subscribers (broadcast streams), while Rx Observables (Cold Observables) do allow multiple subscribers, and each subscription will receive all events.
While transitioning from Observables to Dart Streams, developers need to keep in mind that Dart Streams emit their events asynchronously, and are not synchronized by default.
For example, last, length, and other methods always return Futures:
1 Stream.fromIterable([1, 2, 3, 4]) 2 .last 3 .then(print); // prints '4' 4
The given differences illuminate the contrast and help in recognizing when to employ Observables or Streams. With Flutter RxDart, you start with Dart Streams, then enhance them with extension methods provided by RxDart, offering the much-needed boost for managing data streams.
One aspect that makes Flutter applications stand out in the field of cross-platform application development is state management. If managed efficiently, apps can perform exceptionally with a smooth UI experience. That's where Flutter RxDart steps in, facilitating state management in a more effortless and streamlined manner.
RxDart brings the power of streams and reactive programming to Flutter, making state management a breeze. States in Flutter equate to the values that can change over time. Guess what? That's exactly what streams are all about!
A vital concept in this scenario is BehaviorSubject. As discussed before, BehaviorSubject is a special type of stream provided by RxDart that holds the most recent value, and it can be accessed synchronously.
Flutter provides a handy out-of-the-box widget called StreamBuilder that automatically registers a listener to a Stream and invokes the builder whenever an event is emitted. StreamBuilder and RxDart harmoniously work hand-in-hand to create a reactive Flutter application.
Let's look at a simple Flutter RxDart usage with StreamBuilder:
1 import 'package:flutter/material.dart'; 2 import 'package:rxdart/rxdart.dart'; 3 4 void main() { 5 runApp(MaterialApp(home: MyApp())); 6 } 7 8 class MyApp extends StatelessWidget { 9 final BehaviorSubject<String> _subject = BehaviorSubject<String>.seeded("Hello, RxDart!"); 10 11 @override 12 Widget build(BuildContext context) { 13 return Scaffold( 14 appBar: AppBar( 15 title: Text('RxDart with StreamBuilder'), 16 ), 17 body: Padding( 18 padding: EdgeInsets.all(16.0), 19 child: StreamBuilder<String>( 20 stream: _subject.stream, 21 builder: (context, snapshot) { 22 if (snapshot.hasData) { 23 return Text( 24 snapshot.data!, 25 style: Theme.of(context).textTheme.headline4, 26 ); 27 } else { 28 return CircularProgressIndicator(); 29 } 30 }, 31 ), 32 ), 33 ); 34 } 35 } 36
In the example above, a StreamBuilder is implemented that listens to the _subject stream. Whenever data is added to the stream, it automatically builds the widget.
When working with asynchronous programming, specifically streams of data, one common issue that might arise is backpressure. It's a condition where the stream is producing data faster than its consumer (subscriber) can handle. Backpressure can lead to performance issues or even crashes.
RxDart introduces several operators that assist in managing backpressure scenarios in Flutter applications. Let's check out an example using the debounceTime operator to tackle backpressure:
1 // Keyboard input stream that emits every keyup event 2 final inputStream = querySelector('#input')!.onKeyUp; 3 4 inputStream 5 .map((event) => (event.currentTarget as InputElement).value) 6 .debounceTime(Duration(milliseconds: 200)) // RxDart extension method 7 .listen((value) => print('User is typing: $value')); 8
In this example, if the user is typing too fast, we don't want to handle every single keyup event as it may put an unwanted load on our application. Thus, we debounce the keyup stream and listen to it only if the user hasn't typed anything for the last 200 milliseconds. This way, RxDart helps us in managing backpressure.
RxDart goes beyond just managing asynchronous data streams. It also supports advanced features like combining, merging, and switching between different streams which can be useful for complex Flutter applications.
Advanced RxDart Concepts for Flutter developers
Let's look at some examples for these advanced concepts:
In RxDart, you can combine multiple streams into one stream that will emit all values from every given stream in order of subscription. Let's look at how to apply the CombineLatestStream operator to two streams:
1 final firstStream = Stream.fromIterable([1, 2]); 2 final secondStream = Stream.fromIterable(['A', 'B']); 3 4 Rx.combineLatest2(firstStream, secondStream, (a, b) => '$a $b') 5 .listen(print); // prints '1 A', '2 B' 6
MergeStream merges multiple streams into one stream that emits all data from the given streams in the exact order they were emitted.
1 final firstStream = Stream.fromIterable([1, 2]); 2 final secondStream = Stream.fromIterable(['A', 'B']); 3 4 Rx.merge([firstStream, secondStream]) 5 .listen(print); // prints 1, 2, A, B 6
SwitchLatestStream takes a Stream of Streams (high-order Stream) as input and always emits values from the most recently provided Stream.
1 final triggerSwitch = PublishSubject<int>(); 2 final firstStream = Rx.timer('A', Duration(seconds: 3)); 3 final secondStream = Rx.timer('B', Duration(seconds: 1)); 4 5 triggerSwitch 6 .switchMap((value) => value == 1 ? firstStream : secondStream) 7 .listen(print); // if switch is triggered before 3 seconds, it will print B 8 9 triggerSwitch.add(1); 10 11 // After 2 seconds, trigger to switch to the faster stream 12 Future.delayed(Duration(seconds: 2), () => triggerSwitch.add(2)); 13
One of the strong suits of RxDart that Flutter developers can capitalize on is these advanced functionalities which can help in handling complex Flutter apps elegantly and effectively.
While using RxDart with Flutter, developers can face some challenges. But don't worry, we're here to address these common pitfalls and provide concise solutions.
Not Closing Streams: One common mistake is not closing streams when they're no longer needed. This can lead to memory leaks. Just like opening a Stream, it's important to close them too.
Solution: Always remember to close your streams, typically in dispose method in Flutter:
1 BehaviorSubject<int> counter; 2 3 @override 4 void dispose() { 5 counter.close(); 6 super.dispose(); 7 } 8
Not Handling Stream Errors: When dealing with streams, error handling is often overlooked. Your stream might throw an error in certain cases, and if not caught, it can result in a crash.
Solution: Always wrap your stream in a try-catch block or use onError as a method to handle errors gracefully:
1 stream.listen( 2 (data) { /* handle data */ }, 3 onError: (error) { /* handle error */ }, 4 onDone: () { /* handle done */ }, 5 ); 6
Debugging RxDart applications can be challenging due to the asynchronous nature of streams. However, Dart provides a debugging utility to Dump Stack Traces that helps in debugging.
Ideally, you would need to handle errors gracefully and write tests for error scenarios to ensure better debugging and error handling in your Flutter RxDart applications.
To fully understand the potential of RxDart in Flutter, let's see how we can use it in a real-world scenario. In this scenario, we'll be reading the Konami Code as user keyboard input. For those unfamiliar, the Konami Code is a secret code sequence historically used in video games, almost like an easter egg!
First, let's import RxDart and define the ASCII values that correspond to the key codes in the Konami Code:
1 import 'package:rxdart/rxdart.dart'; 2 3 const konamiKeyCodes = const <int>[ 4 KeyCode.UP, 5 KeyCode.UP, 6 KeyCode.DOWN, 7 KeyCode.DOWN, 8 KeyCode.LEFT, 9 KeyCode.RIGHT, 10 KeyCode.LEFT, 11 KeyCode.RIGHT, 12 KeyCode.B, 13 KeyCode.A 14 ]; 15
Next, we need to listen for key-up events. We'll use RxDart to buffer the last ten key codes, and then check if they match the Konami Code sequence:
1 document.onKeyUp 2 .map((event) => event.keyCode) 3 .bufferCount(10, 1) 4 .where((lastTenKeyCodes) => const IterableEquality<int>().equals(lastTenKeyCodes, konamiKeyCodes)) 5 .listen((_) => result.innerHtml = 'KONAMI!') 6
In this example, we are listening for keyup events, transforming the event to emit only the key code, buffering the last ten emitted key codes, and then checking to see if the last ten key codes match the Konami Code!
This example demonstrates how elegantly RxDart handles complex asynchronous data sequences allowing Flutter developers to create robust and efficient applications.
Upgrades are required as technology advances, and we must adapt our code to support these modifications. The same is true for RxDart. The Observable class has been deprecated since the release of RxDart 0.23.x, and Dart 2.6's extension methods are being utilized instead.
In order to upgrade your code to support the latest version, RxDart provides an automatic upgrade solution using the rxdart_codemod package.
To automatically update your code, follow the instructions included with the rxdart_codemod package. Simply running the package will assist in refactoring the code to support RxDart 0.23.x.
This way, you can ensure your code is always up-to-speed with the latest updates of RxDart in Flutter and harness the benefits of new features and performance improvements.
Throughout this post, we traversed the diverse landscape of RxDart in Flutter. We started from the basics, went through advanced concepts, and also looked at a real-world example to cap it off.
Future of RxDart in Flutter
With the ever-growing Flutter ecosystem and the consistent evolution of reactive programming, RxDart has a promising future. Being capable of providing more refined solutions for handling asynchronous data streams in Flutter, RxDart is bound to become even more popular among Flutter developers.
RxDart captures the essence of reactive programming and integrates smoothly with Flutter, making it an influential asset for mobile app developers. It embraces Dart Streams' power and enhances them with additional classes, methods, and functionalities that streamlined asynchronous stream management. With mastery over RxDart, you can create reactive applications in Flutter that are robust, efficient, and provide a smooth user experience.
Remember, regardless of the technology you work with, constant learning and practising are the keys to becoming a proficient software developer. Be it Flutter RxDart or any other tech stack, keep exploring and keep coding!
Your journey into the world of reactive programming with Flutter RxDart starts here. Good luck!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.