compose-modifier-and-layout-style

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose modifier and layout style

Compose Modifier与布局风格

Core principle

核心原则

A composable that emits layout is a leaf the parent places — the parent decides position, size, alignment, padding. The composable's job is structure (what's inside), not placement (where it goes). Three rules follow:
  • Declare a
    modifier
    parameter and apply it to the root
    , so the parent can actually do its job. Hardcoding
    .fillMaxWidth()
    on a composable's root takes that decision away from every future caller.
  • Construct modifier chains as one fluent expression, not stepwise reassignments. Both compile to the same thing, but the chain reads as intent in one pass.
  • Conditional rendering belongs where the condition applies. A layout call whose only content is one
    if
    exists solely to hold the condition — push the
    if
    outside instead.
These travel together because the same composable usually triggers all three: you declare its parameters (rule 1), the caller constructs a chain to position it (rules 2), and the body has a conditional you might be tempted to wrap (rule 3).
输出布局的可组合函数是由父组件放置的叶子节点——父组件决定其位置、大小、对齐方式和内边距。可组合函数的职责是定义内部结构(包含内容),而非放置逻辑(位置)。由此衍生三条规则:
  • 声明
    modifier
    参数并应用到根节点
    ,这样父组件才能履行其职责。在可组合函数的根节点硬编码
    .fillMaxWidth()
    会剥夺后续所有调用方的布局决策权。
  • 将modifier链构建为连贯的表达式,而非分步重新赋值。两种写法编译结果一致,但链式写法能一次性清晰传达意图。
  • 条件渲染应放在条件生效的位置。若某个布局调用的唯一内容是单个
    if
    语句,那么该布局仅用于承载条件——应将
    if
    语句移到布局外部。
这三条规则通常同时适用,因为同一个可组合函数往往会触发所有三种情况:你需要声明其参数(规则1),调用方会构建链来定位它(规则2),而函数体中可能存在你想包裹的条件语句(规则3)。

When to use this skill

何时使用此规范

  • You're writing a
    @Composable fun
    that calls a layout (
    Box
    ,
    Column
    ,
    Row
    ,
    LazyColumn
    ,
    Text
    ,
    Image
    ,
    Surface
    ,
    Card
    ,
    Layout { … }
    , anything from
    compose.foundation.layout
    or
    compose.material*
    ) and its signature has no
    modifier
    parameter, or has one that isn't applied to the root, or has a hardcoded
    .fillMaxWidth()
    /
    .padding(...)
    on the root.
  • You see
    var m = Modifier
    followed by
    m = m.padding(…)
    ,
    m = m.background(…)
    , etc.
  • A
    modifier = …
    argument has three or more chained calls on a single line.
  • A composable's body is
    Layout { if (cond) Content() }
    — one conditional, nothing else.
  • 你正在编写一个调用布局的
    @Composable fun
    (如
    Box
    Column
    Row
    LazyColumn
    Text
    Image
    Surface
    Card
    Layout { … }
    ,或
    compose.foundation.layout
    compose.material*
    中的任意组件),但其签名中没有
    modifier
    参数,或虽有参数但未应用到根节点,或根节点硬编码了
    .fillMaxWidth()
    /
    .padding(...)
  • 你看到
    var m = Modifier
    后跟着
    m = m.padding(…)
    m = m.background(…)
    等赋值语句。
  • modifier = …
    参数在单行上有三个或更多链式调用。
  • 可组合函数的函数体是
    Layout { if (cond) Content() }
    ——仅包含一个条件语句,无其他内容。

1. Declare a
modifier
parameter

1. 声明
modifier
参数

