Cơ bản về RxAndroid (Phần 2)

Sau phần trước, chúng ta sẽ tiếp tục tìm hiểu về RxJava và cách chúng được sử dụng trong Android. Mỗi cách dùng sẽ được làm rõ qua các ví dụ minh hoạ.

4. Subject

Trước khi đi vào những dòng code, chúng ta hãy làm quen với một khái niệm nữa trong RxJava, đó là Subject. Subject là một đối tượng đặc biệt mà đóng vai trò của cả Observable và Observer. Có thể tưởng tượng Subject như là một đường ống. Bạn có thể đặt mọi thứ vào một đầu của Subject và nó sẽ đi ra ở đầu còn lại.

Có một vài kiểu Subject, nhưng chúng ta sẽ bắt đầu tìm hiểu kiểu đơn giản nhất là PublishSubject. Với PublishSubject, ngay khi bạn đặt một cái gì đó ở một đầu của đường ống, nó ngay lập tức sẽ đi ra đầu kia.

Chúng ta hãy cùng đi tìm hiểu đầu ra của đường ống trước. Chúng ta nói rằng Subject là Observable, nghĩa là chúng ta có thể quan sát chúng như với bất kỳ Observable khác. Đây là cách chúng ta quan sát mọi thứ ra khỏi đầu kia của đường ống. Sau đây, chúng ta thiết lập một Observable đơn giản chỉ thay đổi mCounterDisplay hiển thị tới người dùng.

mCounterEmitter = PublishSubject.create();
mCounterEmitter.subscribe(new Observer<Integer>() {

    @Override
    public void onCompleted() { }

    @Override
    public void onError(Throwable e) { }

    @Override
    public void onNext(Integer integer) {
        mCounterDisplay.setText(String.valueOf(integer));
    }
});

Không giống như những ví dụ trước, lần này chúng ta sẽ gọi phương thức onNext() nhiều lần. Mỗi lần một giá trị mới được phát ra, chúng ta sẽ thay đổi giá trị của mCounterDisplay bằng giá trị mới. Nhưng làm thế nào để PublishSubject phát ra các giá trị. Hãy cùng xem xét đoạn code sau:

mIncrementButton.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        mCounter++;
        mCounterEmitter.onNext(mCounter);
    }
});

Chúng ta thấy ở đây, mIncrementButton thực hiện 2 việc trong phương thức callback onClick():

  • Tăng giá trị của biến mCounter.
  • Gọi phương thức onNext() của đối tượng mCounterEmitter với giá trị mới của mCounter.

Bởi vì Subject cũng là một Observer nghĩa là chúng có một phương thức onNext(). Tức là chúng ta có thể đơn giản đặt một thứ gì đó vào đường ống bằng cách đơn giản là gọi trực tiếp phương thức onNext(). Giá trị này sẽ đi tới onNext() của Observer mà chúng ta đăng lý lúc đầu. Điều này giống như trên một đầu của đường ống, chúng ta quan sát button được click, rồi sau đó gửi chúng tới Observer đang quan sát ở bên kia đường ống.

5. Map()

Hãy cùng tạo một Activity để hiển thị một số. Đây sẽ là một Activity đơn giản, qua đó giới thiệu một khái niệm mới: map. Bạn có thể nghĩ map như là một chức năng mà lấy một giá trị thì ứng với đầu ra là một giá trị khác. Thông thường có một số mối quan hệ giữa giá trị đặt vào map và giá trị tương ứng ở đầu ra.

Hãy cùng tạo một Single chỉ phát ra giá trị duy nhất là 4:

Single.just(4).map(new Func1<Integer, String>() {

    @Override
    public String call(Integer integer) {
        return String.valueOf(integer);
    }
    })
   .subscribe(new SingleSubscriber<String>() {

    @Override
    public void onSuccess(String value) {
        mValueDisplay.setText(value);
    }

    @Override
    public void onError(Throwable error) { }
});

Chúng ta muốn hiển thị giá trị mà Single sẽ phát ra, vì vậy cần chuyển đổi nó từ một Integer sang String. Cách thực hiện ở đây là dùng map(). Giống như đã nói ở trên, map sẽ lấy từ giá trị đầu vào (Integer) tương ứng cho một giá trị đầu ra (String). Cụ thể, Single sẽ phát ra một giá trị số nguyên Integer là 4, sử dụng map() để chuyển đổi nó thành String, và sau đó Observer sẽ đảm nhận việc hiển thị.

Đây là một cách sử dụng khá tầm thường của chức năng map. Nhưng map thực sự rất mạnh. Như chúng ta sẽ thấy ở ví dụ tiếp map có thể được sử dụng để thực thi code tuỳ ý và giúp chúng ta chuyển đổi dữ liệu theo những cách rất hữu ích.

