reduxing ui: borrowing the best of web to make android better

76
Borrowing the best of the web to make native better Brandon Kase & Christina Lee

Upload: christina-lee

Post on 13-Apr-2017

162 views

Category:

Technology


1 download

TRANSCRIPT

Borrowing the best of the webto make native better

Brandon Kase & Christina Lee

Why are we here?Let me tell you a story!

about a buttona complicated button

Take 1:boss asks for friend buttonwe say, 'OK! We'll do it this week!'we do not finish it in a weekwoah...this is very complicated!boss is less than thrilled

Take 2:boss asks for friend buttonwe say, 'OK! We'll do it this week!'we modify our approachwe do not finish it in a weekwe finish it in a day!boss is thrilledBrandon and Christina still have jobs, hurray!

What went wrongFetch data before view transitionsOptimistically update componentsSend server requests and react to responses

Side EffectsTouch global state, make network requestsSide-effects are bad, but necessary

Most Common PitfallsMutabilityAsynchronicity

As developers, we are expected to handleoptimistic updates, server- side rendering,

fetching data before performing routetransitions, and so on... This complexity is

difficult to handle as we’re mixing twoconcepts that are very hard for the human

mind to reason about: mutation andasynchronicity. I call them Mentos and Coke”

Motivation | Redux

Luckily, we are not alone

Web AlliesThe web faces all of these challenges and moreUnlike native; however, it's iteration rate is fastNot "one" UI framework

So what did the web come up with?

Ways to manage side-effectsView

State

Ways to manage side-effectsSeparation of concernsMutability and asynchronicity are decoupled

Flux

Redux// The current application state (list of todos and chosen filter) let previousState = { visibleTodoFilter: 'SHOW_ALL', todos: [ { text: 'Read the docs.', complete: false } ] }

// The action being performed (adding a todo) let action = { type: 'ADD_TODO', text: 'Understand the flow.' }

// Your reducer returns the next application state let nextState = todoApp(previousState, action)

-- Dan Abaramov's Data Flow example in Redux

Cycle.js

Cycle.jsinputs = read effects you care aboutoutputs = write effects you want performedall application logic is pure!

Cycle.js

from cycle.js.org

Common TraitsUnidirectional and circular data flowsSeparation of concerns

How do we benefit on Android?

Logic is made easyImplicit data flow of your app becomes explicit.Immutable views of a mutable world

Debugging is made easyAll edge cases caught at compile-time.Single source of truth.Time Travel

How can you adopt thisView:

React nativeAnvil

State:

Direct port of Redux/Cycle/etc.

We focused on StateDon't have to fight Android's UI frameworkEasy to introduce

Native (kotlin)Single-Atom-State (like redux)Pure Functional Reactive (like cycle)Composable (like cycle)

Aside: RxJava

Aside: RxJavaReactive programming is programming with

asynchronous data streams

Andre Staltz

Global event emitter << Event buses << Data stream

Aside: RxJavaWhen we say streams, we mean push-based event streams,

not pull-based infinite list streams

Aside: RxJavaWhat can this look like in practice?

Streams of button tapsStreams of snapshots of changing dataStreams from network responses

Aside: RxJavaRxJava

Reactive programming with streamsTools to combine and transform those streams

Aside: RxJava

from RxMarbles

Aside: RxJava

from RxMarbles

Aside: RxJavaObservable.just(1,2,3)

Example

Example

0:09 0:14

Not just a counter

Not just a counter

1. View-Model Statedata class /*View-Model*/ State( val numLikes: Int, val numComments: Int, val showNewHighlight: Boolean, val imgUrl: String?, val showUndo: Boolean )

2. View Intentions// Mode is either tapped or untapped data class ViewIntentions( val photos: Observable<Photo>, val modes: Observable<Mode.Sum>, val globalReadTs: Observable<Long> )

3. Model State// Mode is either tapped or untapped data class /*Model*/ State( val photo: Photo?, val isNew: Boolean, val mode: Mode.Sum, ): RamState<...>

