我的一次RxJava使用。

Landroid發表於2017-12-26

業務

最近需要實現一個通過 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)分類一個個看,總會找到。下圖是常用的組合操作符:

我的一次RxJava使用。

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了,就為了這個空指標異常。

相關文章