Android app development using the reactive programming paradigm (RxJava)
原作者:Arif Nadeem
如果你已經看過了RxJava或其他的ReactiveX庫的點贊數,你一定會同意我的說法:響應式編程的學習曲線很陡峭,而之所以形成這種學習體驗,則是因爲沒有好的學習嚮導和書籍。
我探究了響應式編程(尤其是RxJava)背後的基本原理。我不想從RxJava的基礎知識說起,你可以從這篇博客裏找到對此的介紹。我想給你展示的是怎麼使用RxJava和RxAndroid開發一個基礎的Android App,從中你可以體會到RxJava和RxAndroid帶來的便利。
爲了開始在Android應用中使用RxJava,你需要使用以下的庫工程:
- Retrofit2
- RxJava
- RxAndroid, RxJava在Android上的擴展庫
- Gson
- Picasso
- Retrolambda,讓代碼更精巧,可讀性更好
注意:我在工程裏使用了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實現的內容可能有更好的方式實現,請在評論裏指出。(譯者注:這也是畢業後第一次翻譯完整的英語文章,有不合適的地方希望得到指正,謝謝)