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 
   } 
}

results matching ""

    No results matching ""