LiveData beyond ViewModels by Marcos Damasceno

LiveData beyond ViewModels by Marcos Damasceno

1b77dd441f657f5aefb3e21283b252e6?s=128

GDG Montreal

November 24, 2018
Tweet

Transcript

  1. 1.

    LiveData beyond ViewModels +Tasha Ramesh linkedin.com/in/akshataramesh Software Engineer @ Tinder

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder
  2. 2.

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  3. 3.

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  4. 4.

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  5. 5.

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  6. 15.
  7. 16.
  8. 17.
  9. 18.
  10. 22.

    CardContainer ContentContainer CardContainer HeadlineText PreviewCarousel CardContainer ContentContainer Summary Biography Anthem

    CardContainer ContentContainer CardContainer HeadlineText PreviewCarousel CardContainer ContentContainer
  11. 23.
  12. 24.
  13. 29.

    Summary Biography Anthem CardContainer ContentContainer HeadlineText PreviewCarousel PageIndicator PicturesCarousel Summary

    Biography Anthem CardContainer ContentContainer HeadlineText PreviewCarousel PageIndicator PicturesCarousel
  14. 36.

    Activity Fragment Fragment View View View View View View View

    View Fragment Activities, Fragments, and Views
  15. 37.

    Views are barebones Presenters handle the complexity of managing the

    Views Complex logic within the Presenter can be tested The MVP Approach
  16. 38.

    Are Views still barebones? But they need to coordinate their

    children… Presenters gain more responsibility Complex logic within the Presenter is more tedious to test The MVP Approach - With Complex View Hierarchy
  17. 43.

    val newLayoutParams = getLayoutParams() newLayoutParams.width = 100 newLayoutParams.height = 200

    viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { view.viewTreeObserver.removeOnPreDrawListener(this) ... } } ) setLayoutParams(newLayoutParams)
  18. 46.

    Embrace that UI is intrinsically reactive Android Framework API can

    be asynchronous UI needs to be able to handle configuration changes Components need to be choreographed and coordinated The True Nature of UI
  19. 49.

    Allows to form a reactive bridge to the core of

    the app ViewModels solve the need to handle UI across configuration changes View Models
  20. 50.

    Allows to form a reactive bridge to the core of

    the app Inherently bound to lifecycle of Activity or Fragment ViewModels solve the need to handle UI across configuration changes View Models
  21. 51.
  22. 53.
  23. 55.

    LiveData Bound to lifecycle of parent lifecycle owner (fragment, activity,

    etc) Can be used to form connections between UI components that need to be interactive
  24. 56.

    LiveData Bound to lifecycle of parent lifecycle owner (fragment, activity,

    etc) Can be used to form connections between UI components that need to be interactive Acts like a BehaviorSubject from RxJava
  25. 57.
  26. 58.
  27. 75.

    LiveData version of Rx operators Out of the box we

    have Map and SwitchMap LiveData - Transformations.map.switchMap
  28. 77.

    One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val

    birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x
  29. 78.

    One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val

    birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x
  30. 79.

    One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val

    birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x private fun convertDate(date: String) : Date { val dateFormatter = SimpleDateFormat.getDateInstance() return dateFormatter.parse(date) } x
  31. 80.

    One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val

    birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x private fun convertDate(date: String) : Date { val dateFormatter = SimpleDateFormat.getDateInstance() return dateFormatter.parse(date) } x
  32. 86.

    Transformations.switchMap One-to-one dynamic transformation private fun currentPhotoForIndex(index: Int) LiveData<String> {...}

    val currentPhotoIndex = LiveData<Int> val currentPhotoByIndex = Transformations.switchMap(currentPhotoIndex) {index -> currentPhotoForIndex(index) }
  33. 88.

    Treating UI as a Network Activity Stopped Fragment Fragment Fragment

    Fragment Fragment Fragment Activity Started
  34. 89.

    Treating UI as a Network - With ViewModels Activity Stopped

    Fragment Activity Started Fragment Fragment Fragment Fragment Fragment ViewModel ViewModel ViewModel ViewModel ViewModel ViewModel
  35. 90.

    Fragments and ViewModel Imagine a Fragment or ViewGroup with many

    children all interacting with one another, and with their parents
  36. 91.

    Fragments and ViewModel Imagine a Fragment or ViewGroup with many

    children all interacting with one another, and with their parents Even the ViewModel could get complex pretty quickly
  37. 92.

    Fragments and ViewModel \ Imagine a Fragment or ViewGroup with

    many children all interacting with one another, and with their parents Even the ViewModel could get complex pretty quickly We’re using LiveData to handle granular View choreography within the Fragment…
  38. 94.

    Can we simplify? If we took a step back and

    really looked at the changes that the UI goes through
  39. 95.

    Can we simplify? If we took a step back and

    really looked at the changes that the UI goes through Certain transitions of the UI can be grouped together to form states
  40. 96.

    Can we simplify? If we took a step back and

    really looked at the changes that the UI goes through Certain transitions of the UI can be grouped together to form states UI can react to those states not even caring what caused those states in the first place
  41. 101.

    A B C Action Action Action Action Action Action State

    Machines State + Action = State Quantifiable manifestation of a reactive system. And since UI is intrinsically reactive...
  42. 102.

    State Machines For when UX can be behaviorally and visually

    disassembled into distinct State + Action = State A B C Action Action Action Action Action Action states, actions, & side effects
  43. 104.

    Actions SideEffects Update Store Make Api Call Start Background Task

    Fragment Button1 click listener drag listener Button2 click listener Button1Click Button1Drag Button2Click
  44. 106.

    sealed class State { x abstract fun consumeAction(action: Action): State

    } x object StateA : x State() { x override fun consumeAction(action: Action): State { x return when (action) { x Button1Click -> StateB Button2Click -> StateC } } x } x States and Actions
  45. 107.

    sealed class State { x abstract fun consumeAction(action: Action): State

    } x object StateA : x State() { x override fun consumeAction(action: Action): State { x return when (action) { x Button1Click -> StateB Button2Click -> StateC } } x } x A A States and Actions
  46. 108.

    sealed class State { x abstract fun consumeAction(action: Action): State

    } x object StateA : x State() { x override fun consumeAction(action: Action): State { x return when (action) { x Button1Click -> StateB Button2Click -> StateC } } x } x A B Button1Click A States and Actions
  47. 109.

    sealed class State { x abstract fun consumeAction(action: Action): State

    } x object StateA : x State() { x override fun consumeAction(action: Action): State { x return when (action) { x Button1Click -> StateB Button2Click -> StateC } } x } x Button2Click C States and Actions A B Button1Click A
  48. 110.

    A A sealed class State(open val sideEffect: SideEffect? = null)

    { x abstract fun consumeAction(action: Action): State } x data class StateA( override val sideEffect: SideEffect? = null ) : State(sideEffect) { x override fun consumeAction(action: Action): State { x return when (action) { Button1Click -> StateB Button2Click -> StateC(SideEffect.UpdateStore) x } x } x } x States, Actions, & Side Effects
  49. 111.

    A B Button1Click A sealed class State(open val sideEffect: SideEffect?

    = null) { x abstract fun consumeAction(action: Action): State } x data class StateA( override val sideEffect: SideEffect? = null ) : State(sideEffect) { x override fun consumeAction(action: Action): State { x return when (action) { Button1Click -> StateB Button2Click -> StateC(SideEffect.UpdateStore) x } x } x } x States, Actions, & Side Effects
  50. 112.

    Button2Click C + Update Store sealed class State(open val sideEffect:

    SideEffect? = null) { x abstract fun consumeAction(action: Action): State } x data class StateA( override val sideEffect: SideEffect? = null ) : State(sideEffect) { x override fun consumeAction(action: Action): State { x return when (action) { Button1Click -> StateB Button2Click -> StateC(SideEffect.UpdateStore) x } x } x } x A B Button1Click A States, Actions, & Side Effects
  51. 114.

    Container Handle State Action State ViewModel StateLiveData Action Action Child

    view Child view listener listener listener Transmitting States Using ViewModels & x LiveData
  52. 116.

    class FlowViewModel : ViewModel() {x val stateTransitions = MutableLiveData<State>() init

    {x stateTransitions.value = StateA }x fun updateState(action: Action) {x val currentState = stateTransitions.value stateTransitions.value = currentState }x }x
  53. 117.

    class FlowViewModel : ViewModel() {x val stateTransitions = MutableLiveData<State>() init

    {x stateTransitions.value = StateA }x fun updateState(action: Action) {x val currentState = stateTransitions.value stateTransitions.value = currentState }x }x
  54. 118.

    class FlowViewModel : ViewModel() {x val stateTransitions = MutableLiveData<State>() init

    {x stateTransitions.value = StateA }x fun updateState(action: Action) {x val currentState = stateTransitions.value stateTransitions.value = currentState }x }x
  55. 119.

    class FlowViewModel : ViewModel() {x val stateTransitions = MutableLiveData<State>() init

    {x stateTransitions.value = StateA }x fun updateState(action: Action) {x val currentState = stateTransitions.value stateTransitions.value = currentState }x }x
  56. 120.

    class FlowViewModel : ViewModel() {x val stateTransitions = MutableLiveData<State>() init

    {x stateTransitions.value = StateA }x fun updateState(action: Action) {x val currentState = stateTransitions.value stateTransitions.value = currentState.consumeState(action) }x }x
  57. 121.

    class FlowViewModel : ViewModel() {x val stateTransitions = MutableLiveData<State>() init

    {x stateTransitions.value = StateA }x fun updateState(action: Action) {x val currentState = stateTransitions.value stateTransitions.value = currentState.consumeState(action) }x }x
  58. 122.

    class ParentFragment : Fragment() {x override fun onActivityCreated(savedInstanceState: Bundle?) {x

    super.onActivityCreated(savedInstanceState) . . . . . . flowViewModel.stateTransitions.observe(this, Observer<State> { state -> when (state) { is StateA -> { // configure appearance of child views for StateA }x is StateB -> { // configure appearance of child views for StateB }x is StateC -> { // configure appearance of child views for StateC }x }x })x }x }x
  59. 123.

    class ParentFragment : Fragment() {x override fun onActivityCreated(savedInstanceState: Bundle?) {x

    super.onActivityCreated(savedInstanceState) . . . . . . flowViewModel.stateTransitions.observe(this, Observer<State> { state -> when (state) { is StateA -> { // configure appearance of child views for StateA }x is StateB -> { // configure appearance of child views for StateB }x is StateC -> { // configure appearance of child views for StateC }x }x })x }x }x
  60. 124.

    class ParentFragment : Fragment() {x override fun onActivityCreated(savedInstanceState: Bundle?) {x

    super.onActivityCreated(savedInstanceState) flowViewModel.stateTransitions.observe(this, Observer<State> { state -> . . . })x }x override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button1.setOnClickListener { flowViewModel.updateState(Action.Button1Click) }x . . . . . . }x }x
  61. 125.

    class ParentFragment : Fragment() {x override fun onActivityCreated(savedInstanceState: Bundle?) {x

    super.onActivityCreated(savedInstanceState) flowViewModel.stateTransitions.observe(this, Observer<State> { state -> . . . })x flowViewModel.sideEffects.observe(this, Observer<SideEffect> { sideEffect -> handleSideEffect(sideEffect) })x }x x }x
  62. 145.

    class ParentFragment : Fragment() {x override fun onActivityCreated(savedInstanceState: Bundle?) {x

    super.onActivityCreated(savedInstanceState) flowViewModel.stateTransitions.observe(this, Observer<State> { state -> . . . }) flowViewModel.stateTransitions.buttonGroupSubstates.observe( this, Observer<ButtonGroupState> { buttonGroupStates -> // configure appearance of child buttons within button group } )x }x }x
  63. 146.

    class StateTransitions(initialState: State) : MutableLiveData<State>() {x init {x value =

    initialState }x val buttonGroupSubStates: LiveData<ButtonGroupState>x get() = Transformations.map(this) {x state ->x when (state) {x TextInputUnfocused -> ButtonGroupState.AllInactive TextInputFocused -> ButtonGroupState.AllInactive GifInputUnfocused -> ButtonGroupState.GifButtonActive BitmojiUnfocused -> ButtonGroupState.BitmojiButtonActive else -> . . .x }x }x fun updateState(action: Action) {x value = value?.consumeAction(action)x }x }x
  64. 147.

    class StateTransitions(initialState: State) : MutableLiveData<State>() {x init {x value =

    initialState }x val buttonGroupSubStates: LiveData<ButtonGroupState>x get() = Transformations.map(this) {x state ->x when (state) {x TextInputUnfocused -> ButtonGroupState.AllInactive TextInputFocused -> ButtonGroupState.AllInactive GifInputUnfocused -> ButtonGroupState.GifButtonActive BitmojiUnfocused -> ButtonGroupState.BitmojiButtonActive else -> . . .x }x }x fun updateState(action: Action) {x value = value?.consumeAction(action)x }x }x
  65. 148.

    class StateTransitions(initialState: State) : MutableLiveData<State>() {x init {x value =

    initialState }x val buttonGroupSubStates: LiveData<ButtonGroupState>x get() = Transformations.map(this) {x state ->x when (state) {x TextInputUnfocused -> ButtonGroupState.AllInactive TextInputFocused -> ButtonGroupState.AllInactive GifInputUnfocused -> ButtonGroupState.GifButtonActive BitmojiUnfocused -> ButtonGroupState.BitmojiButtonActive else -> . . .x }x }x fun updateState(action: Action) {x value = value?.consumeAction(action)x }x }x class StateTransitions(initialState: State) : MutableLiveData<State>() {x init {x value = initialState }x val buttonGroupSubStates: LiveData<ButtonGroupState>x get() = Transformations.map(this) {x state ->x when (state) {x TextInputUnfocused -> ButtonGroupState.AllInactive TextInputFocused -> ButtonGroupState.AllInactive GifInputUnfocused -> ButtonGroupState.GifButtonActive BitmojiUnfocused -> ButtonGroupState.BitmojiButtonActive else -> . . .x }x }x fun updateState(action: Action) {x value = value?.consumeAction(action)x }x }x
  66. 149.

    class StateTransitions(initialState: State) : MutableLiveData<State>() {x init {x value =

    initialState }x val buttonGroupSubStates: LiveData<ButtonGroupState>x get() = Transformations.map(this) {x state ->x when (state) {x TextInputUnfocused -> ButtonGroupState.AllInactive TextInputFocused -> ButtonGroupState.AllInactive GifInputUnfocused -> ButtonGroupState.GifButtonActive BitmojiUnfocused -> ButtonGroupState.BitmojiButtonActive else -> . . .x }x }x fun updateState(action: Action) {x value = value?.consumeAction(action)x }x }x class StateTransitions(initialState: State) : MutableLiveData<State>() {x init {x value = initialState }x val buttonGroupSubStates: LiveData<ButtonGroupState>x get() = Transformations.map(this) {x state ->x when (state) {x TextInputUnfocused -> ButtonGroupState.AllInactive TextInputFocused -> ButtonGroupState.AllInactive GifInputUnfocused -> ButtonGroupState.GifButtonActive BitmojiUnfocused -> ButtonGroupState.BitmojiButtonActive else -> . . .x }x }x fun updateState(action: Action) {x value = value?.consumeAction(action)x }x }x
  67. 151.

    Simplifies interaction between child views and their parents Ensures that

    all UI elements behave predictably - even across hard to test UI interactions - even across configuration changes ViewModels + LiveData + States Offers the flexibility to create UI abstractions
  68. 153.

    Containers can also be ViewGroups, not just Fragments Using data

    binding, we can reduce the boilerplate of binding ViewModels/LiveData to container or child ViewGroups Taking this further… Custom implementations of LiveData can serve as further abstractions
  69. 154.

    LiveData beyond ViewModels +Tasha Ramesh linkedin.com/in/akshataramesh Software Engineer @ Tinder

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder