2015年11月28日 星期六

Android 開發(一百零五) 架構建立,利用retrofit & rxjava 來將資料與UI分離

由於最近有點空閒,所以花了點時間看了一下retrofit & rxjava 搭配起來可以做到什麼成果

在講解之前,不知道大家有沒有一樣的經驗,在寫project時,api的code總是跟UI code綁在一起,造成程式邏輯上很複雜,舉個例子來說,例如 https://api.github.com/repos/square/retrofit/contributors 這隻api ,我希望針對回來的json,filter 出login field為JakeWharton的人,以及contributions field大於10的人,並且分別在UI上做相對應的顯示.

可想而知,作法就會是

  1. 取得json List
  2. 針對List filter 出login field 為JakeWharton的物件
  3. 針對List filter 出contributions 大於 10的物件
  4. 顯示到UI上

從code上面看起來就會

List<GithubJson> list = api.getGithubJson();
List<GithubJson> JakeWhartonList = dofilterEquals(list,   "login", "JakeWharton");
loginListCustomView.setData(JakeWhartonList);
List<GithubJson> OverTenList = dofilterOver(list, "contributions", 10);
contributionsListCustomView.setData(JakeWhartonList);

這樣看起來似乎很漂亮…..那是因為這只是個範例,實際上的code應該會比上面這段code複雜好幾倍,所以這也就讓我們開始思考是否有更漂亮的解決方案.

上面這段code最主要的問題在於,資料,處理資料以及UI顯示的程式碼被混雜在一起,造成之後維護上的麻煩,而且重點是無法測試,如果將上面的code寫在activity or fragment 裡,就會發現當ui 呈現錯誤時,無法知道到底是ui錯誤,資料錯誤或是filter錯誤.

所以為了達到我們想要的目標可測試的程式首先就必須先把ui & 資料分離,目標

MyAppClient apiclient = new MyAppClient();
    apiclient.getFilterAndOverTen(new MyAppClient.ICallBack() {
        @Override
        public void callBack(MyWrapper o) {
            //use MyWrapper to updateUI;
        }
    });

為了達到這個目標,我們可以利用rxjava的特性

SimpleService.GitHub github = retrofit.create(SimpleService.GitHub.class);

Observable<List<SimpleService.Contributor>> call = github.contributors("square", "retrofit");

取得observable之後也就是我們想要取得的資料,接著針對資料做filter的動作

       Observable<List<SimpleService.Contributor>> a = call.observeOn(Schedulers.io())
            .subscribeOn(Schedulers.newThread())
            .flatMap(new Func1<List<SimpleService.Contributor>, Observable<SimpleService.Contributor>>() {
                @Override
                public Observable<SimpleService.Contributor> call(List<SimpleService.Contributor> contributors) {
                    return Observable.from(contributors);
                }
            })
            .filter(new Func1<SimpleService.Contributor, Boolean>() {
                @Override
                public Boolean call(SimpleService.Contributor contributor) {
                    return contributor.login.contains("JakeWharton");
                }
            }).toList()
            .doOnNext(new Action1<List<SimpleService.Contributor>>() {
                @Override
                public void call(List<SimpleService.Contributor> contributors) {
                    Log.d("Ted", "size " + contributors.size());
                }
            });

首先我們要先filter出login field 為jakewharton 的人,所以我們必須針對list去做filter,花了一點時間做研究後,發現必須將

Observable<List<SimpleService.Contributor>> 轉為 Observable<SimpleService.Contributor>

filter的動作才可以正確執行
上面的程式碼就是做這樣的事情,接著另一部分的filter當然也是依樣畫葫蘆,當兩個東西都filter完成後,接著重點就是將兩個filter好的東西合併在一起

Observable.zip(a, b, new Func2<List<SimpleService.Contributor>, List<SimpleService.Contributor>, MyWrapper>() {
        @Override
        public MyWrapper call(List<SimpleService.Contributor> contributors, List<SimpleService.Contributor> contributors2) {
            return new MyWrapper(contributors, contributors2);
        }
    }).subscribe(new Action1<MyWrapper>() {
        @Override
        public void call(MyWrapper o) {
            Log.d("TEd","go");
            callBack.callBack(o);
        }
    });

可以注意到,zip就是將兩個物件合起來,然後合成MyWrapper,回傳給外層呼叫的fragment or activity,這樣就完成了

之後如果ui有所改變,data還是可以重用,如果data有所改變,修改的程式也只會限定在data那包裡面,封裝的效果就出來了,當然需要測試的話也可以輕鬆地做測試了.

最後,還是要依照慣例附上sample囉 https://github.com/nightbear1009/retrofit_rxjava_architech