我的一次RxJava使用。

1,524 阅读7分钟

业务

最近需要实现一个通过 TCP 来给智能主机配置WiFi的需求。由于在同一个 WiFi 局域网下可能会存在多个主机,于是就在进入到 WiFi 设置 Activity 后就弹出一个对话框来显示当前局域网有哪些主机,这都是小case,难点是要求在切换网络的时候能够自动重新搜索主机。

实现

网络监听

首先说下网络变化监听,大家都知道这个是通过广播来实现。由于我是适配到7.1系统。官方文档上有一段话:

Apps targeting Android 7.0 (API level 24) and higher must register the following broadcasts with registerReceiver(BroadcastReceiver, IntentFilter). Declaring a receiver in the manifest does not work. CONNECTIVITY_ACTION

官方Broadcasts文档

请自备梯子。

大概意思是7.0以上的 CONNECTIVITY_ACTION 只能通过动态注册去接收该类广播。而对广播的处理还需要根据API的版本做不同的处理,由于我在项目中使用了RxJava,这里我就使用了下面的库来处理网络切换的广播,简直不要太爽。 ReactiveNetwork

RxJava在Android上的全家桶

哎!现在离开 RxJava 都不会写代码了。

搜索对话框

弹框使用的是使用的是 DialogFragment官方帮助文档

void showDialog() {
    mStackLevel++;

    // DialogFragment.show() will take care of adding the fragment
    // in a transaction.  We also want to remove any currently showing
    // dialog, so make our own transaction and take care of that here.
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment prev = getFragmentManager().findFragmentByTag("dialog");
    if (prev != null) {
        ft.remove(prev);
    }
    ft.addToBackStack(null);

    // Create and show the dialog.
    DialogFragment newFragment = MyDialogFragment.newInstance(mStackLevel);
    newFragment.show(ft, "dialog");
}

上面代码是文档中的示例代码,完美。但有一行坑爹的代码 ft.addToBackStack(null); 如果有这行代码,并不能解决重复显示对话框的问题,需要把这行代码去掉。因为show函数里面已经把这次事务加入到后退栈里面了。

 public int show(FragmentTransaction transaction, String tag) {
        mDismissed = false;
        mShownByMe = true;
        transaction.add(this, tag);
        mViewDestroyed = false;
        mBackStackId = transaction.commit();
        return mBackStackId;
    }

简直石乐志。

这还不算最惨的,最惨的是下面这个异常。也是由于这个才会写了这篇博客,使用Fragment基本都碰到过的一个异常。 java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState

这是什么鬼。。。 还是看官方文档是怎么说的。

Caution: You can commit a transaction using commit() only prior to the activity saving its state (when the user leaves the activity). If you attempt to commit after that point, an exception will be thrown. This is because the state after the commit can be lost if the activity needs to be restored. For situations in which its okay that you lose the commit, use commitAllowingStateLoss().

大概意思就是不要在Activity的保存状态(也就是 onSaveInstanceState 回调)之后去调用 comment() 函数,不然就会抛出这个异常。这是因为当 Activity 重新创建的时候拿不到Fragment的之前的状态(因为你是在 onSaveInstanceState 回调之后还调用的 commit()),但是如果你不在乎这些状态的话可以使用 commitAllowingStateLoss() 函数来避免这个异常。

那么来看看 DialogFragment 有没有使用这个函数的 show() 函数,找了一下还真有:

  /** {@hide} */
    public void showAllowingStateLoss(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commitAllowingStateLoss();
    }

然而,看到 @hide 注释了吗?尼玛是个隐藏函数。想调用的话就得通过反射,我想还是算了,谷歌爸爸隐藏肯定有理由的,还是老老实实地用 show() 函数吧。 于是我这里就决定在 onResume() 回调的时候才来调用 show() 函数。

初步实现

 protected void onCreat() {
   ReactiveNetwork.observeNetworkConnectivity(getApplicationContext())
                .subscribeOn(Schedulers.io())
                .filter(ConnectivityPredicate.hasState(NetworkInfo.State.CONNECTED, NetworkInfo.State.DISCONNECTED))
                .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(Disposable disposable) throws Exception {
                        mNetWorkChangeDisposable = disposable;
                    }
                })
                .throttleLast(500, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean isHostWifi) throws Exception {
                        ....
                        //弹出搜索对话框
                        showSearchLocalHostDialog("选择主机");
                        ....
                        Log.i("ws", "HostWiFiConfigActivity 网络变化");
                    }
                });
 }
  @Override
    protected void onDestroy() {
        if (mNetWorkChangeDisposable != null && !mNetWorkChangeDisposable.isDisposed()) {
            mNetWorkChangeDisposable.dispose();
        }
        super.onDestroy();
    }

上面的代码就完成了网络切换的监听,是不是很easy。我刚开始也是这样想的,后面发现自己还是太年轻了。 这里说明下,为什么要在 onCreate() 订阅而在 onDestroy() 取消。

  • 不要频繁的注册和注销广播
  • 用户可能石乐志地跑到设置界面去切换网络,这时候需要能够监听到这个网络变换事件。

Bug过来找你吹牛B了

这个bug其实很容易发现。就是当你进入设置界面,这时候app会进入后台,然后切换wifi。 这时候监听到了网络切换事件,接着弹出搜索框。然后你懂得,那个异常就出来了。也许你会说直接在 onResume() 回调中弹出 Dialog,这样确实能解决这个问题。但是尼玛有些手机(比如某米手机)可以在下拉的设置里面切换wifi,这时候尼玛 onResume() 回调根本不执行。难受!

