「あれ、5月16日は3人しかレッスン入ってない?4人入れたはず・・
…いや、タップしてみたら4人だった。」

GridViewの罠
月謝袋アプリ v1.8.0 のリリース申請が完了した。今回のアップデートには、繁体字中国語(台湾・香港)対応に加えて、もう一つ大きな改善が入っている。カレンダー一覧画面の UI 大改善だ。
Before: 「3人 + 「+N」」表示の罠
これまでのカレンダー画面は、各日のセルに 最大3人まで生徒名を表示し、それ以上は 「+2」のような数字で省略する仕様になっていた。
理由はシンプルで、 iPhone の画面が狭いから。

GridView で固定アスペクト比のセルを並べる構造だったので、人数が多いと表示しきれないのだ。
dart// 修正前のコード(抜粋)
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
childAspectRatio: 0.62, // ← 固定
),
itemBuilder: (context, index) {
// …
return Column(
children: [
// 日付
Text('$day'),
// 生徒名は最大3人まで
…showNames.take(3).map((name) => …),
// 残りは「+N」で省略
if (showNames.length > 3)
Text('+${showNames.length - 3}'),
],
);
},
);
これだと、 ** ぱっと見では何人いるか分からない** という致命的な弱点があった。
「明日のレッスン、3人だけだと思って準備していたら、実は6人いた」みたいなミスにつながる可能性がある。
改善の方針:「画面が広い時は全員表示、狭い時は工夫する」
最初は素朴に「iPad では全員表示にして、iPhone は今まで通り」と思った。MediaQuery で判定すればよさそう、と。
dartfinal isTablet = MediaQuery.of(context).size.shortestSide >= 600;
iPad は画面が広いんだから、 セルを縦に伸ばして全員表示すればいい。
そう考えて、最初の実装はこんな感じだった:
dart
// セルのアスペクト比を、月内の最大レッスン数に応じて変える
int maxLessonsInMonth = 1;
for (int day = 1; day <= lastDay.day; day++) { final count = getShowNames(day).length; if (count > maxLessonsInMonth) maxLessonsInMonth = count;
}
final double aspectRatio = isTablet
? (1.0 / (1.0 + maxLessonsInMonth * 0.35)).clamp(0.25, 0.62)
: 0.62;
月内の最大レッスン数(例: 5人) に応じて、 全セルのアスペクト比を変更する。
ところが、これを動かしてみると…