For composables that emit layout, prefer a
modifier
parameter after required parameters and before content/lambda parameters, with a default of
Modifier
. The name is exactly
modifier
— not
mod
, not
m
, not
wrapperModifier
.
kotlin
// ❌ BAD — no modifier param; caller can't position, size, or constrain this
@Composable
fun HomeScreenHeader(title: String, subtitle: String) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text(title, style = MaterialTheme.typography.headlineLarge)
        Text(subtitle, style = MaterialTheme.typography.bodyMedium)
    }
}
kotlin
// ✅ GOOD — parent decides width and padding; the composable describes structure only
@Composable
fun HomeScreenHeader(
    title: String,
    subtitle: String,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text(title, style = MaterialTheme.typography.headlineLarge)
        Text(subtitle, style = MaterialTheme.typography.bodyMedium)
    }
}
The caller now writes
HomeScreenHeader(title, subtitle, Modifier.fillMaxWidth().padding(horizontal = 16.dp))
once, at the home screen — the only place that knows the layout actually wants those.
对于输出布局的可组合函数,建议在必填参数之后、内容/ lambda参数之前添加
modifier
参数,默认值为
Modifier
。参数名称必须是
modifier
——不能是
mod
m
wrapperModifier
kotlin
// ❌ 错误示例 — 无modifier参数;调用方无法定位、调整大小或约束此组件
@Composable
fun HomeScreenHeader(title: String, subtitle: String) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text(title, style = MaterialTheme.typography.headlineLarge)
        Text(subtitle, style = MaterialTheme.typography.bodyMedium)
    }
}
kotlin
// ✅ 正确示例 — 父组件决定宽度和内边距;可组合函数仅描述结构
@Composable
fun HomeScreenHeader(
    title: String,
    subtitle: String,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text(title, style = MaterialTheme.typography.headlineLarge)
        Text(subtitle, style = MaterialTheme.typography.bodyMedium)
    }
}
现在调用方可在首页一次性编写
HomeScreenHeader(title, subtitle, Modifier.fillMaxWidth().padding(horizontal = 16.dp))
——只有首页才知道该布局确实需要这些属性。

2. Apply the caller's modifier to the root, and apply it first

2. 将调用方提供的modifier应用到根节点,且优先应用

When the root layout already takes other arguments (alignment, arrangement, padding that's intrinsic to the composable), the caller-provided modifier still goes on the root layout's
modifier
parameter — and the composable's local chain is appended after.
kotlin
// ❌ BAD — modifier accepted but never applied
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Image(painter = rememberAsyncImagePainter(url), contentDescription = null)
}

// ❌ BAD — applied to a child, not the root; caller's size/position changes don't take
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Box {
        Image(
            painter = rememberAsyncImagePainter(url),
            contentDescription = null,
            modifier = modifier,
        )
    }
}

// ❌ BAD — caller's modifier ends up last, so the composable's own size wins
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Image(
        painter = rememberAsyncImagePainter(url),
        contentDescription = null,
        modifier = Modifier
            .clip(CircleShape)
            .size(48.dp)
            .then(modifier),
    )
}
kotlin
// ✅ GOOD — caller's modifier first, then the composable's intrinsic chain
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Image(
        painter = rememberAsyncImagePainter(url),
        contentDescription = null,
        modifier = modifier
            .clip(CircleShape)
            .size(48.dp),
    )
}
Order matters: in a modifier chain, the earlier segment is the outer wrapper. The caller's modifier should be the outermost so caller-provided
.size(...)
or
.padding(...)
can override the composable's defaults rather than being overridden by them.
当根布局已包含其他参数(如对齐方式、排列方式、属于可组合函数固有属性的内边距)时,调用方提供的modifier仍应传入根布局的
modifier
参数——且可组合函数自身的modifier链应追加在其后。
kotlin
// ❌ 错误示例 — 接收了modifier但从未应用
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Image(painter = rememberAsyncImagePainter(url), contentDescription = null)
}

// ❌ 错误示例 — 应用到子节点而非根节点;调用方的大小/位置修改无效
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Box {
        Image(
            painter = rememberAsyncImagePainter(url),
            contentDescription = null,
            modifier = modifier,
        )
    }
}

// ❌ 错误示例 — 调用方的modifier排在最后,导致可组合函数自身的大小设置优先级更高
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Image(
        painter = rememberAsyncImagePainter(url),
        contentDescription = null,
        modifier = Modifier
            .clip(CircleShape)
            .size(48.dp)
            .then(modifier),
    )
}
kotlin
// ✅ 正确示例 — 调用方的modifier在前,然后是可组合函数的固有链
@Composable
fun Avatar(url: String, modifier: Modifier = Modifier) {
    Image(
        painter = rememberAsyncImagePainter(url),
        contentDescription = null,
        modifier = modifier
            .clip(CircleShape)
            .size(48.dp),
    )
}
顺序至关重要:在modifier链中,更早的片段是外层包装。调用方的modifier应处于最外层,这样调用方提供的
.size(...)
.padding(...)
才能覆盖可组合函数的默认值,而非被默认值覆盖。

3. Don't hardcode layout decisions on the root

