This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021

Hi , 🙂

The world is big and small, with many and few components.

We’ve seen a lot about status page components that are common in development, but what about in JetPack Compose? Although there are also big guy wrote related demo, but if you want to apply to the actual, it is not enough.

This paper is to solve how to customize a status page tool in line with the actual development, and analyze the specific principle and design ideas.

rendering

This rendering is simple, just a normal status page, so it’s not worth talking about. Let’s look at the basic functionality needed to implement a status page component.

Demand analysis

  • supportcomposeview
  • Layered design, introduced on demand
  • Support global/local configuration default default page
  • Supports global retry and anti-shake processing
  • .

For example, for composing a status page component in a View, you already know how to do that, but Compose? So let’s start thinking about how to design the status page component StateX.

The basic idea

For those of you who have written compose’s code, you should know that it is much simpler. Because compose is a declarative programming idea, which we can think of as data-driven, the simplest way to do it is:

Define a variable, and each time you change it, the corresponding place where the variable is used will trigger recombination, so you can write pseudo-code like this:

   val state = mutableStateOf (Loading)

   when(state){

     Loading -> {}

      Error -> {}
  
      Content -> {
       // Load error, change the state
       state = Error
     }

      xxx

 }
Copy the code

Yes, the implementation in Compose is that simple, and the principle is easy to understand.


Deficiency in

But if you do, you could be walking into a trap, right? Just think, does this really meet our actual business scenario?

Let’s restore a real business scenario.

This is a list page showing the top user likes. In our general thinking, we would write:

  1. First show loading
  2. The request data
  3. Request successful – set data, error – display default page

For compose, we’ll write pseudocode along the same lines.

