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

Livedata beyond ViewModels: A stateful approach for UI

Livedata beyond ViewModels: A stateful approach for UI

The UI on Android is completely reactive and so should be your code. With the increasing complexity of animations, interaction and user experience, new challenges arise when writing code. While we may have great solutions for Core and Architecture, little is talked on how to make your UI code more responsive and easier to maintain.

In this talk, we will go see the challenge of communicating between distinct parts of the apps and how can we fix that by using LiveData.

We will look at how Transformations can help us create a reactive representation of our UI models and make the communication between different custom views simpler and less coupled.

Marcos Paulo Souza Damasceno

November 21, 2018
Tweet

More Decks by Marcos Paulo Souza Damasceno

Other Decks in Technology

Transcript

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

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

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  3. +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  4. +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  5. +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh

    linkedin.com/in/akshataramesh Software Engineer @ Tinder
  6. CardContainer ContentContainer CardContainer HeadlineText PreviewCarousel CardContainer ContentContainer Summary Biography Anthem

    CardContainer ContentContainer CardContainer HeadlineText PreviewCarousel CardContainer ContentContainer
  7. Summary Biography Anthem CardContainer ContentContainer HeadlineText PreviewCarousel PageIndicator PicturesCarousel Summary

    Biography Anthem CardContainer ContentContainer HeadlineText PreviewCarousel PageIndicator PicturesCarousel
  8. Activity Fragment Fragment View View View View View View View

    View Fragment Activities, Fragments, and Views
  9. Views are barebones Presenters handle the complexity of managing the

    Views Complex logic within the Presenter can be tested The MVP Approach
  10. 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
  11. val newLayoutParams = getLayoutParams() newLayoutParams.width = 100 newLayoutParams.height = 200

    viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { view.viewTreeObserver.removeOnPreDrawListener(this) ... } } ) setLayoutParams(newLayoutParams)
  12. 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
  13. Allows to form a reactive bridge to the core of

    the app ViewModels solve the need to handle UI across configuration changes View Models
  14. 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
  15. 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
  16. 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
  17. LiveData version of Rx operators Out of the box we

    have Map and SwitchMap LiveData - Transformations.map.switchMap
  18. One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val

    birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x
  19. One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val

    birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x
  20. 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
  21. 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
  22. 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) }
  23. Treating UI as a Network Activity Stopped Fragment Fragment Fragment

    Fragment Fragment Fragment Activity Started
  24. Treating UI as a Network - With ViewModels Activity Stopped

    Fragment Activity Started Fragment Fragment Fragment Fragment Fragment ViewModel ViewModel ViewModel ViewModel ViewModel ViewModel
  25. Fragments and ViewModel Imagine a Fragment or ViewGroup with many

    children all interacting with one another, and with their parents
  26. 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
  27. 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…
  28. Can we simplify? If we took a step back and

    really looked at the changes that the UI goes through
  29. 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
  30. 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
  31. 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...
  32. 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
  33. Actions SideEffects Update Store Make Api Call Start Background Task

    Fragment Button1 click listener drag listener Button2 click listener Button1Click Button1Drag Button2Click
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. Container Handle State Action State ViewModel StateLiveData Action Action Child

    view Child view listener listener listener Transmitting States Using ViewModels & x LiveData
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. LiveData beyond ViewModels +Tasha Ramesh linkedin.com/in/akshataramesh Software Engineer @ Tinder

    +Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder