Làm chức năng search sử dụng RxJava operator.

Ngày nay thì gần như bất cứ ứng dụng nào cũng có hỗ trợ chức năng tìm kiếm, giúp cho bạn tìm thấy thứ mình cần 1 cách nhanh chóng. Do đây là 1 chức năng rất quan trọng nên chúng ta cần phải tìm ra cách để implement nó 1 cách tốt nhất có thể. Trong bài này, chúng ta sẽ cùng tìm hiểu cách để làm chức năng search sử dụng các operator của RxJava.

Dưới đây là 1 số khái niệm mà chúng ta sẽ sử dụng để implement tính năng này:

  • PublishSubject
  • Filter Operator
  • Debounce Operator
  • DistinctUntilChanged Operator
  • SwitchMap Operator

Để bắt đầu thì đầu tiên chúng ta phải làm thế nào đó để có thể theo dõi được sự kiện text thay đổi trong SearchView. Chúng ta có thể làm điều này bằng cách sử dụng PublishSubject. Với view thì bạn có thể dùng loại view khác ví dụ như EditText, nhưng ở đây thì tôi sẽ dùng luôn SearchView của Android và implement text change listener của view này.

public class RxSearchObservable {

    public static Observable<String> fromView(SearchView searchView) {

        final PublishSubject<String> subject = PublishSubject.create();

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String s) {
                subject.onComplete();
                return true;
            }

            @Override
            public boolean onQueryTextChange(String text) {
                subject.onNext(text);
                return true;
            }
        });

        return subject;
    }
}

OK, với đoạn code trên thì khi người dùng gõ 1 chữ thì chúng ta sẽ nhận được event onNext() từ PublishSubject, còn khi user bấm submit (enter) trên bàn phím thì nhận được event onCompleted(). Chúng ta sẽ sử dụng class này kết hợp với các operator khác để hoàn thành chức năng này như sau:


RxSearchObservable.fromView(searchView)
                .debounce(300, TimeUnit.MILLISECONDS)
                .filter(new Predicate<String>() {
                    @Override
                    public boolean test(String text) throws Exception {
                        if (text.isEmpty()) {
                            return false;
                        } else {
                            return true;
                        }
                    }
                })
                .distinctUntilChanged()
                .switchMap(new Function<String, ObservableSource<String>>() {
                    @Override
                    public ObservableSource<String> apply(String query) throws Exception {
                        return dataFromNetwork(query);
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String result) throws Exception {
                        textViewResult.setText(result);
                    }
                });
               

Giờ thì đã đến lúc chúng ta trả lời câu hỏi là tại sao phải dùng những operator này và kết hợp chúng lại với nhau thì sẽ ra được kết quả như thế nào:

  • debounce() : operator Debounce được sử dụng với 1 hằng số thể hiện thời gian. Operator này được dùng để xử lý case khi người dùng gõ "a", "ab", "abc" trong 1 khoảng thời gian ngắn => Nó sẽ tạo quá nhiều request tới server. Nhưng do người dùng chỉ quan tâm tới kết quả cuối cùng thôi ("abc"), cho nên chúng ta cần phải loại bỏ kết quả của "a" và "ab". Tốt hơn nữa là chúng ta không cần phải search với từ khoá "a" và "ab" luôn. Debounce sẽ chờ cho đến khi hết khoảng thời gian chúng ta cung cấp trước khi làm bất cứ thứ gì, nên nếu có bất kì chữ nào được gõ trong khoảng thời gian đấy thì nó sẽ bỏ qua những chữ ở đằng trước và lại reset khoảng thời gian chờ. Ví dụ ở trên thì khi người dùng gõ "a", nó sẽ bắt đầu chờ 300ms, nếu trong khoảng 300ms đấy lại có chữ "b" xuất hiện thì nó sẽ bỏ qua chữ "a" đầu tiên và lại bắt đầu chờ 300ms tiếp. Chỉ khi hết 300ms trôi qua mà ko có chữ nào được gõ thêm thì nó mới bắt đầu dùng đoạn text cuối cùng để search.

  • filter(): Cái này thì chắc là quá quen thuộc rồi, nhưng ở đây chúng ta dùng là để loại bỏ những kí tự thừa (ví dụ như toàn là dấu cách hoặc dấu cách ở cuối) để đỡ phải tạo request.

  • distinctUntilChanged(): Chúng ta sử dụng operator này để tránh việc tạo các request trùng nhau. Ví dụ kết quả search hiện tại đang cho từ khoá "abc", nhưng người dùng nó lại xoá chữ c đi và lại gõ chữ c vào (trong khoảng thời gian 300ms) thì cuối cùng PublishSubject vẫn sẽ emit ra chuỗi "abc" mà thôi, cho nên chúng ta chả cần phải search lại làm gì cả.

  • switchMap(): operator này được dùng để tránh việc chúng ta cho hiển thị những kết quả search mà không còn cần thiết nữa. Tưởng tượng trường hợp là khi mà người dùng gõ "ab" xong lại ngập ngừng 1 lúc quá 300ms mới gõ "c" thì trong lúc đó chúng ta đã tạo 1 request lên server với query là "ab" rồi. Nhưng lúc này thì chúng ta không còn quan tâm đến kết quả của "ab" trả ra gì nữa vì cuối cùng thì ông người dùng chỉ quan tâm đến "abc" thôi. Sử dụng switchMap() ở chỗ này sẽ giúp chúng ta luôn hiện được kết quả search cho query gần nhất được gõ vào và bỏ qua tất cả những kết quả cho các query khác.

Yeah, vậy là chúng ta đã làm xong chức năng này rồi. Thử tưởng tượng xem nó sẽ mất thời gian và công sức đến mức nào nếu không có RxJava.

Bài viết xin được dừng tại đây. Nếu bạn quan tâm đến code mẫu thì hãy xem ở repo này nhé.

Bài viết được dịch từ Implement Search Using RxJava Operators của tác giả Amit Shekhar.