失敗: 全部のセルが間延びする
これは GridView の仕様で、 同じ列のセルは全部同じ高さになるから当然なのだ。
「1日に5人いる日があるから全体を縦長にする」と、 5人いない日のセルも縦長になって、画面全体に間延びした空白だらけのカレンダーになってしまう。
これじゃ駄目だ。生徒が多い日のセルだけ伸びてほしい。
解決: GridView を捨てて「週ごとに動的高さ」へ
ここで発想を切り替えた。 GridView ではなく、 週ごとの Row を縦に並べる構造にすればいい。そうすれば、 その週内で最大の生徒数に合わせて高さを決められる。
// 月を週単位に分割
final List> weeks = [];
List currentWeek = [];
// 月初の空セル
for (int i = 0; i < startWeekday; i++) {
currentWeek.add(null);
}
// 日付を追加
for (int day = 1; day <= lastDay.day; day++) {
currentWeek.add(day);
if (currentWeek.length == 7) {
weeks.add(currentWeek);
currentWeek = [];
}
}
// 各週の最大生徒数を計算
List weekMaxStudents = [];
for (final week in weeks) {
int weekMax = 0;
for (final day in week) {
if (day == null) continue;
final count = getShowNames(day).length;
if (count > weekMax) weekMax = count;
}
weekMaxStudents.add(weekMax);
}
そして、各週を Row でレンダリングする時に、その週の最大生徒数に合わせた高さを SizedBox で指定する。
dartColumn(
children: List.generate(weeks.length, (weekIdx) {
final week = weeks[weekIdx];
final maxStudents = weekMaxStudents[weekIdx];
// この週の高さ = ヘッダー高 + (最大生徒数 × 1人分) + 余白
final double weekHeight = (headerHeight +
maxStudents * namePillHeight +
8).clamp(minCellHeight, double.infinity);
return SizedBox(
height: weekHeight,
child: Row(
children: week.map((day) {
return Expanded(child: buildDayCell(day));
}).toList(),
),
);
}),
);
これで実現できたのは:
✅ 空白多い→コンパクトな高さ
✅ 3〜4人の週→中程度
✅ 5人以上いる週→ だけ縦長
それぞれの週が 適切な高さで表示されるようになった。
さらに踏み込んだ改善:「3人制限」自体を廃止
最初は「iPad だけ全員表示」と思っていたが、スクショを見直して気づいた。
iPhone でも、スクロール可能なら全員表示できるじゃないか。
dart// iPad: 全員表示、iPhone: take(3) → 「+2」表示
final visibleNames = isTablet ? showNames : showNames.take(3).toList();
これを:
dart// 全画面サイズで全員表示(スクロール可能なので制限不要)
final visibleNames = showNames;
final hiddenCount = 0;
に変更。全プラットフォームで全員表示するように統一した。
スクロールできれば、画面の狭さは問題じゃないのだ。 「+N」のような省略表示よりも、下にスクロールして全部見られる方が直感的。
さらに細かい改善: 凡例の位置
カレンダーの下に「生徒名の色凡例(チップ)」が固定表示されていたが、これも見直した。
Before:
┌────────────┐
│ カレンダー │ ← 画面の半分
│ │
├────────────┤
│ 凡例チップ │ ← 残り半分
└────────────┘
After:
┌────────────┐
│ カレンダー │ ← 画面のほぼ全部使える
│ │
│ │ ← スクロール
│ ────────── │
│ 凡例チップ │ ← スクロール先に
└────────────┘
普段は凡例不要だし、必要な時はスクロールすれば見えるので、 カレンダー本体の表示エリアを最大化できる。

今回の改善で、毎月のレッスン管理が 「ぱっと見で混雑日が分かる」 ようになりました。
技術メモ
- GridView は固定アスペクト比に縛られる
GridView.builder の SliverGridDelegateWithFixedCrossAxisCount は、 全セルが同じアスペクト比になる。
「行ごとに高さを変えたい」ようなケースでは、 GridView ではなく Column + Row の組み合わせの方が柔軟。 - MediaQuery.of(context).size.shortestSide >= 600 で iPad 判定
これで iPad mini も含めて iPad 全機種を判定できる。 - clamp でアスペクト比の暴走を防ぐ
dart.clamp(0.25, 0.62)
これで「異常に縦長」や「異常に横長」を防げる。 - SingleChildScrollView + Column の組み合わせ
GridView を使わなくても、 SingleChildScrollView 内の Column で 動的高さの行を縦に並べることは普通に可能。
// 高さは「その週の最大生徒数」に応じて自動計算される
// → 同じ月でも、月によって混雑する週は違う
// 例: 5月のレッスン状況
SizedBox(height: 60, child: Row(...)), // 1週目(GWで休講)
SizedBox(height: 120, child: Row(...)), // 2週目
SizedBox(height: 140, child: Row(...)), // 3週目
SizedBox(height: 180, child: Row(...)), // 4週目(発表会前で混雑)
SizedBox(height: 80, child: Row(...)), // 5週目(月末)
シンプルだけど、 これで 週ごとの動的高さが実現できる。
ここでの「動的」は、 その月の生徒データに応じて毎回違う高さになる という意味です。
まとめ
✅ GridView の固定アスペクト比の限界は、 Column + Row 構造で乗り越えられる
✅ iPad と iPhone で見え方を最適化(フォントサイズ、表示文字数を調整)
リリース申請も完了したので、 数日以内に App Store / Google Play で v1.8.0 が公開される予定。
台湾・香港の老師の皆さんにも、ぜひ使ってもらえると嬉しいです。
