background

In the Android development world, we are familiar with MVVM and other architecture design, most projects will use RxJava to build THE MVVM architecture. This article introduces the MvRx and Epoxy frameworks of Airbnb, including the following:

  • MvRx and Epoxy frame applications
  • How does MvRx initialize ViewModel and State
  • How MvRx implements data and view association (changing State automatically triggers view redraw)
  • How does Epoxy Implement DIFF Updates

The open source world is rich and colorful, and I hope to open a window into the beauty of responsive programming frameworks from a wider perspective.

Introduction to MvRx+Epoxy Foundation

Introduction to the

*MvRx (ModelView ReactiveX, pronounced mavericks) is the Android framework from Airbnb that we use for nearly all product development at Airbnb.When we began creating MvRx, our goal was not to create yet another architecture pattern for Airbnb, it was to make building products easier, faster, and more fun. All of our decisions have built on that. We believe that for MvRx to be successful, it must be effective for building everything from the simplest of screens to the most complex in our app.

Epoxy (I ˈ paks ē) is an Android library for building complex screens in a RecyclerView. We developed Epoxy at Airbnb to simplify the process of working with RecyclerViews, and to add the missing functionality we needed. We now use Epoxy for most of the main screens in our app and it has improved our developer experience greatly.

Both libraries are from Airbnb, and here’s an excerpt from github’s project description:

  • MvRx is an application architecture built by Airbnb, which is used in almost all of Airbnb’s products
  • Epoxy mainly helps build complex multi-view type Recyclerview

As you can see, MvRx and Epoxy are both designed to make development easier, faster, more fun

Design concept

MvRx uses a combination of the following technologies and concepts:

  • Kotlin
  • Android Architecture Components
  • RxJava
  • React (conceptually)
  • Epoxy (optional)

In fact, MvRx and Epoxy are available separately and are optional in the MvRx architecture specification. MvRx introduces a series of concepts of React based on Google Architecture, but is more inclined to the responsive design of data layer. With MvRx alone, views need to update themselves after data changes, and this update is often imperative. (In React, data changes ➡️ diff computed view changes ➡️ rerender, all automatically performed by the framework, the developer’s task is simply to describe the view)

Epoxy is described as an aid to RecyclerView. Airbnb thought that adapter configured items ina mechanical and confusing way, which could not well support some of their complex scenes such as viewType, Pagination, tablet, and item animations, so they adopted a new way to describe lists. With epoxy, the view layer is responsive, developers only describe views and their relationship to data, and diff and updates are done by the framework.

MvRx+Epoxy, combined with Kotlin’s syntactic sugar, writes code that looks a lot like React. The application developed in this way has the following characteristics:

  • Declarative UI
  • Responsive Architecture (MVVM)

use

The core concept

  • State

MvRxState contains all the data needed to render the screen. State is immutable and its implementation class must be an immutable Kotlin Data class. Only ViewModel can modify State. To change the State, call the Copy method of the Kotlin data class and create a new State object. The State change triggers the View’s invalidate() method to redraw the interface.

  • ViewModel

MvRxViewModel is completely inherited from ViewModel in Google Application architecture. The difference is that MvRxViewModel contains a State instead of LiveData, and View can only observe changes in State. You must pass in an initialState (the initialState of the View) when you create the ViewModel.

  • View

The BaseMvRxFragment interface implements the MvRxView interface, which has an invalidate() method. The invalidate() method is called whenever State changes. The View State is ephemeral, and invalidate() needs to be overwritten to continually update the View. For optimization, do not update the View if the State value is the same as last time, or use epoxy (Automatic diff). A View can either observe changes in the entire State or just observe changes in one or a few properties of the State.

  • Async

If data comes from a network and needs to be described as being loaded, Async can be used for this type of data. Async is a sealed class with four subclasses: Uninitialized, Loading, Success and Fail. MvRxViewModel provides an extension to an Observable called execute that converts an Observable to Async. When called Observable.execute(), it is automatically subscribed. Then emit Async events.

Simple Example

 //1. Abstract data State
data class HelloWorldState(val title: String = "Hello World") : MvRxState

 //2. Inherit MvRxViewModel and implement ViewModel
