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

Conductor: Architecture and Alternatives

Conductor: Architecture and Alternatives

In a world of Architecture Components and Jetpack, what place does a view-navigation library like Conductor have? I think quite a lot!

Links
-----
Conductor: https://github.com/bluelinelabs/Conductor
Managing State with RxJava by Jake Wharton: https://youtu.be/0IKHxjkgop4
Combining Conductor and Navigation: https://bit.ly/2MD9LSn
Combination repo: https://github.com/prolificinteractive/navigation-conductor
My website: https://chrishorner.codes

7b1e567c19126de48554fe8e5e767395?s=128

Chris Horner

June 27, 2018
Tweet

Transcript

  1. Conductor Architecture and Alternatives @chris_h_codes

  2. Navigation is hard

  3. Navigation is hard • Picking the difference between up and

    back
  4. Navigation is hard • Picking the difference between up and

    back • Sometimes up is… not up?
  5. Navigation is hard • Picking the difference between up and

    back • Sometimes up is… not up? • What happens when we deep link?
  6. Navigation is hard • Picking the difference between up and

    back • Sometimes up is… not up? • What happens when we deep link? • Bottom navigation gets involved
  7. Navigation is hard • Picking the difference between up and

    back • Sometimes up is… not up? • What happens when we deep link? • Bottom navigation gets involved
  8. Google’s making it worse “The bottom navigation bar remains in

    view when navigating through an app’s hierarchy.” - Google’s new design guidelines
  9. There’s a term for this

  10. None
  11. Single Activity apps

  12. Single Activity apps • Control over UI elements remaining on

    screen
  13. Single Activity apps • Control over UI elements remaining on

    screen • Easier shared element transitions
  14. Single Activity apps • Control over UI elements remaining on

    screen • Easier shared element transitions • Single and stable entry point for your UI
  15. Single Activity apps • Control over UI elements remaining on

    screen • Easier shared element transitions • Single and stable entry point for your UI
 - Kind of like main() with lifecycle callbacks
  16. Fragments…

  17. Fragment transactions

  18. Fragment transactions val newFragment = NextFragment() val transaction = getSupportFragmentManager().beginTransaction()

    transaction.replace(R.id.fragment_container, newFragment) transaction.addToBackStack(null) transaction.commit()
  19. Fragment transactions val newFragment = NextFragment() val transaction = getSupportFragmentManager().beginTransaction()

    transaction.replace(R.id.fragment_container, newFragment) transaction.addToBackStack(null) transaction.commitAllowingStateLoss()
  20. Fragment transactions val newFragment = NextFragment() val transaction = getSupportFragmentManager().beginTransaction()

    transaction.replace(R.id.fragment_container, newFragment) transaction.addToBackStack(null) transaction.commitNow()
  21. Fragment transactions val newFragment = NextFragment() val transaction = getSupportFragmentManager().beginTransaction()

    transaction.replace(R.id.fragment_container, newFragment) transaction.addToBackStack(null) transaction.commitNowAllowingStateLoss()
  22. Fragment lifecyle

  23. Fragment lifecyle

  24. Fragment lifecyle

  25. Fragment identity crisis

  26. Fragment identity crisis <fragment android:name="codes.chrishorner.fragments.Example" android:id="@+id/exampleFragment" android:layout_width="match_parent" android:layout_height="match_parent" />

  27. Fragment identity crisis class MyFragment : Fragment() { }}

  28. Fragment identity crisis class MyFragment : Fragment() { override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) }} }}
  29. Fragment identity crisis class MyFragment : Fragment() { @Inject lateinit

    var ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Do some dependency injection... }} }}
  30. Fragment identity crisis class MyFragment : Fragment() { @Inject lateinit

    var ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Do some dependency injection... }} override fun onStart() { super.onStart() // Use your injected dependencies to modify sub-views. }} }}
  31. Applying your MV-Whatever

  32. Applying your MV-Whatever • Maybe a Fragment is a presenter?

    A controller? A view?
  33. Applying your MV-Whatever • Maybe a Fragment is a presenter?

    A controller? A view? • Whatever it is, it does a lot!
  34. Applying your MV-Whatever • Maybe a Fragment is a presenter?

    A controller? A view? • Whatever it is, it does a lot! • Receive lifecycle events • Manage view hierarchies • Save instance state • Deal with non-configuration instance object passing • Handle a back stack
  35. So what’s Conductor? github.com/bluelinelabs/Conductor

  36. How Conductor works Fragment Controller

  37. How Conductor works Fragment Controller “Think of it as a

    lighter-weight and more predictable Fragment alternative with an easier to manage lifecycle.” - The Conductor documentation
  38. How Conductor works • Represents a screen; a node in

    your navigation graph • Must have a View; cannot be headless • Cannot be declared in XML • Survives orientation changes by default • Permits passing arguments in a constructor* abstract class Controller
  39. Declaring a Controller class MyController : Controller() { }}

  40. Declaring a Controller class MyController : Controller() { override fun

    onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout.my_layout, container, false) } }}
  41. Declaring a Controller class MyController : Controller() { override fun

    onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout.my_layout, container, false) }} override fun onAttach(view: View) { … } override fun onDetach(view: View) { … } }}
  42. Navigating Controllers

  43. Navigating Controllers class MainActivity : AppCompatActivity() { }}

  44. Navigating Controllers class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) } }} <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" />
  45. Navigating Controllers class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) }} }} <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" />
  46. Navigating Controllers class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) val container: ViewGroup = findViewById(R.id.container) val router = Conductor.attachRouter(this, container, savedInstanceState) }} }}
  47. Navigating Controllers class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main) val container: ViewGroup = findViewById(R.id.container) val router = Conductor.attachRouter(this, container, savedInstanceState) if (!router.hasRootController()) { router.setRoot(RouterTransaction.with(HomeController())) } }} }}
  48. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

    View) { }} }}
  49. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

    View) { view.events.ofType<NavigateNext>() }} }}
  50. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

    View) { view.events.ofType<NavigateNext>().subscribe { }} }} }}
  51. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

    View) { view.events.ofType<NavigateNext>().subscribe { val transaction = RouterTransaction.with(NextController()) }} }} }}
  52. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

    View) { view.events.ofType<NavigateNext>().subscribe { val transaction = RouterTransaction.with(NextController()) router.pushController(transaction) }} }} }}
  53. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

    View) { view.events.ofType<NavigateNext>().subscribe { val transaction = RouterTransaction.with(NextController()) .pushChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler()) router.pushController(transaction) }} }} }}
  54. Handling change

  55. Handling change • Swaps the View of one Controller out

    for another • …or not! You’re in control • Your opportunity to provide custom animations / transitions • Comes in a variety of built in flavours abstract class ControllerChangeHandler
  56. Handling change • Comes in a variety of built in

    flavours class FadeChangeHandler : AnimatorChangeHandler class HorizontalChangeHandler : AnimatorChangeHandler class VerticalChangeHandler : AnimatorChangeHandler
  57. Handling change abstract class AnimatorChangeHandler

  58. Handling change class FadeAndScaleChangeHandler : AnimatorChangeHandler() { }}

  59. Handling change class FadeAndScaleChangeHandler : AnimatorChangeHandler() { override fun getAnimator(container:

    ViewGroup, from: View?, to: View?, isPush: Boolean, toAddedToContainer: Boolean): Animator { } }}
  60. Handling change class FadeAndScaleChangeHandler : AnimatorChangeHandler() { override fun getAnimator(container:

    ViewGroup, from: View, to: View, isPush: Boolean, toAddedToContainer: Boolean): Animator { val set = AnimatorSet() set.playTogether( ObjectAnimator.ofFloat(to, View.SCALE_X, 0.9f, 1f), ObjectAnimator.ofFloat(to, View.SCALE_Y, 0.9f, 1f), ObjectAnimator.ofFloat(to, View.ALPHA, 0f, 1f)) return set
  61. set.playTogether( ObjectAnimator.ofFloat(to, View.SCALE_X, 0.9f, 1f), ObjectAnimator.ofFloat(to, View.SCALE_Y, 0.9f, 1f), ObjectAnimator.ofFloat(to,

    View.ALPHA, 0f, 1f)) return set Handling change
  62. Handling change abstract class TransitionChangeHandler

  63. Handling change class ListToDetailsChangeHandler : TransitionChangeHandler() { override fun getTransition(container:

    ViewGroup, from: View?, to: View?, isPush: Boolean): Transition { } }
  64. Serious customisation override fun getTransition(container: ViewGroup, from: View?, to: View?,

    isPush: Boolean): Transition { return TransitionSet() // Slide the Alarm content on screen. .addTransition(Slide() .addTarget(to.container) .setInterpolator(LinearOutSlowInInterpolator())) // Slide bottom nav bar off screen. .addTransition(BottomViewSlide() .addTarget(from.bottomNav) .setDuration(270) .setInterpolator(sharpCurveInterpolator()))
  65. Where do I bind my views?

  66. Where do I bind my views? • A Controller is

    not the V in your MV-Whatever • A good place to initialise the wiring between model and view • Don’t make a Controller do too much
  67. Fragments were never a good V in your MV-Whatever

  68. Activity

  69. Activity

  70. Fragment A

  71. Fragment A onCreate()

  72. Fragment A View A onCreate() onCreateView()

  73. Fragment A View A onCreate() onCreateView() onStart() onResume()

  74. Fragment A View A onCreate() onCreateView() onStart() onResume() Navigate!

  75. Fragment A View A onCreate() onCreateView() onStart() onResume() onPause() onStop()

  76. Fragment A onCreate() onCreateView() onStart() onResume() onPause() onStop() onDestroyView()

  77. Fragment A Fragment B

  78. Fragment A Fragment B onCreate()

  79. Fragment A Fragment B onCreate() View B onCreateView() onStart() onResume()

  80. Fragment A Fragment B onCreate() View B onCreateView() onStart() onResume()

    Go back!
  81. Fragment A Fragment B onCreate() onCreateView() onStart() onResume() onPause() onStop()

    onDestroyView()
  82. Fragment A

  83. Fragment A View A onCreateView()

  84. Fragment A View A onCreateView() onCreate()

  85. Fragment A View A

  86. A pattern I don’t mind

  87. A pattern I don’t mind View Controller

  88. A pattern I don’t mind View Controller State

  89. A pattern I don’t mind View Controller State Event

  90. A pattern I don’t mind View Controller Observable<Event> Observable<State>

  91. A pattern I don’t mind View Controller Observable<Event> Observable<State>

  92. sealed class State { object Idle : State() object Loading

    : State() data class Error(val message: String) : State() data class Success(val items: List<String>) : State() }
  93. fun display(state: State) { when (state) { is Idle ->

    is Loading -> is Error -> is Success -> }} }}
  94. class HomeView(...) : FrameLayout(...) { fun display(state: State) { when

    (state) { is Idle -> is Loading -> is Error -> is Success -> }} }} }
  95. class HomeView(...) : FrameLayout(...) { fun display(state: State) { <codes.chrishorner.example.HomeView

    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Sub views go here> </codes.chrishorner.example.HomeView>
  96. class HomeView(...) : FrameLayout(...) { fun display(state: State) { when

    (state) { is Idle -> is Loading -> is Error -> is Success -> }} }} }}
  97. class HomeView(context: Context, ...) : FrameLayout(...) { fun display(state: State)

    { when (state) { is Idle -> is Loading -> is Error -> is Success -> }} }} }}
  98. class HomeView(context: Context, ...) : FrameLayout(...) { // RxBinding helps

    a lot here val events: Observable<Event> = ... fun display(state: State) { when (state) { is Idle -> is Loading -> is Error -> is Success -> }} }} }}
  99. class HomeController : Controller() { }}

  100. class HomeController : Controller() { override fun onAttach(view: View) {

    }} override fun onDetach(view: View) { }} }}
  101. class HomeController : Controller() { override fun onAttach(view: View) {

    states.subscribe { view.display(it) }
 view.events.subscribe { .. } }} override fun onDetach(view: View) { }} }}
  102. class HomeController : Controller() { private val disposables = CompositeDisposable()

    override fun onAttach(view: View) { disposables += states.subscribe { view.display(it) } disposables += view.events.subscribe { .. } }} override fun onDetach(view: View) { }} }}
  103. class HomeController : Controller() { private val disposables = CompositeDisposable()

    override fun onAttach(view: View) { disposables += states.subscribe { view.display(it) } disposables += view.events.subscribe { .. } }} override fun onDetach(view: View) { disposables.clear() }} }}
  104. class HomeController : Controller() { private val disposables = CompositeDisposable()

    override fun onAttach(view: View) { disposables += states.subscribe { view.display(it) } disposables += view.events.subscribe { .. } }} override fun onDetach(view: View) { disposables.clear() }} }}
  105. class HomeController : Controller() { private val disposables = CompositeDisposable()

    override fun onAttach(view: View) { if (view !is HomeView) throw IllegalArgumentException() disposables += states.subscribe { view.display(it) } disposables += view.events.subscribe { .. } }} override fun onDetach(view: View) { disposables.clear() }} }}
  106. class HomeController : Controller() { }}

  107. class HomeController : Controller() { override fun onCreateView(inflater: LayoutInflater, container:

    ViewGroup): View { return inflater.inflate(R.layout.home, container, false) }} }}
  108. R.layout.home <codes.chrishorner.example.HomeView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Sub views go here>

    </codes.chrishorner.example.HomeView>
  109. What’s to like? • Simple “states go in, events come

    out” way of thinking • Test output of view states, rather than interactions with methods • Clear separation of concerns
  110. More on streams… Managing State with RxJava by Jake Wharton

    https:/ /youtu.be/0IKHxjkgop4 <> Observable<State> Observable<Event> Observable<Action> Observable<Result>
  111. What about Architecture Components?

  112. What about Architecture Components • LifecycleOwners • LiveData • ViewModel

    • Room • Paging • Navigation • WorkManager
  113. What about Architecture Components • LifecycleOwners • LiveData • ViewModel

  114. ViewModel class MyActivity : AppCompatActivity { fun onCreate(savedInstanceState: Bundle) {

    val model = ViewModelProviders.of(this).get(MyViewModel.class) model.getUsers().observe(this, { users -> // Update UI. }) } } LiveData
  115. ViewModel class MyActivity : AppCompatActivity { fun onCreate(savedInstanceState: Bundle) {

    val model = ViewModelProviders.of(this).get(MyViewModel.class) model.getUsers().observe(this, { users -> // Update UI. }) } } LifecycleOwner
  116. Activity created Activity rotated finish() Finished onCreate onStart onResume onPause

    onStop onDestroy onCleared() ViewModel scope onCreate onStart onResume onPause onStop onDestroy
  117. Does Controller replace ViewModel?

  118. dependencies { implementation 'com.bluelinelabs:conductor-archlifecycle:0.1.1' } abstract class LifecycleController : Controller,

    LifecycleOwner
  119. What about the new Navigation component?

  120. Out of the box advantages android.arch.navigation:navigation-fragment

  121. Out of the box advantages • Avoid touching FragmentTransaction with

    your bear hands • Simplified deep linking • Type safe passing of arguments between destinations • Visualise and edit your navigation graph
  122. None
  123. Can we combine them?

  124. Can we combine them? Navigating Conductor and the Navigation Architecture

    Component Tiven Jeffery https:/ /bit.ly/2MD9LSn (long medium link) GitHub repo https:/ /github.com/prolificinteractive/navigation-conductor
  125. Limitations • Navigator feels like the simple cousin of Router

    • No transitions; only R.anim or R.animator resources • Strange having both Router and NavController hold back stack
  126. Summary

  127. Summary • Simple in the right ways; configurable when you

    need it • Good starting point to play with different architectures • An alternative to many architecture components; or an addition
  128. Conductor Architecture and Alternatives chris_h_codes chris-horner chrishorner.codes