【Flutter】Column と Row に追加される spacing をみてみる
初めに
以下の Issue で Column や Row に spacing
を導入することが議論されており、Master Channel で確認できるようになっていたので、ざっくり触ってみてみたいと思います。
記事の対象者
- Flutter 学習者
要約
今回の内容を要約すると以下のようになります。
-
spacing
を使えば複数のSizedBox
やGap
を配置することなく、均等な間隔で要素を配置することができるようになる - SwiftUI の
VStack(spacing: 16)
のように各要素の間の指定が可能になる - Jetpack Compose の
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { }
のように各要素の間の指定が可能になる
実装
基本的には要約の通りですが、2024年11月17日現在の Master Channel で Column, Row に spacing
プロパティが追加されており、要素を等間隔に並べたい場合に SizedBox
や Gap
を多用することなく実装できるようになっています。
まずは Flutter の Master Channel を使用するようにします。
以下のコマンドを実行することで Channel の切り替えができます。
flutter channel master
FVM を使用している場合は以下のコマンドで Channel の切り替えができます。
fvm use master
要素を等間隔に並べるUIとしてユーザーの新規登録画面を例にとって実装していきます。
今までの実装では以下のようなコードになります。
同じ高さの SizedBox
を間に挟んで実装することが多いかと思います。
Column(
children: [
const _CustomTextField(
hintText: 'メールアドレス',
),
const SizedBox(height: 16), // ここ
const _CustomTextField(
hintText: 'パスワード',
),
const SizedBox(height: 16), // ここ
const _CustomTextField(
hintText: 'パスワード(確認)',
),
const SizedBox(height: 16), // ここ
ElevatedButton(onPressed: () {}, child: const Text('登録')),
],
),
このようなコードを、追加された spacing
を用いて実装すると以下のようになります。
これで複数の SizedBox
を記述する必要がなくなります。また、各要素の間隔が統一されるため、「A と B の要素の間隔だけ違う」といった間違いが少なくなるかと思います。
Column(
+ spacing: 16,
children: [
const _CustomTextField(
hintText: 'メールアドレス',
),
- const SizedBox(height: 16),
const _CustomTextField(
hintText: 'パスワード',
),
- const SizedBox(height: 16),
const _CustomTextField(
hintText: 'パスワード(確認)',
),
- const SizedBox(height: 16),
ElevatedButton(onPressed: () {}, child: const Text('登録')),
],
),
spacing
の指定は Row でも同様にできます。
SwiftUI の場合
SwiftUI の場合は VStack に spacing
を付与することで同じようなレイアウトになるかと思います。
コードは以下のようなイメージです。
VStack(spacing: 16) {
TextField(text: $email) {
Text("メールアドレス")
}
TextField(text: $password) {
Text("パスワード")
}
TextField(text: $confirmPassword) {
Text("パスワード(確認)")
}
}
Jetpack Compose の場合
Jetpack Compose の場合は Column
の verticalArrangement
に Arrangement.spacedBy(16.dp)
を付与することで、各要素の垂直方向に間隔を空けることができ、同じようなレイアウトになるかと思います。
コードは以下のようなイメージです。
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
TextField(
value = email.value,
onValueChange = {
email.value = it
},
)
TextField(
value = password.value,
onValueChange = {
password.value = it
},
)
TextField(
value = confirmPassword.value,
onValueChange = {
confirmPassword.value = it
},
)
Button(onClick = {}) {
Text("登録")
}
}
詳細
最後に少し内部の実装をみてみたいと思います。
Master Channel に切り替えて Column
の内容をのぞいてみると以下のようになっています。
Column
, Row
は Flex
を継承するようになっており、 spacing
を受け取るようになっています。
class Column extends Flex {
const Column({
super.key,
super.mainAxisAlignment,
super.mainAxisSize,
super.crossAxisAlignment,
super.textDirection,
super.verticalDirection,
super.textBaseline,
super.spacing, // ここ
super.children,
}) : super(
direction: Axis.vertical,
);
}
Flex
の中身をみてみると、 createRenderObject
で spacing
を渡していることがわかります。
RenderFlex createRenderObject(BuildContext context) {
return RenderFlex(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context),
verticalDirection: verticalDirection,
textBaseline: textBaseline,
clipBehavior: clipBehavior,
spacing: spacing, // ここ
);
}
さらに RenderFlex
の中身をみてみると以下のようなコードがあります。(みやすいように省略している部分が多々あります)
RenderFlex
の performLayout
で _distributeSpace
というメソッドに spacing
が渡されていることがわかります。
void performLayout() {
final BoxConstraints constraints = this.constraints;
final (double leadingSpace, double betweenSpace) = mainAxisAlignment._distributeSpace(remainingSpace, childCount, flipMainAxis, spacing);
}
_distributeSpace
メソッドは以下のようになっています。
このメソッドで返却しているレコード型の変数はそれぞれ以下のような意味かと思います。
-
leadingSpace
: 要素の前に来るスペース(Columnの場合は要素の上のスペース)の大きさ -
betweenSpace
: 要素ごとの間のスペースの大きさ
(double leadingSpace, double betweenSpace) _distributeSpace(double freeSpace, int itemCount, bool flipped, double spacing) {
assert(itemCount >= 0);
return switch (this) {
MainAxisAlignment.start => flipped ? (freeSpace, spacing) : (0.0, spacing),
MainAxisAlignment.end => MainAxisAlignment.start._distributeSpace(freeSpace, itemCount, !flipped, spacing),
MainAxisAlignment.spaceBetween when itemCount < 2 => MainAxisAlignment.start._distributeSpace(freeSpace, itemCount, flipped, spacing),
MainAxisAlignment.spaceAround when itemCount == 0 => MainAxisAlignment.start._distributeSpace(freeSpace, itemCount, flipped, spacing),
MainAxisAlignment.center => (freeSpace / 2.0, spacing),
MainAxisAlignment.spaceBetween => (0.0, freeSpace / (itemCount - 1) + spacing),
MainAxisAlignment.spaceAround => (freeSpace / itemCount / 2, freeSpace / itemCount + spacing),
MainAxisAlignment.spaceEvenly => (freeSpace / (itemCount + 1), freeSpace / (itemCount + 1) + spacing),
};
}
上記のコードについて少し詳しくみてみます。
MainAxisAlignment.start
の場合は以下のようになっています。
flipped
の値に関わらず、受け取った spacing
の値をレコード型の2つ目のフィールドの betweenSpace
として返却しています。
MainAxisAlignment.start => flipped ? (freeSpace, spacing) : (0.0, spacing),
fliped の詳細
fliped
は辿っていくと以下のようになっています。
Column
を例にとると、verticalDirection
の値はデフォルトで VerticalDirection.down
となっています。つまり、デフォルトではスクロール方向が上から下になっています。これを VerticalDirection.up
とするとスクロール方向が下から上に反転するため、 fliped
が true
になります。
bool get _flipMainAxis => firstChild != null && switch (direction) {
Axis.horizontal => switch (textDirection) {
null || TextDirection.ltr => false,
TextDirection.rtl => true,
},
Axis.vertical => switch (verticalDirection) {
VerticalDirection.down => false,
VerticalDirection.up => true,
},
};
MainAxisAlignment.spaceBetween
spaceAround
spaceEvenly
の場合は以下のようになっています。 freeSpace
を itemCount
などで分割し、それに spacing
の値を足した値を betweenSpace
として返却しています。
MainAxisAlignment.spaceBetween => (0.0, freeSpace / (itemCount - 1) + spacing),
MainAxisAlignment.spaceAround => (freeSpace / itemCount / 2, freeSpace / itemCount + spacing),
MainAxisAlignment.spaceEvenly => (freeSpace / (itemCount + 1), freeSpace / (itemCount + 1) + spacing),
これまでのコードで、 _distributeSpace
メソッドの中で変更された betweenSpace
は、レイアウトの段階で以下のように使われています。(わかりやすいように他の部分を省略しています。)
void performLayout() {
final (double leadingSpace, double betweenSpace) = mainAxisAlignment._distributeSpace(remainingSpace, childCount, flipMainAxis, spacing);
childMainPosition += _getMainSize(child.size) + betweenSpace;
}
childMainPosition
は名前の通り、 Flex の子要素のメイン軸における位置を示しています。 _getMainSize(child.size)
と betweenSpace
を足し合わせることで childMainPosition
の位置を変更しています。
この部分からも spacing
の値がそれぞれの子要素に追加してスペースを設けているというイメージができるかと思います。
また、これまでの SizedBox
や Gap
はこのコードにおける _getMainSize(child.size)
の方でサイズが計算されるかと思うので、これまでの実装とは少し異なる実装をしていることがわかります。
以上です。
まとめ
最後まで読んでいただいてありがとうございました。
冒頭でも述べましたが、 spacing
は現在 Master Channel にあり、 Stateble Channel では使用できませんが、使用できるようになれば楽にレイアウトができる部分が増えるかと思います。
誤っている点やもっと良い書き方があればご指摘いただければ幸いです。
参考
Discussion