val initialState = State( photo = null, isNew = false, mode = Mode.untapped )

4. View Intentions => Model State ChangesState changes? We want functional code. We want

immutability.

Think of a state change as a function

func change(currentState: State) -> State /*nextState */

4. View Intentions => Model State Changesval model: (ViewIntentions) -> Observable<(State) -> State> = { intentions -> val modeChanges: Observable<(State) -> State> = intentions.modes.map{ /*...*/ }

val photoChanges: Observable<(State) -> State> = intentions.photos.map{ /*...*/ }

val tsChanges: Observable<(State) -> State> = intentions.globalReadTs.map{ /*...*/ }

Observable.merge( modeChanges, photoChanges, tsChanges) }

4. View Intentions => Model State Changesval modeChanges: Observable<(State) -> State> = intentions.modes.map{ mode -> { state: State -> State(state.photo, state.isNew, mode) } }

5. Model State => View-Model Stateval viewModel: (Observable<Model.State>)->Observable<ViewModel.State> = { stateStream -> stateStream .map{ state -> val undoable = state.mode == Mode.tapped val likes = state.photo?.like_details ?: emptyList() val comments = state.photo?.comments ?: emptyList() ViewModel.State( numLikes = likes.sumBy { it.multiplier }, numComments = comments.count, showNewHighlight = state.isNew, imgUrl = /* ... */, showUndo = /*...*/ ) } }

6. View-Model => Mutate the Viewclass PhotoComponent( viewIntentions: ViewIntentions, view: PhotoCellView ): StartStopComponent by Component( driver = /* ... */, model = /* ... */ )

6. View-Model => Mutate the Viewdriver = ViewDriver<ViewIntentions, ViewModel.State>( intention = viewIntentions, onViewState = { old, state -> if (old?.imgUrl != state.imgUrl) { view.setImg(state.imgUrl) } /* ... */ } ),

6. View-Model => Mutate the Viewmodel = ViewDriver.makeModel( initialState = Model.initialState, createState = Model.createState, model = Model.model, viewModel = ViewModel.viewModel )

Stick it in a recycler-view, hook up the side-effects into viewintentions and you're done

ViewIntentionsThe inputs to your componentThe photo, the mode, the tap timestamp

ModelTransform the inputs into state changesChange mode, change isNew, change photo

ViewModelTransform model state to view-model stateExtract photo url, like counts, etc

ComponentApply mutations to your view based on your view-modelUse the View-Model to change the underlying Androidview

Under the hoodEnforce viewintentions/model/view-model structureRxJava does heavy-liftingand a magic scan

Under the hood

Implementation of Redux in one-linemodelStream.scan(initialState, { currentState, transform -> transform(currentState) })

BonusCycle.js-like side-effect driversConfigurable model state persistance within stateAuto-start and stop components onPause/onResume

Just like cycle

read effects are inputswrite effects are outputs

Effects are decoupled from business logic

What does it look like in production?

Results: The GoodIt wouldn't compile

Results: The GoodWhen it did compile, it worked!

Results: The GoodIncredibly modular and composableLEGO-like plug-and-play

Results: The GoodEasy to test (by hand + by unit test) & debugREALLY EASYMocking inputs is trivialUI component is defined ONLY by it's state

Results: The GoodEasy to maintainSpec change?

(possibly) add an input streamadd another map in the model

Results: The BadRamp up necessary

Results: The BadAnimations are hard

chase or interpolate underlying state?probably additive animations (google it)

Results: The Bad?Boiler plate

(screenshot of files)

Always the 4 piecescyklic repo has counter example in one file

Results: The SurprisingIt's actually not slow

No noticeable perf hit

Results: The Conclusion

Results: The ConclusionWe have more powerful tools now

(i.e. Kotlin + Functional programming)

Let's use them

Question everything

ThanksBrandon Kase Christina Lee

[email protected] [email protected]

@bkase_ @runchristinarun

bkase.com

Github: bkase/cyklic