神刀安全网

Redux-ing UI bugs: Borrowing the Best of the Web to Make Native Better

About the Speaker: Christina Lee

Christina is currently the Android lead at Highlight. In addition to trying to convince the world to use Kotlin on a daily basis, she also enjoys building beautiful UIs, extolling the virtues of Rx, and reading well documented APIs.

@RunChristinaRun

About the Speaker: Brandon Kase

Brandon Kase is a full-stack software engineer at Highlight, where he is currently working on Shorts. He is excited that strong static typing and functional programming are becoming mainstream, and believes that techniques once reserved for academia will help industry produce more reliable software. Brandon is in general fascinated by anything and everything related to well-designed programming languages and libraries.

@bkase_

We want to help you borrow the best parts of web development to make your native Android development experience better.

This all started because of a button…

It’s a really complicated button for social media to add friends. We were initially supposed to finish it in a week, but we didn’t finish it in a week after we realized it was really complicated. We modified our approach on our second try to make this button, and we took into consideration all the things that went wrong the first time around. On our second try we finished making this button in a day – hooray!

What Went Wrong?

What went wrong was our network call and the requirement to update the button immediately. Besides that you also have to store the state locally – all of these complexities can be classified under the umbrella of side effects.

The most common categories of side effect pitfalls are mutability and asynchronicity. These aren’t by any means the only ways you can have side effects, but they’re the most commons ones you’re going to run into if you have problems. When you make asynchronous calls that’s really useful as a developer, but the problem is that each of these things is really hard to reason about. And so when you try to mutate things and make asynchronous calls all at the same time, things go wrong.

What the Web Came Up With

There are two ways that they’re separating the concerns. They’re making sure that when you have mutability and you have asynchronicity, they’re separated.

The most striking thing about this slide is that all the data is in a straight line and it’s all flowing in exactly the same direction.

Flux starts with actions and those actions need to go to the right place. You have a dispatcher, and the reason you need a dispatcher is that there can be multiple stores for multiple views. At the end of the day however, what you’re doing is you’re taking an action, routing it to the thing that cares about that action and then, in that store, you’re mutating and telling the view to render that new thing. Your view can then emit an action – you can tap on it, you can interact with it and then we start the whole process again.

Redux builds off of Flux and it tries to address some of the shortcomings that Flux has, but it’s very similar. Here are three blocks here because those three blocks really illustrate what Redux is and what it’s about.

let previousState={   visibleTodoFilter: 'SHOW_ALL',   todo: [{     text: 'Read the docs.',     complete: false   }] }  let action = {   type: 'ADD_TODO',   text: 'Understand the flow.' }  let nextState = todoApp(previousState, action)

The first block defines a previous state or the current state of a to do app. In the middle, we have an action, so we have a previous state and we have an action and then all of the magic happens in that last line.

Suppose we have an action and a state, and use a reducer to create the next state. And what’s really important here and differentiates it from Flux is that here we only have one state representing the app, there are no multiple stores.

Cycle is a little bit different. Your entire component code and application logic lives in this black box and it’s all pure, without any side effects.

If you want to react to network requests, they go through this illustrated cycle. This gray part is provided automatically, and when it completes, it comes back through and you get it at the beginning. In other words, the input to your components are the side effects that you want to read from. The output is the write effects that you want to do to the world. And all the application logic is pure. This is an example of a counter:

function main(sources) {   const decrement$ = sources.DOM     .select('decrement').events('click').map(ev => -1);    const increment$ = sources.DOM     .select('increment').events('click').map(ev => +1);    const action$ = Observable.merge(decrement$, increment$);   const count$ = actions$.startWith(0).scan((x,y)) => x+y);    const vtree$ = count$.map(count=>     div([       button('.decrement', 'Decrement'),       button('.increment', 'Increment'),       p('Counter: ' + count)     ])     return { DOM: vtree$};   ) }

Picture an increment button, a decrement button, and somewhere where we display a count. You can see it follows that there’s a data flow graph next to it and that says the same thing. What do these things have in common? They are all unidirectional, they’re all circular and they separate mutability and asynchronicity.

