編輯推薦:稀土掘金,這是一個高質量的技術乾貨分享社區,web前端、Android、iOS、設計資源和產品,滿足你的學習慾望。
一個完美的移動世界永遠不會失去連接,而服務端也永遠不會返回錯誤。
構建一個很棒的app對於用戶來說是幸福的事而對於開發者來說則是痛苦的事。用戶點擊一個按鈕就阻塞了所有操作的時代已經過去了,那是要死人的。
讓我們來創建一個更好的文本框搜索功能並關注以下需求
-
儘可能少的請求
-
對用戶儘可能少的錯誤信息
RX 的邏輯相當簡單,重點在完善細微的細節上。
讓我們從簡單的邏輯開始:
當用戶輸入內容的時候我們發出了一個網絡請求然後獲得結果:
1
2
3
|
RxTextView.textChanges(searchEditText) .flatMap(Api::searchItems) .subscribe( this ::updateList, t->showError()); |
減少網絡請求
以上存在兩個問題:
-
每輸入一個字母(對的這很坑)比如:用戶快速輸入了一個“a”,然後“ab”然後“abc”然後又糾正爲“ab”並最終想搜索“abe”。這樣你就做了5次網絡請求。想象一在網速很慢的時候是個什麼情況。
-
你還面臨一個線程賽跑的問題。比如:用戶輸入了“a”,然後是“ab”。“ab”的網絡調用發生在前而”a“的調用發生在後。那樣的話updateList() 將根據 “a”的請求結果來執行。
解決:
添加調節行爲:
你需要的是debounce() 。根據我的經驗,取值在100–150毫秒效果最好。如果你的服務器需要額外的300毫秒那麼你可以在0.5秒之內做UI更新。
1
2
3
4
|
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”調用的時候前面的那個會被取消。這樣你總是能得到最近的請求數據。
1
2
3
4
|
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來解決。
因此你可以用:
1
2
3
4
5
|
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調用因爲網絡連接的問題發生在其它調用之前呢?
你需要這樣的一個重試機制:
1
2
3
4
5
|
RxTextView.textChanges(searchEditText) .debounce(150, MILLISECONDS) .switchMap(Api::searchItems) .retryWhen( new RetryWithConnectivity()) .subscribe( this ::updateList, t->showError()); |
如何進一步改進呢?添加一個超時(timeout)。就如我們的用戶體驗設計師 Leander Lenzing 所說的:“1秒對於用戶來說是一個很長的時間”。所以上面的代碼應該這樣:
1
2
3
4
5
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
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); } } |
以上。你節制了你的請求,你總是能得到最近的請求結果,你有重試連接的智能超時處理機制。