RxAndroid項目實踐-使用RxJava響應式編碼實現知乎日報的歡迎界面

介紹

我的項目中,使用RxAndroid和RxJava搭建項目。在剛開始是很痛苦的,每個操作符和方法都不熟悉。現在項目完成過半,對RxAndroid開始有些熟悉。就從一個實際功能開發入手,總結我對RxAndroid的理解。

效果演示

效果演示

說明:
- 從用戶角度:是一張華麗麗圖片的放大,然後進入主頁,很好的用戶體驗。(參考自:知乎日報)
- 從App的角度:異步屬性動畫Animator的播放和後臺線程的網絡鏈接,全部完成後同步,activity跳轉。

思路分析

基本的思路開始分析

  • 圖片的縮放:需要屬性動畫,設置需要控制的View控件屬性,屬性值和差值器。開始播放
  • 邏輯處理:邏輯判斷的情況是,首先判斷用戶是否登錄過,如果登錄過並且上次登錄時間已經超過某個設定好的差值,需要重新聯網獲取token值(token值是整個App的Hpps聯網的關鍵參數)。如果沒有登錄屬於第一次打開App就沒有網絡操作。最後統一finish跳轉到主頁。
  • 屬性動畫和邏輯處理的同步:屬性動畫的播放時間可以預先設置,但是網絡返回結果時間未知,需要同步判定返回結果。

實現的關鍵點:同步/異步的統一

如果是沒用使用Rx響應式,就需要接口通信,各種回調處理。並且分別需要監聽屬性動畫的播放開始和結束還有網絡連接的成功,同時還需要做異常處理。想想都頭大。

從響應式方式思路分析

首先看看RxJava是什麼

“a library for composing asynchronous and event-based programs using observable sequences for the Java VM”(一個在 Java VM 上使用可觀測的序列來組成異步的、基於事件的程序的庫)。這就是 RxJava ,概括得非常精準。

其實,屬性動畫的開始和結束,網絡連接的成功和失敗都是事件流。我們只要把它們串聯起來,訂閱觀察就可以了。
屬性動畫不是事件流,那我們把它包裝起來讓它有發送事件的能力,至於網絡連接RxJava的好搭檔Retrofit早就已經實現好了。

代碼實踐

佈局文件/動畫文件

在項目中新建Activity類命名WelcomeActivity。
先看它的佈局文件

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/img_welcome"
    android:scaleType="centerCrop"
    android:src="@drawable/welcome_second" />

就一個ImageView控件已經設置好的圖片資源。

 <activity
            android:name=".Welcome.WelcomeActivity"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

manifest定義成啓動項和theme,下面的@style/AppTheme.NoActionBar定義在Style-v21中,因爲很多屬性只支持到API21+,才能實現全屏。

<style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:windowContentTransitions">true</item>
    </style>

屬性動畫的文件:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:ordering="together"
    >
    <objectAnimator android:duration="2000"
        android:valueFrom="1f"
        android:valueTo="1.2f"
        android:valueType="floatType"
        android:propertyName="scaleY"
        android:interpolator="@android:interpolator/decelerate_cubic"
        />
    <objectAnimator android:duration="2000"
        android:valueFrom="1f"
        android:valueTo="1.2f"
        android:valueType="floatType"
        android:propertyName="scaleX"
        android:interpolator="@android:interpolator/decelerate_cubic"
        />
</set>

interpolator差值器使用的是:decelerate類型,實現先快後慢,符合Material Design規範。

前期準備

剛思路分析的時候說到Animator屬性動畫不具備發送事件的能力,那我們就給它包裝出這個能力

MyRxObservable類

它的功能是:用Observable.create創建出一個可觀察的Observable對象,並且實現非空檢查

public class MyRxObservable {
    public static Observable<Void> add(Animator animator){
        Utils.checkNotNull(animator,"Animation is null");
        return Observable.create(new AnimatorOnSubscribe(animator));
    }
}

AnimatorOnSubscribe類

它的功能是:實現發送事件的能力,從Animator的監聽器中得到回調,得到發送和完成的通知

/**
 * Created by LiCola on  2016/04/10  15:39
 */
public class AnimatorOnSubscribe implements Observable.OnSubscribe<Void> {
    final Animator animator;

    public AnimatorOnSubscribe(Animator animator) {
        this.animator = animator;
    }