There are two really beneficial parts to structures that we just studied. First, the logic is made easy as it’s simple to visualize how data is flowing through your app.

The second big thing is debugging: Because things are immutable and because there’s a single source of truth for state, you get several nice properties. It’s really easy to listen in on the state and know exactly what’s happening in your app at any time because, of course, your app is just the state object. Everything you need to know about your app at any point in time is represented by that object.

How Can You Android this For Android?

You can focus on Views or you can focus on state. For the view, you can use React Native, or Anvil which is an open source library and replaces the XML structure with reactively pushed views.

For the state, you’ll need to port it yourself. We haven’t really come across a framework that does this yet, so you’re going to be looking at some upfront costs for implementing it yourself.

Android provides us with callbacks for button presses, but then we take the presses of decrement button and the increment button and combined them in different ways, then eventually output a count, and then that goes back to Android’s UI which is where we write it to the View.

The framework is native and written in Kotlin. For those that don’t know, Kotlin is a terse, Java-like language. It’s fully interoperable with Java. You can call Java functions from Kotlin, Kotlin from Java. It also has this property called single-atom-state. The entire state of your component, no matter how complicated, stays in one place. It’s purely functional-reactive like Cycle.

To show an example of taking web principles to apply them to Android, we need to know a little bit about RxJava. RxJava is fundamentally just Reactive programming with asynchronous data streams. RxJava is this Reactive programming, but it adds a lot of functionality.

Now, there are two functions that are going come up often in the following example: Map, and Merge. Map takes a piece of data that’s in your stream and it applies a function to it to emit a different piece of data.

Merge takes two observables and we’re pushing objects onto them, so the data that you push on either one of the component functions gets emitted at exactly the same time in the resulting observable.

The Example

data class /*View-Model*/ State {   val numLikes: Int,   val numComments: Int,   val showNewHighlight: Boolean,   val imgUrl: String?,   val showUndo: Boolean }

In the ViewModel state, we’re defining the fields that we could need to define one of those squares. You saw multiple text views that had the number of comments and the number of likes. When there’s new data, we would need to show some sort of highlighted state.

// Mode is either tapped or untapped  data class ViewIntentions {   val photos: Observable<Photo>,   val modes: Observable<Mode.Sum>,   val globalReadTs: Observable<Long> }

Regarding the kind of inputs we would need: the photo object has the number of likes and comments, as well as a bunch of other metadata. In our stream of photos, it could be the same image over and over and over, but since we’re in a RecyclerView, we could be reusing a cell and actually get a different image.

// 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 )

The model state takes those inputs and it splits them into the data that’s useful to us. The ViewIntentions are streams of data, but the model is the current state at any given time. We have an initial state defined here at the bottom of the screen, and this is important when you’re doing something in this sort of structure because you’re working with streams and streams don’t guarantee that you have data, because you don’t know if it’s emitted any data. You need to be careful to define reasonable base states, reasonable initial states, so that your user isn’t left wondering what’s going on.

You can think about state change as a function. So, rather than like saying state.(x=5) , we’re going to emit a function from the current state to the next state.

4. View Intentions => Model State Changes

val model: (ViewIntentions) -> Observable<(State) -> State> = {   intentions ->   val modeChanges: Observable<(State) -> State> =     intentions.nodes.map{}    val photoChanges: Observable<(State) -> State> =     intentions.photos.map{}    val tsChanges: Observable<(State) -> State> =     intentions.globalReadTs.map{}    Observable.merge(     modeChanges, photoChanges, tsChanges   ) }

So, here is the model function. If you look at the signature, it’s takes our ViewIntentions , which is our input and it needs to output a stream of state changes that’s the (State) -> State functions. We’re taking the modes and we’re mapping them to produce some state change function. And at the end, we take all

these state change streams and then collapse them into one.

val modeChanges: Observable<(State) -> State> =   intentions.modes.map{  mode ->     {state: State -> State(state.photo, state.isNew, mode)} }

