Constraint layouts and flow
Two more layouts. The first, ConstraintLayout, is in almost every XML codebase you will join and
was the standard answer for complex screens there; in Compose you will rarely need it, and this
lesson shows you why by building the same thing both ways. The second, FlowRow, Mandi Bhav needs
today: a row of commodity filters that wraps onto a second line instead of falling off the edge of
the screen.
ConstraintLayout, and why Compose rarely needs it
In the XML view system, every nested layout could measure its children more than once, and the
cost multiplied with depth. ConstraintLayout let you describe a complex screen as one flat level
of views placed relative to each other, and it became the default. Compose measures each child
once, as lesson 1 showed, so a flat hierarchy buys almost nothing. The Compose documentation for
ConstraintLayout now opens with a warning recommending other layouts, and repeats it under
guidelines, barriers and chains.
It still earns its place in a few cases:
- Relationships between siblings that are not in the same row or column: "this text ends where the widest of those two begins" (a barrier).
- Swapping whole arrangements, such as a different layout in landscape, by passing a different
ConstraintSetto the same content. - Migrating an XML screen where keeping the same constraints makes the review easier.
Adding it
ConstraintLayout for Compose is a separate library, androidx.constraintlayout:constraintlayout-compose,
with its own version numbers: the Compose BOM does not manage it, so you choose the version
yourself. Look it up on Google Maven rather than copying one from a blog post; on 8 November 2026
the latest stable was 1.1.2. In gradle/libs.versions.toml:
[versions]
constraintlayoutCompose = "1.1.2"
[libraries]
androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayoutCompose" }
and in app/build.gradle.kts, in dependencies:
implementation(libs.androidx.constraintlayout.compose)
A price header with constraints
The header of a price card with the commodity on the left, the modal price on the right sitting on the commodity's baseline, and the market underneath, never running into the price:
@Composable
fun PriceHeader(price: CommodityPrice, modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier.fillMaxWidth()) {
val (commodity, market, modal) = createRefs()
val priceStart = createStartBarrier(modal)
Text(
text = price.commodity,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.constrainAs(commodity) {
top.linkTo(parent.top)
start.linkTo(parent.start)
},
)
Text(
text = price.market,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.constrainAs(market) {
top.linkTo(commodity.bottom)
start.linkTo(parent.start)
end.linkTo(priceStart, margin = 8.dp)
width = Dimension.fillToConstraints
},
)
Text(
text = formatRupees(price.modalPrice),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.constrainAs(modal) {
baseline.linkTo(commodity.baseline)
end.linkTo(parent.end)
},
)
}
}
| Piece | What it does |
|---|---|
createRefs() |
Makes names for the children, destructured into vals |
Modifier.constrainAs(ref) { … } |
Gives a child its name and its constraints |
top.linkTo(commodity.bottom) |
Pins one edge to another child's edge, or to parent |
baseline.linkTo(…) |
Pins text baselines together |
createStartBarrier(modal) |
A line at the start of the furthest-left of the given children |
Dimension.fillToConstraints |
Take exactly the space between the linked edges |
Run in a Card on the emulator with Vashi wheat, it shows "Wheat" at the top left, "₹3,000" at the
top right on the same baseline, and "Vashi APMC, Navi Mumbai" underneath.
Now the same header with lesson 1's layouts:
@Composable
fun RowPriceHeader(price: CommodityPrice, modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth()) {
Column(modifier = Modifier.weight(1f).alignByBaseline()) {
Text(text = price.commodity, style = MaterialTheme.typography.titleLarge)
Text(text = price.market, style = MaterialTheme.typography.bodyMedium)
}
Text(
text = formatRupees(price.modalPrice),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.alignByBaseline(),
)
}
}
On the emulator the two looked the same: same positions, same baseline. A Column
reports the baseline of its first text, so alignByBaseline() on the whole column lines the price
up with "Wheat"; weight(1f) keeps the market clear of the price, which is what the barrier did.
The decision: Mandi Bhav does not use ConstraintLayout. The Row version is half the length,
uses ideas every Compose developer reads at a glance, and needs no extra library. The library costs
a dependency to keep updated on its own schedule, some APK size, and a second layout language for
the next developer to learn. If you added it to try the example, remove both lines again; the
checkpoint for this module does not have them. Keep the knowledge for the XML screens you will
meet in the XML module, and for the rare layout that a Row and a Column cannot express.
FlowRow: a row that wraps
Mandi Bhav now has six commodities. A trader wants to tap "Onion" and see only onions. Seven
filter chips ("All" plus six) do not fit across a phone. This is lesson 1's trap again: in a plain
Row on the emulator, "All" to "Wheat" fitted, Soybean and Tur dal vanished, and the row grew a
tall empty band underneath where the squeezed chips had been measured.
FlowRow places children left to right like a Row and, when the next one does not fit, starts
a new line:
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = selectedCommodity == null,
onClick = { selectedCommodity = null },
label = { Text("All") },
)
for (commodity in commodities) {
FilterChip(
selected = selectedCommodity == commodity,
onClick = {
selectedCommodity =
if (selectedCommodity == commodity) null else commodity
},
label = { Text(commodity) },
)
}
}
FlowRow and FilterChip are both stable in the Compose version this project uses: neither needs
an @OptIn. A FilterChip shows a selected state, and tapping a selected chip again here clears
the filter, which is what people expect from a filter.
FlowRow parameter |
What it does |
|---|---|
horizontalArrangement |
Spacing between items on a line, as in a Row |
verticalArrangement |
Spacing between lines, as in a Column |
itemVerticalAlignment |
How items of different heights line up within a line |
maxItemsInEachRow |
Start a new line after this many, even if more would fit |
maxLines |
Stop after this many lines |
FlowColumn does the same top to bottom.
What a FlowRow costs: height. At the largest font size on the emulator, the seven chips took
three lines. A horizontally scrolling Row keeps one line, at the cost of hiding chips off the
edge, where many users never find them. For seven filters, wrapping is the better trade.
Filtering, out of the composable and under test
Lesson 2 filtered inline. With two filters it is time to take the logic out of the composable,
into plain Kotlin that a unit test can call. Create ui/PriceFilter.kt:
package com.riztechacademy.mandibhav.ui
import com.riztechacademy.mandibhav.model.CommodityPrice
/** Each commodity once, in the order it first appears: the filter chips. */
fun commoditiesIn(prices: List<CommodityPrice>): List<String> =
prices.map { it.commodity }.distinct()
/**
* The prices to show for a search query and an optional chosen commodity.
* The query matches commodity or market, ignoring case and surrounding spaces;
* a null commodity means "all".
*/
fun filterPrices(
prices: List<CommodityPrice>,
query: String,
commodity: String?,
): List<CommodityPrice> {
val q = query.trim()
return prices.filter { price ->
(commodity == null || price.commodity == commodity) &&
(q.isEmpty() ||
price.commodity.contains(q, ignoreCase = true) ||
price.market.contains(q, ignoreCase = true))
}
}
In PricesScreen, replace the three lines that computed q and visible with:
var query by remember { mutableStateOf("") }
var selectedCommodity by remember { mutableStateOf<String?>(null) }
val commodities = commoditiesIn(prices)
val visible = filterPrices(prices, query, selectedCommodity)
and put the FlowRow between the OutlinedTextField and the empty-results Text. New imports:
FlowRow from androidx.compose.foundation.layout and FilterChip from material3.
Then the tests, in app/src/test/java/com/riztechacademy/mandibhav/ui/PriceFilterTest.kt:
package com.riztechacademy.mandibhav.ui
import com.riztechacademy.mandibhav.data.SamplePrices
import org.junit.Assert.assertEquals
import org.junit.Test
class PriceFilterTest {
private val prices = SamplePrices.all
@Test
fun commoditiesAreDistinctInFirstSeenOrder() {
assertEquals(
listOf("Onion", "Tomato", "Potato", "Wheat", "Soybean", "Tur dal"),
commoditiesIn(prices),
)
}
@Test
fun emptyQueryAndNoCommodityShowsEverything() {
assertEquals(prices, filterPrices(prices, query = "", commodity = null))
}
@Test
fun commodityChipKeepsOnlyThatCommodity() {
val onions = filterPrices(prices, query = "", commodity = "Onion")
assertEquals(3, onions.size)
assertEquals(setOf("Onion"), onions.map { it.commodity }.toSet())
}
@Test
fun queryMatchesMarketIgnoringCaseAndSpaces() {
val lasalgaon = filterPrices(prices, query = " lasalgaon ", commodity = null)
assertEquals(listOf(SamplePrices.lasalgaonOnion, SamplePrices.lasalgaonSoybean), lasalgaon)
}
@Test
fun queryAndCommodityCombine() {
assertEquals(
listOf(SamplePrices.puneTomato),
filterPrices(prices, query = "pune", commodity = "Tomato"),
)
assertEquals(emptyList<Any>(), filterPrices(prices, query = "vashi", commodity = "Onion"))
}
}
./gradlew testDebugUnitTest
All ten tests pass: these five and module 3's five for formatRupees. What extracting it buys:
the rules about case, spaces and combining filters are now checked in milliseconds on every build,
without an emulator. When module 5 moves the state into a ViewModel, these functions move with it
unchanged.
The chips' selection, like the query, is lost when the phone rotates. Module 5 fixes both.
Check your work
Why ConstraintLayout mattered in XML and matters less in Compose. Nested XML layouts could
measure children repeatedly; Compose measures each child once, so nesting is cheap.
Where constraintlayout-compose's version comes from. You choose it; the Compose BOM does not
manage it. Check Google Maven for the current stable version.
What a barrier is. A virtual line at the extreme edge of a set of children, which others can be constrained to.
Why Mandi Bhav keeps the Row version. Shorter, familiar, and no extra dependency, for an
identical result.
What happened to seven chips in a plain Row. The last two vanished, squeezed to nothing, and
the row grew tall around them.
Why the filter logic moved to PriceFilter.kt. So it can be unit-tested without Compose or an
emulator, and moved to a ViewModel later unchanged.
Practice 3. On the emulator phone five chips fitted on the first line; with the cap, four do and the other three make the second line. In landscape, where all seven fit on one line, they now break after four as well. Use it when a grid-like look matters more than using the width.
Practice 4. The import fails first: Unresolved reference 'constraintlayout'., then every
name from it. Removing a dependency breaks every file that uses it, which is a reason to keep the
number of files that do small.
Practice 5. Add this test; it passes, because trim() turns the query into an empty string:
@Test
fun queryOfOnlySpacesShowsEverything() {
assertEquals(prices, filterPrices(prices, query = " ", commodity = null))
}
Practice
- Add
PriceFilter.kt, the chips and the tests. Run the tests, then the app: tap "Onion", type "pune", then tap "Onion" again to clear it. - Replace
FlowRowwithRowand look at the chips on the emulator. PutFlowRowback. - Add
maxItemsInEachRow = 4to theFlowRow. What changes on a phone, and on a landscape screen? - Break it on purpose: add
ConstraintLayout, buildPriceHeader, then delete theimplementation(libs.androidx.constraintlayout.compose)line and build. Read the first error. Then remove the library and the header properly. - Harder: write a test that a query of only spaces shows every price. Predict whether it passes
before you run it, and say which line in
filterPricesdecides it.
Official documentation
- ConstraintLayout in Compose — The DSL, decoupled constraint sets, guidelines, barriers and chains, with the warnings.
- Flow layouts in Compose —
FlowRowandFlowColumn, item weights, and limiting lines. - Chip — The four Material chip types and when to use each.
Next: Mandi Bhav's own theme, with its colours and type.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.
About the internship