@Composable
fun Test(a) {
    var state = remember {
        mutableStateOf(StateEnum.LOADING)
    }
    when (state.value) {
        StateEnum.LOADING -> {
        }
        StateEnum.CONTENT -> {
            // Display success
        }
        StateEnum.ERROR -> {
            // Display error}}// Get the result
    val data = getData()
    if (data is Success) {
        state.value = StateEnum.CONTENT
    } else if (data is Error) {
        state.value = StateEnum.ERROR
    }
}
Copy the code

Is this process right? If so, congratulations, you’ve fallen into the old rut and the code will loop forever.

In a traditional view, it’s a command callback, because the corresponding method is only executed at command time, and we don’t have to worry about irrelevant methods being called. In compose, where the recombination will perform all the calls and determine if it is needed, we must consider how to avoid repeated recombination.

So what if getData() continues after changing state?


How to solve it?

You might be thinking, well, why don’t I just write the request logic in CONTENT?

Yes, but the question is, how can Loading be shown?

Can I trigger the request logic in Loading?

You can do it, but how? I know it works, but how do you encapsulate it?

So is there an easy, packaged component that I can refer to or use?

To solve these problems, I wrote a simple component called StateX that you can copy and change on your own.

Parsing StateX

To design a component that can be used by both compose and View, it is inevitable to have two models, which are designed in layers and can be imported on demand. For common modules, separate modules need to be added to the base component. Therefore, StateX is divided into three modules:

  • The Basic base layer, which includes some basic configurations that compose and View share
  • Compose belongs to the separate model of compose
  • The view belongs to a separate Model of the View layer

Thanks to @nuggets Range’s StateLayout, the core code for view comes from here, and the reason is simple enough to use.


Basic layer -Basic design

Now that compose and View are supported, what are the basic features required?

enum class StateEnum {
    LOADING,
    EMPTY,
    ERROR,
    CONTENT
}
Copy the code
interface IState {
    val state: StateEnum
    var enableNullRetry: Boolean
    var enableErrorRetry: Boolean
		
  	/** The load is successful *@param[tag] can pass arbitrary data and will be received at the callback
    fun showContent(tag: Any? = null). }Copy the code

We define a base interface that represents the interface common to compose and View, with StateEnum representing the corresponding state enumeration.

But what about the compose and View configuration items?

Since the two configurations must be different, is there a way to unify the two Settings as well?

For ease of setup, I defined a static class for StateX.

object StateX {
    /** By default, click anti-shake time */
    var defaultClickTime = 600L

    /** Empty data retry switch */
    var enableNullRetry = true

    /** Abnormal retry switch */
    var enableErrorRetry = true
}
Copy the code

At first glance, it doesn’t look like much. This static class just corresponds to some basic common configuration items and doesn’t seem to have much to do with other Model configuration items. However, Kotlin supports extension functions and methods, so that, with a unique StateX entry, we can add StateX based extension functions to the corresponding compose and View models for easy addition of configuration items. It’s that simple.


Compose layer design

The configuration design

The configuration layer is a simple class, and we define an internal modified static StateComposeConfig object, It is easy to initialize composement-config for internal component access while defining the StateX extension function composeConfig.

class StateComposeConfig {...internal var emptyComponent: stateComponentBlock = {}
    ...
    internal var onContent: stateBlock? = null.fun onContent(block: stateBlock) {
        this.onContent = block
    }
		...

    fun emptyComponent(component: stateComponentBlock) {
        this.emptyComponent = component
    }
}

/** Internal StateCompose configuration */
internal val composeConfig by lazy(LazyThreadSafetyMode.PUBLICATION) {
    StateComposeConfig()
}

/** Configures the state-compose configuration */
fun StateX.composeConfig(config: StateComposeConfig. () - >Unit) {
    composeConfig.apply(config)
}
Copy the code

Interface design

Compose should also be aware of load failures, errors, successes, and loading, with a value corresponding to the current state.

interface IStateCompose : IState {

    /** The value attached to the current state */
    val tag: Any?

    /** Error callback */
    fun onError(block: stateBlock). }Copy the code

Concrete implementation class

The concrete implementation class StateComposeImpl is also very simple and concise. We keep an internal _internalState variable that represents the current State and wrap it with State so that when we call the showXxx() method to display the concrete State, We update internally the corresponding state and the attached value, so that _internalState is updated, and then the reorganization at the call is triggered.

The reason for keeping a tag is that in practice, when we display the error page, the corresponding copy is updated according to the specific error, rather than unchanged. Therefore, we need to cache a tag corresponding to the current state, which is convenient for us to use in reorganization.

class StateComposeImpl constructor(stateEnum: StateEnum = StateEnum.CONTENT) : IStateCompose {
		
  	// This is a type alias, just to save extra writing in the method arguments,
  	// The downside is that it may reduce readability, depending on itself
  	// internal typealias stateBlock = (tag: Any?) -> Unit
  	// Call showContent when the data is loaded.
    private var onRefresh: stateBlock? = null
  	// Exception callback, global error callback used by default
    private var onError: stateBlock? = composeConfig.onError
    ...

    /** The current internal mutable state */
    private var _internalState by mutableStateOf(StateEnum.CONTENT)

    /** Current state internal cache tag */
    private var _internalTag: Any?

    override val state: StateEnum
        get() = _internalState
    override val tag: Any?
        get() = _internalTag

    override fun onError(block: stateBlock) {
        this.onError = block
    }
  	...

    override fun showError(tag: Any?).{ onError? .invoke(tag) newState(StateEnum.ERROR, tag) } ...private fun newState(newState: StateEnum, tag: Any?). {
        _internalState = newState
        _internalTag = tag
    }
}
Copy the code

StateCompose

The StateCompose component is a specific Compose component that we provide externally. You only need to pass in the corresponding controller and override the component corresponding to the corresponding state. By default, the component is globally defined. In addition, we buffered the Error in the Error callback and called the showLoading() method when retried, triggering the onRefresh callback refresh.

@Composable
fun StateCompose(
    stateControl: IStateCompose,
    loadingComponentBlock: stateComponentBlock 
  		= composeConfig.loadingComponent,
    ...
    contentComponentBlock: stateComponentBlock.) {
    when (stateControl.state) {
        StateEnum.LOADING ->
      			loadingComponentBlock(stateControl, stateControl.tag)
        StateEnum.CONTENT ->
      			contentComponentBlock(stateControl, stateControl.tag)
      	StateEnum.ERROR ->
      			if (stateControl.enableErrorRetry) {
            StateBoxComposeClick(block = {
                stateControl.showLoading(null)
            }) {
                errorComponentBlock(stateControl, stateControl.tag)
            }
        } elseerrorComponentBlock(stateControl, stateControl.tag) ... }}Copy the code

extension

In order to better solve the actual problems that cannot be solved directly in the UI, we will pull the viewModel, which provides the following extension for easy use:

/** Generate an IStateCompose in the ViewModel@paramStateEnum Default state * */
inline fun ViewModel.lazyState(
    stateEnum: StateEnum = StateEnum.CONTENT,
    crossinline obj: StateComposeImpl. () - >Unit= {}): Lazy<IStateCompose> = lazy(LazyThreadSafetyMode.PUBLICATION) {
    StateComposeImpl(stateEnum).apply(obj)
}

/** * This method can be used to facilitate initialization of state when state is cached in ViewModel * the advantage is that the only thing initialized can be placed in the [block] callback without worrying about repeated initialization *@paramComposeState State to remember State * */
@Composable
inline fun rememberState(
    composeState: IStateCompose.crossinline block: IStateCompose. () - >Unit= {}): IStateCompose = currentComposer.cache(false) {
    composeState.apply(block)
}


/** * A new IStateCompose is generated directly by recording the state of the IStateCompose@paramStateEnum Default state *@paramBlock uses * */ for the IStateCompose callback
@Composable
inline fun rememberState(
    stateEnum: StateEnum = StateEnum.CONTENT,
    crossinline block: IStateCompose. () - >Unit= {}): IStateCompose = currentComposer.cache(false) {
    StateComposeImpl(stateEnum).apply(block)
}
Copy the code

use

As shown in the figure, we define a current state in the viewModel and a method to load the data. In the Ui section, we use a rememberState method to cache the current state, where we can also initialize a partial callback to state. And loading data is enabled, which triggers the onRefresh callback, i.e. loading page data, which calls our getData() method inside the ViewModel. When the data is loaded, we can directly drive the state to display the current successful loading status, which triggers the external reorganization. Our StateCompose will then display the success page.

The small egg:

To accommodate the fact that sometimes we may not want to manage state in the viewModel, I also provide another extension, rememberState.

To cache an IStateCompose state, but this scenario is really rare, so it depends on your business.

It’s as simple as that. The state page for Compose is already shared, and you can use StateX to change it.

As for the view part of the design, we can know a look at the source code, and we have used the view for many years, this is not the focus of this article.

conclusion

This article is a common example of Compose implementation, which will help you understand the Compose programming philosophy. I will continue to delve into the source code design of Compose and the scenario solution in practice.

If this article was helpful to you, please like it and support it

Thank you

[Open source project] The Compose version of Compose is easy to use

One line of Android code builds the default page for the entire application