使用RxJava來改進用戶體驗

jopen 8年前發布 | 10K 次閱讀 Android開發 移動開發

一個完美的移動世界永遠不會失去連接,而服務端也永遠不會返回錯誤。

構建一個很棒的app對于用戶來說是幸福的事而對于開發者來說則是痛苦的事。用戶點擊一個按鈕就阻塞了所有操作的時代已經過去了,那是要死人的。

1-kCcW9ytw2jQwYqmvnhGjuw.gif

讓我們來創建一個更好的文本框搜索功能并關注以下需求

  • 盡可能少的請求

  • 對用戶盡可能少的錯誤信息

RX 的邏輯相當簡單,重點在完善細微的細節上。

讓我們從簡單的邏輯開始:

當用戶輸入內容的時候我們發出了一個網絡請求然后獲得結果:

RxTextView.textChanges(searchEditText)
     .flatMap(Api::searchItems)
     .subscribe(this::updateList, t->showError());

減少網絡請求

以上存在兩個問題:

  1. 每輸入一個字母(對的這很坑)比如:用戶快速輸入了一個“a”,然后“ab”然后“abc”然后又糾正為“ab”并最終想搜索“abe”。這樣你就做了5次網絡請求。想象一在網速很慢的時候是個什么情況。

  2. 你還面臨一個線程賽跑的問題。比如:用戶輸入了“a”,然后是“ab”。“ab”的網絡調用發生在前而”a“的調用發生在后。那樣的話updateList() 將根據 “a”的請求結果來執行。

解決:

添加調節行為:

你需要的是debounce() 。根據我的經驗,取值在100?150毫秒效果最好。如果你的服務器需要額外的300毫秒那么你可以在0.5秒之內做UI更新。

RxTextView.textChanges(searchEditText)
     .debounce(150, MILLISECONDS)
     .flatMap(Api::searchItems)
     .subscribe(this::updateList, t->showError());

殺死前面的請求:

引入 switchMap來替代flatMap。它會停止前面發出的items。所以如果在0+150ms時你搜索“ab”,在 0+300ms時搜索“abcd”,但是“ab”的網絡調用需要 150ms以上的時間才能完成,那么到了開始“abcd”調用的時候前面的那個會被取消。這樣你總是能得到最近的請求數據。

RxTextView.textChanges(searchEditText)
     .debounce(150, MILLISECONDS)
     .switchMap(Api::searchItems)
     .subscribe(this::updateList, t->showError());

2. No error functionality / no network functionality

如果所有的網絡調用都失敗,那么你將不能再次觀察到text的改變。

這可以通過添加 error catching functionality來解決。

因此你可以用:

RxTextView.textChanges(searchEditText)
     .debounce(150, MILLISECONDS)
     .switchMap(Api::searchItems)
     .onErrorResumeNext(t-> empty())
     .subscribe(this::updateList);

Don’t do that. Let’s make it smarter. What if the searchItems() api call above calls because of connectivity? Or even more “UX-depressingly” brief connectivity that the user didn’t notice?

別這么做。讓我們讓它更智能些。要是 searchItems() api調用因為網絡連接的問題發生在其它調用之前呢?

你需要這樣的一個重試機制:

RxTextView.textChanges(searchEditText)
     .debounce(150, MILLISECONDS)
     .switchMap(Api::searchItems)
     .retryWhen(new RetryWithConnectivity())
     .subscribe(this::updateList, t->showError());

如何進一步改進呢?添加一個超時(timeout)。就如我們的用戶體驗設計師 Leander Lenzing 所說的:“1秒對于用戶來說是一個很長的時間”。所以上面的代碼應該這樣:

RxTextView.textChanges(searchEditText)
     .debounce(150, MILLISECONDS)
     .switchMap(Api::searchItems)
     .retryWhen(new RetryWithConnectivityIncremental(context, 5, 15, SECONDS))
     .subscribe(this::updateList, t->showErrorToUser());

那么RetryWithConnectivityIncremental 和RetryWithConnectivity 會做些什么呢?它將等待5秒讓手機網絡暢通,如果超過則會拋出一個異常。如果用戶重試它則會等待更長的超時時間(比如15秒)。

這里是代碼:

BroadcastObservable.java hosted with ? by GitHub

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Looper;

import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;

public class BroadcastObservable implements Observable.OnSubscribe<Boolean> {

    private final Context context;

    public static Observable<Boolean> fromConnectivityManager(Context context) {
        return Observable.create(new BroadcastObservable(context))
                .share();
    }

    public BroadcastObservable(Context context) {
        this.context = context;
    }

    @Override
    public void call(Subscriber<? super Boolean> subscriber) {
        BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                subscriber.onNext(isConnectedToInternet());
            }
        };

        context.registerReceiver(receiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

        subscriber.add(unsubscribeInUiThread(() -> context.unregisterReceiver(receiver)));
    }

    private boolean isConnectedToInternet() {
        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = manager.getActiveNetworkInfo();
        return networkInfo != null && networkInfo.isConnected();
    }

    private static Subscription unsubscribeInUiThread(final Action0 unsubscribe) {
        return Subscriptions.create(() -> {
            if (Looper.getMainLooper() == Looper.myLooper()) {
                unsubscribe.call();
            } else {
                final Scheduler.Worker inner = AndroidSchedulers.mainThread().createWorker();
                inner.schedule(() -> {
                    unsubscribe.call();
                    inner.unsubscribe();
                });
            }
        });
    }

}

RetryWithConnectivityIncremental.java hosted with ? by GitHub

import android.content.Context;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import rx.Observable;
import rx.functions.Func1;

public class RetryWithConnectivityIncremental implements Func1<Observable<? extends Throwable>, Observable<?>> {
    private final int maxTimeout;
    private final TimeUnit timeUnit;
    private final Observable<Boolean> isConnected;
    private final int startTimeOut;
    private int timeout;

    public RetryWithConnectivityIncremental(Context context, int startTimeOut, int maxTimeout, TimeUnit timeUnit) {
        this.startTimeOut = startTimeOut;
        this.maxTimeout = maxTimeout;
        this.timeUnit = timeUnit;
        this.timeout = startTimeOut;
        isConnected = getConnectedObservable(context);
    }

    @Override
    public Observable<?> call(Observable<? extends Throwable> observable) {
        return observable.flatMap((Throwable throwable) -> {
            if (throwable instanceof RetrofitError && ((RetrofitError) throwable).getKind() == RetrofitError.Kind.NETWORK) {
                return isConnected;
            } else {
                return Observable.error(throwable);
            }
        }).compose(attachIncementalTimeout());
    }

    private Observable.Transformer<Boolean, Boolean> attachIncementalTimeout() {
        return observable -> observable.timeout(timeout, timeUnit)
                .doOnError(throwable -> {
                    if (throwable instanceof TimeoutException) {
                        timeout = timeout > maxTimeout ? maxTimeout : timeout + startTimeOut;
                    }
                });
    }

    private Observable<Boolean> getConnectedObservable(Context context) {
        return BroadcastObservable.fromConnectivityManager(context)
                .distinctUntilChanged()
                .filter(isConnected -> isConnected);
    }

}

以上。你節制了你的請求,你總是能得到最近的請求結果,你有重試連接的智能超時處理機制。

英文原文:Improving UX with RxJava 


注:

還可以參考Hanks 在簡書上的譯文:http://www.jianshu.com/p/33c548bce571 以及譯文作者根據文章制作的一個demo:https://github.com/hanks-zyh/RxSerach 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!