I got an android project I\'m beginning to work on, and I want its structure to be as robust as possible.
I\'m coming from a WPF MVVM background and I\'ve been reading a
First of all, Android doesn't force you to use any architecture. Not only that but it also makes it somewhat difficult to try to follow to any. This will require you to be a smart developer in order to avoid creating a spaghetti codebase :)
You can try to fit in any pattern you know and you like. I find that the best approach will in some way get into your guts as you develop more and more applications (sorry about that but as always, you'll have to make lots of mistakes until you start doing it right).
About the patterns you know, let me do something wrong: I'll mix three different patterns so you get the feeling of what does what in android. I believe the Presenter/ModelView should be somewhere in the Fragment or Activity. Adapters might sometimes do this job as they take care of inputs in lists. Probably Activities should work like Controllers too. Models should be regular java files whereas the View should lay in layout resources and some custom components you might have to implement.
I can give you some tips. This is a community wiki answer so hopefully other people might include other suggestions.
I think there are mainly two sensible possibilities:
Personally, I have only been involved in projects using the first approach but I really would like to try the later as I believe it could make things more organized. I see no advantage in having a folder with 30 unrelated files but that's what I get with the first approach.
So, all strings, styles, ids used in the context of "ViewPost" should start be "@id/view_post_heading" (for a textview for example), "@style/view_post_heading_style", "@string/view_post_greeting".
This will optimize autocomplete, organization, avoid name colision, etc.
I think you'll want to use base classes for pretty much everything you do: Adapters, Activities, Fragments, Services, etc. These might be useful at least for debugging purposes so you know which events are happening in all your activity.
I think it would be more helpful to explain MVVM in android through an example. The complete article together with the GitHub repo info is here for more info.
Let’s suppose the same benchmark movie app example introduced in the first part of this series. User enters a search term for a movie and presses the ‘FIND’ button, based on which the app searches for the list of movies including that search term and shows them. Clicking on each movie on the list then shows its details.
I will now explain how this app is implemented in MVVM followed by the complete android app, which is available on my GitHub page.
When the user clicks on the ‘FIND’ button on the View, a method is called from the ViewModel with the search term as its argument:
main_activity_button.setOnClickListener({
showProgressBar()
mMainViewModel.findAddress(main_activity_editText.text.toString())
})
The ViewModel then calls the findAddress
method from the Model to search for the movie name:
fun findAddress(address: String) {
val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
override fun onSuccess(t: List<MainModel.ResultEntity>) {
entityList = t
resultListObservable.onNext(fetchItemTextFrom(t))
}
override fun onError(e: Throwable) {
resultListErrorObservable.onNext(e as HttpException)
}
})
compositeDisposable.add(disposable)
}
When the response comes from the Model, the onSuccess method of the RxJava observer carries the successful result, but as the ViewModel is View agnostic, it does not have or use any View instance to pass the result for showing. It instead triggers an event in the resultListObservable by calling resultListObservable.onNext(fetchItemTextFrom(t)) , which is observed by the View:
mMainViewModel.resultListObservable.subscribe({
hideProgressBar()
updateMovieList(it)
})
So the observable plays a mediator role between the View and ViewModel:
Here’s the full code for the View. In this example, View is an Activity class, but Fragment can also be equally used:
class MainActivity : AppCompatActivity() {
private lateinit var mMainViewModel: MainViewModel
private lateinit var addressAdapter: AddressAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mMainViewModel = MainViewModel(MainModel())
loadView()
respondToClicks()
listenToObservables()
}
private fun listenToObservables() {
mMainViewModel.itemObservable.subscribe(Consumer { goToDetailActivity(it) })
mMainViewModel.resultListObservable.subscribe(Consumer {
hideProgressBar()
updateMovieList(it)
})
mMainViewModel.resultListErrorObservable.subscribe(Consumer {
hideProgressBar()
showErrorMessage(it.message())
})
}
private fun loadView() {
setContentView(R.layout.activity_main)
addressAdapter = AddressAdapter()
main_activity_recyclerView.adapter = addressAdapter
}
private fun respondToClicks() {
main_activity_button.setOnClickListener({
showProgressBar()
mMainViewModel.findAddress(main_activity_editText.text.toString())
})
addressAdapter setItemClickMethod {
mMainViewModel.doOnItemClick(it)
}
}
fun showProgressBar() {
main_activity_progress_bar.visibility = View.VISIBLE
}
fun hideProgressBar() {
main_activity_progress_bar.visibility = View.GONE
}
fun showErrorMessage(errorMsg: String) {
Toast.makeText(this, "Error retrieving data: $errorMsg", Toast.LENGTH_SHORT).show()
}
override fun onStop() {
super.onStop()
mMainViewModel.cancelNetworkConnections()
}
fun updateMovieList(t: List<String>) {
addressAdapter.updateList(t)
addressAdapter.notifyDataSetChanged()
}
fun goToDetailActivity(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(this, DetailActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
class AddressAdapter : RecyclerView.Adapter<AddressAdapter.Holder>() {
var mList: List<String> = arrayListOf()
private lateinit var mOnClick: (position: Int) -> Unit
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]
holder.itemView.setOnClickListener { mOnClick(position) }
}
override fun getItemCount(): Int {
return mList.size
}
infix fun setItemClickMethod(onClick: (position: Int) -> Unit) {
this.mOnClick = onClick
}
fun updateList(list: List<String>) {
mList = list
}
class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView)
}
}
Here is the ViewModel:
class MainViewModel() {
lateinit var resultListObservable: PublishSubject<List<String>>
lateinit var resultListErrorObservable: PublishSubject<HttpException>
lateinit var itemObservable: PublishSubject<MainModel.ResultEntity>
private lateinit var entityList: List<MainModel.ResultEntity>
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
private lateinit var mainModel: MainModel
private val schedulersWrapper = SchedulersWrapper()
constructor(mMainModel: MainModel) : this() {
mainModel = mMainModel
resultListObservable = PublishSubject.create()
resultListErrorObservable = PublishSubject.create()
itemObservable = PublishSubject.create()
}
fun findAddress(address: String) {
val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
override fun onSuccess(t: List<MainModel.ResultEntity>) {
entityList = t
resultListObservable.onNext(fetchItemTextFrom(t))
}
override fun onError(e: Throwable) {
resultListErrorObservable.onNext(e as HttpException)
}
})
compositeDisposable.add(disposable)
}
fun cancelNetworkConnections() {
compositeDisposable.clear()
}
private fun fetchItemTextFrom(it: List<MainModel.ResultEntity>): ArrayList<String> {
val li = arrayListOf<String>()
for (resultEntity in it) {
li.add("${resultEntity.year}: ${resultEntity.title}")
}
return li
}
fun doOnItemClick(position: Int) {
itemObservable.onNext(entityList[position])
}
}
and finally the Model:
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>>
}
}
Full article here