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

Chris Horner

June 27, 2018
Tweet

More Decks by Chris Horner

Other Decks in Programming

Transcript

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

    back • Sometimes up is… not up? • What happens when we deep link?
  2. 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
  3. 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
  4. Google’s making it worse “The bottom navigation bar remains in

    view when navigating through an app’s hierarchy.” - Google’s new design guidelines
  5. Single Activity apps • Control over UI elements remaining on

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

    screen • Easier shared element transitions • Single and stable entry point for your UI
  7. 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
  8. Fragment transactions val newFragment = NextFragment() val transaction = getSupportFragmentManager().beginTransaction()

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

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

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

    transaction.replace(R.id.fragment_container, newFragment) transaction.addToBackStack(null) transaction.commitNowAllowingStateLoss()
  12. Fragment identity crisis class MyFragment : Fragment() { override fun

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

    var ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Do some dependency injection... }} }}
  14. 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. }} }}
  15. Applying your MV-Whatever • Maybe a Fragment is a presenter?

    A controller? A view? • Whatever it is, it does a lot!
  16. 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
  17. 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
  18. 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
  19. Declaring a Controller class MyController : Controller() { override fun

    onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout.my_layout, container, false) } }}
  20. 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) { … } }}
  21. 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" />
  22. 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" />
  23. 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) }} }}
  24. 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())) } }} }}
  25. Navigating Controllers class MyController : Controller() { override fun onAttach(view:

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

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

    View) { view.events.ofType<NavigateNext>().subscribe { val transaction = RouterTransaction.with(NextController()) router.pushController(transaction) }} }} }}
  28. 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) }} }} }}
  29. 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
  30. Handling change • Comes in a variety of built in

    flavours class FadeChangeHandler : AnimatorChangeHandler class HorizontalChangeHandler : AnimatorChangeHandler class VerticalChangeHandler : AnimatorChangeHandler
  31. Handling change class FadeAndScaleChangeHandler : AnimatorChangeHandler() { override fun getAnimator(container:

    ViewGroup, from: View?, to: View?, isPush: Boolean, toAddedToContainer: Boolean): Animator { } }}
  32. 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
  33. 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()))
  34. 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
  35. sealed class State { object Idle : State() object Loading

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

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

    (state) { is Idle -> is Loading -> is Error -> is Success -> }} }} }
  38. 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>
  39. class HomeView(...) : FrameLayout(...) { fun display(state: State) { when

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

    { when (state) { is Idle -> is Loading -> is Error -> is Success -> }} }} }}
  41. 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 -> }} }} }}
  42. class HomeController : Controller() { override fun onAttach(view: View) {

    states.subscribe { view.display(it) }
 view.events.subscribe { .. } }} override fun onDetach(view: View) { }} }}
  43. 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) { }} }}
  44. 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() }} }}
  45. 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() }} }}
  46. 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() }} }}
  47. class HomeController : Controller() { override fun onCreateView(inflater: LayoutInflater, container:

    ViewGroup): View { return inflater.inflate(R.layout.home, container, false) }} }}
  48. 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
  49. More on streams… Managing State with RxJava by Jake Wharton

    https:/ /youtu.be/0IKHxjkgop4 <> Observable<State> Observable<Event> Observable<Action> Observable<Result>
  50. ViewModel class MyActivity : AppCompatActivity { fun onCreate(savedInstanceState: Bundle) {

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

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

    onStop onDestroy onCleared() ViewModel scope onCreate onStart onResume onPause onStop onDestroy
  53. 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
  54. 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
  55. 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
  56. 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