Skip to content

Declarative UI Layout

What is declarative layout?

A declarative layout is a UI paradigm where you define all possible UI states upfront during the MVVM business logic construction phase, and the UI updates itself automatically based on data changes. This is data-driven UI logic, rather than the traditional approach where you fetch data first and then manually update the UI.

Currently, this paradigm is used not only in Jetpack Compose, but also in iOS SwiftUI, Google Flutter, and React Native by Facebook.

A mindset shift

This is different from the traditional approach of writing a fixed page and updating it with the latest data. Jetpack Compose introduces a concept called recomposition (official Android term).

My understanding of recomposition: it’s like having built all possible UI states in advance, and then displaying the appropriate UI based on the latest data.

Scope of recomposition: This is a key point to understand.


Example

Take a scrolling list layout as an example. In XML, it might look like this:

xml
<FrameLayout>
    <RecyclerView
        android:id="@+id/rv_list"/>

    <ErrorLayout
        android:id="@+id/error_layout"/>

    <EmptyLayout
        android:id="@+id/empty_layout"/>
</FrameLayout>
kotlin
class CommendFragment: Fragment() {
    // ... omitted: ViewDataBinding, RecyclerView Adapter, ViewModel instantiation, etc.

    override fun onViewCreate() {
        viewModel.uiData.observe(this.viewLifecycleOwner) {
            when (it) {
                is Success -> {
                    binding.rvList.visible = View.VISIBLE
                    binding.errorLayout.visible = View.GONE
                    binding.emptyLayout.visible = View.GONE

                    rvListAdapter.setData(it.data)
                }
                is Failed -> {
                    binding.rvList.visible = View.GONE
                    binding.errorLayout.visible = View.VISIBLE
                    binding.emptyLayout.visible = View.GONE
                }
                is Empty -> {
                    binding.rvList.visible = View.GONE
                    binding.errorLayout.visible = View.GONE
                    binding.emptyLayout.visible = View.VISIBLE
                }
            }
        }
    }
}

Using Jetpack Compose, the same page can be built like this:

kotlin
@Composable
fun SuccessUI() {
    val viewModel = viewModels<MyViewModel>()
    val data = viewModel.uiDataState.value as? Success ?: return

    // Build the UI based on data
}

@Composable
fun FailedUI() {
    // Build the Failed UI
}

@Composable
fun EmptyUI() {
    // Build the Empty UI
}

@Composable
fun HostUI() {
    val viewModel = viewModels<MyViewModel>()
    val uiDataState = viewModel.uiDataState.value

    when (uiDataState) {
        is Success -> SuccessUI()
        is Failed -> FailedUI()
        is Empty -> EmptyUI()
    }
}

Key advantages of Jetpack Compose

  • UI separation: SuccessUI, FailedUI, and EmptyUI are completely independent.
  • In team projects, the HostUI structure and empty interfaces can be defined by an architect or feature owner, while other team members implement individual UI screens.
  • Using XML often leads to tightly coupled code. When multiple developers work on different UI states, PRs can result in merge conflicts.
  • Compose allows placing different UI states in separate files, reducing merge conflicts and improving team collaboration efficiency.

Current challenges with Jetpack Compose

1. Lack of familiarity with controls

  • In traditional layouts, using LinearLayout with weight or ConstraintLayout constraints is intuitive.
  • In Compose, it may not be immediately obvious which composable fits best.
  • This is essentially a familiarity problem. Frequent use of Row and Column will resolve this.

2. Performance issues

  • In my experiment, using LazyHorizontalColumn + Coil (for image loading in Compose) to create a waterfall-style photo gallery caused slower image loading and frame drops when compared to traditional RecyclerView + Glide.

Just something casual. Hope you like it. Built with VitePress