class HelloWorldViewModel(initialState: HelloWorldState) : MvRxViewModel<HelloWorldState>(initialState){
    fun getMoreExcited(a) = setState { copy(title = "$title!")}}//3. Inherit BaseMvRxFragment and override invalidate
class HelloWorldFragment : BaseMvRxFragment() {
    private val viewModel: HelloWorldViewModel by fragmentViewModel()
    private lateinit var marquee: Marquee

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? =
        inflater.inflate(R.layout.fragment_hello_world, container, false).apply {
 findViewById<Toolbar>(R.id.toolbar).setupWithNavController(findNavController())
            marquee = findViewById(R.id.marquee)
    }

 override fun invalidate(a)= withState(viewModel) { state-> marquee.setTitle(state.title) marquee.setOnClickListener { viewModel.getMoreExcited() } }}Copy the code

1. Abstract data State

  • State must be an immutable Kotlin data class

2, inherit MvRxViewModel, implement ViewModel

There are two ways to define a ViewModel:

  • ViewModel requires no additional arguments, such as example
  • The ViewModel contains construction parameters other than initialState, so you need to implement the factory method
The first parameter must be initialState

class HelloWorldViewModel(
        initialState: HelloWorldState,
        private val dadJokeService: DadJokeService
) : MvRxViewModel<HelloWorldState>(initialState) {

    fun getMoreExcited(a) = setState { copy(title = "$title!")}// Define an accompanying object that implements the MvRxViewModelFactory interface

 companion object : MvRxViewModelFactory<HelloWorldViewModel, HelloWorldState> {
        override fun create(viewModelContext: ViewModelContext, state: HelloWorldState): HelloWorldViewModel {
            val service: DadJokeService by viewModelContext.activity.inject()
            return HelloWorldViewModel(state, service)
        }
    }
}
Copy the code

Once defined, MvRx provides three extension functions for Fragments to get:

  • By fragmentViewModel() : ViewModel of the Fragment cycle
  • By activityViewModel() : ViewModel of the Activity cycle, mainly used to share State between different fragments
  • By existViewModel() : ViewModel for the Activity cycle, but it must already exist, or an exception will be thrown if it doesn’t. This method is used when there are pre-data dependencies, such as when multiple fragments collaborate and rely on data set by the previous Fragment

Also note:

  • The ViewModel constructor must pass in an initialState
  • Only the ViewModel can change State, and a new object should be created by calling the Copy method of the Kotlin data class

3, Inherit BaseMvRxFragment, rewrite invalidate

  • Use the factory method withState() to get the State value and redraw the view

The introduction of Epoxy

The examples above only use MvRx. Next, we introduce the Epoxy library, and the Fragment needs some transformation. (The State and ViewModel parts are exactly the same)

First, Epoxy’s two key components:

  • EpoxyModel: Describe what the (Item) view looks like
  • EpoxyController: Used to control which EpoxyModels to be displayed on RecyclerView and where

1. Create EpoxyModel

In fact, the EpoxyModel is automatically generated by the framework. Epoxy automatically generates a Model_ extension of the original class name by customizing the View and annotating it:

//1. Customize the View, annotate it
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class Marquee @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
    private val titleView: TextView
    private val subtitleView: TextView

    init {
        inflate(context, R.layout.marquee, this)
        titleView = findViewById(R.id.title)
        subtitleView = findViewById(R.id.subtitle)
        orientation = VERTICAL
 }

    @ModelProp 
    fun setImgUrl(imgUrl: String) { 
        //show image with you own way 
    }

    @TextProp
    fun setTitle(title: CharSequence) {
        titleView.text = title
    }

    @TextProp
    fun setSubtitle(subtitle: CharSequence?). {
         subtitleView.visibility = if (subtitle.isNullOrBlank()) View.GONE else View.VISIBLE
         subtitleView.text = subtitle
    }

    @CallbackProp
    fun setClickListener(clickListener: OnClickListener?). {
        setOnClickListener(clickListener)
    }
}
Copy the code
  • @modelView annotation class, autoLayout describes the width and height of the item when added to RecyclerView
  • The @modelProp annotation method can have only one parameter. Using this annotation, the resulting EpoxyModel will have corresponding fields
  • @textProp annotation method, parameter type must be CharSequence. This annotation is even more convenient when the field is a String, and the resulting EpoxyModel contains several overloaded methods to use Android String resources directly
  • CallbackProp annotation method. The parameter type is a callback interface. The callback interface is handled by this annotation in that, unlike normal fields, it does not affect the View display and needs to be unbound (set to NULL to prevent memory leaks) when the item rolls off the screen