3. 不要在根节点硬编码布局决策

If the composable's root has
.fillMaxWidth()
,
.padding(horizontal = 16.dp)
,
.height(56.dp)
, etc., the caller can't not have them. Those are layout choices the parent should own.
kotlin
// ❌ BAD — every caller now fills max width whether they want to or not
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),   // ← hardcoded
    ) { Text(text) }
}

// ✅ GOOD — caller adds .fillMaxWidth() if (and only if) they want it
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(onClick = onClick, modifier = modifier) { Text(text) }
}
The carve-out is for modifiers that are part of the identity of the composable — what makes an
Avatar
an avatar (the
.clip(CircleShape)
and a default
.size(48.dp)
), not where it sits on the screen. Test: can you imagine a caller wanting a version of this composable without that modifier? If yes, push it out. If no (an avatar without
clip(CircleShape)
isn't an avatar), keep it — but put it after the caller's modifier in the chain (see §2).
如果可组合函数的根节点包含
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(56.dp)
等设置,调用方无法取消这些设置。这些布局选择应由父组件掌控。
kotlin
// ❌ 错误示例 — 所有调用方现在都必须填充最大宽度,无论是否需要
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),   // ← 硬编码
    ) { Text(text) }
}

// ✅ 正确示例 — 调用方仅在需要时添加.fillMaxWidth()
@Composable
fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    Button(onClick = onClick, modifier = modifier) { Text(text) }
}
例外情况是属于可组合函数标识属性的modifier——即构成
Avatar
本质的属性(如
.clip(CircleShape)
和默认的
.size(48.dp)
),而非其在屏幕上的位置。测试方法:你能否想象调用方需要一个没有该modifier的可组合函数版本?如果是,将其移到外部;如果否(没有
clip(CircleShape)
的头像就不是头像),则保留它——但要将其放在调用方modifier链的后面(参见第2节)。

4. Construct modifier chains as one fluent expression

4. 将modifier链构建为连贯的表达式

Recomposition re-runs the composable body — every modifier expression is re-evaluated. Reassigning
var modifier =
step-by-step looks plausible but breaks the visual flow, invites further mutation, and produces nothing a chain doesn't.
kotlin
// ❌ BAD — visual flow broken into reassignments; `var` invites more mutation
@Composable
fun Demo() {
    var m = Modifier
    m = m.padding(16.dp)
    m = m.fillMaxSize()
    Box(m) { }
}

// ❌ ALSO BAD — same shape, dressed up with .then()
@Composable
fun Demo() {
    var m = Modifier
    m = m.padding(16.dp)
    m = m.then(Modifier.fillMaxSize())
    Box(m) { }
}
kotlin
// ✅ GOOD
@Composable
fun Demo() {
    val m = Modifier
        .padding(16.dp)
        .fillMaxSize()
    Box(m) { }
}
val
, not
var
: once the chain is built, nothing should re-bind it. The reassignment shape is what makes
var
look necessary; the chain shape doesn't need it.
重组会重新运行可组合函数体——每个modifier表达式都会被重新计算。分步重新赋值
var modifier =
看似合理,但会破坏视觉流程,引发更多突变,且无法产生链式写法之外的任何效果。
kotlin
// ❌ 错误示例 — 视觉流程被拆分为多次赋值;`var`容易引发更多突变
@Composable
fun Demo() {
    var m = Modifier
    m = m.padding(16.dp)
    m = m.fillMaxSize()
    Box(m) { }
}

// ❌ 同样错误 — 用.then()包装,但本质相同
@Composable
fun Demo() {
    var m = Modifier
    m = m.padding(16.dp)
    m = m.then(Modifier.fillMaxSize())
    Box(m) { }
}
kotlin
// ✅ 正确示例
@Composable
fun Demo() {
    val m = Modifier
        .padding(16.dp)
        .fillMaxSize()
    Box(m) { }
}
使用
val
而非
var
:链构建完成后,不应重新绑定它。分步赋值的写法让
var
看起来是必需的,但链式写法不需要它。

Inline at the call site is fine for short chains

短链可直接在调用处内联

