monolithic architecture to a clean architecture with RxJava based on Fernando Cejas proposal for a single use-case. This presentation assumes you have used retrofit in the past but haven’t tried RxJava or the MVP (model-view-presenter) pattern yet. Keep in mind that the solutions I will present here are based on my experiences and my failed attempts to create a better and scalable architecture.
this topic but very few are easy to comprehend for beginners. I will, too, quote several paragraphs from different blog posts that were key for me to understand these concepts but I highly encourage you to read them entirely. Therefore, all the credits go to them and everyone else responsible to move the android community forward.
do the following: 1. Create an activity 2. Do an http request to get the detail of one post. (/posts/1) { "userId": 1, "id": 1, "title": “this is a title", "body": “this is a detailed message of the post…” }
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mApi = ApiProvider.getApi().create(PostApi.class); loadPost(1); //consider this in onStart } private void loadPost(final int postId){ mApi.getPostById(postId, new Callback<Post>() { @Override public void success(Post post, Response response) { //TODO bind the content of the post to the layout here Toast.makeText(MainActivity.this,post.getBody(),Toast.LENGTH_SHORT).show(); loadComments(postId); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } private void loadComments(int postId){ mApi.getPostComments(postId, new Callback<List<Comment>>() { @Override public void success(List<Comment> commentList, Response response) { //TODO bind the content of the contents to the layout here Toast.makeText(MainActivity.this,"total comments="+commentList.size(),Toast.LENGTH_SHORT).show(); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } } HomeActivity Retrofit
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mApi = ApiProvider.getApi().create(PostApi.class); loadPost(1); //consider this in onStart } private void loadPost(final int postId){ mApi.getPostById(postId, new Callback<Post>() { @Override public void success(Post post, Response response) { //TODO bind the content of the post to the layout here Toast.makeText(MainActivity.this,post.getBody(),Toast.LENGTH_SHORT).show(); loadComments(postId); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } private void loadComments(int postId){ mApi.getPostComments(postId, new Callback<List<Comment>>() { @Override public void success(List<Comment> commentList, Response response) { //TODO bind the content of the contents to the layout here Toast.makeText(MainActivity.this,"total comments="+commentList.size(),Toast.LENGTH_SHORT).show(); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } } HomeActivity Retrofit
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mApi = ApiProvider.getApi().create(PostApi.class); loadPost(1); //consider this in onStart } private void loadPost(final int postId){ mApi.getPostById(postId, new Callback<Post>() { @Override public void success(Post post, Response response) { //TODO bind the content of the post to the layout here Toast.makeText(MainActivity.this,post.getBody(),Toast.LENGTH_SHORT).show(); loadComments(postId); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } private void loadComments(int postId){ mApi.getPostComments(postId, new Callback<List<Comment>>() { @Override public void success(List<Comment> commentList, Response response) { //TODO bind the content of the contents to the layout here Toast.makeText(MainActivity.this,"total comments="+commentList.size(),Toast.LENGTH_SHORT).show(); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } } HomeActivity Retrofit
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mApi = ApiProvider.getApi().create(PostApi.class); loadPost(1); //consider this in onStart } private void loadPost(final int postId){ mApi.getPostById(postId, new Callback<Post>() { @Override public void success(Post post, Response response) { //TODO bind the content of the post to the layout here Toast.makeText(MainActivity.this,post.getBody(),Toast.LENGTH_SHORT).show(); loadComments(postId); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } private void loadComments(int postId){ mApi.getPostComments(postId, new Callback<List<Comment>>() { @Override public void success(List<Comment> commentList, Response response) { //TODO bind the content of the comments to the layout here Toast.makeText(MainActivity.this,"total comments="+commentList.size(),Toast.LENGTH_SHORT).show(); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } } HomeActivity Retrofit
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mApi = ApiProvider.getApi().create(PostApi.class); loadPost(1); //consider this in onStart } private void loadPost(final int postId){ mApi.getPostById(postId, new Callback<Post>() { @Override public void success(Post post, Response response) { //TODO bind the content of the post to the layout here Toast.makeText(MainActivity.this,post.getBody(),Toast.LENGTH_SHORT).show(); loadComments(postId); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } private void loadComments(int postId){ mApi.getPostComments(postId, new Callback<List<Comment>>() { @Override public void success(List<Comment> commentList, Response response) { //TODO bind the content of the contents to the layout here Toast.makeText(MainActivity.this,"total comments="+commentList.size(),Toast.LENGTH_SHORT).show(); } @Override public void failure(RetrofitError error) { //TODO handle error } }); } } HomeActivity Retrofit
use-cases. • Activities contain a lot of application logic (and sometimes the logic is repeated across activities). • It is nearly impossible to test each feature separately (http request, UI views behaviour, etc.) • Very error prone. • Doesn’t scale very well.
to read, since they become decoupled from the logic. • However the logic is now implemented in the presenter. • The presenter is still very coupled to the implementation of concrete data sources. • Easier to test the UI but still hard to test the logic of the application. • The domain model objects are the ones instantiated by gson (inside retrofit) where field names must match with the json response (if you don’t use annotations).
and Subscribers. An Observable emits items; a Subscriber consumes those items. There is a pattern to how items are emitted. An Observable may emit any number of items (including zero items), then it terminates either by successfully completing, or due to an error. For each Subscriber it has, an Observable calls Subscriber.onNext() any number of times, followed by either Subscriber.onComplete() or Subscriber.onError(). This looks a lot like your standard observer pattern, but it differs in one key way - Observables often don't start emitting items until someone explicitly subscribes to them.” source: danlew.net
and Subscribers. An Observable emits items; a Subscriber consumes those items. There is a pattern to how items are emitted. An Observable may emit any number of items (including zero items), then it terminates either by successfully completing, or due to an error. For each Subscriber it has, an Observable calls Subscriber.onNext() any number of times, followed by either Subscriber.onComplete() or Subscriber.onError(). This looks a lot like your standard observer pattern, but it differs in one key way - Observables often don't start emitting items until someone explicitly subscribes to them.” source: danlew.net
parameter: @GET("/posts/{postId}") void getPostById(@Path("postId") int postId, Callback<Post> callback); we change the return type to Observable<Post> @GET("/posts/{postId}") Observable<Post> getPostById(@Path("postId") int postId);
call(Post post) { return mApi.getCommentsByPostId(post.getId()); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Comment>>() { @Override public void onCompleted() {} @Override public void onError(Throwable e) {} @Override public void onNext(List<Comment> comments) { } }); Notice how in the same chain, we are performing 2 HTTP requests.
call(Post post) { return mApi.getCommentsByPostId(post.getId()); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Comment>>() { @Override public void onCompleted() {} @Override public void onError(Throwable e) {} @Override public void onNext(List<Comment> comments) { } }); Notice how in the same chain, we are performing 2 HTTP requests.
call(Post post) { return mApi.getCommentsByPostId(post.getId()); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Comment>>() { @Override public void onCompleted() {} @Override public void onError(Throwable e) {} @Override public void onNext(List<Comment> comments) { } }); Notice how in the same chain, we are performing 2 HTTP requests.
call(Post post) { return mApi.getCommentsByPostId(post.getId()); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Comment>>() { @Override public void onCompleted() {} @Override public void onError(Throwable e) {} @Override public void onNext(List<Comment> comments) { } }); Notice how in the same chain, we are performing 2 HTTP requests.
call(Post post) { return mApi.getCommentsByPostId(post.getId()); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Comment>>() { @Override public void onCompleted() {} @Override public void onError(Throwable e) {} @Override public void onNext(List<Comment> comments) { } }); Notice how in the same chain, we are performing 2 HTTP requests.
• RxAndroid adds a specific scheduler for the UI thread; 2. Contains operators to transform, filter and convert multiple sets of data; 3. Handles errors in a clean and organised way;
operators. For a more detailed explanation about all the Rx operators, please check http://rxmarbles.com/ as well as all the other websites in the resources of this presentation.
the solution 1 with a small but important difference. The data sources are now accessed through an interface. • Each presenter now becomes abstracted of the concrete data sources. • The model (of MVP) is an object that contains the business logic and data sources. • RxJava was used in the presenters and in the models.
to read, since they become decoupled from the logic (just like solution 1). • The presenter is decoupled from the implementation of concrete data sources but still contains fair amounts of application logic. • Easier to test the UI and the data sources. • Still hard to test the logic of the application. Logic may be repeated across presenters.
is not an architectural pattern, it’s only responsible for the presentation layer.” - Antonio Leiva • “You want to separate business logic from user interface (UI) logic to make the code easier to understand and maintain.” - MSDN, MVP objectives • Martin fowler has a blog post only about GUI architectures - http://martinfowler.com/eaaDev/uiArchs.html
model app Retrofit SQLite remote model data local model PostLocalRepository PostRemoteRepository PostRepository PostService domain model domain IPostRepository
model Retrofit app SQLite remote model data local model PostLocalRepository PostRemoteRepository PostRepository domain model domain PostService IPostRepository
data local model Android Phone Module Java Library Android Library PostLocalRepository PostRemoteRepository PostRepository PostService domain model domain IPostRepository
data local model Android Phone Module Java Library Android Library Module Dependencies PostLocalRepository PostRemoteRepository PostRepository PostService domain model domain IPostRepository
mRepository; public PostService(IPostRepository mRepository) { this.mRepository = mRepository; } @Override public Observable<Post> getPostById(int postId) { return mRepository.getPostById(postId); } @Override public Observable<List<Comment>> getPostComments(int postId) { return mRepository.getPostComments(postId); } } Each service could use different repositories, if needed. HomeActivity HomePresenter IHomeView Retrofit PostService PostRepository PostRemoteRepository
IPostRepository mRemote; //Ex. retrofit IPostRepository mLocal; //Ex. sqlite public PostRepository(IPostRepository mRemote, IPostRepository mLocal) { this.mRemote = mRemote; this.mLocal = mLocal; } @Override public Observable<Post> getPostById(int postId) { return mRemote.getPostById(postId); } @Override public Observable<List<Comment>> getPostComments(int postId) { return mRemote.getPostComments(postId); } } Each repository should be able to sync data between the local and the remote databases. For demonstration purposes, we will use the remote database only. HomeActivity HomePresenter IHomeView Retrofit PostService PostRepository PostRemoteRepository
test the data sources, the UI and each domain service. • There is a concrete object model for each layer. • Great for implementing dependency injection mechanisms (ex: dagger) • Easier to scale and maintain. • Implementing new features can take longer than the other solutions but it isn’t more complex.
fits your needs and the problem you are trying to solve. • Different apps have different needs. • Some could use several different api’s, others may not need a local database, etc. • The syncing algorithm (online vs offline) also varies a lot between companies and products. • There is no silver bullet. • It’s always about trade-offs.