    @Override
    public void call(final Subscriber<? super Void> subscriber) {
        checkUiThread();//檢查是否在UI線程調用,否則不能播放動畫
        AnimatorListenerAdapter adapter=new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                subscriber.onNext(null);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                subscriber.onCompleted();    
            }
        };

        animator.addListener(adapter);
        animator.start();//先綁定監聽器再開始
    }
}

以上實現參考自:JakeWharton大神的RxBinding項目的點擊事件 RxView.clicks的包裝處理

網絡操作

private Observable<TokenBean> getUserToken(String username, String password) {
        return RetrofitGsonRx.service.httpsTokenRx(username, password);
    }

這是我的實現,涉及到Retrofit的使用,我這裏就不詳細介紹了。源碼在我的項目HttpUtils包中

RxJava實現

實現了上面的代碼,我們就有了可以觀察的事件流,現在就把他們串聯起來

//初始化動畫文件 綁定控件
  Animator animation = AnimatorInflater.loadAnimator(mContext, R.animator.welcome_animator);
        animation.setTarget(mImageView);
 MyRxObservable.add(animation)
                .subscribeOn(AndroidSchedulers.mainThread())//指定訂閱的Observable對象的call方法運行在ui線程中
                .observeOn(Schedulers.io())
                .filter(new Func1<Void, Boolean>() {
                    @Override
                    public Boolean call(Void aVoid) {
                    //filter過濾:判斷是否登錄過,如果false就會跳過下面的操作
                        return isLogin;
                    }
                })
                .filter(new Func1<Void, Boolean>() {
                    @Override
                    public Boolean call(Void aVoid) {
                    //過濾:判斷上次登錄是否超過時間差
                        Long lastTime= (Long) SPUtils.get(getApplicationContext(),Constant.LOGINTIME,0L);
                        return System.currentTimeMillis()-lastTime >mTimeDifference;           
                    }
                })
                .flatMap(new Func1<Void, Observable<TokenBean>>() {
                    @Override
                    public Observable<TokenBean> call(Void aVoid) {
                        //flatMap轉換:和 map() 有一個相同點:它也是把傳入的參數轉化之後返回另一個對象。但需要注意,和 map()不同的是, flatMap() 中返回的是個 Observable 對象
                        //上面兩步都爲true才能到這裏,取出用戶信息,開始聯網。
                        String userAccount= (String) SPUtils.get(getApplicationContext(),Constant.USERACCOUNT,"");
                        String userPassword= (String) SPUtils.get(getApplicationContext(),Constant.USERPASSWORD,"");
                        return getUserToken(userAccount,userPassword);
                    }
                })
               .observeOn(AndroidSchedulers.mainThread())//最後統一回到UI線程中處理
                .subscribe(new Subscriber<TokenBean>() {
                    @Override
                    public void onCompleted() {
                            //所有操作完成,統一回調這裏,實現Activity跳轉功能
                        MainActivity.launch(WelcomeActivity.this);
                        finish();
                    }

                    @Override
                    public void onError(Throwable e) {
                    //異常處理,根據每個項目而定,這裏不具體寫
                        Logger.d(e.toString());
                    }

                    @Override
                    public void onNext(TokenBean tokenBean) {
                       //只有網絡成功纔會回調這裏,這裏可以保存網絡數據。
                    }
                });

運行代碼

我在每個代碼運行的關鍵部分都打上Log輸出,觀察程序運行

第一次打開App

這是第一次運行App的日誌打印:
可以看到:因爲沒有登錄過,所有隻有動畫的開始、結束,然後跳轉。跳轉時間由動畫時間決定。

04-10 20:19:55.990 8111-8111/ D/Demo: [BaseActivity:onResume:95]: WelcomeActivity @3eb05556
04-10 20:19:56.049 8111-8670/ D/Demo: [WelcomeActivity:call:85]: isLogin= false
04-10 20:19:56.050 8111-8111/ D/Demo: [AnimatorOnSubscribe:onAnimationStart:32]: onAnimationStart
04-10 20:19:58.054 8111-8111/ D/Demo: [AnimatorOnSubscribe:onAnimationEnd:39]: onAnimationEnd
04-10 20:19:58.055 8111-8111/ D/Demo: [WelcomeActivity:onCompleted:123]:
04-10 20:19:58.098 8111-8111/ D/Demo: [BaseActivity:onPause:124]: WelcomeActivity @3eb05556

登錄用戶再次打開App

