Giới thiệu về RxJava - Phần 1: Cơ bản
Bài đăng này đã không được cập nhật trong 7 năm
Mở đầu
RxJava là một chủ đề hot với các Android dev trong thời gian gần đây. Tuy nhiên việc tiếp cận ban đầu với nó thực sự là khó khăn với nhiều người. Lập trình chức năng tác động ngược (Functional Reactive Programming) sẽ khó để hiểu ngay được khi mà hầu hết chúng ta đều đã quen với lối lập trình mệnh lệnh (Imperative Programming), nhưng một khi bạn đã hiểu về nó thì bạn sẽ thấy nó rất tuyệt vời!
Bài viết này được dịch từ một series rất nổi tiếng của Dan Lew là Grokking RxJava về RxJava . Mục tiêu của bài viết là giới thiệu cho các bạn sự thú vị của RxJava và cách sử dụng nó chứ không đi sâu vào giải thích chi tiết tất cả mọi thứ. Sau khi các bạn đã hiểu được phần cơ bản thì có thể tự tìm thêm rất nhiều tài liệu về RxJava để có thể khám phá sự thú vị của RxJava và Reactive Programming.
Có thể bạn sẽ quan tâm:
Phần cơ bản
Phần cơ bản nhất của Reactive bao gồm các Observable
và các Subscriber
(1).
- Một
Observable
phát ra một hoặc nhiều item. - Một
Subscriber
sử dụng các item đó.
Có một ví dụ về cách mà các item được phát ra. Một Observable
sẽ phát ra một số các item (bao gồm cả không có item). Sau đó nó sẽ kết thúc việc phát itemt do đã hoàn thành hay là có lỗi xảy ra. Với mỗi Subscrbver
mà Observable
có thì nó sẽ gọi đến hàm Subscriber.onNext()
một số lần nào đó, cùng với đó là một trong hai phương thức Subscriber.onCompleted()
hoặc Subscriber.onError()
.
Điều này nghe có vẻ khá là giống với mẫu thiết kế observer tiêu chuẩn, tuy nhiên nó khác ở một điểm chính đó là các Observable
thường không bắt đầu phát ra item cho đến khi có ai đó thực sự đăng ký đến nó (2).
Chuẩn bị
Trước khi đi vào các ví dụ cụ thể, bạn cần tạo sẵn một project Android và thêm các dependencies sau
- rxjava
compile 'io.reactivex:rxjava:1.1.0'
Hello, World
Hãy cùng xem cấu trúc này hoạt động ra sao bằng việc bắt đầu với một ví dụ cụ thể nhé. Đầu tiên bạn hãy tạo một Observable
cơ bản:
Observable<String> myObservable = Observable.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> sub) {
sub.onNext("Hello, world!");
sub.onCompleted();
}
}
);
Ở trên, Observable
phát ra chuỗi "Hello, world!" sau đó kết thúc. Giờ chúng ta sẽ tạo một Subscriber
để sử dụng dữ liệu đó.
Subscriber<String> mySubscriber = new Subscriber<String>() {
@Override
public void onNext(String s) { System.out.println(s); }
@Override
public void onCompleted() { }
@Override
public void onError(Throwable e) { }
};
Đoạn code trên sẽ thực hiện in ra từng chuỗi được phát ra bởi Observable
.
Giờ thì ta đã có myObservable
và mySubscriber
, ta sẽ kết nối chúng với nhau bằng việc sử dụng hàm subscribe()
.
myObservable.subscribe(mySubscriber);
// Outputs "Hello, world!"
Khi mà có sự đăng ký, myObservable
sẽ gọi hàm onNext()
và onCompleted()
của các đối tượng đăng ký tới nó, cho ta kết quả là mySubscriber
cho ra chuỗi "Hello, world!" và sau đó kết thúc.
Làm cho code đơn giản hơn
Thực sự có rất nhiều cách để có thể in ra chuỗi "Hello, world!". Nhưng tôi lựa chọn cách dài dòng vì tôi muốn các bạn có thể hiểu rõ điều gì đang xảy ra. Có rất nhiều cách rút gọn trong RxJava để bạn có thể code dễ dàng hơn.
Đầu tiên, hãy đơn giản hóa Observable
. RxJava có rất nhiều hàm tạo sẵn Observable
cho các trường hợp phổ biến. Với tình huống này, Observable.just()
phát ra một item rồi kết thúc là phù hợp, minh họa trong ví dụ sau (3):
Observable<String> myObservable = Observable.just("Hello, world!");
Tiếp theo chúng ta hãy cùng xử lý Subscriber
cho ngắn gọn nào. Chúng ta tạm thời không cần quan tâm tới onCompleted()
hay onError(),
nên ta có thể sử dụng một class đơn giản hơn để định nghĩa sẽ làm gì trong onNext()
:
Action1<String> onNextAction = new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
};
Các Action có thể định nghĩa từng phần của một Subscriber
. Observable.subscribe()
có thể xử lý một, hai hoặc ba tham số là Action
ứng với onNext()
, onError()
và onCompleted()
. Thay thế Subscriber
ở ví dụ trước thì ta sẽ có đoạn như sau:
myObservable.subscribe(onNextAction, onErrorAction, onCompletedAction);
Tuy nhiên, chúng ta chỉ cần tham số đầu tiên, vì chúng ta sẽ tạm bỏ qua onError()
và onCompleted()
, chúng ta sẽ sẽ có đoạn code ngắn gọn hơn:
myObservable.subscribe(onNextAction);
// Outputs "Hello, world!"
Bây giờ, bạn có thể bỏ các biến đi bằng cách móc nối các lời gọi hàm với nhau:
Observable.just("Hello, world!")
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});
Cuối cùng, sử dụng lambdas trong Java 8 để rút ngắn đoạn code của Action1
:
Observable.just("Hello, world!")
.subscribe(s -> System.out.println(s));
Nếu bạn code Android, mà Android thì chưa thực sự hỗ trợ Java 8 thì bạn có thể sử dụng retrolambda để có thể rút ngắn code ngay lập tức.
Sự biến đổi
Hãy cùng khám phá tiếp nào.
Giả sử tôi muốn thêm chữ ký vào output "Hello, world!". Có một cách có thể đó là thay đổi Observable
:
Observable.just("Hello, world! -Dan")
.subscribe(s -> System.out.println(s));
Cách này chỉ hoạt động khi bạn hoàn toàn kiểm soát được Observable
, nhưng bạn sẽ không đảm bảo được cho trường hợp nếu bạn đang sử dụng lib của người khác. Và thêm một vấn đề nữa đó là sẽ ra sao nếu tôi muốn sử dựng Observable
ở nhiều chỗ nhưng chỉ có vài lần tôi muốn thêm chữ ký?
Vậy thay vào đó, sao chúng ta không thử sửa Subscriber
nhỉ?
Observable.just("Hello, world!")
.subscribe(s -> System.out.println(s + " -Dan"));
Câu trả lời này chưa được thỏa mãn cho lắm, nhưng vì nhiều lí do mà tôi muốn các Subscriber
của tôi trở nên đơn giản nhất có thể vì như vậy tôi có thể cho chúng chạy trên main thread. Ở một mức độ khái niệm hơn, các Subscriber
là những thứ phản ứng linh hoạt chứ không phải là thứ đột biến.
Vậy nó sẽ rất là tuyệt nếu ta có thể biến đổi chuỗi "Hello, world!" với một vài bước trung gian.
Giới thiệu về các Operator
Với các operator thì chúng ta sẽ xử lý các vấn đề về biến đổi item. Các operator có thể được dùng giữa nguồn Observable
và Subscriber
cuối cùng để có thể thao tác với các item được phát ra. RxJava cung cấp rất nhiều operator, nhưng trước hết ta sẽ tập trung vào một nhóm nhỏ trước.
Với tình huống ở trên, operator map()
có thể được dùng để biến đổi một item được phát ra thành một item khác:
Observable.just("Hello, world!")
.map(new Func1<String, String>() {
@Override
public String call(String s) {
return s + " -Dan";
}
})
.subscribe(s -> System.out.println(s));
Chúng ta có thể đơn giản code bằng cách sử dụng các lambda:
Observable.just("Hello, world!")
.map(s -> s + " -Dan")
.subscribe(s -> System.out.println(s));
Ngầu quá phải không nào? Operator map()
đơn thuần là một Observable
mà biến đổi một item. Chúng ta có thể móc nối nhiều lời gọi map()
với nhau, biến đổi dữ liệu đến dạng hoàn hảo nhất mà Subscriber có thể sử dụng được.
Tìm hiểu thêm về operator map()
Một thú vị của map()
là nó không cần phải phát ra các item cùng loại với nguồn Observable
.
Giả sử rằng Subscriber
của tôi không thích phát ra chuỗi kí tự gốc mà thay vào đó muốn phát ra mã hash của chuỗi kí từ đó:
Observable.just("Hello, world!")
.map(new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.hashCode();
}
})
.subscribe(i -> System.out.println(Integer.toString(i)));
Thật thú vị phải không nào, chúng ta bắt đầu với một chuỗi String nhưng Subscriber
của chúng ta lại nhận được một số nguyên Integer. Một lần nữa, chúng ta có thể sử dụng các lambda để làm ngắn gọn code:
Observable.just("Hello, world!")
.map(s -> s.hashCode())
.subscribe(i -> System.out.println(Integer.toString(i)));
Như tôi đã nói ở trước, chúng ta muốn Subscriber
của chúng ta làm ít việc nhất có thể. Vì thế hãy sử dụng một map()
khác để có thể chuyển đổi mã hash sang một chuỗi String:
Observable.just("Hello, world!")
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));
Bạn có nhìn thấy không, Observable
và Subscriber
của chúng ta đã trở lại dạng code trước. Chúng ta chỉ cần thêm một vài bước biến đổi ở giữa. Chúng ta còn có thể thêm việc biến đổi chữ ký của tôi vào nữa:
Observable.just("Hello, world!")
.map(s -> s + " -Dan")
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));
Tiếp theo sẽ là gì đây?
Tại thời điểm này có thể bạn sẽ nghĩ rằng "Điều đó thật lạ cho một đoạn code đơn giản". Cũng đúng, nó là một ví dụ đơn giản. Tuy nhiên sẽ có hai ý kiến bạn nên theo:
- Ý kiến thứ nhất:
Observable
vàSubscriber
có thể làm mọi thứ
Observable
có thể là một truy vấn database, Subscriber
nhận các kết quả và hiển thị chúng trên màn hình. Observable
có thể là một click trên màn hình, Subscriber
phản ứng với hành động đó. Observable
có thể là một luồng các byte được đọc từ mạng internet, Subscriber
có thể ghi chúng vào đĩa cứng.
Đó là một framework tổng quát mà có thể xử lý hầu hết các vấn đề.
- Ý kiến thứ hai: Observable và Subscriber là độc lập với các bước biến đổi giữa chúng
Tôi có thể dùng bao nhiêu lời gọi operator map()
mà tôi muốn giữa dữ liệu nguồn Observable
và Subscriber
cuối cùng của nó. Nó khiến cho việc thao tác với dữ liệu trở nên dễ dàng. Chỉ cần các operator làm việc với đúng dữ liệu input/ouput thì tôi có thể tạo các móc nối chạy mãi mãi (4).
Kết hợp hai ý kiến với nhau và bạn có thể nhìn thấy một hệ thống với rất nhiều tiềm năng. Khoan đã, các bạn nghĩ mà xem, chúng ta mới chỉ có một operator là map()
, điều này giới hạn khả năng của chúng ta. Trong các phần tiếp theo tôi sẽ giới thiệu với các bạn các operator phù hợp để bạn dùng khi sử dụng RxJava.
Chú thích
(1) Thực ra phần nhỏ nhất là Observer
, tuy nhiên thường thì chúng ta sẽ sử dụng Subscriber
để kết nối với các Observable
.
(2) Có 2 loại Observable
là "hot" và "cold". Một hot Observable
phát ra các item mọi lúc, dù cho không có ai đang lắng nghe nó cả. Một cold Observable
chỉ phát ra các item khi nó có một Subscriber (và đây cũng là cái sẽ được sử dụng trong các ví dụ). Sự phân biệt này thực sự không quan trọng lắm khi bạn mới bắt đầu học RxJava.
(3) Có thể nói Observable.just()
không hoàn toàn giống y như đoạn code mà tôi giới thiệu ở phần trên, nhưng tôi sẽ giải thích trong các phần sau.
(4) Thực ra thì không hẳn là mãi mãi, vì một lúc nào đó máy tính sẽ đến giới hạn, nhưng ở đây tôi muốn nói lên ý tưởng là như vậy.
Có thể bạn sẽ quan tâm
Nguồn
Bài viết được dịch từ bài gốc Grokking RxJava, Part 1: The Basics của tác giá Dan Lew.
All rights reserved