解决

我想到 RxLifecycle 这个库可以在某个事件中可以自动取消事件,以前有大概了解过,它是通过 takeUntil 来取消事件。

下面是 takeUntil 操作符的说明:

discard any items emitted by an Observable after a second Observable emits an item or terminates

翻译下就是:第二个 Observable 一旦发射了 item,源 Observable 就会丢弃后续的 item

下面是动态图:

TakeUntil

然而这不符合我的需求,我好奇的是它为什么可以监听到 Activity 的生命周期,看了下 RxLifecycle 源码!里面有个常量:

    private final BehaviorSubject<ActivityEvent> lifecycleSubject = BehaviorSubject.create();

BehaviorSubject 是个什么?它是一种特殊的存在,它既可以是 Obaservable 也可以是 Observer

Rxjava 源码里面这个类的注释:

Subject that emits the most recent item it has observed and all subsequent observed items to each subscribed

意思就是一旦订阅了就会发送最近的一个及后续的 item

 // observer will receive the "one", "two" and "three" events, but not "zero"
 //observer可以接收到"one", "two" and "three"事件,但是接收不了"zero"
  BehaviorSubject<Object> subject = BehaviorSubject.create();
  subject.onNext("zero");
  subject.onNext("one");
  subject.subscribe(observer);
  subject.onNext("two");
  subject.onNext("three");

不得不佩服国外友人的注释就是写的好。

然后就就开始搬砖了

    protected PublishSubject<Event> mEventSubject = PublishSubject.create();

    enum Event {
    // Activity life Events
        CREATE,
        START,
        RESUME,
        PAUSE,
        STOP,
        DESTROY,
    }

    @Override
    protected void onResume() {
        super.onResume();
        mEventSubject.onNext(Event.RESUME);
    }

    @Override
    protected void onStop() {
        super.onStop();
        mEventSubject.onNext(Event.STOP);
    }

这时候我就去想有什么操作符可以同时监听多个 Observable 的事件。以前做登录界面的时候,有个需求就是当用户名跟密码都有输入的时候,登录按钮才能点击。这两个业务差不多,所以可以使用同样的操作符来处理。 其实我有时也不记得操作符,就去 ReactiveX 官网上去找,这种操作肯定是组合操作,找到组合(Combining Observables)分类一个个看,总会找到。下图是常用的组合操作符:

CombineLatest — when an item is emitted by either of two Observables, combine the latest item emitted by each Observable via a specified function and emit items based on the results of this function

CombineLatest动态图

很明显 CombineLatest 符合我们的要求,上面的大概意思是:只要这些源 ObservableSources 其中的一个发送了item,就会通过一个指定的函数来组合所有源 ObservableSources 最近的值,并发送组合后的结果。

开始码代码:

        Observable.combineLatest(reactiveNetwork, mEventSubject.distinctUntilChanged(), new BiFunction<Connectivity, Event, Boolean>() {
            @Override
            public Boolean apply(Connectivity connectivity, Event event) throws Exception {
            //这里只有为resume事件的时候才返回true。
             return event == Event.RESUME;     
            }
        })
        .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(Disposable disposable) throws Exception {
                        mNetWorkChangeDisposable = disposable;
                    }
                })
                .throttleLast(500, TimeUnit.MILLISECONDS)
                .filter(new Predicate<Boolean>() {
                    @Override
                    public boolean test(Boolean isShowSearchDialog) throws Exception {
                        //过滤掉不符合条件的item
                        return isShowSearchDialog;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean isHostWifi) throws Exception {
                    ...
                                showSearchLocalHostDialog("选择主机");
                    ...
                    }
                });

这样基本就很完美了,当然还存在一个问题。就是按home键退到后台再返回app时,也会弹出对话框。这个就是小事了,只要修改下 combineLatest 操作符里面的判断条件。

 return TextUtils.equals(connectivity.getTypeName().toUpperCase(), "WIFI")
                        && event == Event.RESUME
                        && !TextUtils.equals(connectivity.getExtraInfo().replace("\"", ""), mCurrentWiFiSSID);

也可以在 reactiveNetwork 上面使用 filter 或者 distinctUntilChanged(new BiPredicate<Connectivity, Connectivity>() { }) 这样更优美。

withLatestFrom 操作符

再介绍一下 withLatestFrom 操作符。CombineLatest 操作符只要其中任何一个源 ObservableSources 发送了事件就会被触发。withLatestFrom 操作符就有点不一样了。

这个操作符的说明:

It is similar to combineLatest, but only emits items when the single source Observable emits an item

也就是当源 Observable 只要发送过一个事件后,就可以通过另外一个 Observable 来触发。也就是只会去响应 other Observable 的事件。而不是两个都响应。

withLatestFrom动态图

查看 RxJava 源码了解 withLatestFrom 操作符的参数 combiner 的说明:

@param combiner the function to call when this ObservableSource emits an item and the other ObservableSource has already emitted an item, to generate the item to be emitted by the resulting ObservableSource

翻译下就是:当源 ObservableSource 也就是调用 withLatestFrom 操作符的对象每发送一个 item 并且 other ObservableSource(也就是 withLatestFrom 操作符的第一个参数) 已经发送过一个 item了,就会调用该函数。

备注

想要DialogFragment在对话框外点击不能取消可以在 onCreateView 中这样设置:

if (getDialog() != null) {
            getDialog().setCanceledOnTouchOutside(false);
        }

其实是可以不用做非空判断,不过实在是怕了空指针异常。是时候去学习Kotlin了,就为了这个空指针异常。