2. Use EpoxyModel in Controller

This generates a MarqueeModel_ class, which is used in buildModels() :

class HelloWorldEpoxyFragment : BaseFragment() {
    private val viewModel: HelloWorldViewModel by fragmentViewModel()

    //2. Create epoxyController and override buildModels()
    override fun epoxyController(a) = simpleController(viewModel) { state ->
        // The configured items are displayed in RecyclerView in order
        marquee {
            id("marquee")    // You must provide an ID for diff. Otherwise it will crash
            title(state.title)
            clickListener { _ -> viewModel.getMoreExcited() }
         }
     }
}
Copy the code

3, integrate to RecyclerView

Finally modify the Fragment to call requestModelBuild() to redraw the interface when receiving an invalidate() notification:

 //3. Trigger redraw when data is updated
override fun invalidate(a) = recyclerView.requestModelBuild()
Copy the code

It can be seen that after the introduction of EpoxyModel, the development mode of RecyclerView is no longer to write Adapter, but to define EpoxyModel one by one. After the interface is divided into EpoxyModel one by one, the reuse of elements also becomes very simple. The development of the whole interface is just like building blocks.

Realize the principle of

MvRx

Define and get the ViewModel

by fragmentViewModel()

Notice that when we get the ViewModel, we just define it, we don’t create initialState and ViewModel, the framework does that for us.

According to the MvRx specification, viewModels can only be obtained through the activityViewModel()/fragmentViewModel()/existingViewModel() provided by the framework. MvRx will do this for us by obtaining the ViewModel in these ways:

  1. Returns a Lazy subclass that triggers the creation of the ViewModel when onCreate is hosted
  2. The reflection constructs initialState and ViewModel
  3. Call the ViewModel Subscribe () method to subscribe to State changes, and call back the View’s invalidate() method if State changes

The following uses by fragmentViewModel() as an example, focusing on how to get initialState and create the one returned by the ViewModel.

Create the ViewModel in the following order:

  1. Reflection executes the create() method on the Companion object of MvRxViewModel
  2. If this fails, the reflection gets the MvRxViewModel constructor created, requiring that the ViewModel must have only one constructor and that the constructor must have only one parameter of type MvRxState (initialState).

InitialState is created in the following order:

  1. Reflection initialState() method creation of the Companion object that executes MvRxViewModel
  2. If that fails, the reflection gets State’s parameter-constructor is created, requiring that the constructor have only one argument of type Parcel
  3. If it fails, the default no-argument constructor for reflection to get State is created

Finally, notice that the ViewModel has been created and wrapped around lifecycleAwareLazy() :

class lifecycleAwareLazy<out T>(private val owner: LifecycleOwner, initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    
    @Volatile
    @SuppressWarnings("Detekt.VariableNaming")
    private var _value: Any? = UninitializedValue
    // final field is required to enable safe publication of constructed instance
    private val lock = this

    init {
        owner.lifecycle.addObserver(object : LifecycleObserver {
            @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
            fun onStart(a) {
                if(! isInitialized()) value owner.lifecycle.removeObserver(this)}})}}Copy the code

LifecycleAwareLazy is a Lazy implementation class. Lazy is not created when the ViewModel is used, but it listens for the host lifecycle. The ViewModel is created when onCreate() is hosted. This is done to perform some initial data logic as quickly as possible, such as requesting the network in ViewModel Init (). That is, init() of the ViewModel starts executing when it hosts onCreate.

Modify the data

fun setState(reducer: S.() -> S)

fun Observable.execute(stateReducer: S.(Async) -> S)

Here’s a question 🤔 :

  1. Why is setState/execute a lambda?
  2. How does execute convert Observable to Async?

