Share
Embed
Transcript
154 slides
-
LiveData beyond ViewModels +Tasha Ramesh linkedin.com/in/akshataramesh Software Engineer @ Tinder
+Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder -
+Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh
linkedin.com/in/akshataramesh Software Engineer @ Tinder -
+Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh
linkedin.com/in/akshataramesh Software Engineer @ Tinder -
+Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh
linkedin.com/in/akshataramesh Software Engineer @ Tinder -
+Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder +Tasha Ramesh
linkedin.com/in/akshataramesh Software Engineer @ Tinder -
CardContainer ContentContainer CardContainer HeadlineText PreviewCarousel CardContainer ContentContainer Summary Biography Anthem
CardContainer ContentContainer CardContainer HeadlineText PreviewCarousel CardContainer ContentContainer -
Summary Biography Anthem CardContainer ContentContainer HeadlineText PreviewCarousel PageIndicator PicturesCarousel Summary
Biography Anthem CardContainer ContentContainer HeadlineText PreviewCarousel PageIndicator PicturesCarousel -
Activity Fragment Fragment View View View View View View View
View Fragment Activities, Fragments, and Views -
Views are barebones Presenters handle the complexity of managing the
Views Complex logic within the Presenter can be tested The MVP Approach -
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 -
val newLayoutParams = getLayoutParams() newLayoutParams.width = 100 newLayoutParams.height = 200
viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { view.viewTreeObserver.removeOnPreDrawListener(this) ... } } ) setLayoutParams(newLayoutParams) -
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 -
Allows to form a reactive bridge to the core of
the app ViewModels solve the need to handle UI across configuration changes View Models -
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 -
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 -
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 -
LiveData version of Rx operators Out of the box we
have Map and SwitchMap LiveData - Transformations.map.switchMap -
One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val
birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x -
One-to-one static transformation Transformations.map.switchMap private val user = MutableLiveData<User>() val
birthdate: LiveData<Date> = Transformations.map(user) { convertDate(it.birthDate) } x -
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 -
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 -
Transformations.switchMap One-to-one dynamic transformation private fun currentPhotoForIndex(index: Int) LiveData<String> {...}
val currentPhotoIndex = LiveData<Int> -
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) } -
Treating UI as a Network Activity Stopped Fragment Fragment Fragment
Fragment Fragment Fragment Activity Started -
Treating UI as a Network - With ViewModels Activity Stopped
Fragment Activity Started Fragment Fragment Fragment Fragment Fragment ViewModel ViewModel ViewModel ViewModel ViewModel ViewModel -
Fragments and ViewModel Imagine a Fragment or ViewGroup with many
children all interacting with one another, and with their parents -
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 -
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… -
Fragments and ViewModel Fragment View View View View View ViewModel
LiveData LiveData LiveData LiveData -
Can we simplify? If we took a step back and
really looked at the changes that the UI goes through -
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 -
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 -
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... -
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 -
Fragment Button1 click listener drag listener Button2 click listener Button1Click
Button1Drag Button2Click Actions -
Actions SideEffects Update Store Make Api Call Start Background Task
Fragment Button1 click listener drag listener Button2 click listener Button1Click Button1Drag Button2Click -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
Container Handle State Action State ViewModel StateLiveData Action Action Child
view Child view listener listener listener Transmitting States Using ViewModels & x LiveData -
StateLiveData Current State Action Current State Action Consumer Action Consumer
Action Consumer StateLiveData State -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
LiveData beyond ViewModels +Tasha Ramesh linkedin.com/in/akshataramesh Software Engineer @ Tinder
+Marcos Damasceno @marcospaulosd Senior Software Engineer @ Tinder