1. 程式人生 > >使用響應式程式設計(RxJava)開發Android App

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

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

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

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

注意:我在工程裡使用了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實現的內容可能有更好的方式實現,請在評論裡指出。(譯者注:這也是畢業後第一次翻譯完整的英語文章,有不合適的地方希望得到指正,謝謝)