In MvRxViewModel, the two main methods for updating State are setState and execute, both of which take a lambda (extension function on State) as an argument. Execute is an extension function of Observable. Rxjava is often used in logical processing. Execute can easily transform Observable into Async object. Execute calls are actually setState.

//BaseMvRxViewModel.kt

protected fun setState(reducer: S. () - >S) {
    if (debugMode) {
        / /...
 } else {
        stateStore.set(reducer)
    }
}
Copy the code

As you can see, setState simply forwards the call to the set() method of MvRxStateStore, and the lambda block is then stored in a queue for execution. The scheduling of tasks in queues involves the design of a double queue.

Double queue design

//RealMvRxStateStore.kt
class RealMvRxStateStore<S : Any>(initialState: S) : MvRxStateStore<S> {
    // State is stored in the Observable
    private val subject: BehaviorSubject<S> = BehaviorSubject.createDefault(initialState)

    /** * A subject that is used to flush the setState and getState queue. The value emitted on the subject is * not used. It is only used as a signal to flush the queues. */

 private val flushQueueSubject = BehaviorSubject.create<Unit> ()private val jobs = Jobs<S>()

    init {
        // All set/ GET tasks are processed in the same background thread
        flushQueueSubject.observeOn(Schedulers.newThread())
        // We don't want race conditions with setting the state on multiple background threads
        // simultaneously in which two state reducers get the same initial state to reduce.
            .subscribe( { _ -> flushQueues()  } , ::handleError)
        // Ensure that state updates don't get processes after dispose.
            .registerDisposable()

    }


    / / for the State
 override fun get(block: (S) - >Unit) {
        jobs.enqueueGetStateBlock(block)
        flushQueueSubject.onNext(Unit)}/ / update the State
 override fun set(stateReducer: S. () - >S) {
        jobs.enqueueSetStateBlock(stateReducer)
        flushQueueSubject.onNext(Unit)}private class Jobs<S> {
        // Dual queue design, set/ GET tasks are stored in two queues
        private val getStateQueue = LinkedList<(state: S) -> Unit> ()private var setStateQueue = LinkedList<S.() -> S>()

        @Synchronized
        fun enqueueGetStateBlock(block: (state: S) - >Unit) {
            getStateQueue.add(block)
        }

        @Synchronized
        fun enqueueSetStateBlock(block: S. () - >S) {
            setStateQueue.add(block)
        }

        @Synchronized
        fun dequeueGetStateBlock(a): ((state: S) -> Unit)? {
            return getStateQueue.poll()
        }

        @Synchronized
        fun dequeueAllSetStateBlocks(a): List<(S.() -> S)>? {

            // do not allocate empty queue for no-op flushes
            if (setStateQueue.isEmpty()) return null

            val queue = setStateQueue
            setStateQueue = LinkedList()
            
            return queue
        }
    }

 private tailrec fun flushQueues(a) {
        // 1. Complete the set task
        flushSetStateQueue() 

        // perform the first get task
        val block = jobs.dequeueGetStateBlock() ?: return
        
        block(state)
        // 3. Execute again (in case the block calls the set task again)
        flushQueues()
    }

    private fun flushSetStateQueue(a) {
        val blocks = jobs.dequeueAllSetStateBlocks() ?: return

        for (block in blocks) {
            val newState = state.block()
            // do not coalesce state change. it's more expected to notify for every state change.
            if(newState ! = state) {// Update State to trigger the change callback
                subject.onNext(newState)
            }
        }
    }

}
Copy the code

As you can see, both the update and get State methods pass in a lambda block, and these blocks end up running in a background thread (to solve the concurrent State modification problem). However, MvRx designs a double-queue task scheduling algorithm for scheduling these blocks, which makes the priority of setState block higher than that of getState block.

In a dual-queue, setStateQueue has a higher priority. Before fetching a task from getStateQueue, all tasks in setStateQueue must be executed. This design mainly solves a competition problem, namely getState block calls setState block.

Consider this code:

getState { state ->

     if (state.isLoading) return
     setState { state ->
     state.copy(isLoading = true)}// make a network call
 }
Copy the code

If executed twice in a row, two network requests may be issued, but they are not intended. If the state is not Loading for the first time, set it to Loading, and then request the network. Return the second time it has loaded.

