$30 off During Our Annual Pro Sale. View Details »

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

Chris Horner

June 27, 2018
Tweet

More Decks by Chris Horner

Other Decks in Programming

Transcript

  1. Conductor
    Architecture and Alternatives
    @chris_h_codes

    View Slide

  2. Navigation is hard

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  9. There’s a term for this

    View Slide

  10. View Slide

  11. Single Activity apps

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  16. Fragments…

    View Slide

  17. Fragment transactions

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. Fragment lifecyle

    View Slide

  23. Fragment lifecyle

    View Slide

  24. Fragment lifecyle

    View Slide

  25. Fragment identity crisis

    View Slide

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

    View Slide

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

    View Slide

  28. Fragment identity crisis
    class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    }}
    }}

    View Slide

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

    View Slide

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

    View Slide

  31. Applying your
    MV-Whatever

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  35. So what’s Conductor?
    github.com/bluelinelabs/Conductor

    View Slide

  36. How Conductor works
    Fragment Controller

    View Slide

  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

    View Slide

  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

    View Slide

  39. Declaring a Controller
    class MyController : Controller() {
    }}

    View Slide

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

    View Slide

  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) { … }
    }}

    View Slide

  42. Navigating Controllers

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  54. Handling change

    View Slide

  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

    View Slide

  56. Handling change
    • Comes in a variety of built in flavours
    class FadeChangeHandler : AnimatorChangeHandler
    class HorizontalChangeHandler : AnimatorChangeHandler
    class VerticalChangeHandler : AnimatorChangeHandler

    View Slide

  57. Handling change
    abstract class AnimatorChangeHandler

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  62. Handling change
    abstract class TransitionChangeHandler

    View Slide

  63. Handling change
    class ListToDetailsChangeHandler : TransitionChangeHandler() {
    override fun getTransition(container: ViewGroup,
    from: View?,
    to: View?,
    isPush: Boolean): Transition {
    }
    }

    View Slide

  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()))

    View Slide

  65. Where do I bind my
    views?

    View Slide

  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

    View Slide

  67. Fragments were never a
    good V in your MV-Whatever

    View Slide

  68. Activity

    View Slide

  69. Activity

    View Slide

  70. Fragment A

    View Slide

  71. Fragment A onCreate()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  77. Fragment A Fragment B

    View Slide

  78. Fragment A Fragment B onCreate()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  82. Fragment A

    View Slide

  83. Fragment A
    View A onCreateView()

    View Slide

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

    View Slide

  85. Fragment A
    View A

    View Slide

  86. A pattern I don’t mind

    View Slide

  87. A pattern I don’t mind
    View
    Controller

    View Slide

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

    View Slide

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

    View Slide

  90. A pattern I don’t mind
    View
    Controller
    Observable
    Observable

    View Slide

  91. A pattern I don’t mind
    View
    Controller
    Observable
    Observable

    View Slide

  92. sealed class State {
    object Idle : State()
    object Loading : State()
    data class Error(val message: String) : State()
    data class Success(val items: List) : State()
    }

    View Slide

  93. fun display(state: State) {
    when (state) {
    is Idle ->
    is Loading ->
    is Error ->
    is Success ->
    }}
    }}

    View Slide

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

    View Slide

  95. class HomeView(...) : FrameLayout(...) {
    fun display(state: State) {
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >


    View Slide

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

    View Slide

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

    View Slide

  98. class HomeView(context: Context, ...) : FrameLayout(...) {
    // RxBinding helps a lot here
    val events: Observable = ...
    fun display(state: State) {
    when (state) {
    is Idle ->
    is Loading ->
    is Error ->
    is Success ->
    }}
    }}
    }}

    View Slide

  99. class HomeController : Controller() {
    }}

    View Slide

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

    View Slide

  101. class HomeController : Controller() {
    override fun onAttach(view: View) {
    states.subscribe { view.display(it) }

    view.events.subscribe { .. }
    }}
    override fun onDetach(view: View) {
    }}
    }}

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  106. class HomeController : Controller() {
    }}

    View Slide

  107. class HomeController : Controller() {
    override fun onCreateView(inflater: LayoutInflater,
    container: ViewGroup): View {
    return inflater.inflate(R.layout.home, container, false)
    }}
    }}

    View Slide

  108. R.layout.home
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >


    View Slide

  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

    View Slide

  110. More on streams…
    Managing State with RxJava by Jake Wharton
    https:/
    /youtu.be/0IKHxjkgop4
    <>
    Observable
    Observable
    Observable
    Observable

    View Slide

  111. What about Architecture
    Components?

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  116. Activity created
    Activity rotated
    finish()
    Finished
    onCreate
    onStart
    onResume
    onPause
    onStop
    onDestroy
    onCleared()
    ViewModel
    scope
    onCreate
    onStart
    onResume
    onPause
    onStop
    onDestroy

    View Slide

  117. Does Controller replace
    ViewModel?

    View Slide

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

    View Slide

  119. What about the new
    Navigation component?

    View Slide

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

    View Slide

  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

    View Slide

  122. View Slide

  123. Can we combine them?

    View Slide

  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

    View Slide

  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

    View Slide

  126. Summary

    View Slide

  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

    View Slide

  128. Conductor
    Architecture and Alternatives
    chris_h_codes
    chris-horner
    chrishorner.codes

    View Slide