Introduction
In part 3 of my series on architectural patterns in android, I wrote about MVP along with a sample android app and showed how using it can lead to more scalable, maintainable and extendable code.
There are two things about MVP I am not a fan of:
- larger number of lines of boilerplate code compared to other architectural patterns: The following table compares the number of lines of code for a benchmark app written with no architecture, MVC and MVP
- handling lifecycle-dependent operations like cancelling network connection when onStop is called, or disposing the observables in RxJava has to be done manually
The above motivated me to write a very handy MVP library (called ‘easy-mvp’) for implementing the MVP architectural pattern in android.
In this post, I am going to show in simple steps how you can write the exact same sample movie app of part 3 with absolutely no boilerplate code using ‘easy-mvp’ library. The library code is on GitHub for anyone wanting to dig into details.
Objective
Build a movie app in MVP architecture, which shows the user a list of movies containing a search keyword. Check here for more details and screenshots of the app. The full code is in the ‘mvp-easymvp’ branch of my Github repo.
Implementation Steps
Step 1
Add this to your app build.gradle :
1 |
implementation 'com.digigene.android:easy-mvp:+' |
Step 2
Create your View, e.g. MainView , by extending the BaseViewImpl< > from the library. Inside the < > put the name of your Presenter, say MainPresenter :
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 |
class MainView : BaseViewImpl<MainPresenter>() { private lateinit var addressAdapter: AddressAdapter override fun getFragmentLayout(): Int { return R.layout.main_fragment_layout } override fun setListeners() { main_fragment_button.setOnClickListener({ mPresenter.findAddress(main_fragment_editText.text.toString()) }) addressAdapter setItemClickMethod { mPresenter.doWhenItemIsClicked(it) } addressAdapter.setItemShowMethod { mPresenter fetchItemTextFrom it } } override fun introduceViewElements() { addressAdapter = AddressAdapter() main_fragment_recyclerView.adapter = addressAdapter } fun showProgressBar() { main_fragment_progress_bar.visibility = View.VISIBLE } fun hideProgressBar() { main_fragment_progress_bar.visibility = View.GONE } fun updateMovieList(t: List<MainModel.ResultEntity>) { addressAdapter.updateList(t) addressAdapter.notifyDataSetChanged() } fun showErrorMessage(s: String) { Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() } infix fun goToActivity(item: MainModel.ResultEntity) { var bundle = Bundle() bundle.putString(DetailActivity.Constants.RATING, item.rating) bundle.putString(DetailActivity.Constants.TITLE, item.title) bundle.putString(DetailActivity.Constants.YEAR, item.year) bundle.putString(DetailActivity.Constants.DATE, item.date) var intent = Intent(activity, DetailActivity::class.java) intent.putExtras(bundle) startActivity(intent) } class AddressAdapter : RecyclerView.Adapter<AddressAdapter.Holder>() { var mList: List<MainModel.ResultEntity> = arrayListOf() private lateinit var mOnClick: (item: MainModel.ResultEntity) -> Unit private lateinit var mOnShowItem: (item: MainModel.ResultEntity) -> String 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 = mOnShowItem(mList[position]) holder.itemView.setOnClickListener { mOnClick(mList[position]) } } override fun getItemCount(): Int { return mList.size } infix fun setItemClickMethod(onClick: (item: MainModel.ResultEntity) -> Unit) { this.mOnClick = onClick } infix fun setItemShowMethod(onShowItem: (item: MainModel.ResultEntity) -> String) { this.mOnShowItem = onShowItem } fun updateList(list: List<MainModel.ResultEntity>) { mList = list } class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) } } |
Step 3
Create your Presenter, MainPresenter in this example, by extending BasePresenterImpl<MainView,MainModel> , where MainModel is the class name for your Model :
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 |
class MainPresenter : BasePresenterImpl<MainView>() { private val mainModel: MainModel = MainModel() fun findAddress(address: String) { val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() { override fun onSuccess(t: List<MainModel.ResultEntity>) { mView.hideProgressBar() mView.updateMovieList(t) } override fun onStart() { mView.showProgressBar() } override fun onError(e: Throwable) { mView.hideProgressBar() mView.showErrorMessage("Error retrieving data: ${e.message}") } }) compositeDisposable.add(disposable) } infix fun fetchItemTextFrom(it: MainModel.ResultEntity): String { return "${it.year}: ${it.title}" } fun doWhenItemIsClicked(it: MainModel.ResultEntity) { mView.goToActivity(it) } } |
Step 4:
Create your Model, e.g. MainModel :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MainModel { private var mRetrofit: Retrofit? = null fun fetchAddress(address: String): Single<List<MainModel.ResultEntity>>? { return getRetrofit()?.create(MainModel.AddressService::class.java)?.fetchLocationFromServer(address) } private fun getRetrofit(): Retrofit? { 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 } class ResultEntity(val title: String, val rating: String, val date: String, val year: String) interface AddressService { @GET("getMoviesByTitle") fun fetchLocationFromServer(@Query("title") title: String): Single<List<ResultEntity>> } } |
Step 5
Finally, create an Activity, namely MainActivity , and introduce the MainView in it as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class MainActivity : AppCompatActivity() { private lateinit var mMainView: MainView private lateinit var mMainPresenter: MainPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) mMainPresenter = MainPresenter() mMainView = MainView() mMainView.mPresenter = mMainPresenter fragmentManager.beginTransaction().add(R.id.main_activity_holder, mMainView).commit() } } |
where main_activity is a layout holding a ViewGroup for populating the View’s UI elements:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.digigene.android.moviefinder.MainActivity"> <android.support.constraint.ConstraintLayout android:id="@+id/main_activity_holder" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> </android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout> |
Conclusion
Using ‘easymvp’ for the sample movie app renders considerable reduction in the number of lines of code, from 238 to 217:
Cancelling network requests and RxJava observables when the app stops showing in the foreground is also very easy by adding them to the mCompositeDisposable in the presenter. The library then automatically handles the lifecycle and cancels the network request and disposes the observable once the app goes to the background, so no memory leak will happen in that respect.