For the sake of analysis, the two calls are simplified as follows:

getStateA {
    setStateA {}
}

getStateB {
    setStateB {}
}
Copy the code

If there is only one queue, blocks are executed in the same order as inserts:

GetStateA -> getStateB -> setStateA -> setStateB

But for a dual-queue design:

  1. After both getState blocks are inserted into the queue
  • setStateQueue: []
  • getStateQueue: [A, B]
  1. After the first getState block is executed
  • setStateQueue: [A]
  • getStateQueue: [B]
  1. Because setStateQueue has a higher priority, setStateA is executed first
  • setStateQueue: []
  • getStateQueue: [B]
  1. Finally, setStateB is executed
  • setStateQueue: []
  • getStateQueue: []

The final order of execution of blocks will be:

GetStateA ->setStateA->getStateB ->setStateB

Rendering the callback

fun invalidate()

As you can see from the previous section, State is not simply stored as a member variable, but also exists in a BehaviorSubject object. The BehaviorSubject derives from the Subject, which implements both Observable and Observer interfaces, meaning that the Subject can be either an Observer or an observed. In MvRxStateStore, updating the State value triggers the observer to be notified of the change.

The place to register an observer is when the ViewModel is first fetched:

inline fun <T, reified VM : BaseMvRxViewModel<S>.reified S : MvRxState> T.fragmentViewModel(
    viewModelClass: KClass<VM> = VM::class.crossinline keyFactory: () -> String = { viewModelClass.java.name }
) where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) {

 MvRxViewModelProvider.get(
        viewModelClass.java,
        S::class.java,
        FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), this),
        keyFactory()
    ).apply {

        // After creating the ViewModel, register change listeners directly with its State Observable
        subscribe(this@fragmentViewModel, subscriber = { postInvalidate() } ) }

 }
Copy the code

Once the ViewModel is created, the BehaviorSubject directly registers an observer with the BehaviorSubject. Once the State changes, the BehaviorSubject calls back to the mVRxView.postinvalidate () method, which finally triggers invalidate() to re-render the view.

Here’s a question 🤔 :

The LifeCycleOwner lifecycle is automatically aware when listeners are registered with LiveData, and if the host is no longer active, the observer is not notified of the change. This is an Observer registered. Is the lifecycle aware? If so, how?

summary

Sequence diagram of the above process:

  1. MvRxViewModel instance is created when Fragment onCreate()
  2. As the ViewModel reads or modifies data, these operations are placed in dual queues and processed sequentially in a background thread. In a dual-queue, write operations have a higher priority than read operations. All write operations must be completed before a read operation
  3. State is an observable. The ViewModel watches State and notifies the ViewModel whenever State changes. The ViewModel calls Fragment invalidate() to trigger a view refresh

Epoxy

Epoxy resin, also known as artificial resin, artificial resin, resin adhesive, etc. It is a kind of very important thermosetting plastics, widely used in adhesives, coatings and other uses. Glue data State and view RecyclerView

To build the Model

requestModelBuild()

When data changes, the interface needs to be updated by calling the requestModelBuild() method of EpoxyController:

public abstract class EpoxyController {
    / /...

    private final Runnable buildModelsRunnable = new Runnable() {
      @Override
      public void run() {
        // Do this first to mark the controller as being in the model building process.
        threadBuildingModels = Thread.currentThread();

   
        // This is needed to reset the requestedModelBuildType back to NONE.
        // As soon as we do this another model build can be posted.
        cancelPendingModelBuild();

        helper.resetAutoModels();    
        modelsBeingBuilt = new ControllerModelList(getExpectedModelCount());
        timer.start("Models built");


        try {

          // An abstract method that needs to be overridden to describe its own interface when implementing EpoxyController

          //1.build models
          buildModels();
        } catch (Throwable throwable) {

          timer.stop();
          modelsBeingBuilt = null;
          hasBuiltModelsEver = true;
          threadBuildingModels = null;
          stagedModel = null;

          throw throwable;
        }

        addCurrentlyStagedModelIfExists();

        timer.stop();

        //2. Interceptor
        runInterceptors();

        filterDuplicatesIfNeeded(modelsBeingBuilt);
        modelsBeingBuilt.freeze();

        timer.start("Models diffed");
        
        //3.diff models
        adapter.setModels(modelsBeingBuilt);
        // This timing is only right if diffing and model building are on the same thread
        timer.stop();

    
        modelsBeingBuilt = null;
        hasBuiltModelsEver = true;
        threadBuildingModels = null; }};// Request an immediate refresh
    public void requestModelBuild() {
      if (isBuildingModels()) {
        throw new IllegalEpoxyUsage("Cannot call `requestModelBuild` from inside `buildModels`");
      }

      if (hasBuiltModelsEver) {
        requestDelayedModelBuild(0);
      } else{ buildModelsRunnable.run(); }}}Copy the code