For one or two calls, build the modifier inline. The "extract to a
val
" rule only earns its keep when the chain is long enough to be worth naming, or when the same chain repeats.
kotlin
// ✅ GOOD — short chain inline
Box(modifier = Modifier.fillMaxWidth()) {}
Box(modifier = Modifier.padding(8.dp).background(Color.Red)) {}
对于1-2个调用,可直接在调用处构建modifier。只有当链足够长值得命名,或同一链重复出现时,才需要将其提取为
val
kotlin
// ✅ 正确示例 — 短链内联
Box(modifier = Modifier.fillMaxWidth()) {}
Box(modifier = Modifier.padding(8.dp).background(Color.Red)) {}

Conditional segments stay on the chain

条件片段保留在链中

A common reason to reach for
var
is "the modifier depends on a condition." It doesn't — splice the condition inline:
kotlin
// ✅ GOOD — conditional inside the chain, still one expression
Box(
    modifier = Modifier
        .fillMaxWidth()
        .then(if (selected) Modifier.background(Color.Red) else Modifier),
)
Modifier
(the empty modifier) is the identity element for
.then
— it lets you keep the chain shape when one branch contributes nothing.
使用
var
的常见原因是“modifier依赖于条件”。其实无需如此——可将条件内联到链中:
kotlin
// ✅ 正确示例 — 条件在链内,仍为单个表达式
Box(
    modifier = Modifier
        .fillMaxWidth()
        .then(if (selected) Modifier.background(Color.Red) else Modifier),
)
Modifier
(空modifier)是
.then
的标识元素——当其中一个分支无贡献时,它能保持链式写法的结构。

5. Multiline formatting at the call site

5. 调用处的多行格式化

When a
modifier
argument's chain has three or more calls, format multiline with one call per line. Indent the chain so the dotted calls align beneath the value.
kotlin
// ❌ BAD — three+ calls on one line; hard to scan
Box(
    modifier = modifier.fillMaxSize().padding(16.dp).weight(1f),
)

// ✅ GOOD
Box(
    modifier = modifier
        .fillMaxSize()
        .padding(16.dp)
        .weight(1f),
)
One or two calls stay on a single line — the threshold is the call count, not the character count. If a single call has very long arguments, that's a different problem (extract a
val
, or shorten the arguments).
This applies only to a parameter named
modifier
. Other fluent-style arguments aren't covered here.
modifier
参数的链包含三个或更多调用时,应格式化为多行,每行一个调用。缩进链,使带点的调用对齐在值的下方。
kotlin
// ❌ 错误示例 — 三个及以上调用在一行;难以阅读
Box(
    modifier = modifier.fillMaxSize().padding(16.dp).weight(1f),
)

// ✅ 正确示例
Box(
    modifier = modifier
        .fillMaxSize()
        .padding(16.dp)
        .weight(1f),
)
1-2个调用应保留在单行——判断标准是调用次数,而非字符数。如果单个调用的参数很长,那是另一个问题(提取为
val
,或缩短参数)。
此规则仅适用于名为
modifier
的参数。其他流畅风格的参数不在此范围内。

6. Hoist single conditionals out of the layout

6. 将单个条件语句移到布局外部

When a layout's only content is one
if
, the layout exists solely to "hold" the conditional. Move the
if
outside — the layout will only exist when it has something to show.
kotlin
// ❌ BAD — Column always emitted; only its inner content is conditional
@Composable
fun A() {
    Column {
        if (showHeader) {
            Text("Title")
            Text("Subtitle")
        }
    }
}

// ✅ GOOD — Column only exists when it has content
@Composable
fun A() {
    if (showHeader) {
        Column {
            Text("Title")
            Text("Subtitle")
        }
    }
}
The benefit isn't a performance win — the runtime handles both fine — it's that the second form reads as "header section, conditionally." The first reads as "always-on column that may or may not have content."
当布局的唯一内容是单个
if
语句时,该布局仅用于“承载”条件语句。将
if
语句移到外部——只有当有内容要显示时,布局才会存在。
kotlin
// ❌ 错误示例 — Column始终被输出;只有内部内容是条件性的
@Composable
fun A() {
    Column {
        if (showHeader) {
            Text("Title")
            Text("Subtitle")
        }
    }
}

// ✅ 正确示例 — 仅当有内容时Column才存在
@Composable
fun A() {
    if (showHeader) {
        Column {
            Text("Title")
            Text("Subtitle")
        }
    }
}
好处并非性能提升——运行时能很好地处理两种写法——而是第二种写法的语义更清晰:“标题区域,按需显示”。第一种写法则是“始终存在的列,可能有内容也可能没有”。

The carve-outs (and why)