6. Kết hợp linh hoạt tất cả cách dùng

Hãy cùng tạo một Activity giúp người dùng tìm kiếm các thành phố theo tên. Trong Activity này, chúng ta sẽ đặt tất cả những cách dùng đã tìm hiểu ở trên kết hợp với nhau để thực hiện một ví dụ lớn. Chúng ta cũng sẽ tìm hiểu thêm một khái niệm mới debounce.

Chúng ta muốn thiết lập một PublishSubject mà nó nhận giá trị nhập vào từ người dùng thông qua một search box, lấy một danh sách gợi ý dựa trên truy vấn đó, và sau đó hiển thị chúng:

mTextWatchSubscription = mSearchResultsSubject
    .debounce(400, TimeUnit.MILLISECONDS)
    .observeOn(Schedulers.io())
    .map(new Func1<String, List<String>>() {

        @Override
        public List<String> call(String s) {
            return mRestClient.searchForCity(s);
        }
    })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<List<String>>() {

        @Override
        public void onCompleted() { }

        @Override
        public void onError(Throwable e) { }

        @Override
        public void onNext(List<String> cities) {
            handleSearchResults(cities);
        }
    });

mSearchInput.addTextChangedListener(new TextWatcher() {

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        mSearchResultsSubject.onNext(s.toString());
    }

    @Override
    public void afterTextChanged(Editable s) { }
});

Điều đầu tiên bạn thấy là debounce(). Đây là gì, và tại sao chúng ta lại cần nó? Nếu bạn nhìn và cách thiết lập TextWatcher, bạn sẽ thấy các giá trị mới đi vào PublishSubject mọi lúc, mỗi khi người dùng thêm vào hoặc xoá đi một ký tự để search. Tuy nhiên chúng ta không muốn mọi thay đổi như vậy đều được gửi lên server. Chúng ta muốn chờ đợi một chút cho người dùng ngừng gõ (vì vậy sẽ chắc chắn có một truy vấn tốt), và sau đó mới gửi yêu cầu search lên server.

Đó là những gì debounce() cho phép chúng ta làm. Ở đây, nó nói với mSearchResultsSubject rằng chỉ phát ra giá trị cuối cùng nhập vào sau khi không có một giá trị mới trong vòng 400ms. Về cơ bản , điều này có nghĩa là subject sẽ không phát ra string cần tìm kiếm cho đến khi người dùng đã không thay đổi string này sau 400ms, và ở cuối mỗi 400ms nó chỉ phát ra những giá trị cuối cùng mà người dùng nhập vào. Thật hoàn hảo! Điều này sẽ giúp chúng ta tránh các tìm kiếm không cần thiết và UI được thay đổi liên tục theo kết quả của chuỗi nhập vào.

Chúng ta muốn sử dụng những gì mà debounce phát ra để truy vấn tới server thông qua RestClient. Vì truy vấn RestClient của chúng ta là một hoạt động IO, nên cần quan sát chúng trên IO Scheduler. Vậy, cần có thiết lập observeOn(Schedulers.io()).

Vậy, bây giờ chúng ta đang phát các truy vấn tìm kiếm vào IO Scheduler. Đây là điều kỳ diệu để sử dụng map. Chúng ta sẽ sử dụng map để chuyển đổi tương ứng truy vấn tìm kiếm với một danh sách kết quả. Bởi vì map có thể chạy bất kỳ chức năng tuỳ ý, chúng ta sẽ sử dụng RestClient để thực hiện chuyển truy vấn tìm kiếm vào danh sách kết quả thực tế muốn hiển thị.

Vì chúng ta thiết lập map chạy trên IO Scheduler, và chúng ta muốn sử dụng các kết quả mà nó phát ra để hiển thị trên view, chúng ta cần phải chuyển đổi lại UI thread. Vì vậy, cần thêm thiết lập observeOn(AndroidSchedulers.mainThread()). Bây giờ các kết quả tìm kiếm đã được phát ra trên UI thread. Chúng ta cơ bản đã cài đặt theo thứ tự phát ra như sau:

mSearchResultsSubject
         |
         |
         V
      debounce
        |||
        |||
         V
        map
         |
         |
         V
      observer

Kí hiệu | đại diện cho hành động xảy ra trên luồng chính UI Thread, ||| đại diện cho luồng IO Scheduler.

Cuối cùng, kết quả từ việc tìm kiếm sẽ được hiển thị tới người dùng.

Kết luận

Những ví dụ trên đã cung cấp những hiểu biết cơ bản về RxJava, và còn nhiều khía cạnh khác chúng ta có thể khám phá.

Các bạn có thể tìm hiểu thêm RxRelays cho Subject "an toàn hơn", hay tìm hiểu cụ thể hơn về Schedulers, observeOn, and subscribeOn tại http://reactivex.io/documentation/scheduler.html.