レシピ#5-7: テキストサイズ変更への対応 - ダッシュボードとアクセシビリティ
Flutterアプリを実用的に開発する際、ユーザーのテキストサイズ設定への対応が重要になります。特に、ダッシュボードやグラフ、精密なレイアウトでは、テキストサイズ変更によってUIが崩れることがあります。
このレシピでは、アクセシビリティとUI安定性のバランスを取る方法を学びます。
💡 このレシピの進め方: 以下のコードはすべて、自分で書き写すのではなく Cursor / Claude Code に依頼して生成させましょう。各ステップの見出しをそのままプロンプトとして使えます。コードリストは「Claudeが生成する結果の例」として読んでください。
🎯 なぜテキストサイズ変更対応が必要か?
アクセシビリティの重要性
iOS・Androidでは、ユーザーがシステム設定でテキストサイズを変更できます:
- 視力が弱い人は、テキストを大きくして読みやすくする
- デフォルトサイズ以外を使うユーザーは意外と多い
- アクセシビリティ対応はアプリの品質指標の一つ
問題:UIが崩れる
テキストサイズが大きくなると:
❌ ダッシュボードのカードが崩れる
❌ グラフのラベルが重なる
❌ ボタンが画面からはみ出る
❌ 表組みのレイアウトが崩壊
特に問題になる場面: - ダッシュボード(データ表示が密集) - グラフ・チャート(軸ラベル、凡例) - テーブル・リスト(精密なレイアウト) - ナビゲーションバー(固定サイズ想定)
💡 基本概念:TextScaler
TextScalerとは?
Flutterでは、MediaQuery.textScalerOf(context)でユーザーのテキストサイズ設定(TextScaler)を取得できます:
※ 以前のtextScaleFactorはFlutter 3.16で非推奨になりました。現在はTextScalerを使います。
final textScaler = MediaQuery.textScalerOf(context);
// 数値の拡大率が必要な場合は scale(1.0) で取得
// デフォルト: 1.0
// ユーザーが「大」に設定: 1.2〜1.3
// ユーザーが「最大」に設定: 2.0〜3.0
final scaleFactor = textScaler.scale(1.0);
全てのTextウィジェットに自動適用されます:
Text(
'Hello',
style: TextStyle(fontSize: 16), // ユーザー設定で自動拡大
)
// 拡大率が1.5の場合、実質 16 * 1.5 = 24px
🛠️ 解決策:TextScalerの制御
方法1: アプリ全体で固定する(非推奨)
❌ 推奨しない方法:
MaterialApp(
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling, // 常に固定
),
child: child!,
);
},
home: MyHomePage(),
)
問題点: - アクセシビリティを完全に無視 - ユーザーの設定を尊重しない - ストアのレビューで批判される可能性
方法2: 特定の画面だけ固定する(推奨)
✅ 推奨:ダッシュボードや精密レイアウトのみ固定
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling, // この画面だけ固定
),
child: Scaffold(
appBar: AppBar(title: Text('ダッシュボード')),
body: _buildDashboard(),
),
);
}
}
メリット: - 通常の画面ではユーザー設定を尊重 - ダッシュボードだけUI崩れを防ぐ - バランスの取れたアプローチ
方法3: 最大値を制限する(ベストプラクティス)
✅ 最も推奨:過度な拡大を防ぐ
💬 プロンプト例: 「MaterialAppのbuilderで、アプリ全体のテキスト拡大率を
TextScaler.linearを使って最大1.3倍までに制限するコードを書いてください。」
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
final data = MediaQuery.of(context);
// 現在の拡大率を数値として取得
final scaleFactor = data.textScaler.scale(1.0);
return MediaQuery(
data: data.copyWith(
// 最大1.3倍まで許可(それ以上は固定)
textScaler: TextScaler.linear(scaleFactor.clamp(1.0, 1.3)),
),
child: child!,
);
},
home: MyHomePage(),
);
}
}
メリット: - アクセシビリティを一定程度尊重 - 極端なUI崩れを防ぐ - 実用的なバランス
推奨範囲: - 一般的なアプリ: 1.0〜1.5 - ダッシュボード系: 1.0〜1.3 - シンプルなアプリ: 1.0〜2.0(制限なし)
🎨 実践例
例1: ダッシュボードアプリ
問題: - グラフのラベルが重なる - カードレイアウトが崩れる
解決策:
💬 プロンプト例: 「売上グラフとメトリクスカードを持つダッシュボード画面を作成し、
TextScaler.noScalingでテキスト拡大を固定してUI崩れを防いでください。」
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling, // ダッシュボードは固定
),
child: Scaffold(
appBar: AppBar(title: Text('売上ダッシュボード')),
body: Column(
children: [
_buildSalesChart(), // グラフ
_buildMetricsCards(), // メトリクスカード
],
),
),
);
}
Widget _buildSalesChart() {
return Container(
height: 200,
child: LineChart(/* ... */),
);
}
Widget _buildMetricsCards() {
return Row(
children: [
_MetricCard(label: '今日の売上', value: '¥123,456'),
_MetricCard(label: '今月の売上', value: '¥9,876,543'),
],
);
}
}
例2: 通常の画面とダッシュボードを混在
ベストプラクティス:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
// 通常の画面:ユーザー設定を尊重
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ユーザーのテキストサイズ設定(TextScaler)は自動適用
return Scaffold(
appBar: AppBar(title: Text('ホーム')),
body: ListView(
children: [
ListTile(title: Text('設定')),
ListTile(title: Text('ダッシュボード')),
],
),
);
}
}
// ダッシュボード:固定
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling, // 固定
),
child: Scaffold(
appBar: AppBar(title: Text('ダッシュボード')),
body: _buildComplexLayout(),
),
);
}
}
例3: グラフのラベルだけ固定
より細かい制御:
class ChartWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// グラフエリア:テキスト拡大設定を無視
MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: Container(
height: 200,
child: LineChart(/* ... */),
),
),
// 説明テキスト:ユーザー設定を尊重
Padding(
padding: EdgeInsets.all(16),
child: Text(
'このグラフは過去30日間の売上推移を表しています。',
// ユーザーのTextScalerが適用される
),
),
],
);
}
}
📐 レイアウト崩れを防ぐ設計
1. Flexibleレイアウトを使う
❌ 固定サイズに依存:
Row(
children: [
Container(width: 100, child: Text('ラベル')),
Container(width: 200, child: Text('値')),
],
)
// テキスト拡大率が大きいと、テキストがはみ出る
✅ Flexibleを使う:
Row(
children: [
Flexible(flex: 1, child: Text('ラベル')),
Flexible(flex: 2, child: Text('値')),
],
)
// テキストサイズが変わっても自動調整
2. overflowを適切に処理
テキストが長い場合の対策:
Text(
'非常に長いテキストがここに入ります',
overflow: TextOverflow.ellipsis, // 省略記号で表示
maxLines: 1,
)
Text(
'非常に長いテキストがここに入ります',
overflow: TextOverflow.fade, // フェードアウト
maxLines: 2,
)
3. FittedBoxを使う
テキストを自動的にフィットさせる:
Container(
width: 100,
height: 50,
child: FittedBox(
fit: BoxFit.scaleDown, // はみ出る場合だけ縮小
child: Text('長いテキスト', style: TextStyle(fontSize: 20)),
),
)
⚠️ 注意: - FittedBoxは便利だが、読みにくくなる可能性 - アクセシビリティとのトレードオフ
♿ アクセシビリティとのバランス
推奨アプローチ
1. 基本方針: - 通常の画面: ユーザー設定を尊重(TextScalerをそのまま適用) - ダッシュボード・グラフ: 固定 or 最大値制限 - 理由を明確に: ストア説明やアプリ内で説明
2. 代替手段を提供:
💬 プロンプト例: 「テキスト拡大を固定したダッシュボード画面に、代替手段として拡大表示モードへ遷移するズームボタンをAppBarに追加してください。」
// ダッシュボードでテキスト拡大を固定する場合
class DashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
),
child: Scaffold(
appBar: AppBar(
title: Text('ダッシュボード'),
actions: [
// 拡大表示ボタンを提供
IconButton(
icon: Icon(Icons.zoom_in),
onPressed: () => _showLargeView(context),
),
],
),
body: _buildDashboard(),
),
);
}
void _showLargeView(BuildContext context) {
// 拡大表示モードを提供
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LargeTextDashboard(),
),
);
}
}
3. テストする:
// iOSシミュレータ:設定 > アクセシビリティ > 画面表示とテキストサイズ > さらに大きな文字
// Androidエミュレータ:設定 > ユーザー補助 > フォントサイズ
// または、開発中にコードで確認
MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(2.0), // 極端なケースをテスト
),
child: MyWidget(),
)
🎯 ベストプラクティスまとめ
✅ 推奨事項
-
アプリ全体で最大値を制限
dart textScaler: TextScaler.linear(data.textScaler.scale(1.0).clamp(1.0, 1.3)) -
ダッシュボード・グラフは固定
dart data.copyWith(textScaler: TextScaler.noScaling) -
通常の画面はユーザー設定を尊重 - 設定画面、リスト、記事閲覧など
-
Flexibleレイアウトを使う - 固定サイズに依存しない設計
-
代替手段を提供 - 拡大表示モード、詳細画面など
-
実機でテスト - 様々なテキストサイズ設定でUIを確認
❌ 避けるべき事項
-
アプリ全体でTextScaler.noScalingに固定 - アクセシビリティを無視
-
固定サイズに依存したレイアウト - UI崩れの原因
-
理由なくユーザー設定を無視 - ストアレビューで批判される
-
テストせずにリリース - 実際のユーザー環境で確認が必要
📝 実装チェックリスト
開発時
- [ ] ダッシュボード・グラフ画面でTextScalerを固定または制限
- [ ] 通常の画面ではユーザー設定を尊重
- [ ] Flexibleレイアウトを使用
- [ ] overflow処理を適切に設定
テスト時
- [ ] テキストサイズ「小」でテスト
- [ ] テキストサイズ「標準」でテスト
- [ ] テキストサイズ「大」でテスト
- [ ] テキストサイズ「最大」でテスト
- [ ] UI崩れがないか確認
リリース前
- [ ] アクセシビリティ対応をストア説明に記載
- [ ] 必要に応じて代替手段を提供
- [ ] ユーザーガイドに説明を追加
🔍 AIと一緒に実装する
Cursor / Claude Codeでの指示例
Composer(Ctrl+I)で:
「ダッシュボード画面を作成してください。
- テキストサイズ変更に対応するため、textScalerをTextScaler.noScalingに固定
- 売上グラフ、メトリクスカード(今日/今月の売上)を表示
- レイアウトはFlexibleを使って崩れないように」
Chat(Ctrl+L)で:
「このダッシュボード画面で、ユーザーがテキストサイズを「最大」に設定した場合の
UI崩れを防ぐにはどうすればいいですか?アクセシビリティも考慮した解決策を教えてください。」
まとめ
テキストサイズ変更対応は、実用的なアプリ開発で重要です:
- アクセシビリティを尊重 - 通常の画面ではユーザー設定を適用
- UI安定性を確保 - ダッシュボード・グラフでは固定または制限
- バランスを取る - 最大値制限(1.0〜1.3)が実用的
- 代替手段を提供 - 拡大表示モードなど
- 実機でテスト - 様々な設定で確認
これにより、アクセシビリティとUI品質の両立が可能になります。
次のステップ: 実際のダッシュボードやグラフを含むアプリで、テキストサイズ対応を実装してみましょう。
➡️ 関連レシピ:
- #5-4: レスポンシブデザインの基礎
- #4-1: Cursorを使った効率的な開発ワークフロー
➡️ 次のセクションへ: セクション6: 他のシェフとの協業