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:
<FrameLayout>
<RecyclerView
android:id="@+id/rv_list"/>
<ErrorLayout
android:id="@+id/error_layout"/>
<EmptyLayout
android:id="@+id/empty_layout"/>
</FrameLayout>
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:
@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
, andEmptyUI
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 orConstraintLayout
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
andColumn
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 traditionalRecyclerView
+Glide
.