hikage-runtime

Maven CentralMaven metadata URLAndroid Min SDK

This is the runtime dependency for Hikage, which you can implement to use state management features.

Configure Dependency

You can add this module to your project using the following method.

We recommend that you first refer to hikage-bom to use BOM for unified version management.

Add dependency in your project's gradle/libs.versions.toml.

[versions]
hikage-runtime = "<version>"

[libraries]
hikage-runtime = { module = "com.highcapable.hikage:hikage-runtime", version.ref = "hikage-runtime" }

Configure dependency in your project's build.gradle.kts.

implementation(libs.hikage.runtime)

Please change <version> to the version displayed at the top of this document.

Traditional Method

Configure dependency in your project's build.gradle.kts.

implementation("com.highcapable.hikage:hikage-runtime:<version>")

Please change <version> to the version displayed at the top of this document.

Function Introduction

You can view the KDoc click hereopen in new window.

State Management

Hikage provides a lightweight state management solution for View-based layouts.

Unlike Jetpack Compose recomposition, Hikage does not rebuild the layout tree when state changes. State changes take effect through observer callbacks and mutate existing View instances.

Hikage provides two state types:

  • NonNullState holds non-null values.
  • NullableState holds nullable values.

It is recommended to use View.setState(...) inside layout components. It subscribes the observer when the View is attached to window, automatically cancels it when detached, and subscribes again with the latest state when reattached.

If you need to observe state changes directly, observe(...) returns a StateSubscription. You can call cancel() to cancel it manually.

The following example

val textState = mutableStateOf("Hello, World!")
val drawableState = mutableStateOfNull<Drawable>()

var text by textState
var drawable by drawableState

LinearLayout(
    lparams = LayoutParams(matchParent = true),
    init = {
        orientation = LinearLayout.VERTICAL
    }
) {
    TextView {
        setState(textState) {
            this.text = it
        }
    }
    ImageView {
        setState(drawableState) {
            setImageDrawable(it)
        }
    }
    Button {
        this.text = "Click Me!"
        setOnClickListener {
            text = "Hello, Hikage!"
            drawable = drawableResource(R.drawable.ic_my_drawable)
        }
    }
}

In the example above, mutableStateOf creates a non-null state and mutableStateOfNull creates a nullable state.

When clicking the button, the existing TextView and ImageView are updated through state callbacks.

Notice

When using state.observe(...) directly, or using setState(...) on a non-View object, the observer is a long lifecycle subscription and will not be automatically released with View detach.

If the observer target may be destroyed before the state object, keep the returned StateSubscription and call cancel() when it is no longer needed.

Lifecycle State

For ViewModel scenarios, the recommended model is:

ViewModel owns state
Hikage builds View tree
hikage-runtime binds Flow/LiveData to View mutation

The com.highcapable.hikage.runtime.lifecycle provides lifecycle-aware adapters for StateFlow, Flow, LiveData and effect flows.

The View receiver variants automatically find LifecycleOwner from the view tree. If there is no available owner, pass lifecycleOwner explicitly.

All lifecycle-aware APIs collect or observe when the lifecycle reaches Lifecycle.State.STARTED by default. You can customize this with minActiveState.

StateFlow

StateFlow is the most common state holder in modern ViewModel usage. It has a current value, so setState(...) applies state.value immediately and keeps collecting updates with lifecycle.

The following example

data class LoginUiState(
    val username: String = "",
    val password: String = ""
) {
    val canSubmit get() = username.isNotBlank() && password.isNotBlank()
}

class LoginViewModel : ViewModel() {

    private val mutableUiState = MutableStateFlow(LoginUiState())

    val uiState = mutableUiState.asStateFlow()

    fun updateUsername(value: String) {
        mutableUiState.update { it.copy(username = value) }
    }
}

Used in Hikage layout.

The following example

TextInputEditText {
    doOnTextChanged { text, _, _, _ ->
        viewModel.updateUsername(text.toString())
    }
}

Button {
    setState(viewModel.uiState) {
        isEnabled = it.canSubmit
    }
}

Flow

For regular Flow, you can use setState(flow, initialValue) when you need an initial value before the first emission.

The following example

TextView {
    setState(
        flow = viewModel.runtimeTicker,
        initialValue = 0
    ) {
        text = "Flow ticker: $it s"
    }
}

If you do not need an initial value, use collectState(...).

The following example

TextView {
    collectState(viewModel.runtimeTicker) {
        text = "collectState mirrored $it"
    }
}

LiveData

LiveData is also supported for existing Android projects. If the LiveData already has an initialized value, Hikage applies it before observing.

The following example

TextView {
    setState(viewModel.compatibilityStatus) {
        text = it
    }
}

Effect

For one-shot UI events such as toast, navigation and dialog display, use collectEffect(...).

Unlike state collection, effects are collected one by one.

The following example

collectEffect(viewModel.effects, lifecycleOwner = this) { effect ->
    when (effect) {
        is LoginEffect.Toast -> toast(effect.message)
        LoginEffect.NavigateBack -> finish()
    }
}

Tips

collectEffect(...) accepts any Flow, including SharedFlow and Channel.receiveAsFlow().

Notice

When the receiver is not a View, or when the View is not attached to a view tree with a LifecycleOwner, pass lifecycleOwner explicitly.

The following example

setState(
    state = viewModel.uiState,
    lifecycleOwner = this
) {
    // Update non-View object state here.
}

All lifecycle-aware APIs return StateSubscription, so you can cancel them manually when necessary.