As you can see, the requestModelBuild() method posts a buildModelsRunnable task to the queue for serial processing via modelBuildHandler. The task calls the buildModels() method (which we overwrote when we created EpoxyController), goes to our code describing the interface, and builds all the Models.

ModelBuildHandler is passed in the constructor and mainLooper is used by default, meaning that each buildModels() default is thrown into the main thread’s message queue and processed serially. To process in child threads, use AsyncEpoxyController.

Notice that requestDelayedModelBuild is called, which is related to frequency limited updates.

Debounce Frequency update

Users sometimes trigger multiple data updates in quick succession, and it is best to build the Model directly from the last data, since rebuilding the Model each time is not necessary and affects performance. In order for Developers to be able to call requestModelBuild() at any time without worrying about the details, Epoxy uses a bit to track whether buildModel tasks have been posted.

    // Request a delay to refresh
    // If the previous request has not been executed, the previous request will be cancelled
    public synchronized void requestDelayedModelBuild(int delayMs) {

      if (isBuildingModels()) {
        throw new IllegalEpoxyUsage(
            "Cannot call `requestDelayedModelBuild` from inside `buildModels`");
      }

    
      if (requestedModelBuildType == RequestedModelBuildType.DELAYED) {
        cancelPendingModelBuild(); // Cancel the last delay request
      } else if (requestedModelBuildType == RequestedModelBuildType.NEXT_FRAME) {
        return; // The request is executing, abort the call
      }
    
      requestedModelBuildType =
          delayMs == 0 ? RequestedModelBuildType.NEXT_FRAME : RequestedModelBuildType.DELAYED;
      modelBuildHandler.postDelayed(buildModelsRunnable, delayMs);
    }
Copy the code

Tag bit requestedModelBuildType:

@Retention(RetentionPolicy.SOURCE)

@IntDef({RequestedModelBuildType.NONE, RequestedModelBuildType.NEXT_FRAME, RequestedModelBuildType.DELAYED})

private @interface RequestedModelBuildType {
    int NONE = 0;        // There is currently no request
    int NEXT_FRAME = 1;  // A request is being executed
    int DELAYED = 2;     // There is a request currently, but it is waiting to be executed
}
Copy the code

Frequency limited update policies are as follows:

  • If the call is delayed refresh (delayMs>0), if the last delayed request has not been executed, the last request will be cancelled and the request will be added to the delay queue for execution.
  • If the call is immediate update (delayMs=0), the call is abandoned if there is a request in progress at the time of the call.

Take immediate updates as an example:

Suppose the user triggers the following flush in succession (I +1 each time) :

requestModelBuildA-> requestModelBuildB-> requestModelBuildC -> requestModelBuildD

When the first requestModelBuildA is executed, the requestedModelBuildType tag is set to NEXT_FRAME and an update task is posted. B then returns when it encounters the NEXT_FRAME flag, until buildModelA ** is set to NONE. After C post, D is blocked again.

It may end up being executed only twice:

buildModelA -> buildModelC

Each time, the latest value of the current State is taken, resulting in I =4.

diff

From the above, after buildModels() is built, adapter.setModels notifies the data to update: Diff still uses RecyclerView DiffUtil algorithm to compare whether the two EpoxyModels are the same item through ID () and whether the contents are consistent through equals().

EpoxyModel is automatically generated by EPOXYView for each custom View annotated by @ModelView. This generated class overrides the equals() and hashCode() methods to calculate a result that identifies the state of the View based on the values of all the fields. That is, whenever a field value changes, the state of the View changes and this item needs to be updated again.

