业务
最近需要实现一个通过 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
请自备梯子。
大概意思是7.0以上的 CONNECTIVITY_ACTION
只能通过动态注册去接收该类广播。而对广播的处理还需要根据API的版本做不同的处理,由于我在项目中使用了RxJava,这里我就使用了下面的库来处理网络切换的广播,简直不要太爽。
哎!现在离开 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() { @Override public void accept(Disposable disposable) throws Exception { mNetWorkChangeDisposable = disposable; } }) .throttleLast(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer () { @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
。
下面是动态图:
然而这不符合我的需求,我好奇的是它为什么可以监听到 Activity
的生命周期,看了下 RxLifecycle
源码!里面有个常量:
private final BehaviorSubjectlifecycleSubject = 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
不得不佩服国外友人的注释就是写的好。
然后就就开始搬砖了
protected PublishSubjectmEventSubject = 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
的事件。以前做登录界面的时候,有个需求就是当用户名跟密码都有输入的时候,登录按钮才能点击。这两个业务差不多,所以可以使用同样的操作符来处理。 其实我有时也不记得操作符,就去 官网上去找,这种操作肯定是组合操作,找到组合(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
符合我们的要求,上面的大概意思是:只要这些源 ObservableSources
其中的一个发送了item,就会通过一个指定的函数来组合所有源 ObservableSources
最近的值,并发送组合后的结果。
开始码代码:
Observable.combineLatest(reactiveNetwork, mEventSubject.distinctUntilChanged(), new BiFunction() { @Override public Boolean apply(Connectivity connectivity, Event event) throws Exception { //这里只有为resume事件的时候才返回true。 return event == Event.RESUME; } }) .doOnSubscribe(new Consumer () { @Override public void accept(Disposable disposable) throws Exception { mNetWorkChangeDisposable = disposable; } }) .throttleLast(500, TimeUnit.MILLISECONDS) .filter(new Predicate () { @Override public boolean test(Boolean isShowSearchDialog) throws Exception { //过滤掉不符合条件的item return isShowSearchDialog; } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer () { @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
的事件。而不是两个都响应。
查看 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了,就为了这个空指针异常。