例外情况(及原因)

  • Layout carries visual semantics that aren't conditional. When the layout call passes
    modifier
    ,
    contentAlignment
    ,
    horizontalArrangement
    , or
    verticalAlignment
    , those arguments describe the container, not the content. Hoisting the conditional either loses those (the container collapses with the content) or duplicates them into both branches. Leave it.
    kotlin
    // ✅ KEEP AS-IS — modifier on the container is doing visible work
    @Composable
    fun A(modifier: Modifier = Modifier) {
        Box(modifier = modifier) {
            if (something) {
                Text("Bleh1")
                Text("Bleh2")
            }
        }
    }
  • There are siblings to the
    if
    .
    The layout has other content; the
    if
    is just one piece. Hoisting either pulls the siblings out (changing the layout) or leaves a different shape behind. Leave it.
  • if … else …
    with both branches contributing composables.
    Both branches do work; nothing to hoist; the layout is the shared container.
    kotlin
    // ✅ KEEP AS-IS — both branches contribute to the layout
    Box {
        if (something) Text("Hint") else innerTextField()
    }
  • 布局承载非条件性的视觉语义。当布局调用传递
    modifier
    contentAlignment
    horizontalArrangement
    verticalAlignment
    时,这些参数描述的是容器而非内容。将条件语句移到外部要么会丢失这些属性(容器随内容一起消失),要么会在两个分支中重复这些属性。保留原写法。
    kotlin
    // ✅ 保持原样 — 容器上的modifier在发挥可见作用
    @Composable
    fun A(modifier: Modifier = Modifier) {
        Box(modifier = modifier) {
            if (something) {
                Text("Bleh1")
                Text("Bleh2")
            }
        }
    }
  • if
    语句有同级内容
    。布局有其他内容;
    if
    只是其中一部分。将条件语句移到外部要么会把同级内容移出(改变布局),要么会留下不同的结构。保留原写法。
  • if … else …
    的两个分支都提供可组合函数
    。两个分支都有作用;没有可移动的内容;布局是共享容器。
    kotlin
    // ✅ 保持原样 — 两个分支都对布局有贡献
    Box {
        if (something) Text("Hint") else innerTextField()
    }

Quick reference

快速参考

SymptomDiagnosisFix
@Composable fun Foo(text: String)
with
Column
/
Box
/
Text
in body
No
modifier
param (§1)
Add
modifier: Modifier = Modifier
; pass to root
modifier: Modifier = Modifier
declared but never referenced
Param ignored (§2)Apply to root layout's
modifier
arg
modifier
passed to a child, not the root
Wrong target (§2)Move to the outermost layout's
modifier
modifier = Modifier.x().y().then(modifier)
Caller's modifier last (§2)Reorder:
modifier = modifier.x().y()
modifier = modifier.fillMaxWidth().padding(...)
on a general-purpose component
Layout hardcoded (§3)Remove the hardcoded calls; let callers add them
Sibling composables in the file don't have
modifier
either
Spreading anti-patternFix this one; fix siblings opportunistically
mod: Modifier = Modifier
or
wrapperModifier: Modifier = Modifier
Wrong name (§1)Rename to exactly
modifier
var m = Modifier
followed by
m = m.xxx()
reassignments
Stepwise modifier construction (§4)One fluent chain on a
val
, or build inline
var m = Modifier; m = m.then(Modifier.xxx())
Same shape via
.then
(§4)
Collapse
.then(Modifier.x())
to
.x()
in the chain
Modifier branch needs a conditionReaching for
var
(§4)
.then(if (c) Modifier.x() else Modifier)
inside the chain
modifier = modifier.a().b().c()
on one line
Long chain not formatted (§5)One call per line, indented under the value
Layout { if (cond) X() }
with no other content and no layout-tuning args
Hoist (§6)Move the
if
outside the layout
Box(modifier = …) { if (cond) X() }
Layout carries semantics — leave (§6 carve-out)Keep as-is
Box { if (cond) X() else Y() }
Both branches contribute — leave (§6 carve-out)Keep as-is
症状诊断修复方案
@Composable fun Foo(text: String)
的函数体包含
Column
/
Box
/
Text
modifier
参数(第1节)
添加
modifier: Modifier = Modifier
;传递给根节点
声明了
modifier: Modifier = Modifier
但从未引用
参数被忽略(第2节)应用到根布局的
modifier
参数
modifier
传递给子节点而非根节点
目标错误(第2节)移到最外层布局的
modifier
参数
modifier = Modifier.x().y().then(modifier)
调用方的modifier排在最后(第2节)重新排序:
modifier = modifier.x().y()
通用组件的
modifier = modifier.fillMaxWidth().padding(...)
布局被硬编码(第3节)移除硬编码调用;让调用方添加
文件中的同级可组合函数也没有
modifier
反模式扩散修复当前组件;适时修复同级组件
mod: Modifier = Modifier
wrapperModifier: Modifier = Modifier
参数名称错误(第1节)重命名为
modifier
var m = Modifier
后跟着
m = m.xxx()
赋值语句
分步构建modifier(第4节)
val
构建连贯链,或直接内联构建
var m = Modifier; m = m.then(Modifier.xxx())
.then()
实现的分步写法(第4节)
.then(Modifier.x())
合并为链中的
.x()
Modifier分支需要条件判断试图使用
var
(第4节)
在链中使用
.then(if (c) Modifier.x() else Modifier)
modifier = modifier.a().b().c()
在一行
长链未格式化(第5节)每行一个调用,缩进对齐
Layout { if (cond) X() }
无其他内容且无布局调优参数
应移到外部(第6节)
if
语句移到布局外部
Box(modifier = …) { if (cond) X() }
布局承载语义——保留(第6节例外)保持原样
Box { if (cond) X() else Y() }
两个分支都有贡献——保留(第6节例外)保持原样

