Disclaimer This presentation focuses on how I evolved from a 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.
Disclaimer There are many great blog posts and talks about 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.
Use Case Using the fake public api http://jsonplaceholder.typicode.com/ we will 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…” }
Monolithic architecture • The whole application is implemented in a single app module. • It’s usually fast to get things working. • Activities are responsible to get the data they need as well as presenting it.
public class MainActivity extends AppCompatActivity {
private PostApi mApi;
@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() { @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); }
private void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List 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(); }
public class MainActivity extends AppCompatActivity {
private PostApi mApi;
@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() { @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); }
private void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List 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(); }
public class MainActivity extends AppCompatActivity {
private PostApi mApi;
@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() { @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); }
private void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List 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(); }
public class MainActivity extends AppCompatActivity {
private PostApi mApi;
@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() { @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); }
private void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List 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(); }
public class MainActivity extends AppCompatActivity {
private PostApi mApi;
@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() { @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); }
private void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List 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(); }
Monolithic architecture • Can get very messy to implement complex 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.
private int userId; private int id; private String title; private String body;
public int getUserId() { return userId; } public int getId() { return id; } public String getTitle() { return title; } public String getBody() { return body; }
protected Post(Parcel in) { userId = in.readInt(); id = in.readInt(); title = in.readString(); body = in.readString(); } … domain model
public void loadPost(final int postId){ mApi.getPostById(postId, new Callback() { public void success(Post post, Response response) { mView.onPostLoaded(post); }
@Override public void failure(RetrofitError error) { mView.onError(error.getMessage()); } }); }
public void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List commentList, Response response) { mView.onCommentsLoaded(commentList); }
public void loadPost(final int postId){ mApi.getPostById(postId, new Callback() { public void success(Post post, Response response) { mView.onPostLoaded(post); }
@Override public void failure(RetrofitError error) { mView.onError(error.getMessage()); } }); }
public void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List commentList, Response response) { mView.onCommentsLoaded(commentList); }
public void loadPost(final int postId){ mApi.getPostById(postId, new Callback() { public void success(Post post, Response response) { mView.onPostLoaded(post); }
@Override public void failure(RetrofitError error) { mView.onError(error.getMessage()); } }); }
public void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List commentList, Response response) { mView.onCommentsLoaded(commentList); }
public void loadPost(final int postId){ mApi.getPostById(postId, new Callback() { public void success(Post post, Response response) { mView.onPostLoaded(post); }
@Override public void failure(RetrofitError error) { mView.onError(error.getMessage()); } }); }
public void loadComments(int postId){ mApi.getPostComments(postId, new Callback>() { @Override public void success(List commentList, Response response) { mView.onCommentsLoaded(commentList); }
public void loadPost(final int postId){ mApi.getPostById(postId, new Callback() { public void success(Post post, Response response) { mView.onPostLoaded(post); }
Solution 1 review • Activities become much lighter and easier 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).
RxJava “The basic building blocks of reactive code are Observables 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
RxJava “The basic building blocks of reactive code are Observables 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
Retrofit with RxJava Instead of adding a Callback as a parameter: @GET("/posts/{postId}") void getPostById(@Path("postId") int postId, Callback callback); we change the return type to Observable @GET("/posts/{postId}") Observable getPostById(@Path("postId") int postId);
RxJava niceties 1. Helps a lot with its threading API. • RxAndroid adds a specific scheduler for the UI thread; 2. Contains operators to transform, filter and convert multiple sets of data;
RxJava niceties 1. Helps a lot with its threading API. • 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;
RxJava Besides transformations and concatenations, RxJava provides many other useful 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.
MVP architecture 2 HomeActivity HomePresenter IHomeView Business Logic User Interface Data Sources domain model Retrofit app SQLite IPostsModel IPostsModel
Solution 2 flow • The flow is the same as 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.
Solution 2 review • Activities become much lighter and easier 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.
MVP architecture • “First thing to clarify is that MVP 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
HomeActivity HomePresenter IHomeView Business Logic User Interface Data Sources view model app data Data Sources PostService domain model domain IPostRepository
HomeActivity HomePresenter IHomeView Business Logic User Interface Data Sources view model app Retrofit SQLite remote model data local model PostLocalRepository PostRemoteRepository PostRepository PostService domain model domain IPostRepository
HomeActivity HomePresenter IHomeView Business Logic User Interface Data Sources view model Retrofit app SQLite remote model data local model PostLocalRepository PostRemoteRepository PostRepository domain model domain PostService IPostRepository
HomeActivity HomePresenter IHomeView view model Retrofit app SQLite remote model data local model Android Phone Module Java Library Android Library PostLocalRepository PostRemoteRepository PostRepository PostService domain model domain IPostRepository
HomeActivity HomePresenter IHomeView view model Retrofit app SQLite remote model data local model Android Phone Module Java Library Android Library Module Dependencies PostLocalRepository PostRemoteRepository PostRepository PostService domain model domain IPostRepository
Solution 3 flow public class PostService implements IPostService{
IPostRepository mRepository;
public PostService(IPostRepository mRepository) { this.mRepository = mRepository; }
@Override public Observable getPostById(int postId) { return mRepository.getPostById(postId); }
@Override public Observable> getPostComments(int postId) { return mRepository.getPostComments(postId); } } Each service could use different repositories, if needed. HomeActivity HomePresenter IHomeView Retrofit PostService PostRepository PostRemoteRepository
@Override public Observable getPostById(int postId) { return mRemote.getPostById(postId); }
@Override public Observable> 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
Clean architecture • Every layer is decoupled. • Easier to 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.
Take-away • Design and implement the architecture the way it 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.