使用響應式程式設計(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 完成下面這些任務:
- 在使用者輸入電影或電視劇名字的同時,根據已經輸入的部分字元進行匹配,提供建議列表
- 當用戶點選了某條建議,我們通過一個API查詢,顯示出對應的電影詳情
- 當用戶點選了鍵盤上的搜尋按鈕,我們需要展示所有匹配的電影的詳情列表
- 允許使用者根據型別對結果進行過濾
- 允許使用者輸入多個名字,我們獲取所有的結果展示給使用者(使用傳統的程式設計方法達成這一任務可不簡單)
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實現的內容可能有更好的方式實現,請在評論裡指出。(譯者注:這也是畢業後第一次翻譯完整的英語文章,有不合適的地方希望得到指正,謝謝)