Scope
There are many ways of writing android applications especially for simpler cases, but how they compare to each other is a question of android architecture whose answer distinguishes developers. A beginner in android development might be able to build an application that meets the same functionality as that built by an experienced developer. It is obvious they are different, but in what ways?
There must be some principles for comparing android codes upon which most seasoned developers agree. This both minimises opinionated views and sheds light on the path towards writing better apps.
I believe the most important and starting point of writing any application is the choice of android architecture. Prior to 2016 there was not much talk about it and Google did not prioritise any architecture over another, at least explicitly, even though the question was always there and discussed in communities. In late 2016, Florina wrote some quality series on android architectures (part 1, part 2 and part 3) followed by contributions to the architecture blueprints here. Google I/O 2017 was the first time android architecture components were publicly announced to accentuate the need for architecture in android apps (here).
In spite of the above, I think there are still developers who don’t acknowledge the significance of architecture, its choice, implementation and pros and cons in real projects… those calling network apis in the view or … 😉 This is why this series of posts is more of a practical approach and involves implementing a complete working sample app in all conventional android architectures:
- no-architecture
- MVC
- MVP
- MVVM
The intent is to compare and draw the advantages and disadvantages of each.
The Clean Architecture of Uncle Bob also can be applied to either MVP or MVVP in android by following some rules, which I will describe at the end of the series.
The sample app I am building here for benchmarking the architectures above and discussing them is a simple movie app showing the list of movies including a search term. Clicking on any movie would then show some info about it in another page.
The GitHub repository for the samples included in this series is here.
Are you ready? … oh wait,…
Before starting the journey, here is a heads-up on the gear you need to pack with you before getting on-board:
- Kotlin: I understand you might not have much knowledge of it handy and it is not a must for android development, but Kotlin saves a lot of boilerplate in the code, making it much more readable and is now an official language of android.
- RxJava/RxAndroid: Again, one can get a grasp of android architecture without it, but for MVVM there needs to be some tool for raising events inside the view model and listening to them in the view. One solution to this is EventBus , which I am not a fan of in the presence of reactive programming, i.e RxJava/RxAndroid. Of Google’s architectural components, LiveData is introduced as an alternative to RxJava/RxAndroid for MVVM. I prefer RxJava to that though, and will use it herein. I might add another post on LiveData and why I prefer RxJava to it in the future.
- Dagger 2: Best tool for implementing Dependency Inversion Principle (DIP) in android– if you don’t have any idea about DIP, this post might help. DIP is not necessary for comparing the architectures above, and I have not used it here for simplicity. However, since some DIP tool is required for implementing the Clean Architecture, I have used it there.
Buckled up? Let’s go.
Android architecture for apps
App architecture is all about identifying the main components of the app, describing their relations and getting the big picture of the app to address both functional and non-functional requirements. This definition is general and applicable to apps of any operating system.
The common component in apps of any OS is the one responsible for showing the graphical user interface to the users. In android architecture, the Activity is the component rendering the UI. So, in any android app architecture the view must be either an extension of Activity or its subcomponent, i.e. Fragment.
The most basic android app in terms of architecture would therefore be the one handling everything inside the view layer, which is the Activity or Fragment. The “no-architecture” term I used in the previous section pertains to this case, because having the view layer is inevitable in every app.
1- “no-architecture”
As the name suggests, there is no layering here, the whole app consists of two Activities: MainActivity for showing the search results and DetailActivity for the details of the selected movie. The complete code for this case is in the “no-architecture” branch of the GitHub repo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
class MainActivity : AppCompatActivity() { object Constants { const val RATING = "rating" const val TITLE = "title" const val YEAR = "year" const val DATE = "date" } private val compositeDisposable: CompositeDisposable = CompositeDisposable() private lateinit var addressAdapter: AddressAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) addressAdapter = AddressAdapter(onClick = { item -> var bundle = Bundle() bundle.putString(RATING, item.rating) bundle.putString(TITLE, item.title) bundle.putString(YEAR, item.year) bundle.putString(DATE, item.date) var intent = Intent(this, DetailActivity::class.java) intent.putExtras(bundle) startActivity(intent) }) main_activity_recyclerView.adapter = addressAdapter respondToClicks() } private fun respondToClicks() { main_activity_button.setOnClickListener({ findAddress(main_activity_editText.text.toString()) }) } override fun onResume() { super.onResume() hideProgressBar() } private fun findAddress(address: String) { val disposable: Disposable = fetchAddress(address)!!.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeWith(object : DisposableObserver<List<ResultEntity>?>() { override fun onNext(t: List<ResultEntity>) { hideProgressBar() updateRecyclerView(t) } override fun onStart() { showProgressBar() } override fun onComplete() { } override fun onError(e: Throwable) { main_activity_progress_bar.visibility = View.GONE Toast.makeText(this@MainActivity, "Error retrieving data: ${e.message}", Toast.LENGTH_SHORT).show() } }) compositeDisposable.add(disposable) } private fun showProgressBar() { main_activity_progress_bar.visibility = View.VISIBLE } private fun hideProgressBar() { main_activity_progress_bar.visibility = View.GONE } override fun onStop() { super.onStop() compositeDisposable.clear() } private fun updateRecyclerView(t: List<ResultEntity>) { addressAdapter.updateList(t) addressAdapter.notifyDataSetChanged() } class AddressAdapter(val onClick: (item: ResultEntity) -> Unit) : RecyclerView.Adapter<AddressAdapter.Holder>() { var mList: List<ResultEntity> = arrayListOf() override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder { val view = LayoutInflater.from(parent!!.context).inflate(R.layout.item, parent, false) return Holder(view) } override fun onBindViewHolder(holder: Holder, position: Int) { holder.itemView.item_textView.text = "${mList[position].year}: ${mList[position].title}" holder.itemView.setOnClickListener { onClick(mList[position]) } } override fun getItemCount(): Int { return mList.size } fun updateList(list: List<ResultEntity>) { mList = list } class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) } private var mRetrofit: Retrofit? = null private fun fetchAddress(address: String): Observable<List<ResultEntity>>? { if (mRetrofit == null) { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build() mRetrofit = Retrofit.Builder().baseUrl("http://bechdeltest.com/api/v1/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(client).build() } return mRetrofit?.create(AddressService::class.java)?.fetchLocationFromServer(address) } interface AddressService { @GET("getMoviesByTitle") fun fetchLocationFromServer(@Query("title") title: String): Observable<List<ResultEntity>> } class ResultEntity(val title: String, val rating: String, val date: String, val year: String) } |
What’s wrong with the “no-architecture”?
Short practical answer: Neither testable, maintainable or scalable.
Short fundamental answer: Not meeting SOLID principles
Detailed answer:
Testability: Suppose you want to test if the correct method is called when the user clicks on search, or you want to make sure the progress bar is shown until the result is returned from the network. Neither of this is possible here, because for this to happen you need to mock some part in order to test the other part. Since there is only one part here (MainActivity), mocking is not possible and so is the testing.
Maintainability: Let’s say you need to change the endpoint for the network search or you’re asked to show some animation after the results have been loaded. For these and any other change request, all the changes need to be made to the view, i.e. MainActivity, even if the issue of maintenance is with the business logic. This renders maintainability very difficult if not impossible in the long run.
Scalability: Same as maintainability, any feature to be added to the app has something to do with the view, making it impractical at some stage.
Not meeting SOLID principles: It breaks the Open/Close principle for the MainActivity class having many responsibilities rather than one. Also, the code is not open for extension and closed for modification.
In the next part, I am going to restructure the same app in MVC and show its strengths in improving the shortcomings listed above as well as its weaknesses.