使用RxJava 提升用户体验

编辑推荐:稀土掘金,这是一个高质量的技术干货分享社区,web前端、Android、iOS、设计资源和产品,满足你的学习欲望。

一个完美的移动世界永远不会失去连接,而服务端也永远不会返回错误。

构建一个很棒的app对于用户来说是幸福的事而对于开发者来说则是痛苦的事。用户点击一个按钮就阻塞了所有操作的时代已经过去了,那是要死人的。

1-kCcW9ytw2jQwYqmvnhGjuw.gif

让我们来创建一个更好的文本框搜索功能并关注以下需求

  • 尽可能少的请求

  • 对用户尽可能少的错误信息

RX 的逻辑相当简单,重点在完善细微的细节上。

让我们从简单的逻辑开始:

当用户输入内容的时候我们发出了一个网络请求然后获得结果:

扫描二维码关注公众号,回复: 1893915 查看本文章
1
2
3
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更新。

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);
     }
 
}

以上。你节制了你的请求,你总是能得到最近的请求结果,你有重试连接的智能超时处理机制。

英文原文:Improving UX with RxJava 

猜你喜欢

转载自blog.csdn.net/candyguy242/article/details/80791468