When NOT to apply

何时不适用

  • Composables that don't emit layout. A
    @Composable fun computeColor(): Color
    or a
    @Composable @ReadOnlyComposable
    accessor doesn't emit a layout node. No
    modifier
    parameter needed (and a
    @ReadOnlyComposable
    couldn't accept one — see
    compose-state-authoring
    ).
  • @Preview
    functions.
    Previews are throwaway entry points; the framework calls them with no caller. A
    modifier
    parameter would be unused dead weight.
  • Test-only composables inside
    *Test
    sources whose only caller is
    composeTestRule.setContent { … }
    . Same reasoning as previews.
  • Internal layout primitives that take a
    modifier
    as their first required parameter
    (very rare; framework-level). The rule is "first optional param"; some private utilities legitimately have
    modifier
    upfront as required.
  • Modifier assembled imperatively from animation state. A modifier built by appending values from
    Animatable
    or other procedural sources may legitimately need intermediate variables. The chain isn't the goal; readability is. If the chain becomes a worse expression, write the imperative form.
  • Slot APIs that store modifiers in a data class or builder (rare; usually framework-level code). The fluent-chain idea is about user-site construction.
  • Test composables pinning specific recomposition shapes — usually fine either way; don't refactor test composables purely for style.
The declaration-side rules (§1–§3) should not be skipped merely because "this composable is internal", "only used in one place", "I'd rather not have the extra parameter on the signature", or "we know all the callers already". Those are exactly the rationalisations that produce composables that become single-use the day someone wants to call them twice.
  • 不输出布局的可组合函数
    @Composable fun computeColor(): Color
    @Composable @ReadOnlyComposable
    访问器不输出布局节点。无需
    modifier
    参数(且
    @ReadOnlyComposable
    无法接受该参数——参见
    compose-state-authoring
    )。
  • @Preview
    函数
    。预览是一次性入口点;框架调用它们时没有调用方。
    modifier
    参数会成为未使用的无效代码。
  • *Test
    源文件中的测试专用可组合函数
    ,其唯一调用方是
    composeTestRule.setContent { … }
    。与预览的理由相同。
  • modifier
    作为第一个必填参数的内部布局原语
    (非常罕见;框架级)。规则是“第一个可选参数”;一些私有工具合理地将
    modifier
    作为必填参数放在前面。
  • 从动画状态命令式组装的Modifier。由
    Animatable
    或其他过程化源追加值构建的modifier可能确实需要中间变量。链式写法并非目标;可读性才是。如果链式写法的可读性更差,就使用命令式写法。
  • 在数据类或构建器中存储modifier的插槽API(罕见;通常是框架级代码)。流畅链的理念适用于用户端构建。
  • 固定特定重组结构的测试可组合函数——通常两种写法都可行;不要仅为了风格而重构测试可组合函数。
