Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Reusable Behaviors in Kotlin (Sydney Android Me...

Reusable Behaviors in Kotlin (Sydney Android Meetup, April 2018)

Architecture patterns like MVP help decouple business logic from UX, but used alone they don't solve problems like code duplication and isolation.

In this talk we will learn some techniques to split your code into isolated behaviors and how to compose them with the interface delegation pattern provided natively by Kotlin.

Video: https://www.youtube.com/watch?v=EU0MbpPePcY

Dario Marcato

April 17, 2018
Tweet

Other Decks in Programming

Transcript

  1. MVP Architecture • Each “page” contains UI elements and has

    business logic • Presenter to handle business logic • Contract describe interaction between UI <-> Presenter Contract Presenter
  2. Example: Login Page interface LoginContract { // Producers val loginClicks:

    Observable<Unit> // Consumers val render: Consumer<ViewState> } class LoginPresenter { fun bind(contract: LoginContract, disp: CompositeDisposable) { disp += contract.loginClicks .switchMap { } //TODO do some login stuff... .bind(contract.render) } }
  3. Adding a Loading UX • We need a way to

    tell the user that the app is waiting for data • Contract should describe how to toggle this UI on/off • Presenter should decide when to trigger the Contract
  4. interface LoginContract { // Producers val loginClicks: Observable<Unit> // Consumers

    val render: Consumer<ViewState> } class LoginPresenter { fun bind(contract: LoginContract, disp: CompositeDisposable) { disp += contract.loginClicks .switchMap { } //TODO do some login stuff... .bind(contract.render) } }
  5. interface LoginContract { // Producers val loginClicks: Observable<Unit> // Side

    effects fun showLoading() fun hideLoading() // Consumers val render: Consumer<ViewState> } class LoginPresenter { fun bind(contract: LoginContract, disp: CompositeDisposable) { disp += contract.loginClicks .switchMap { } //TODO do some login stuff... .bind(contract.render) } } How
  6. interface LoginContract { // Producers val loginClicks: Observable<Unit> // Side

    effects fun showLoading() fun hideLoading() // Consumers val render: Consumer<ViewState> } class LoginPresenter { fun bind(contract: LoginContract, disp: CompositeDisposable) { disp += contract.loginClicks .doOnNext { contract.showLoading() } .switchMap { } //TODO do some login stuff... .doOnNext { contract.hideLoading() } .bind(contract.render) } } When
  7. interface LoginContract { // Producers val loginClicks: Observable<Unit> // Side

    effects fun showLoading() fun hideLoading() // Consumers val render: Consumer<ViewState> } class LoginPresenter { fun bind(contract: LoginContract, disp: CompositeDisposable) { disp += contract.loginClicks .doOnNext { contract.showLoading() } .switchMap { } //TODO do some login stuff... .doOnNext { contract.hideLoading() } .bind(contract.render) } }
  8. Reusability of Loading UX • Show/hide loading is a common

    business logic operation • We want to have a consistent UX around the app • …but sometimes we also want a “customized” experience
  9. Reusability of Loading UX • Think about behavior -> Interface

    • Similar to AOP (Aspect Oriented Programming) • Can we achieve that in Kotlin without using a framework? Interface delegation to the rescue!
  10. Behavior of Loading UX • Identify common logic • Extract

    it in a independent Contract Interface • Ability to customize the how, not the when Contract Presenter
  11. Behavior of Loading UX • Identify common logic • Extract

    it in a independent Contract Interface • Ability to customize the how, not the when Contract Presenter Loading Contract
  12. interface LoginContract { // Producers val loginClicks: Observable<Unit> // Side

    effects fun showLoading() fun hideLoading() // Consumers val render: Consumer<ViewState> }
  13. interface LoginContract { // Producers val loginClicks: Observable<Unit> // Side

    effects fun showLoading() fun hideLoading() // Consumers val render: Consumer<ViewState> }
  14. interface LoadingContract { // Side effects fun showLoading() fun hideLoading()

    } interface LoginContract { // Producers val loginClicks: Observable<Unit> // Consumers val render: Consumer<ViewState> }
  15. interface LoadingContract { // Side effects fun showLoading() fun hideLoading()

    } interface LoginContract : LoadingContract { // Producers val loginClicks: Observable<Unit> // Consumers val render: Consumer<ViewState> }
  16. Implementation of Loading UX • Create generic implementations of LoadingContract

    • Compose them with interface delegation pattern Contract Presenter Loading Contract Dialog Loading Contract
  17. class LoginContractImpl(private val root: View) : LoginContract { private var

    loadingDialog: Dialog? = null override val loginClicks = root.findViewById<View>(R.id.button).clicks() override fun showLoading() { loadingDialog = ProgressDialog(root.context).apply { isIndeterminate = true show() } } override fun hideLoading() { loadingDialog?.dismiss() loadingDialog = null } override val render: Consumer<ViewState> = //TODO render some data }
  18. class LoginContractImpl(private val root: View) : LoginContract { private var

    loadingDialog: Dialog? = null override val loginClicks = root.findViewById<View>(R.id.button).clicks() override fun showLoading() { loadingDialog = ProgressDialog(root.context).apply { isIndeterminate = true show() } } override fun hideLoading() { loadingDialog?.dismiss() loadingDialog = null } override val render: Consumer<ViewState> = //TODO render some data }
  19. class DialogLoadingContract(private val context: Context) : LoadingContract { private var

    loadingDialog: Dialog? = null override fun showLoading() { loadingDialog = ProgressDialog(context).apply { isIndeterminate = true show() } } override fun hideLoading() { loadingDialog?.dismiss() loadingDialog = null } } class LoginContractImpl(private val root: View) : LoginContract { override val loginClicks = root.findViewById<View>(R.id.button).clicks() override val render: Consumer<ViewState> = //TODO render some data }
  20. class DialogLoadingContract(private val context: Context) : LoadingContract { private var

    loadingDialog: Dialog? = null override fun showLoading() { loadingDialog = ProgressDialog(context).apply { isIndeterminate = true show() } } override fun hideLoading() { loadingDialog?.dismiss() loadingDialog = null } } class LoginContractImpl(private val root: View) : LoginContract, LoadingContract by DialogLoadingContract(root.context) { override val loginClicks = root.findViewById<View>(R.id.button).clicks() override val render: Consumer<ViewState> = //TODO render some data } Interface delegation
  21. Customization of Loading UX • Different implementations of LoadingContract (how)

    • Business logic stays the same (when) • Consistency, reusability & customization Contract Presenter Loading Contract Dialog Loading Contract Inline Loading Contract
  22. class InlineLoadingContract( private val viewToHide: View, private val progressView: View

    ) : LoadingContract { override fun showLoading() { viewToHide.visibility = View.GONE progressView.visibility = View.VISIBLE } override fun hideLoading() { viewToHide.visibility = View.VISIBLE progressView.visibility = View.GONE } } class DialogLoadingContract( private val context: Context ) : LoadingContract { private var loadingDialog: Dialog? = null override fun showLoading() { loadingDialog = ProgressDialog(context).apply { isIndeterminate = true show() } } override fun hideLoading() { loadingDialog?.dismiss() loadingDialog = null } }
  23. Trivia Time! interface Shape { val sides: Int fun printSides():

    String } class Square : Shape { override val sides: Int = 4 override fun printSides(): String = "Sides: $sides" } class Pentagon : Shape by Square() { override val sides: Int = 5 } fun main(args: Array<String>) { val shape = Pentagon() println(shape.printSides()) } “Sides: 4”
  24. “Downsides” • Don’t mix inheritance and composition • Interface delegation

    is not inheritance • Overriding methods can be misleading • Can’t use super to access delegated instance
  25. Upsides • It’s not inheritance • Higher flexibility, less coupling

    • Focus on designing the behavior, not the hierarchy tree • Easier to accommodate future changes • Less prone to bugs coming from dependencies in the tree
  26. Interface Delegation • Composition over inheritance • Focus on component’s

    behavior • Reusable code decoupled from feature code • Consistent implementation without sacrificing customization https://kotlinlang.org/docs/reference/delegation.html
  27. Bonus: Generics Upper Bounds • Kotlin supports multiple upper bounds

    for generics • Methods can receive “pieces” of the Contract • Modular dependencies
  28. interface LoadingContract { fun showLoading() fun hideLoading() } interface ErrorContract

    { fun showError(error: Exception) } fun <C> processData(contract: C) { contract.showLoading() try { //TODO very important business logic } catch (e: Exception) { contract.showError(e) } finally { contract.hideLoading() } } interface LoginContract : LoadingContract, ErrorContract interface SignUpContract : LoadingContract, ErrorContract How do we define the type of C?
  29. interface LoadingContract { fun showLoading() fun hideLoading() } interface ErrorContract

    { fun showError(error: Exception) } fun <C> processData(contract: C) where C : LoadingContract, C : ErrorContract { contract.showLoading() try { //TODO very important business logic } catch (e: Exception) { contract.showError(e) } finally { contract.hideLoading() } } interface LoginContract : LoadingContract, ErrorContract interface SignUpContract : LoadingContract, ErrorContract
  30. Bonus: Generics Upper Bounds • Declare parameters by describing their

    behaviors • Decouple utility methods from specific implementations • Type safe