這是登錄用戶的打開App日誌:
再次打開App後,因爲有登錄信息,並且超過了登錄時間差,會自動登錄聯網。跳轉時間還是在2000毫秒左右。這是因爲一般的網絡連接在2秒內已經完成沒有影響到完成時間。

04-10 20:26:23.705 12131-12131/ D/Demo: [BaseActivity:onResume:95]: WelcomeActivity @3eb05556
04-10 20:26:23.768 12131-12174/ D/Demo: [WelcomeActivity:call:85]: isLogin= true
04-10 20:26:23.768 12131-12131/ D/Demo: [AnimatorOnSubscribe:onAnimationStart:32]: onAnimationStart
04-10 20:26:23.768 12131-12174/ D/Demo: [WelcomeActivity:call:94]: dTime 133545
04-10 20:26:23.769 12131-12174/ D/Demo: [WelcomeActivity:call:103]: flatMap
04-10 20:26:24.135 12131-12131/ D/Demo: [WelcomeActivity:onNext:135]: https success
04-10 20:26:25.781 12131-12131/ D/Demo: [AnimatorOnSubscribe:onAnimationEnd:39]: onAnimationEnd
04-10 20:26:25.783 12131-12131/ D/Demo: [WelcomeActivity:onCompleted:123]:
04-10 20:26:25.827 12131-12131/ D/Demo: [BaseActivity:onPause:124]: WelcomeActivity @3eb05556

模擬極端條件

因爲網絡狀態太好,從上面的日誌看不出關鍵信息,我修改了部分代碼,強制休眠線程,模擬極端網絡條件。

.flatMap(開始網絡連接)
 .filter(new Func1<TokenBean, Boolean>() {
                    @Override
                    public Boolean call(TokenBean tokenBean) {
                        //強制使線程休眠 因爲是在IO線程不會使得程序異常
                        Logger.d("thread sleep start");
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        Logger.d(Thread.currentThread().toString());
                        Logger.d("thread sleep end");
                        return true;
                    }
                })
                 .observeOn(AndroidSchedulers.mainThread())
                .subscribe(訂閱回調之前)

這是日誌打印:很高興的看到,因爲網絡線程的3秒休眠,動畫已經結束,但是在等待網絡返回成功後,才跳轉,很好的保證了實現邏輯,不會出現動畫播放完成,activity跳轉得不到網絡連接結果。

04-10 20:37:07.482 18732-18732/ D/Demo: [BaseActivity:onResume:95]: WelcomeActivity @3eb05556
04-10 20:37:07.731 18732-18732/ D/Demo: [AnimatorOnSubscribe:onAnimationStart:32]: onAnimationStart
04-10 20:37:07.743 18732-18872/ D/Demo: [WelcomeActivity:call:85]: isLogin= true
04-10 20:37:07.743 18732-18872/ D/Demo: [WelcomeActivity:call:94]: dTime 777520
04-10 20:37:07.744 18732-18872/ D/Demo: [WelcomeActivity:call:103]: flatMap
04-10 20:37:08.516 18732-18872/ D/Demo: [WelcomeActivity:call:111]: thread sleep start
04-10 20:37:09.730 18732-18732/ D/Demo: [AnimatorOnSubscribe:onAnimationEnd:39]: onAnimationEnd
04-10 20:37:11.517 18732-18872/ D/Demo: [WelcomeActivity:call:117]: Thread[RxCachedThreadScheduler-1,5,main]
04-10 20:37:11.517 18732-18872/ D/Demo: [WelcomeActivity:call:118]: thread sleep end
04-10 20:37:11.520 18732-18732/ D/Demo: [WelcomeActivity:onNext:138]: https success
04-10 20:37:11.521 18732-18732/ D/Demo: [WelcomeActivity:onCompleted:126]:
04-10 20:37:12.211 18732-18732/ D/Demo: [BaseActivity:onPause:124]: WelcomeActivity @3eb05556

總結

  1. 這我是在使用RxAndroid一段時間後,對它的完整使用,並且使用RxAndroid很好的實現了複雜功能。避免了各種接口回調的迷之代碼,鏈式調用代碼邏輯清晰。如果Java 8 Lambda函數會減少更多模板代碼。
  2. 完整的日誌打印分析,看到了各種情況下的App運行狀態

項目參考資料:
http://blog.csdn.net/guolin_blog/article/details/43536355
https://github.com/JakeWharton/RxBinding
http://rxjava.yuxingxin.com/
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1012/3572.html#toc_22

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章