声明端规则(第1-3节)不应仅仅因为“此可组合函数是内部的”、“仅在一处使用”、“我不想在签名中添加额外参数”或“我们已经知道所有调用方”而跳过。这些正是导致可组合函数在有人想第二次调用时就变成一次性组件的理由。

Red flags during review

评审中的危险信号

ThoughtReality
"This composable is internal-only — adding
modifier
is over-engineering"
The parameter is eight characters and a default. It's not over-engineering; it's the convention. Skipping it is the over-engineering — it's a custom decision against the grain of every Compose API.
"It's only used in one place, so I know the layout requirements""Only used in one place" describes today. The cost of the parameter is paid once; the cost of refactoring callers when the second use site appears is paid per caller.
"The sibling composables in this file don't have
modifier
either, so I'm matching style"
Spreading an anti-pattern isn't matching style. Fix this one. Fix the siblings opportunistically.
"The parent always wants
.fillMaxWidth()
here"
Then the parent passes
.fillMaxWidth()
. The composable doesn't decide that for callers it hasn't met yet.
"I'll add it when someone needs it"You're someone. You need it now (for the convention). The next caller won't add it either — they'll work around its absence.
"It's a tiny composable — the modifier param is noise"The param is eight characters at the declaration and zero characters at any call site that doesn't need it. The "noise" is imagined.
"I added
modifier
but kept
.fillMaxWidth()
on the root so the home screen doesn't have to"
Then the not-home-screen caller can't unset it. Move the
.fillMaxWidth()
to the caller.
"I need
var
for the modifier because the chain depends on a condition"
A conditional segment is
.then(if (c) Modifier.x() else Modifier)
, still on one chain. No
var
needed.
"Three lines is too few to make multiline"Three chained calls is the threshold. Below three, one line. At or above three, multiline.
"The Column adds nothing but I'll keep it for symmetry"Then hoist the conditional and keep the Column inside the consequent — symmetry preserved, no always-on container.
"I'll put the
if
inside because the layout already exists"
"Already exists" is the bug. The layout shouldn't exist when the condition is false.
想法实际情况
“此可组合函数是内部专用的——添加
modifier
是过度设计”
该参数只有8个字符和一个默认值。这不是过度设计;这是约定。跳过它才是过度设计——这是违背所有Compose API惯例的自定义决策。
“它仅在一处使用,所以我知道布局要求”“仅在一处使用”描述的是现在。添加参数的成本是一次性的;当出现第二个使用场景时,重构调用方的成本是每个调用方都要付出的。
“此文件中的同级可组合函数也没有
modifier
,所以我要保持风格一致”
扩散反模式不是保持风格一致。修复当前组件。适时修复同级组件。
“父组件总是希望这里使用
.fillMaxWidth()
那么父组件应传递
.fillMaxWidth()
。可组合函数不应为尚未遇到的调用方做决定。
“等有人需要时我再添加”你就是需要它的人。你现在就需要它(遵循约定)。下一个调用方也不会添加它——他们会绕过这个缺陷。
“这是一个很小的可组合函数——modifier参数是冗余的”该参数在声明处只有8个字符,在不需要它的调用处是0个字符。所谓的“冗余”是想象出来的。
“我添加了
modifier
但在根节点保留了
.fillMaxWidth()
,这样首页就不用添加了”
那么非首页的调用方无法取消它。将
.fillMaxWidth()
移到调用方。
“我需要用
var
来处理modifier,因为链依赖于条件”
条件片段可以写成
.then(if (c) Modifier.x() else Modifier)
,仍在单个链中。不需要
var
“三行太少,没必要写成多行”三个链式调用就是阈值。少于三个,单行;等于或多于三个,多行。
“Column没有实际作用,但我为了对称保留它”那么将条件语句移到外部,并将Column保留在结果分支中——对称性得以保留,没有始终存在的容器。
“我把
if
放在里面是因为布局已经存在了”
“已经存在”就是问题所在。当条件为false时,布局不应存在。

Related

相关内容

  • compose-slot-api-pattern
    — the other half of declaring a reusable composable's public API: take
    @Composable () -> Unit
    slots for variable content. A reusable component takes both a
    modifier
    parameter and slots — caller owns placement and what to place.
  • compose-slot-api-pattern
    — 声明可复用可组合函数公共API的另一半:接受
    @Composable () -> Unit
    插槽用于可变内容。可复用组件应同时接受
    modifier
    参数插槽——调用方掌控放置位置放置内容。