Regarding mode changes, this is a simple change: we listen for a new mode which is either tapped or untapped, then we’ll return a function from current state to a new state where we maintain the same photo, the same isNew , and we change the mode.

5. Model State => View-Model State

val 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 = /* ...*/         )     } }

We were emitting a bunch of state-arrow-state functions, and what we’re doing behind the scenes is that we’re scanning those observables together into a single observable that emits a state. So, no longer any state change.

At the end of the day, all we really care about is how this component looks and the component has no idea how to render a photo. It’s just image views and text views – it doesn’t know what a photo is. We’re taking the photo and we’re splitting it out into the View state that we need to be able to render the cell. And so, if a photo has a number of likes we split that out into number of likes, and if it has comments and we split that up into the number of comments, and it has a mode and we say whether we should show the highlight.

6. View-Model => Mutate the View

class PhotoComponent(   ViewIntentions: ViewIntentions,   view: PhotoCellView ): StartStopComponent by Component (   driver = /*...*/   model = /*...*/ ) )

This is where we give it the initial input. We provide the View that we’re going to mutate, the native Android View and we want to say that we’re a start-stop component. That means there’s a start method and a stop method. And that’s useful so that you can start on resume stop on pause. When we construct our component, we need to provide a driver and a model.

driver = ViewDriver<ViewIntentions, ViewModel.State>(     intention = ViewIntentions,     onViewState = { old, state ->       if (old?.imgUrl != state.imgUrl) {         view.setImg(state.imgUrl)       }     }   )

View driver needs the inputs and it needs our actual function to mutate the native Android View. What this function gets as input is the prior state and the current state. And the reason we get the prior state is so that you can optimize out expensive mutations to your View because this thing could fire often. In this example, we are looking at whether image URLs are different, then set it in the View.

model = ViewDriver.makeModel(   initialState = Model.initialState,   createState = Model.createState,   model = Model.model,   viewModel = ViewModel.viewModel )

In the model, we used ViewDriver.makeModel to provide our initial state, and createState is something to get the types to work. This produces something generic, but the important thing

is that it’s type-safe.

How does this work? We enforce this separation between the ViewIntentions , model , and ViewModel , and RxJava does all the work, but there’s also one more thing called “scan”.

What scan does is it gives some initial states. So, we’re basically building up on some piece of information over time. Our combination function for this scan (what was sum before) is now a function that we take our current state, which starts at initial state, and we get the new state-arrow-state transformation function, and then we just apply the function to the current state that gives us our next state. Then, we use that as a current state, apply the next transformation function when it comes in and then we have the third state, the fourth, the fifth, and what you get is a stream that gives you the current state changing over time.

This setup has a great advantage because types are our friends, and we don’t always have types when we’re working with web stuff. If you’ve ever worked with JavaScript, you probably feel this pain. Kotlin and Java have type systems and they really help you develop.

It’s also highly composable. When we deployed this in the wild, we had a header view that had components in it, that had buttons in it, and all of those maintained themselves. We used the same button in several places in our app and all we had to do was make sure we started it off with the right streams of data and it worked.

However there are some bad things as well. This is a completely new paradigm for building UI components, so you need practice to do it right. As a consequence, we’re the only people at our office who can really deal with these components, because we haven’t had time to teach our colleagues yet.

Q: Do you have any experience with using Fabric or Crashlytics for this?.

We haven’t needed to do a stack trace on these components because we had state. I don’t recall ever getting into a situation where looking at the state didn’t immediately solve any debugging issue.

Q: How did you solve the Dexcount issue when switching from imperative to functional programming?.

As long as you’re not targeting anything too low, it’s pretty easy, you just have multidex. It eventually worked. But it is a liability and we definitely hit our dex limit.

See the discussion on Hacker News .

Get new videos & tutorials — we won’t email you for any other reason, ever.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Redux-ing UI bugs: Borrowing the Best of the Web to Make Native Better

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址