CONTENT
ここから
この章では、これまで個別に記述してきた読み込み中、表示完了、エラー時のUIを一つのメソッドへ整理します。
状態ごとの条件分岐がbuild()内へ増え続けると、画面全体の構造が読みにくくなります。そこで、ViewerStatusを受け取り、表示すべきWidgetを返す処理へ分離します。
15.1 現在の表示処理を確認する
これまでのStackでは、状態ごとに複数のifを記述していました。
if (_status == ViewerStatus.loading)
_buildLoadingOverlay(),
if (_status == ViewerStatus.error)
_buildErrorOverlay(),
if (_status == ViewerStatus.ready)
_buildGestureGuide(),
この方法でも動作しますが、状態が増えるほど条件分岐も増えます。
また、loading、ready、errorは同時に成立しないため、「現在の状態に対応するWidgetを一つ返す」という形にまとめられます。
15.2 状態別Widgetを返すメソッドを作る
状態に応じた表示を返すメソッドを追加します。
/// 現在のビューアー状態に対応する表示を構築します。
///
/// 入力: なし
/// 出力: 読み込み状態に対応する[Widget]
Widget _buildStatusOverlay() {
switch (_status) {
case ViewerStatus.loading:
return _buildLoadingOverlay();
case ViewerStatus.ready:
return _buildGestureGuide();
case ViewerStatus.error:
return _buildErrorOverlay();
}
}
このメソッドは、現在の_statusを確認し、対応するWidgetを一つ返します。
状態判定が一か所へ集約されるため、どの状態で何が表示されるのか把握しやすくなります。
15.3 switch文を理解する
switch文は、一つの値を複数の候補と比較するときに使用します。
switch (_status) {
case ViewerStatus.loading:
return _buildLoadingOverlay();
case ViewerStatus.ready:
return _buildGestureGuide();
case ViewerStatus.error:
return _buildErrorOverlay();
}
_statusがViewerStatus.loadingなら、読み込み表示を返します。
ViewerStatus.readyなら操作案内、ViewerStatus.errorならエラー画面を返します。
ViewerStatusに定義されているすべての値を処理しているため、defaultは必要ありません。
15.4 switch式で簡潔に記述する
Dartでは、switchを式として記述できます。
/// 現在のビューアー状態に対応する表示を構築します。
///
/// 入力: なし
/// 出力: 読み込み状態に対応する[Widget]
Widget _buildStatusOverlay() {
return switch (_status) {
ViewerStatus.loading => _buildLoadingOverlay(),
ViewerStatus.ready => _buildGestureGuide(),
ViewerStatus.error => _buildErrorOverlay(),
};
}
=>の右側には、各状態で返す値を記述します。
今回のように、状態ごとに一つのWidgetを返す場合は、通常のswitch文よりも簡潔に表現できます。
教材では、処理の流れを理解しやすい方を選んで構いません。完成コードでは、短く読みやすいswitch式を使用します。
15.5 操作案内をメソッドへ分離する
これまでStack内へ直接書いていた操作案内も、専用メソッドへ分けます。
/// 3Dモデルの操作方法を示す案内を構築します。
///
/// 入力: なし
/// 出力: 画面下部へ配置する操作案内[Widget]
Widget _buildGestureGuide() {
return const Positioned(
left: 16,
right: 16,
bottom: 16,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(0xD9151515),
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.swipe,
color: Colors.white,
size: 20,
),
SizedBox(width: 10),
Flexible(
child: Text(
'1本指で回転・2本指で拡大縮小',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
),
);
}
これにより、build()内から長い操作案内のコードを取り除けます。
15.6 Stack内の表示を整理する
状態別の複数のifを削除し、_buildStatusOverlay()だけを配置します。
Stack(
fit: StackFit.expand,
children: [
Flutter3DViewer(
key: _viewerKey,
controller: _controller,
src: ModelSource.modelUrl,
enableTouch: true,
activeGestureInterceptor: true,
progressBarColor: const Color(0xFF151515),
onProgress: (double progressValue) {
debugPrint('Loading progress: $progressValue');
},
onLoad: _handleModelLoaded,
onError: _handleModelError,
),
_buildStatusOverlay(),
],
)
Stackの構造は、次の2層に整理されました。
奥側:Flutter3DViewer
手前:現在の状態に対応する表示
どの状態でも手前に一つのWidgetが返されるため、表示条件をStack側で意識する必要がありません。
15.7 完了時に空Widgetを返す方法
完成版の設計によっては、読み込み完了後に操作案内を常時表示しない場合もあります。その場合は、readyのときに空のWidgetを返します。
Widget _buildStatusOverlay() {
return switch (_status) {
ViewerStatus.loading => _buildLoadingOverlay(),
ViewerStatus.ready => const SizedBox.shrink(),
ViewerStatus.error => _buildErrorOverlay(),
};
}
SizedBox.shrink()は、幅と高さをほとんど持たない空のWidgetです。
nullを返すことはできないため、「何も表示しない」状態をWidgetとして表すときに使用します。
今回の教材では、操作方法を伝えるため、ready時には引き続き操作案内を表示します。
15.8 表示処理と状態変更処理を分ける
状態管理では、次の2種類の処理を分けることが重要です。
状態を変更する処理:
void _handleModelLoaded() {
if (!mounted) {
return;
}
setState(() {
_status = ViewerStatus.ready;
_errorMessage = '';
});
}
状態から表示を決める処理:
Widget _buildStatusOverlay() {
return switch (_status) {
ViewerStatus.loading => _buildLoadingOverlay(),
ViewerStatus.ready => _buildGestureGuide(),
ViewerStatus.error => _buildErrorOverlay(),
};
}
_handleModelLoaded()は、イベントを受けて状態を更新します。
_buildStatusOverlay()は、現在の状態を読み取り、表示するWidgetを決めます。
この二つを分離すると、状態変更の原因と表示結果を個別に確認できます。
15.9 この章で整理するコード
_ModelViewerScreenStateへ、操作案内と状態表示のメソッドを追加します。
/// 3Dモデルの操作方法を示す案内を構築します。
///
/// 入力: なし
/// 出力: 画面下部へ配置する操作案内[Widget]
Widget _buildGestureGuide() {
return const Positioned(
left: 16,
right: 16,
bottom: 16,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(0xD9151515),
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.swipe,
color: Colors.white,
size: 20,
),
SizedBox(width: 10),
Flexible(
child: Text(
'1本指で回転・2本指で拡大縮小',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
),
);
}
/// 現在のビューアー状態に対応する表示を構築します。
///
/// 入力: なし
/// 出力: 読み込み状態に対応する[Widget]
Widget _buildStatusOverlay() {
return switch (_status) {
ViewerStatus.loading => _buildLoadingOverlay(),
ViewerStatus.ready => _buildGestureGuide(),
ViewerStatus.error => _buildErrorOverlay(),
};
}
Stackは次のように簡潔になります。
Stack(
fit: StackFit.expand,
children: [
Flutter3DViewer(
key: _viewerKey,
controller: _controller,
src: ModelSource.modelUrl,
enableTouch: true,
activeGestureInterceptor: true,
progressBarColor: const Color(0xFF151515),
onProgress: (double progressValue) {
debugPrint('Loading progress: $progressValue');
},
onLoad: _handleModelLoaded,
onError: _handleModelError,
),
_buildStatusOverlay(),
],
)
15.10 build()を読みやすく保つ
build()には、画面全体の構造が分かる程度の記述を残します。
詳細なローディングUI、エラーUI、操作案内は、それぞれ専用メソッドへ移します。
理想的には、build()を読んだときに次の構造がすぐ分かる状態を目指します。
Scaffold
└── SafeArea
└── Padding
└── Column
├── タイトル
├── 3D表示領域
└── クレジット
細かな装飾をメソッドへ分離しても、分離しすぎると処理を追いにくくなります。画面上で一つの意味を持つまとまりを基準に分けます。
15.11 この章の到達目標
この章の終了時点で、次の内容を確認します。
- 状態ごとの表示を一つのメソッドへ集約できる
switch文とswitch式の違いを説明できるenumのすべての状態を処理できるSizedBox.shrink()で何も表示しない状態を表せる- 状態変更処理と表示生成処理を分離できる
- 長いWidgetを役割ごとのメソッドへ分けられる
build()から画面全体の構造を読み取れる 次章では、Flutter3DControllerを使ってカメラを初期位置へ戻すボタンを実装します。