Data binding

After calculating the data set of travel volume, RecyclerView Adapter will re-run the process:

OnBindViewHolder () calls EpoxyModel’s bind() method to set data to the View:

summary

Sequence diagram of the above process:

  1. RequestModelBuild () triggers RecyclerView updates, and repeated update tasks are discarded
  2. First, execute the buildModels() method to build the list of EpoxyModels, which is processed sequentially in a separate thread. Override the method to configure all the items to be displayed by RecyclerView, and the Model cannot be modified after the build
  3. Next, through the list of EPOxyModels, diff compares the state of the model, which is also handled sequentially in a separate thread. The status of the EpoxyModel depends on the equals() and hashCode() methods, which are overridden by each generated EpoxyModel based on the values of all the fields of the EpoxyModel
  4. Finally, the diff result is returned to the main thread and set to RecyclerView. OnBindViewHolder () calls EpoxyModel Bind () to bind the data to the View

summary

How does React build applications

Zh-hans.reactjs.org/docs/thinki…

MvRx relies on the React concept. Let’s look at how React builds applications:

  • Step 1: Divide the UI presented by the design draft into component levels
  • Step 2: Create a static page
  • Step 3: Determine the minimum and complete representation of State
  • Step 4: Determine which component owns State
  • Step 5: Add reverse data Flow (callback)

Here are some of the details:

1. About components

  • Components are defined according to the single function principle, where a component is responsible for only one function
  • Each component needs to match some part of the data model. Because UI structures and data models tend to adhere to the same information structure
  • When writing components, it is recommended to separate the processes of rendering the UI from adding interactions. Because writing a static page for an application requires a lot of code without much interaction detail; Adding interactive features takes a lot of detail and doesn’t require much code

2, About data (props and State)

  • Both props and state are used to hold information and control the presentation of the component. The difference is that the props is how the parent component passes data to the child component, while state is managed within the component itself. State has the ability to trigger changes to the data model. From this perspective, components can be divided into control components and presentation components, with control components owning State and combining other presentation components
  • Only the minimal set of variable states required by the application is retained, and all other data is computed from it
  • The data flow in React is one-way and flows up and down the component hierarchy
  • To determine which component has state, look for the common parent of all components rendered according to that state. If you can’t find one, you can simply create a new component and place it on top of all components (state promotion), and the common parent has state
  • A component at a lower level updates state in a higher-level component by adding a reverse data flow (callback)

3. Composition and inheritance

  • React doesn’t use inheritance to build components. Components can be flexibly customized using props and combinations: components accept any props, which can be of any type (basic datatypes /React elements/functions). To reuse non-UI functionality between components, React recommends extracting it as a separate JavaScript module that components can import directly without inheriting them through Extend

These things can also be applied to native development:

  1. Once you get the design, do a rough breakdown of the page, dividing the UI into a series of hierarchical components
  2. Identify some components that are minimal and complete
  3. The top View of the smallest complete component is regarded as the control View, which has state and contains certain interaction logic with the logic layer. The View below, as a display View, generally has only a few properties and setter/getter methods. The property values are calculated by the parent View and passed from top to bottom, and the events are returned to the control View through callbacks
  4. Code that needs to be reused can be extracted into a separate class or module

Reference

  • Github.com/airbnb/mave…
  • Github.com/airbnb/epox…
  • Zh-hans.reactjs.org/docs/thinki…

We are byte Shenzhen Feishu mobile terminal team, committed to creating the world’s leading document creation and content management tool, currently in shenzhen recruitment hot, interested children can send resume to [[email protected]]

IOS position: job.toutiao.com/s/Nx12FCw

Android position: job.toutiao.com/s/Nx1MJAA

The front position: job.toutiao.com/s/N9ofyju

The back-end position: job.toutiao.com/s/Nx1DXTR

Company benefits include:

Free meals, keep you fat; Monthly housing allowance of 1500 yuan; Apple MacBook Pro; Five insurances and housing fund in full, and purchase additional commercial insurance; Free gym + annual physical examination; 6 days -15 days annual leave and 8 days paid sick leave per year