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

Application Architecture for Droids

Application Architecture for Droids

Exploring MVP architecture patterns for Android in the Star Wars universe.

Avatar for Josh Kovach

Josh Kovach

May 17, 2017
Tweet

More Decks by Josh Kovach

Other Decks in Programming

Transcript

  1. @shekibobo #IOextendedGR MODEL ‣ Network API - Client, Data Model

    ‣ Persistence API - Database, Preferences, System Services ‣ Domain Model - Business Logic, Rules, Interactions
  2. @shekibobo #IOextendedGR VIEW ‣ Update UI from information passed by

    presenter ‣ Route user interactions to the presenter
  3. @shekibobo #IOextendedGR PRESENTER ‣ Routing between view and model layer

    ‣ Listen for predefined actions from the view ‣ Route actions to the appropriate model ‣ Publish new information to the view
  4. @shekibobo #IOextendedGR class LightSaberMVP { } interface View { }

    fun showHilt(hiltImageUrl: String) fun showBlade(color: Int, length: Int) fun hideBlade() interface Presenter { } fun attach() fun detach() fun activate() fun deactivate() class LightSaberPresenter: LightSaberMVP.Presenter interface LightSaberPresenterImpl: LightSaberPresenter
  5. @shekibobo #IOextendedGR class LightSaberPresenter( val view: View, val lightsaber: LightSaber

    ) : LightSaberMVP.Presenter { override fun attach() { this.view.showHilt(lightsaber.imageUrl) } override fun detach() { deactivate() } override fun activate() { lightsaber.activate() view.showBlade(lightsaber.color, lightsaber.length) } override fun deactivate() { lightsaber.deactivate() view.hideBlade() } }
  6. @shekibobo #IOextendedGR class LightSaberMVP { } interface View { }

    fun showHilt(hiltImageUrl: String) fun showBlade(color: Int, length: Int) fun hideBlade() interface Presenter { } fun attach() fun detach() fun activate() fun deactivate()
  7. @shekibobo #IOextendedGR class LightSaberActivity : AppCompatActivity(), LightSaberMVP.View { @Inject lateinit

    var lightsaberPresenter: LightSaberPresenter @BindView(R.id.activator) lateinit var activator: Switch @BindView(R.id.bladeView) lateinit var bladeView: ImageView @BindView(R.id.hiltView) lateinit var hiltView: ImageView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_lightsaber) ButterKnife.bind(this) } @OnCheckedChanged(R.id.activator) fun activatorChanged(isChecked: Boolean) { if (isChecked) lightsaberPresenter.activate() else lightsaberPresenter.deactivate() } override fun onResume() { super.onResume() lightsaberPresenter.attach() } override fun onPause() { lightsaberPresenter.detach() super.onPause() } override fun showHilt(hiltImageUrl: String) { hiltView.loadFromUrl(hiltImageUrl) } override fun showBlade(color: Int, length: Int) { bladeView.setBackgroundColor(color) bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = length } } override fun hideBlade() { bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = 0 } } }
  8. @shekibobo #IOextendedGR class LightSaberActivity : AppCompatActivity(), LightSaberMVP.View { @Inject lateinit

    var lightsaberPresenter: LightSaberPresenter @BindView(R.id.activator) lateinit var activator: Switch @BindView(R.id.bladeView) lateinit var bladeView: ImageView @BindView(R.id.hiltView) lateinit var hiltView: ImageView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_lightsaber) ButterKnife.bind(this) } @OnCheckedChanged(R.id.activator) fun activatorChanged(isChecked: Boolean) { if (isChecked) lightsaberPresenter.activate() else lightsaberPresenter.deactivate() }
  9. @shekibobo #IOextendedGR class LightSaberActivity : AppCompatActivity(), LightSaberMVP.View { @Inject lateinit

    var lightsaberPresenter: LightSaberPresenter @BindView(R.id.activator) lateinit var activator: Switch @BindView(R.id.bladeView) lateinit var bladeView: ImageView @BindView(R.id.hiltView) lateinit var hiltView: ImageView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_lightsaber) ButterKnife.bind(this) } @OnCheckedChanged(R.id.activator) fun activatorChanged(isChecked: Boolean) { if (isChecked) lightsaberPresenter.activate() else lightsaberPresenter.deactivate() } override fun onResume() { super.onResume() lightsaberPresenter.attach()
  10. @shekibobo #IOextendedGR @OnCheckedChanged(R.id.activator) fun activatorChanged(isChecked: Boolean) { if (isChecked) lightsaberPresenter.activate()

    else lightsaberPresenter.deactivate() } override fun onResume() { super.onResume() lightsaberPresenter.attach() } override fun onPause() { lightsaberPresenter.detach() super.onPause() } override fun showHilt(hiltImageUrl: String) { hiltView.loadFromUrl(hiltImageUrl) }
  11. override fun onPause() { lightsaberPresenter.detach() super.onPause() } override fun showHilt(hiltImageUrl:

    String) { hiltView.loadFromUrl(hiltImageUrl) } override fun showBlade(color: Int, length: Int) { bladeView.setBackgroundColor(color) bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = length } } override fun hideBlade() { bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = 0 } } } @shekibobo #IOextendedGR
  12. class LightSaberActivity : AppCompatActivity(), LightSaberMVP.View { @Inject lateinit var lightsaberPresenter:

    LightSaberPresenter @BindView(R.id.activator) lateinit var activator: Switch @BindView(R.id.bladeView) lateinit var bladeView: ImageView @BindView(R.id.hiltView) lateinit var hiltView: ImageView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_lightsaber) ButterKnife.bind(this) } @OnCheckedChanged(R.id.activator) fun activatorChanged(isChecked: Boolean) { if (isChecked) lightsaberPresenter.activate() else lightsaberPresenter.deactivate() } override fun onResume() { super.onResume() lightsaberPresenter.attach() } override fun onPause() { lightsaberPresenter.detach() super.onPause() } override fun showHilt(hiltImageUrl: String) { hiltView.loadFromUrl(hiltImageUrl) } override fun showBlade(color: Int, length: Int) { bladeView.setBackgroundColor(color) bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = length } } override fun hideBlade() { bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = 0 } } } @shekibobo #IOextendedGR MAP USER INPUTS TO PRESENTER ACTIONS ONLY DOES VIEW STUFF ZERO BUSINESS LOGIC
  13. @shekibobo #IOextendedGR @Test @Throws(Exception ::class) fun attach_displaysLightsaberHilt() { val view

    = mock<LightSaberMVP.View>() val presenter = LightSaberPresenter(view, lightsaber) presenter.attach() verify(view).showHilt(lightsaber.imageUrl) } @Test @Throws(Exception ::class) fun activate_tellsViewToShowBlade() { val view = mock<LightSaberMVP.View>() val presenter = LightSaberPresenter(view, lightsaber).apply { attach() } presenter.activate() verify(view).showBlade(Color.RED, 800) } @Test @Throws(Exception ::class) fun deactivate_tellsViewToHideBlade() { val view = mock<LightSaberMVP.View>() val presenter = LightSaberPresenter(view, lightsaber).apply { attach() activate() } presenter.deactivate() verify(view).hideBlade() }
  14. @shekibobo #IOextendedGR @Test fun onAttach_togglesLoadingWhileFetchingClaimsFromServer() { val view = mock<ClaimsPresenter.View>()

    val api = mock<DonutsApi>() whenever(api.getTodayClaims()).thenReturn(Observable.just(listOf())) val presenter = ClaimsPresenter(view, api) presenter.onAttach() val inOrder = inOrder(view, api) inOrder.verify(view).showLoading(true) inOrder.verify(api).getTodayClaims() inOrder.verify(view).showLoading(false) inOrder.verify(view).showNoUsers() } https://collectiveidea.com/blog
  15. @shekibobo #IOextendedGR @Test @Throws(Exception ::class) fun attach_displaysLightsaberHilt() { val view

    = mock<LightSaberMVP.View>() val presenter = LightSaberPresenter(view, lightsaber) presenter.attach() verify(view).showHilt(lightsaber.imageUrl) } @Test @Throws(Exception ::class) fun activate_tellsViewToShowBlade() { val view = mock<LightSaberMVP.View>() val presenter = LightSaberPresenter(view, lightsaber).apply { attach() } presenter.activate() verify(view).showBlade(Color.RED, 800) } @Test @Throws(Exception ::class) fun deactivate_tellsViewToHideBlade() { val view = mock<LightSaberMVP.View>() val presenter = LightSaberPresenter(view, lightsaber).apply { attach() activate() } presenter.deactivate() verify(view).hideBlade() }
  16. @shekibobo #IOextendedGR @Test @Throws(Exception ::class) fun attach_displaysLightsaberHilt() { val view

    = mock<LightSaberMVP.View>() val api = mock<LightSaberAPI>() val presenter = LightSaberPresenter(view, lightsaber, api) presenter.attach() verify(view).showHilt(lightsaber.imageUrl) } @Test @Throws(Exception ::class) fun activate_tellsViewToShowBlade() { val view = mock<LightSaberMVP.View>() val api = mock<LightSaberAPI>() val presenter = LightSaberPresenter(view, lightsaber, api).apply { attach() } presenter.activate() verify(view).showBlade(Color.RED, 800) } @Test @Throws(Exception ::class) fun deactivate_tellsViewToHideBlade() { val view = mock<LightSaberMVP.View>() val api = mock<LightSaberAPI>() val presenter = LightSaberPresenter(view, lightsaber, api).apply { attach() activate() } presenter.deactivate() verify(view).hideBlade() }
  17. @shekibobo #IOextendedGR class LightSaberPresenter( val view: View, val lightsaber: LightSaber

    ) : LightSaberMVP.Presenter { override fun attach() { this.view.showHilt(lightsaber.imageUrl) } override fun detach() { deactivate() } override fun activate() { lightsaber.activate() view.showBlade(lightsaber.color, lightsaber.length) } override fun deactivate() { lightsaber.deactivate() view.hideBlade() } }
  18. @shekibobo #IOextendedGR class LightSaberPresenter( val view: View, val lightsaber: LightSaber,

    val lightsaberAPI: LightSaberAPI ) : LightSaberMVP.Presenter { override fun attach() { this.view.showHilt(lightsaber.imageUrl) } override fun detach() { deactivate() } override fun activate() { lightsaberAPI.activate(lightsaber) .subscribe { view.showBlade(lightsaber.color, lightsaber.length) } } override fun deactivate() { lightsaberAPI.deactivate(lightsaber) .subscribe { view.hideBlade() } } }
  19. @shekibobo #IOextendedGR class LightSaberActivity : AppCompatActivity(), LightSaberMVP.View { @Inject lateinit

    var lightsaberPresenter: LightSaberPresenter @BindView(R.id.activator) lateinit var activator: Switch @BindView(R.id.bladeView) lateinit var bladeView: ImageView @BindView(R.id.hiltView) lateinit var hiltView: ImageView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_lightsaber) ButterKnife.bind(this) } @OnCheckedChanged(R.id.activator) fun activatorChanged(isChecked: Boolean) { if (isChecked) lightsaberPresenter.activate() else lightsaberPresenter.deactivate() } override fun onResume() { super.onResume() lightsaberPresenter.attach() } override fun onPause() { lightsaberPresenter.detach() super.onPause() } override fun showHilt(hiltImageUrl: String) { hiltView.loadFromUrl(hiltImageUrl) } override fun showBlade(color: Int, length: Int) { bladeView.setBackgroundColor(color) bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = length } } override fun hideBlade() { bladeView.layoutParams = ViewGroup.LayoutParams(bladeView.layoutParams).apply { this.height = 0 } } }
  20. @shekibobo #IOextendedGR Implementation Details ‣ There are no standards. ‣

    Common patterns with varying conventions. ‣ There is no One True Way™* * yet
  21. interface Presenter { } fun onCreate() fun onResume() fun onPause()

    fun onDestroy() fun onActivated() fun onDeactivated() @shekibobo #IOextendedGR Implementation Details interface Presenter { } fun attach() fun detach() fun activate() fun deactivate() interface Presenter { } fun onAttached() fun onDetached() fun onActivated() fun onDeactivated()
  22. @shekibobo #IOextendedGR Implementation Details - Lifecycle interface Presenter { }

    fun attach() fun detach() fun activate() fun deactivate() interface Presenter { } fun attach(view: View) fun detach(cache: Bool = false) fun activate() fun deactivate()
  23. @shekibobo #IOextendedGR Implementation Details - Lifecycle interface Presenter { }

    fun attach() fun detach() fun activate() fun deactivate() ‣ Usage: ‣ Presenter owned by View. ‣ Update view as soon as it's attached. ‣ Detach view to cancel or cache.
  24. @shekibobo #IOextendedGR Implementation Details - Lifecycle interface Presenter { }

    fun attach() fun detach() fun activate() fun deactivate() ‣ Benefits: ‣ View is @Nonnull. ‣ Fewer paths to test. ‣ Does not need to track state. ‣ Downsides: ‣ Long-running processes are lost.
  25. @shekibobo #IOextendedGR Implementation Details - Lifecycle interface Presenter { }

    fun attach() fun detach() fun activate() fun deactivate() interface Presenter { } fun attach(view: View) fun detach(cache: Bool = false) fun activate() fun deactivate()
  26. @shekibobo #IOextendedGR Implementation Details - Lifecycle interface Presenter { }

    fun attach(view: View) fun detach(cache: Bool = false) fun activate() fun deactivate() ‣ Usage: ‣ Presenter created without view. ‣ Weak reference to view bound in Activity.onCreate(). ‣ Persisted in PresenterCache when detached.
  27. @shekibobo #IOextendedGR Implementation Details - Lifecycle interface Presenter { }

    fun attach(view: View) fun detach(cache: Bool = false) fun activate() fun deactivate() ‣ Benefits: ‣ Lifecycle independence. ‣ Can be cached in @ApplicationScope PresenterCache. ‣ Downsides: ‣ Presenter owns state instead of model. ‣ View is @Nullable. ‣ Extra testing steps and scenarios. ‣ Attach has important side-effects that complicate testing.
  28. @shekibobo #IOextendedGR Implementation Details - Model ‣ Smarter model. ‣

    Manages in-flight requests outside Activity. ‣ Presenter subscribes to updates from repository. REPOSITORY
  29. @shekibobo #IOextendedGR Implementation Details - View ‣ Usage: ‣ showThing(thing:

    Thing) ‣ showError(throwable: Error) ‣ showLoading(isLoading: Bool, progress: Float) ‣ navigateTo(resource: T) interface View { } fun showHilt(hiltImageUrl: String) fun showBlade(color: Int, length: Int) fun hideBlade()
  30. @shekibobo #IOextendedGR RESOURCES Artwork Ralph McQuarrie Mark Molnar
 Lucasfilm Ltd

    Charles Woods
 swmand4
 Further Reading Presenters are Not for Persisting
 https://hackernoon.com/presenters-are-not-for-persisting-f537a2cc7962 
 Model-View-Presenter: Android Guidelines
 https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-94970b430ddf 
 Clean Architecture with Kotlin + RxJava + Dagger 2
 https://medium.com/uptech-team/clean-architecture-in-android-with-kotlin-rxjava-dagger-2-2fdc7441edfc