使用響應式編程(RxJava)開發Android App

Android app development using the reactive programming paradigm (RxJava)

原作者:Arif Nadeem

如果你已經看過了RxJava或其他的ReactiveX庫的點贊數,你一定會同意我的說法:響應式編程的學習曲線很陡峭,而之所以形成這種學習體驗,則是因爲沒有好的學習嚮導和書籍。

我探究了響應式編程(尤其是RxJava)背後的基本原理。我不想從RxJava的基礎知識說起,你可以從這篇博客裏找到對此的介紹。我想給你展示的是怎麼使用RxJava和RxAndroid開發一個基礎的Android App,從中你可以體會到RxJava和RxAndroid帶來的便利。

Github源碼地址

爲了開始在Android應用中使用RxJava,你需要使用以下的庫工程:

  1. Retrofit2
  2. RxJava
  3. RxAndroid, RxJava在Android上的擴展庫
  4. Gson
  5. Picasso
  6. Retrolambda,讓代碼更精巧,可讀性更好

注意:我在工程裏使用了retrolambda,這可能導致你不能直接從Android
Studio構建出apk。原因是Lambda表達式是從Java8開始支持的,而現在的Android還不支持Java8。你可以在gradle
file文件裏配置java 8和java 7的路徑

對於gradle文件和其他的工程設置請看我的Github工程。

爲了展示如何使用上面那些庫,我會用OMDB API 完成下面這些任務:

  1. 在用戶輸入電影或電視劇名字的同時,根據已經輸入的部分字符進行匹配,提供建議列表
  2. 當用戶點選了某條建議,我們通過一個API查詢,顯示出對應的電影詳情
  3. 當用戶點擊了鍵盤上的搜索按鈕,我們需要展示所有匹配的電影的詳情列表
  4. 允許用戶根據類型對結果進行過濾
  5. 允許用戶輸入多個名字,我們獲取所有的結果展示給用戶(使用傳統的編程方法達成這一任務可不簡單)

RxJava 基礎:在進一步深入之前,我們要先確認一點,我們要理解在Observable(被觀察者)和Subscriber(訂閱者)之間的不同。
在響應式編程裏,有兩個有意思的概念,第一個是Observable(被觀察者),第二個是Subscriber(訂閱者)或Observer(觀察者)。Observable負責做所有的工作,而Subscriber負責監聽Observable的不同狀態,一個Observable可能完成,也可能失敗,這會反應到Subscriber的onComplete函數或者onError函數,還有一個叫onNext的方法,當Observable發出一個事件時它會被調用。

現在我們開始寫代碼,首先我們要定義一個Retrofit單例

public class RetrofitHelper {

    private static final String BASE_URL = "http://www.omdbapi.com";
    private static RetrofitHelper mRetrofitHelper;
    private Retrofit mRetrofit;

    private RetrofitHelper() {
        mRetrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())                  .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
    }

    public static RetrofitHelper getInstance() {
        if (mRetrofitHelper == null) {
            synchronized (RetrofitHelper.class) {
                if (mRetrofitHelper == null)
                    mRetrofitHelper = new RetrofitHelper();
            }
        }
        return mRetrofitHelper;
    }

    public Retrofit getRetrofit() {
        return mRetrofit;
    }

}

這裏注意,爲了引入GsonConverterFactory和RxJavaCallAdapterFactory,需要在build.grable添加下面兩行

compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'

爲了使用Retrofit,我們還需要爲我們的API定義下面的接口

public interface OmdbApiInterface {

@GET("/")
Observable<SearchResults> getSearchResults(@Query("s") String query,
                                         @Query("plot") String plot,
                                         @Query("type") String type,
                                         @Query("r") String format);
@GET("/")
Observable<Movie> getMovie(@Query("t") String title,
                               @Query("plot") String plot,
                               @Query("type") String type,
                               @Query("r") String format);

}

第一個API用來根據用戶輸入的字符搜索匹配的電影列表,第二個API用來根據電影的名字查詢到電影的詳情。通過使用爲RxJava適配的Retrofit2,我們可以方便的從請求得到一個Observable,然後可以對它進行訂閱並監聽它的狀態變化。

現在再看我們怎麼實現給用戶展示搜索建議列表。

我已經實現了SearchView的OnQueryTextListener,當用戶輸入兩個字符以上時,我開始進行API調用。爲了使用RxJava,我們需要定義一個能通過查詢字段獲取搜索結果的Observable

public Observable<SearchResults> getSearchResultsApi(String query, String type) {
 return apiInterface.getSearchResults(query, "short", type, "json");
}

下一個任務是給上面的Observable寫一個Subscriber

private Subscriber<SearchResults> searchResultsSubscriber() {
    return new Subscriber<SearchResults>() {
        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {
            HttpException exception = (HttpException) e;
            Log.e(MovieSearchFragment.class.getName(), "Error: " + exception.code());
        }

        @Override
        public void onNext(SearchResults searchResults) {
            MatrixCursor matrixCursor = CPUtils.convertResultsToCursor(searchResults.getSearch());
            mSearchViewAdapter.changeCursor(matrixCursor);
        }
    };
}

下面是最後一步了,當用戶的輸入字符超過2個的時候,我們就要生成這個訂閱,把事件發出去

@Override
public boolean onQueryTextChange(String newText) {
    if (newText.length() > 2) {
        try {
            if (searchResultsSubscription != null && !searchResultsSubscription.isUnsubscribed()) {
                //Cancel all ongoing requests and change cursor
                searchResultsSubscription.unsubscribe();
                matrixCursor = CPUtils.convertResultsToCursor(new ArrayList<>());
                mSearchViewAdapter.changeCursor(matrixCursor);
            }
            String encodedQuery = URLEncoder.encode(newText, "UTF-8");
            Observable<SearchResults> observable = mOmdbApiObservables.getSearchResultsApi(encodedQuery, mFilterSelection);
            searchResultsSubscription = observable
                    .debounce(250, TimeUnit.MILLISECONDS)
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribeOn(Schedulers.io())
                    .subscribe(searchResultsSubscriber());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
    return true;
}

完事大吉了,現在當用戶輸入時,我們會在下拉列表裏展示搜索建議,注意這一行observeOn(AndroidSchedulers.mainThread()),我們使用了RxAndroid,來實現讓觀察者運行在Android UI線程的目的,我們都知道Android只允許在主線程裏更新 。

確保在onDestroy()函數裏對訂閱解綁

if (searchResultsSubscription != null && !searchResultsSubscription.isUnsubscribed())
    searchResultsSubscription.unsubscribe();

上面的功能很容易實現,現在讓我們看一下RxJava最有趣的一個功能:根據我們的需求把數據組合。

當用戶點擊了搜索按鈕,我們應該給用戶展示一個所有匹配的電影的詳情列表。爲了實現這個功能,我們需要對每一個匹配的電影調用getMovie(),在命令式編程範式裏,我們需要爲每一個請求產生一個線程,等待所有的結果返回時再把他們組合起來,然後再綁定到Adapter上。但是,但是!我們現在有了RxJava,我們得救了。

Observer(譯者注: 應該是Observable吧)

public Observable<List<Movie>> getAllMoviesForSearchApi(String query, String type) {
    return apiInterface.getSearchResults(query, "short", type, "json").subscribeOn(Schedulers.newThread())
            .flatMap(searchResults -> Observable.from(searchResults.getSearch() != null ? searchResults.getSearch() : Collections.emptyList()))
            .flatMap(search -> getSingleMovieForTitleApi(search.getTitle(), type)).toList();
}

public Observable<Movie> getSingleMovieForTitleApi(String title, String type) {
    return apiInterface.getMovie(title, "short", type, "json").subscribeOn(Schedulers.newThread());
}

Subscriber 訂閱者

private Subscriber<List<Movie>> moviesForSearchSubscriber() {
    return new Subscriber<List<Movie>>() {
        @Override
        public void onCompleted() {
            if (mPd.isShowing())
                mPd.dismiss();
            moviesRecyclerAdapter.notifyDataSetChanged();
        }

        @Override
        public void onError(Throwable e) {
            if (mPd.isShowing())
                mPd.dismiss();
            HttpException exception = (HttpException) e;
            Log.e(MovieSearchFragment.class.getName(), "Error: " + exception.code());
        }

        @Override
        public void onNext(List<Movie> movies) {
            if (movies == null || movies.size() == 0)
                showShortToast("No results, is your title correct?");
            for (Movie m : movies) {
                mMovies.add(m);
            }
        }
    };
}

這裏我們首先調用getSearchResults API, 然後對它的調用結果searchResults構建了一個新的Observable,實現對searchResults裏每一項調用getSingleMovieForTitleApi;最後把結果組合成一個List在Adapter裏使用。subscribeOn()方法使請求在單獨的線程執行。

這就是RxJava的神奇,通過四行代碼,我們避免了模版代碼和令人困惑的多線程語法,實現了開闢最佳的線程數進行高效的調用。(譯者注:Schedulers.newThread()爲每一個任務創建新的線程,內部用了線程池)

最後我們看一下怎麼從多個查詢得到結果

Observer(譯者注:同上,認爲應該是Observable)

public Observable<List<Movie>> getMoviesForMultipleQueries(List<String> queries, String type) {
Observable<List<Movie>> observable = Observable.from(queries).flatMap(query -> getAllMoviesForSearchApi(query.trim(), type)).subscribeOn(Schedulers.newThread());
    return observable;
}

Subscriber

private Subscriber<List<Movie>> moviesForMultiQuerySearchSubscriber() {
    return new Subscriber<List<Movie>>() {
        @Override
        public void onCompleted() {
            if (mPd.isShowing())
                mPd.dismiss();
            moviesRecyclerAdapter.notifyDataSetChanged();
        }

        @Override
        public void onError(Throwable e) {
            if (mPd.isShowing())
                mPd.dismiss();
            HttpException exception = (HttpException) e;
            Log.e(MovieSearchFragment.class.getName(), "Error: " + exception.code());
        }

        @Override
        public void onNext(List<Movie> movies) {
            if (movies == null || movies.size() == 0)
                showShortToast("No results, is your title correct?");
            for (Movie m : movies) {
                mMovies.add(m);
            }
        }
    };
}

多麼簡單~我們把多個查詢詞組合成一個列表,然後在每一個查詢詞上調用getAllMoviesForSearchApi,再把結果組合起來用到Adapter裏。

我希望這個嚮導能清晰地闡明關於響應式編程的許多概念,因爲我是個新手,我用RxJava實現的內容可能有更好的方式實現,請在評論裏指出。(譯者注:這也是畢業後第一次翻譯完整的英語文章,有不合適的地方希望得到指正,謝謝)

發佈了36 篇原創文章 · 獲贊 65 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章