Android NotRxJava
Ngày nay nếu bạn là một lập trình viên Android hẳn bạn sẽ được nghe rất nhiều về RxJava. RxJava là một thư viện giúp bạn giải quyết các vấn đề khi xử lí luồng bất đồng bộ (asynchronous event) và nhiều lợi ích khác đã được cộng đồng đánh giá cao. Một khi đã quen sử dụng nó, chắc chắn bạn sẽ dùng nó ...
Ngày nay nếu bạn là một lập trình viên Android hẳn bạn sẽ được nghe rất nhiều về RxJava. RxJava là một thư viện giúp bạn giải quyết các vấn đề khi xử lí luồng bất đồng bộ (asynchronous event) và nhiều lợi ích khác đã được cộng đồng đánh giá cao. Một khi đã quen sử dụng nó, chắc chắn bạn sẽ dùng nó ở bất kì project nào.
Tất nhiên trên mạng giờ có rất nhiều tài liệu hướng dẫn cách sử dụng RxJava. Những khái niệm như Observable, Disposable … được giải thích rõ ràng trên trang web của nhóm phát triển và không thiếu những ví dụ cụ thể. Bài viết này sẽ không giải thích những khái niệm này mà sẽ thảo luận về tư tưởng, về vấn đề mà RxJava giải quyết cho chúng ta - cách tổ chức các code xử lí không đồng bộ. Đối với những bạn chưa từng sử dụng RxJava thì bài viết sẽ cho các bạn những khái niệm bao quát nhất về cách RxJava hoạt động. Chúng ta sẽ đi qua một ví dụ nhỏ về việc xử lí không đồng bộ mà không sử dụng RxJava.
Nếu các bạn vẫn còn thấy tò mò thì chúng ta bắt đầu tìm hiểu nào !
Movie App
Một ví dụ thực tế ngày nay, khi chúng ta có quá nhiều sự lựa chọn về các bộ phim. Để tránh tốn thời gian quý giá, chúng ta hãy xây dựng một app để tìm kiếm và đánh dấu phim. Và nhiệm vụ của app này là :
“Ta có một webservice cung cấp api để show danh sách các bộ phim. Mỗi một bộ phim đều có một parameter là điểm rating của bộ phim đó. Nhiểm vụ của chúng ta là lấy về một danh sách các bộ phim và chọn ra bộ phim có điểm rating cao nhất và lưu vào local storage" Chúng ta tập trung và 3 phần : lấy danh sách phim, xử lí tìm kiếm và lưu dữ liệu phim.
Model and API
public class Movie implements Comparable<Movie> { String title; int rating; @Override public int compareTo(@NonNull Movie another) { return Integer.compare(rating, another.rating); } }
Dưới đây là API lấy danh sách phim và lưu phim
public interface Api { List<Movie> getMovies(); Uri store(Movie movie); }
Chúng ta có một class Helper xử lí business logic
public class MovieHelper { Api api; public Uri saveTopRatedMovie() { List<Movie> movies = api.getMovies(); Movie movie = findTopRated(movies); Uri savedUri = api.store(movie); return savedUri; } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Đoạn code của chúng ta rất rõ ràng và dễ hiểu. Method chính saveTopRatedMovie có 3 function chính mà chúng ta quan tâm là lấy danh sách, tìm kiếm và lưu trữ. Việc chúng ta implement chỉ là gửi vào input, chờ output và chờ các function này làm việc của nó.
Hãy cùng thảo luận về những ưu điểm của cách tiếp cận này
Composition
Như bạn thấy, chúng ta tạo ra function saveTopRatedMovie từ 3 function khác, chúng ta đã thực hiện kết hợp 3 function này lại.
Error propagation
Một trong những điều chúng ta cần lưu ý đó là việc xử lí lỗi. Bất kì function này cũng có thể kết thúc việc xử lí của nó bởi lỗi, trong java chúng ta hay gọi là throwing an Exception. Và chúng ta có thể phân loại và xử lĩ lỗi ở các bậc khác nhau. Một cách phổ biến đó là sử dụng try/catch. Tuy nhiên chúng ta không cần phải try/catch tất cả function nhưng chúng ta có thể khoanh vùng các khu vực có thể xảy ra lỗi bằng cách try/catch block code mà chúng ta nghi ngờ.
try { List<Movie> movies = api.getMovies(); Movie movie = findTopRated(movies); Uri savedUri = api.store(movie); return savedUri; } catch (Exception e){ e.printStackTrace(); return defaultValue; }
Nếu ta try/catch ở block này, ta có thể xử lí bất kì lỗi này xảy ra trong quá trình thực hiện.
Go Async
Tuy nhiên cuộc đời không đơn giản như chúng ta nghĩ, chúng ta sống trong một thế giới mà sự chậm trể là điều tối kị. Cũng như code của chúng ta, đôi khi chúng ta không thể chờ đợi hoặc không muốn chứng kiến việc xử lí một tác vụ mà chúng ta không quá quan tâm đến quá trình của nó. Và thực tế thì khi lập trình Android chúng ta bắt buộc phải làm quen với những dòng code xử lí không đồng bộ.
Một ví dụ cơ bản đó là OnClickListener của Android. Khi muốn xử lí một view click, ta phải đăng kí listener (callback) cho view đó thông báo khi người dùng click vào nó. Và chúng ta không thể bắt người dùng phải click vào view này khi họ không muốn. Vì vậy mà những clicks này luôn không đồng bộ, chúng ta cần lắng nghe những sự kiện này và thông báo khi cần.
Async network call
Các request network luôn tồn tại rủi ro khi bị ảnh hưởng bởi mạng và một vài yếu tố khác. Chúng ta không thể đảm bảo sẽ luôn lấy được kết quả và phải có case xử lí trường hợp này. Giờ hãy update thêm API của chúng ta một callback xử lí.
public interface Api { interface MoviesCallback { void onMoviesReceived(List<Movie> movies); void onReceiveFailed(Exception e); } List<Movie> getMovies(MoviesCallback callback); Uri store(Movie movie); }
MoviesCallback sẽ xử lí kết quả việc call API getMovies sẽ trả về danh sách movie hoặc sẽ trả về lỗi.
Class MovieHelper của chúng ta vì thế cũng thay đổi
public class MovieHelper { Api api; public interface TopMovieCallback { void onTopMovieSaved(Uri uri); void onSavedError(Exception e); } public void saveTopRatedMovie(final TopMovieCallback callback) { List<Movie> movies = api.getMovies(new Api.MoviesCallback() { @Override public void onMoviesReceived(List<Movie> movies) { Movie movie = findTopRated(movies); Uri savedUri = api.store(movie); callback.onTopMovieSaved(savedUri); } @Override public void onReceiveFailed(Exception e) { callback.onSavedError(e); } }); } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Class MovieHelper đã có nhiều thay đổi so với ban đầu. Với hướng tiếp cận không đồng bộ,request lấy danh sách phim cần đính kèm callback để handle các lỗi và thông báo kết quả sau khi hoàn thành.
Hãy thử trường hợp phức tạp hơn, nếu cả 2 API call đều là async.
public interface Api { interface MoviesCallback { void onMoviesReceived(List<Movie> movies); void onReceiveFailed(Exception e); } interface StoreCallback { void onMovieStored(Uri uri); void onStoreFailed(Exception e); } void getMovies(MoviesCallback moviesCallback); void store(Movie movie, StoreCallback storeCallback); }
Và Movie Helper lúc này sẽ là
public class MovieHelper { Api api; public interface TopMovieCallback { void onTopMovieSaved(Uri uri); void onSavedError(Exception e); } public void saveTopRatedMovie(final TopMovieCallback callback) { api.getMovies(new Api.MoviesCallback() { @Override public void onMoviesReceived(List<Movie> movies) { Movie movie = findTopRated(movies); api.store(movie, new Api.StoreCallback() { @Override public void onMovieStored(Uri uri) { callback.onTopMovieSaved(uri); } @Override public void onStoreFailed(Exception e) { callback.onSavedError(e); } }); } @Override public void onReceiveFailed(Exception e) { callback.onSavedError(e); } }); } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Hãy nhìn vào đoạn code mới này, logic vẫn vậy những trông thật tệ, rất nhiều block, code dài và rất khó đọc. Điểm lại những ưu điểm ở cách đoạn code ban đầu khi chúng ta không sử dụng cách tiếp cận không đồng bộ. - Composition đã không còn, chúng ta truyền input nhưng không thể nhận đc output ngay để gửi sang các function khác mà mỗi một function ta phải tạo một callback cho nó và viết code xử lí tiếp tục trong đó. - Error Propagation chúng ta không thể biết lỗi có thể xảy ra ở đâu, và mỗi một function ta đều phải tự viết một method handle lỗi, thậm chí phải re-pass lại nó ( onStoreFailed và onError). - Đoạn code này có khó đọc và dễ có bug hay không ? Chắc chắn có !
Chúng ta sẽ phải sống với đống callback non-composition sao ? Tất nhiên là không, hãy cùng nhau xử lí nào !
Generic callback
Nếu nhìn lại những callback chúng ta đã khai báo chúng ta sẽ phát hiện ra những đặc điểm chung 1. Tất cả đều có một phương thức trả về kết quả (onMoviesReceived, onMovieStored, onTopMovieSaved) 2. Tất cả đều có một phương thức handle error xảy ra trong suốt quá trình thực thi ( onReceiveFailed, onStoreFailed, onSaveError) Ta có thể tạo một generic callback để thay thế cả 3 callback này, tuy nhiên ta không thể thay đổi đặc điểm (kiểu trả về) của chúng. Generic callback có dạng như sau
public interface Callback<T> { void onResult(T result); void onError(Exception e); }
Hãy tạo một ApiWrapper để chỉ định đặc điểm của từng call API
public class ApiWrapper { Api api; public void getMovies(final Callback<List<Movie>> moviesCallback) { api.getMovies(new Api.MoviesCallback() { @Override public void onMoviesReceived(List<Movie> movies) { moviesCallback.onResult(movies); } @Override public void onReceiveFailed(Exception e) { moviesCallback.onError(e); } }); } public void store(Movie movie, final Callback<Uri> uriCallback) { api.store(movie, new Api.StoreCallback() { @Override public void onMovieStored(Uri uri) { uriCallback.onResult(uri); } @Override public void onStoreFailed(Exception e) { uriCallback.onError(e); } }); } }
Lúc này MovieHelper của chúng ta sẽ được loại bỏ dummy logic về handle error và ngắn gọn hơn
public class MovieHelper { ApiWrapper apiWrapper; public interface TopMovieCallback { void onTopMovieSaved(Uri uri); void onSavedError(Exception e); } public void saveTopRatedMovie(final Callback<Uri> uriCallback) { apiWrapper.getMovies(new Callback<List<Movie>>() { @Override public void onResult(List<Movie> movies) { Movie movie = findTopRated(movies); apiWrapper.store(movie,uriCallback); } @Override public void onError(Exception e) { uriCallback.onError(e); } }); } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Như các bạn thấy, chúng ta giảm được một tầng callback bằng cách gửi uriCallback trực tiếp tới apiWrapper.store, các callback generic hoàn toàn giống nhau. Tuy nhiên ta vẫn có thể làm tốt hơn
Keep it separated
Hãy nhìn lại 3 function của chúng ta ( getMovies, store and saveTopRatedMovei). Cả ba function đều theo một pattern. Chúng ta gửi vào hoặc không gửi các arguments ( movie ) và một callback. Tất cả các async operation đều có dạng như vậy ( có hoặc không có regular argument + callback). Ta tách chúng ra thành các tầng để mỗi một async operation sẽ nhận các arguments giá trị rồi trả về một object tạm thời để tiếp tục thực hiện lại việc callback. Bây giờ hãy giả sử object tạm thời của chúng ta có tên là AsyncJob
public abstract class AsyncJob<T> { public abstract void start(Callback<T> callback); } Lúc này các function của chúng ta sẽ trả về object tạm thời này public class ApiWrapper { Api api; public AsyncJob<List<Movie>> getMovies() { return new AsyncJob<List<Movie>>() { @Override public void start(final Callback<List<Movie>> movieCallback) { api.getMovies(new Api.MoviesCallback() { @Override public void onMoviesReceived(List<Movie> movies) { movieCallback.onResult(movies); } @Override public void onReceiveFailed(Exception e) { movieCallback.onError(e); } }); } }; } public AsyncJob<Uri> store(final Movie movie) { return new AsyncJob<Uri>() { @Override public void start(final Callback<Uri> uriCallback) { api.store(movie, new Api.StoreCallback() { @Override public void onMovieStored(Uri uri) { uriCallback.onResult(uri); } @Override public void onStoreFailed(Exception e) { uriCallback.onError(e); } }); } }; } }
Hãy xem MovieHelper của chúng ta thay đổi ra sao
public class MovieHelper { ApiWrapper apiWrapper; public interface TopMovieCallback { void onTopMovieSaved(Uri uri); void onSavedError(Exception e); } public AsyncJob<Uri> saveTopRatedMovie() { return new AsyncJob<Uri>() { @Override public void start(final Callback<Uri> uriCallback) { apiWrapper.getMovies() .start(new Callback<List<Movie>>() { @Override public void onResult(List<Movie> movies) { Movie movie = findTopRated(movies); apiWrapper.store(movie) .start(new Callback<Uri>() { @Override public void onResult(Uri result) { uriCallback.onResult(result); } @Override public void onError(Exception e) { uriCallback.onError(e); } }); } @Override public void onError(Exception e) { uriCallback.onError(e); } }); } }; } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Sự thật là lúc này code rối và phực tạp hơn :v Tuy nhiên hãy đánh giá lợi ích của nó mang lại. Ta đã thực hiện “composed” các function đều cùng trả về cho ta duy nhất object AsyncJob<Uri>. Lúc này phía client chỉ việc tiếp nhận và xử lí object này.
Breaking Things
Hãy xem lại dataflow khi ta tiếp cận async
(async) (sync) (async) query ===========> List<Movie> -------------> Movie ==========> Uri getMovies findTopRated store
Hãy break dataflow này thành các function. Lưu ý một chút : nếu bất kì một operation nào là không đồng bộ thì các operation đi cùng nó cũng phải không đồng bộ. Chúng ta sẽ chia nhỏ method với AsyncJob
public class MovieHelper { ApiWrapper apiWrapper; public AsyncJob<Uri> saveTopRatedMovie() { final AsyncJob<List<Movie>> moviesAsyncJob = apiWrapper.getMovies(); final AsyncJob<Movie> topRatedMovieAsyncJob = new AsyncJob<Movie>() { @Override public void start(final Callback<Movie> callback) { moviesAsyncJob.start(new Callback<List<Movie>>() { @Override public void onResult(List<Movie> movies) { callback.onResult(findTopRated(movies)); } @Override public void onError(Exception e) { callback.onError(e); } }); } }; AsyncJob<Uri> storeMovieAsyncJob = new AsyncJob<Uri>() { @Override public void start(final Callback<Uri> callback) { topRatedMovieAsyncJob.start(new Callback<Movie>() { @Override public void onResult(Movie movie) { apiWrapper.store(movie) .start(new Callback<Uri>() { @Override public void onResult(Uri result) { callback.onResult(result); } @Override public void onError(Exception e) { callback.onError(e); } }); } @Override public void onError(Exception e) { } }); } }; return storeMovieAsyncJob; } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Code nhiều hơn, tuy nhiên đã rõ ràng hơn, low nested callback và meaning variable Một lần nữa, ta hãy làm đoan code này tốt hơn
Simple Mapping
Hãy chú ý đến phần code tìm ra bộ phim có rate cao nhất
final AsyncJob<Movie> topRatedMovieAsyncJob = new AsyncJob<Movie>() { @Override public void start(final Callback<Movie> callback) { moviesAsyncJob.start(new Callback<List<Movie>>() { @Override public void onResult(List<Movie> movies) { callback.onResult(findTopRated(movies)); } @Override public void onError(Exception e) { callback.onError(e); } }); } };
So với đoạn code ban đầu, dòng code hữu dụng và đáng quan tâm nhất chính là method findTopRated(movies), các phần code còn lại chỉ là thủ tục để ta thực hiện việc trả về một AsyncJob, nó thực sự thừa thãi và không nên có trong main method của chúng ta. Hãy đưa nó ra ngoài để tránh gây chú ý khỏi main method.
Chúng ta cần 2 thứ ở operation này : AsyncJob trả về và function tìm kiếm. Tuy nhiên chúng ta không thể truyền function trực tiếp qua java mà phải truyền nó qua class (hoặc interface) vì vậy cần một định nghĩa về function
public interface Func<T, R> { R call(T t); }
Func interface gồm 2 member, T là agurment truyền vào và R là kết quả trả về Vì chúng ta biến đổi kết quả của một AsyncJob, nghĩa là chúng ta thực hiện mapping giữa các values. Hãy gọi function này là map và nơi đặt function này tốt nhất chính là trong AsyncJob
public abstract class AsyncJob<T> { public abstract void start(Callback<T> callback); public <R> AsyncJob<R> map(final Func<T, R> func) { final AsyncJob<T> source = this; return new AsyncJob<R>() { @Override public void start(final Callback<R> callback) { source.start(new Callback<T>() { @Override public void onResult(T result) { R mapped = func.call(result); callback.onResult(mapped); } @Override public void onError(Exception e) { callback.onError(e); } }); } }; } }
Lúc này MovieHelper sẽ như sau
public class MovieHelper { ApiWrapper apiWrapper; public AsyncJob<Uri> saveTopRatedMovie() { final AsyncJob<List<Movie>> moviesAsyncJob = apiWrapper.getMovies(); final AsyncJob<Movie> topRatedMovieAsyncJob = moviesAsyncJob.map( new Func<List<Movie>, Movie>() { @Override public Movie call(List<Movie> movies) { return findTopRated(movies); } }); AsyncJob<Uri> storeMovieAsyncJob = new AsyncJob<Uri>() { @Override public void start(final Callback<Uri> callback) { topRatedMovieAsyncJob.start(new Callback<Movie>() { @Override public void onResult(Movie movie) { apiWrapper.store(movie) .start(new Callback<Uri>() { @Override public void onResult(Uri result) { callback.onResult(result); } @Override public void onError(Exception e) { callback.onError(e); } }); } @Override public void onError(Exception e) { } }); } }; return storeMovieAsyncJob; } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Advanced Mapping
Hãy thử tiếp tục xử lí đoạn code storeMovieAsyncJob
public AsyncJob<Uri> saveTopRatedMovie() { final AsyncJob<List<Movie>> moviesAsyncJob = apiWrapper.getMovies(); final AsyncJob<Movie> topRatedMovieAsyncJob = moviesAsyncJob.map( new Func<List<Movie>, Movie>() { @Override public Movie call(List<Movie> movies) { return findTopRated(movies); } }); AsyncJob<Uri> storeMovieAsyncJob = topRatedMovieAsyncJob.map( new Func<Movie, Uri>() { @Override public Uri call(Movie movie) { return apiWrapper.store(movie); // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ will not compile // Incompatible types: // Required: Uri // Found: AsyncJob<Uri> } } ); return storeMovieAsyncJob; }
Có vấn đề về kiểu trả về, hãy fix lại một lần nữa
public AsyncJob<Uri> saveTopRatedMovie() { final AsyncJob<List<Movie>> moviesAsyncJob = apiWrapper.getMovies(); final AsyncJob``<Movie> topRatedMovieAsyncJob = moviesAsyncJob.map( new Func<List<Movie>, Movie>() { @Override public Movie call(List<Movie> movies) { return findTopRated(movies); } }); AsyncJob<AsyncJob<Uri>> storeMovieAsyncJob = topRatedMovieAsyncJob.map( new Func<Movie, AsyncJob<Uri>>() { @Override public AsyncJob<Uri> call(Movie movie) { return apiWrapper.store(movie); } } ); return storeMovieAsyncJob; //^^^^^^^^^^^^^^^^^^^^^^^ will not compile // Incompatible types: // Required: AsyncJob<Uri> // Found: AsyncJob<AsyncJob<Uri>> }
Method cần trả về AsynJob<AsyncJob<Uri>>, chúng ta không nên đào sâu hơn mà cần flat lại AsyncJob từ 2 async operations thành một Cái chúng ta cần bây giờ mà có một method sẽ lấy function xử lí và trả về không chỉ R mà là AsyncJob<R>. Method này sẽ hoạt động như map nhưng ngoài ra sẽ flatten AsyncJob. Hãy gọi method này là flatMap. Ta có như sau:
public abstract class AsyncJob<T> { public abstract void start(Callback<T> callback); public <R> AsyncJob<R> map(final Func<T, R> func) { final AsyncJob<T> source = this; return new AsyncJob<R>() { @Override public void start(final Callback<R> callback) { source.start(new Callback<T>() { @Override public void onResult(T result) { R mapped = func.call(result); callback.onResult(mapped); } @Override public void onError(Exception e) { callback.onError(e); } }); } }; } public <R> AsyncJob<R> flatMap(final Func<T, AsyncJob<R>> func) { final AsyncJob<T> source = this; return new AsyncJob<R>() { @Override public void start(final Callback<R> callback) { source.start(new Callback<T>() { @Override public void onResult(T result) { AsyncJob<R> mapped = func.call(result); mapped.start(new Callback<R>() { @Override public void onResult(R result) { callback.onResult(result); } @Override public void onError(Exception e) { callback.onError(e); } }); } @Override public void onError(Exception e) { callback.onError(e); } }); } }; } }
Lúc này MovieHelper của chúng ta sẽ được fix như sau
public AsyncJob<Uri> saveTopRatedMovie() { final AsyncJob<List<Movie>> moviesAsyncJob = apiWrapper.getMovies(); final AsyncJob<Movie> topRatedMovieAsyncJob = moviesAsyncJob.map( new Func<List<Movie>, Movie>() { @Override public Movie call(List<Movie> movies) { return findTopRated(movies); } }); AsyncJob<Uri> storeMovieAsyncJob = topRatedMovieAsyncJob.flatMap( new Func<Movie, AsyncJob<Uri>>() { @Override public AsyncJob<Uri> call(Movie movie) { return apiWrapper.store(movie); } } ); return storeMovieAsyncJob; }
Final Point
Hãy nhìn lại đoạn code và bạn có thấy quen không ?. Bây giờ minh sẽ thay các class anonymous thành lamda java8
public class MovieHelper { ApiWrapper apiWrapper; public AsyncJob<Uri> saveTopRatedMovie() { AsyncJob<List<Movie>> moviesAsyncJob = apiWrapper.getMovies(); AsyncJob<Movie> topRatedMovieAsyncJob = moviesAsyncJob.map(movies -> findTopRated(movies)); AsyncJob<Uri> storeMovieAsyncJob = topRatedMovieAsyncJob.flatMap(movie -> apiWrapper.store(movie)); return storeMovieAsyncJob; } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Lúc này hẳn các bạn đã nhận ra, đoạn code này không khác là sao so với đoạn code ban đầu chúng ta viết
public class MovieHelper { Api api; public Uri saveTopRatedMovie() { List<Movie> movies = api.getMovies(); Movie movie = findTopRated(movies); Uri savedUri = api.store(movie); return savedUri; } private Movie findTopRated(List<Movie> movies) { return Collections.max(movies); } }
Chính xác là như vậy, từ logic đến ngữ nghĩa, mọi thứ đều không khác so với method ban đầu và hãy quay lại các ưu điểm của cách tiếp cận ban đầu - Composition : Ta đã kết hợp các Async operations và giả về duy nhất một kiểu - Error propagation : Tất cả error đều đã có callback tiếp nhận Và cuối cùng xin giới thiệu với các bạn : RxJava Tuy nhiên những gì trên đây chỉ là một phần rất nhỏ về RxJava, ngoài vấn đề về async operation, RxJava còn xử lí thread safe và rất nhiều vấn đề khác. - AsyncJob<T> thực chất chính là Observable<T> và nó có thể gửi không chỉ một mà tất cả kết quả của operation - Callback<T> chính là Observer<T> trong đó ngoài việc nhận kết quả và gửi về lỗi (onNext(T t), onError(Throwable e). Ngoài ra còn có onCompleted() để thông báo rằng Observavle kết thúc việc gửi, nhận item, onSubcribe(Disposable d) để cancel request... - Abstract void start (Callback<T> callback) tương ứng với Disposable dispose(final Observer<? super T> observer) trả về Disposable để kết thục việc nhận kết quả ( hoặc cancel http request) khi chúng ta không cần nữa. - map và flatMap là 2 operation hữu dụng trong Observables
Hy vọng bài viết này sẽ giúp các bạn mường tượng được một chút tư tưởng của RxJava, những ưu điểm của nó mang lại trong việc xử lí các task không đồng bộ. Tất nhiên để hiểu được rõ RxJava là việc không thể một sớm một chiều là thành