Model-View-Presenter
An Architectural Pattern with the following parts:
- Model: Manages data. Respobile for APIs, caching data, and managaing databases.
- Presenter: Middle-man between Model and View, contains all presentaion logic. Reacts to user interactions, using and updating the Model and the View.
- View: (Activity/Fragment) presents the data and fowards user interaction events to the Presenter.
class MainPresenter(val view: MainView, val repository: MainRepository) {
fun onViewCreated() {
}
fun onRefresh() {
}
}
interface MainView {
var refresh: Boolean
fun show(items: List<ITEM>)
fun showError(error: Throwable)
}
class MainPresenter(val view: MainView, val repository: MainRepository) {
fun onViewCreated() {
loadCharacters()
}
fun onRefresh() {
loadCharacters()
}
private fun loadCharacters() {
repository.getAllCharacters()
.subscribeOn(Schedulars.io()) // doesn't block main thread
.observeOn(AndroidSchedulars.mainThread()) //callback on main thread
.subscribe({ items ->
view.show(items)
})
}
}
This can be extrected to
//RxExt.kt
fun <T> Single<T>.applySchedulers(): Single<T> = this
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
//MainPresenter.kt
repository.getAllCharacters()
.applySchedulers()
.subscribe({ items -> view.show(items) })
to prevent memory leaks we can keep all subscriptions in composite
private var subscriptions = CompositeDisposable()
fun onViewDestroyed() {
subscriptions.dispose()
}
we can move this to the presenter interface for reuse
interface Presenter {
fun onViewDestroyed()
}
abstract class BasePresenter : Presenter {
protected var subscriptions = CompositeDisposable()
override fun onViewDestroyed() {
subscriptions.dispose()
}
}
BaseActivityWithPresenter:
abstract class BaseActivityWithPresenter : AppCompatActivity() {
abstract val presenter: Presenter
override fun onDestroy() {
super.onDestroy()
presenter.onViewDestroyed()
}
}
to simply even more we can define a plus assign operator in Ext.kt:
fun <T> Single<T>.applySchedulers(): Single<T> = this
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
fun <T> Single<T>.subscribeBy(
onError: ((Throwable) -> Unit)? = null,
onSuccess: (T) -> Unit
): Disposable = subscribe(onSuccess, { onError?.invoke(it) })
operator fun CompositeDisposable.plusAssign(disposable: Disposable) {
add(disposable)
}
use it like:
class MainPresenter(
val view: MainView,
val repository: MainRepository
) : BasePresenter() {
fun onViewCreated() {
loadCharacters()
}
fun onRefresh() {
loadCharacters()
}
private fun loadCharacters() {
subscriptions += repository.getAllCharacters()
.applySchedulers()
.doOnSubscribe { view.refresh = true }
.doFinally { view.refresh = false }
.subscribeBy(
onSuccess = view::show,
onError = view::showError
)
}
can use RxKotlin instead.
class MainActivity : BaseActivityWithPresenter(), MainView { // 1
override var refresh by bindToSwipeRefresh(R.id.swipeRefreshView)
// 2
override val presenter by lazy
{ MainPresenter(this, MarvelRepository.get()) } // 3
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
recyclerView.layoutManager = GridLayoutManager(this, 2)
swipeRefreshView.setOnRefreshListener
{ presenter.onRefresh() } // 4
presenter.onViewCreated() // 4
}
override fun show(items: List<MarvelCharacter>) {
val categoryItemAdapters = items.map(::CharacterItemAdapter)
recyclerView.adapter = MainListAdapter(categoryItemAdapters)
}
override fun showError(error: Throwable) {
toast("Error: ${error.message}") // 2
error.printStackTrace()
}
}
ViewExt.kt
fun <T : View> RecyclerView.ViewHolder.bindView(viewId: Int)
= lazy { itemView.findViewById<T>(viewId) }
fun ImageView.loadImage(photoUrl: String) {
Glide.with(context)
.load(photoUrl)
.into(this)
}
fun Context.toast(text: String, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, text, length).show()
}
fun Activity.bindToSwipeRefresh(@IdRes swipeRefreshLayoutId: Int): ReadWriteProperty<Any?, Boolean>
= SwipeRefreshBinding(lazy { findViewById<SwipeRefreshLayout>(swipeRefreshLayoutId) })
private class SwipeRefreshBinding(lazyViewProvider: Lazy<SwipeRefreshLayout>) : ReadWriteProperty<Any?, Boolean> {
val view by lazyViewProvider
override fun getValue(thisRef: Any?,
property: KProperty<*>): Boolean {
return view.isRefreshing
}
override fun setValue(thisRef: Any?,
property: KProperty<*>, value: Boolean) {
view.isRefreshing = value
}
}