RxDart là một thư viện reactive functional programming cho ngôn ngữ Dart, dựa trên ReactiveX. Dart đã có một package phù hợp để làm việc với Streams, nhưng RxDart bổ sung thêm một số tính năng để sử dụng tiện lợi hơn.
Chúng ta hãy bắt đầu tìm hiểu về Streams.
Streams and Sinks
Streams đại diện cho luồng dữ liệu (stream) và sự kiện (event), và tại sao nó quan trọng? Với Streams, bạn có thể nghe các thay đổi dữ liệu và sự kiện, đồng thời giải quyết những gì đến từ Streams với listener
. Làm thế nào nó có thể được áp dụng cho Flutter?
Ví dụ: chúng ta có một Widget trong Flutter có tên là StreamBuilder
sẽ build dựa trên snapshot mới nhất dựa trên tương tác với Stream và khi có dữ liệu mới, Widget sẽ tải lại để xử lý. Widget Weekly of Flutter Dev Channel cung cấp nội dung tuyệt vời về cách hoạt động của StreamBuilder
.
Còn về Sinks? Nếu chúng ta có một output của stream, thì chúng ta cần một nơi nào đó để input, đó là những gì Sinks được sử dụng, có vẻ đơn giản phải không? Bây giờ chúng ta hãy xem về BLoC pattern
và cách để chúng ta có thể kết hợp cả hai khái niệm này thành một ứng dụng Flutter tuyệt vời.
BLoC Pattern
BLoC Pattern (Bussiness Logic Component) đã được Paolo Soares công bố chính thức trong hội nghị Dart 2018. Nếu bạn đã xem video, có lẽ bạn đã nhận ra rằng đề xuất ban đầu là sử dụng lại code liên quan đến business logic trong các nền tảng khác, trong trường hợp này là Angular Dart. Ngay lập tức, những gì mà pattern này làm là lấy tất cả business logic code ra khỏi giao diện người dùng và chỉ sử dụng nó trong các lớp BLoC class. Nó giúp cho code có tính độc lập của môi trường và nền tảng, bên cạnh việc đặt các responsibility vào đúng component. Và bây giờ mọi thứ sẽ rõ ràng hơn nhiều, bởi vì BLoC Pattern chỉ dựa vào việc sử dụng Stream.
Nhìn vào hình trên, chúng ta có thể nhận ra kiến trúc flux
. Các Widget gửi dữ liệu/sự kiện đến BLoC class thông qua Sink và được Stream thông báo. Nhận thấy rằng không có business logic nào trong widget, điều đó có nghĩa là những gì đã xảy ra trong BLoC không phải là mối quan tâm của giao diện người dùng. Kiến trúc này cải thiện để thực hiện Unit Test dễ dàng hơn, trong đó các trường hợp kiểm tra business logic chỉ cần áp dụng cho các BLoC class.
Cái nhìn về RxDart
RxDart hiện tại (tại thời điểm của bài đăng này) là phiên bản 0.21.0. Và ở đây mình sẽ nói về một số object mà thư viện mang đến cho chúng ta.
Observable<T> class
Observable cho phép chúng ta gửi thông báo đến các Widget đang quan sát nó và sau đó xử lý luồng dữ liệu. Observable class trong RxDart mở rộng từ Stream, ngụ ý :
- Tất cả các method được định nghĩa trên Stream class cũng tồn tại trên Observable.
- Tất cả các Observable có thể được chuyển tới bất kỳ API nào yêu cầu Dart Stream làm input (bao gồm cả StreamBuilder Widget chẳng hạn).
PublishSubject<T> class
Điều này là khá đơn giản. Subject cho phép gửi dữ liệu, lỗi và các sự kiện đã thực hiện đến listener. Ở đây nó sẽ hoạt động với Sinks, mà chúng ta đã nói trước đây. Xem ví dụ trên:
PublishSubject<int> subject = new PublishSubject<int>();
/*this listener below will print every integer added to the subject: 1, 2, 3, ...*/
subject.stream.listen(print);
subject.add(1);
subject.add(2);
/*but this listener below will print only the integer added after his initialization: 3, .../*
subject.stream.listen(print);
subject.add(3);
BehaviorSubject<T> class
Cái này tương tự như PublishSubject. Nó cũng cho phép gửi dữ liệu, lỗi và các sự kiện đã thực hiện đến listener, nhưng giá trị sau cùng đã được thêm vào subject sẽ được gửi đến các listener được thêm vào sau này. Nhưng sau đó, mọi sự kiện mới sẽ được gửi đến listener một cách bình thường. Xem ví dụ:
BehaviorSubject<int> subject = new BehaviorSubject<int>();
subject.stream.listen(print); // prints 1,2,3
subject.add(1);
subject.add(2);
subject.add(3);
subject.stream.listen(print); // prints 3
ReplaySubject<T> class
ReplaySubject cho phép chúng ta: gửi dữ liệu, lỗi và các sự kiện đã thực hiện đến listener. Nhưng với một sự khác biệt quan trọng ở đây. Khi các giá trị được thêm vào subject, ReplaySubject sẽ lưu trữ chúng và khi stream có listener, tất cả giá trị đã ghi đó sẽ được gởi tới listener. Xem ví dụ:
ReplaySubject<int> subject = new ReplaySubject<int>();
subject.add(1);
subject.add(2);
subject.add(3);
subject.stream.listen(print); // prints 1, 2, 3
RxDart trong thực tế
Trong bài viết này, mình sẽ chỉ cho các bạn một ví dụ đơn giản về việc sử dụng RxDart và các nguyên tắc của BLoC pattern.
Một cách tuyệt vời để bắt đầu nó là ngay từ đầu với ứng dụng Flutter Hello World. Có thể bạn đã quen với increment function (chức năng tăng dần) trên ứng dụng demo này, nhưng để hiểu rõ hơn, chúng ta hãy tạo cả decrement function (chức năng giảm dần). Vì vậy, trước hết, hãy tạo một dự án flutter và nhập rxdart vào dự án của bạn.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
void _decrementCounter() {
setState(() {
_counter--;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('You have pushed the button this many times:',),
new Text('$_counter', style: Theme.of(context).textTheme.display1),
],
),
),
floatingActionButton: new Column(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
new Padding(padding: EdgeInsets.only(bottom: 10), child:
new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
)
),
new FloatingActionButton(
onPressed: _decrementCounter,
tooltip: 'Decrement',
child: new Icon(Icons.remove),
),
])
);
}
}
Như bạn có thể thấy, code này triển khai chức năng tăng và giảm, nhưng vẫn không áp dụng BLoC pattern hoặc thậm chí là Streams. Code này hoạt động và nó khá đơn giản, nhưng nếu bạn chú ý, bạn sẽ thấy rằng chúng ta có hai logic business function trong code giao diện người dùng: tăng và giảm. Vì vậy, hãy tưởng tượng nếu ứng dụng này là một ứng dụng lớn mà bạn đang làm việc chăm chỉ, nhưng bây giờ yêu cầu đã được thay đổi và số gia tăng cần phải thêm hai cùng một lúc. Bạn có đồng ý với mình không (trong trường hợp này) một yêu cầu thay đổi trong logic nghiệp vụ sẽ không ảnh hưởng đến code giao diện người dùng, phải không? Nếu có, tuyệt vời! Bạn hiểu rồi, đó là điểm để phân biệt các responsibility.
Bây giờ, hãy tách nó ra và sử dụng những gì chúng ta đã học được cho đến nay. Hãy tạo CounterBLoC class của chúng ta:
import 'package:rxdart/rxdart.dart';
class CounterBloc {
int initialCount = 0; //if the data is not passed by paramether it initializes with 0
BehaviorSubject<int> _subjectCounter;
CounterBloc({this.initialCount}){
_subjectCounter = new BehaviorSubject<int>.seeded(this.initialCount); //initializes the subject with element already
}
Observable<int> get counterObservable => _subjectCounter.stream;
void increment(){
initialCount++;
_subjectCounter.sink.add(initialCount);
}
void decrement(){
initialCount--;
_subjectCounter.sink.add(initialCount);
}
void dispose(){
_subjectCounter.close();
}
}
Tuyệt vời! Bây giờ hãy để mình giải thích đoạn code trên. Chúng ta đã tạo một class có tên CounterBloc có import thư viện rxdart. Trong trường hợp này, chúng ta cần nhận được initialCount
. Nó cho phép chúng ta biết counter của chúng ta sẽ bắt đầu từ số nào. Mình chọn cho ví dụ này là BehaviorSubeject
, và sau đó mình khởi tạo Subject với dữ liệu được truyền bằng tham số, nói cách khác, khi Widget trở thành một listener của Subject, giá trị đầu tiên được chuyển qua stream sẽ là initialCount được đặt trong CounterBloc constructor
. Bây giờ hãy nói về các method. Trong trường hợp này, chúng ta có bốn method trong class:
- increment(): tăng initialCount và gửi tới các Subject listener bằng cách Sink giá trị mới.
- decment(): giảm initialCount và gửi cho Subject listener bằng cách Sink giá trị mới.
- dispose(): đóng subject đã mở.
- counterObeservable(): trả về một Observable, nói cách khác, object sẽ được sử dụng để thông báo cho các Widget khi các thay đổi xảy ra trong Stream.
Bây giờ chúng ta đã tạo BLoC class, hãy xem việc tích hợp nó với giao diện người dùng.
import 'package:flutter/material.dart';
import 'package:bloc_example/bloc/CounterBloc.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
CounterBloc _counterBloc = new CounterBloc(initialCount: 0);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('You have pushed the button this many times:'),
new StreamBuilder(stream: _counterBloc.counterObservable, builder: (context, AsyncSnapshot<int> snapshot){
return new Text('${snapshot.data}', style: Theme.of(context).textTheme.display1);
})
],
),
),
floatingActionButton: new Column(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
new Padding(padding: EdgeInsets.only(bottom: 10), child:
new FloatingActionButton(
onPressed: _counterBloc.increment,
tooltip: 'Increment',
child: new Icon(Icons.add),
)
),
new FloatingActionButton(
onPressed: _counterBloc.decrement,
tooltip: 'Decrement',
child: new Icon(Icons.remove),
),
])
);
}
@override
void dispose() {
_counterBloc.dispose();
super.dispose();
}
}
Chúng ta đã thay đổi một số thứ trong giao diện người dùng:
- Bây giờ chúng ta khởi tạo CounterBloc với InitialCount = 0.
- Sau đó, chúng ta loại bỏ các increment method và decrement method. Việc thực hiện các phương pháp đó không còn là trách nhiệm của UI nữa.
- Khi FloatingActionButton được nhấp vào, nó sẽ call method tương ứng trong CounterBloc.
- Bây giờ chúng ta sử dụng StreamBuilder để hiển thị dữ liệu của chúng ta trên màn hình. Chúng ta gọi chuyền StreamBuilder dưới dạng Stream
counterObservable
method của chúng ta có sẵn bởi CounterBloc class và chúng ta call builder phải xử lý dữ liệu đến từ Stream và trả về Widget thích hợp.
Tại thời điểm này, ứng dụng có cấu trúc tốt của chúng ta sẽ trông như thế này:
Bài viết được lược dịch từ Wilton Ribeiro.
Chau Le
Cực thích đồ ngọt như sữa, kem. Thích phiêu lưu mạo hiểm nhưng vì Covid mà vô tình trở thành người chia sẻ nội dung lập trình :))
follow me :
Bài viết liên quan
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Flutter cơ bản: Điều cần biết khi lập trình ứng dụng đầu tiên
Aug 23, 2023 • 13 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Flutter là gì? Vì sao nên học công cụ lập trình Flutter?
Aug 19, 2023 • 11 min read
Flutter cơ bản: Widget Tree, Element Tree & Render Tree
Aug 19, 2023 • 10 min read
So sánh StatelessWidget và StatefulWidget
Jul 17, 2022 • 12 min read