CONTENT
ここから
この章では、3Dモデルの読み込みに失敗した場合のエラー表示と、再読み込みボタンを実装します。
通信を利用するアプリでは、常に成功するとは限りません。URLの間違い、ネットワーク切断、サーバー停止、ファイル破損など、さまざまな理由で3Dモデルを取得できないことがあります。
エラーを無視せず、利用者へ状況と次の操作を伝えることが重要です。
14.1 エラー表示に必要な情報
前章までに、エラー発生時は次の状態が保存されています。
_status = ViewerStatus.error;
_errorMessage = error;
_statusは現在の状態を表し、_errorMessageは失敗理由を保持します。
エラー画面では、最低限次の内容を表示します。
- 読み込みに失敗したこと
- エラーの概要
- 再読み込みするためのボタン
詳細なエラー文字列は、利用者にとって分かりにくい場合があります。そのため、見出しは簡潔にし、技術的な内容は補足として小さく表示します。
14.2 エラー用Widgetを作成する
エラー表示をbuild()へ直接書くとコードが長くなるため、専用メソッドへ分離します。
/// 3Dモデル読み込み失敗時の案内を構築します。
///
/// 入力: なし
/// 出力: エラー内容と再読み込みボタンを含む[Widget]
Widget _buildErrorOverlay() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 44,
color: Color(0xFF8B2E2E),
),
const SizedBox(height: 12),
const Text(
'3Dモデルを読み込めませんでした',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
_errorMessage,
textAlign: TextAlign.center,
),
],
),
),
);
}
mainAxisSizeへMainAxisSize.minを指定すると、Columnは必要な高さだけを使用します。
14.3 長いエラーメッセージを省略する
外部パッケージから返されるエラーは、非常に長い場合があります。そのまま表示すると、ボタンが画面外へ押し出される可能性があります。
Text(
_errorMessage.isEmpty
? '通信環境を確認して、もう一度お試しください。'
: _errorMessage,
maxLines: 4,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF666666),
fontSize: 12,
height: 1.5,
),
)
maxLinesは最大行数、TextOverflow.ellipsisは入りきらない内容を省略記号で表します。
エラー文字列が空の場合は、利用者向けの案内文を表示します。
14.4 再読み込みの考え方
再読み込みでは、単に状態をloadingへ戻すだけでは不十分です。
Flutter3DViewerが同じWidgetとして保持されている場合、読み込み処理が最初から実行されない可能性があります。そこで、ビューアーへ異なるKeyを渡し、Flutterに新しいWidgetとして再生成させます。
まず、ビューアー用のKeyを状態として保持します。
Key _viewerKey = UniqueKey();
UniqueKey()は、生成するたびに異なる値を持つKeyです。
14.5 再読み込み処理を作成する
再読み込みボタンから呼び出すメソッドを追加します。
/// 3Dビューアーを再生成し、読み込みを最初から実行します。
///
/// 入力: なし
/// 出力: なし
void _retryLoading() {
setState(() {
_status = ViewerStatus.loading;
_errorMessage = '';
_viewerKey = UniqueKey();
});
}
ここでは、次の3つを同時に更新します。
- 状態を
loadingへ戻す - 以前のエラーメッセージを削除する
- 新しいKeyを生成する
関連する値を同じ
setState()内で更新することで、再読み込み開始時の状態を一貫させます。
14.6 ビューアーへKeyを指定する
Flutter3DViewerへ、先ほどのKeyを渡します。
Flutter3DViewer(
key: _viewerKey,
controller: _controller,
src: ModelSource.modelUrl,
enableTouch: true,
activeGestureInterceptor: true,
progressBarColor: const Color(0xFF151515),
onLoad: _handleModelLoaded,
onError: _handleModelError,
)
_viewerKeyが変更されると、Flutterは以前の3Dビューアーを破棄し、新しい3Dビューアーを生成します。
これにより、同じURLであっても読み込み処理を最初から開始できます。
14.7 再読み込みボタンを追加する
エラー用WidgetへFilledButton.iconを追加します。
FilledButton.icon(
onPressed: _retryLoading,
icon: const Icon(Icons.refresh),
label: const Text('再読み込み'),
)
onPressedへメソッドを渡すことで、ボタンを押したときに再読み込み処理が実行されます。
ボタンの上には、十分な余白を追加します。
const SizedBox(height: 18),
FilledButton.icon(
onPressed: _retryLoading,
icon: const Icon(Icons.refresh),
label: const Text('再読み込み'),
),
14.8 完成したエラー用Widget
エラー表示メソッドを次の内容にします。
/// 3Dモデル読み込み失敗時の案内を構築します。
///
/// 入力: なし
/// 出力: エラー内容と再読み込みボタンを含む[Widget]
Widget _buildErrorOverlay() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xF2FFFFFF),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 22,
vertical: 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 44,
color: Color(0xFF8B2E2E),
),
const SizedBox(height: 12),
const Text(
'3Dモデルを読み込めませんでした',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
_errorMessage.isEmpty
? '通信環境を確認して、もう一度お試しください。'
: _errorMessage,
maxLines: 4,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF666666),
fontSize: 12,
height: 1.5,
),
),
const SizedBox(height: 18),
FilledButton.icon(
onPressed: _retryLoading,
icon: const Icon(Icons.refresh),
label: const Text('再読み込み'),
),
],
),
),
),
),
);
}
14.9 状態に応じてエラーを表示する
Stackの中へ、エラー状態の条件を追加します。
Stack(
fit: StackFit.expand,
children: [
Flutter3DViewer(
key: _viewerKey,
controller: _controller,
src: ModelSource.modelUrl,
enableTouch: true,
activeGestureInterceptor: true,
progressBarColor: const Color(0xFF151515),
onLoad: _handleModelLoaded,
onError: _handleModelError,
),
if (_status == ViewerStatus.loading)
_buildLoadingOverlay(),
if (_status == ViewerStatus.error)
_buildErrorOverlay(),
if (_status == ViewerStatus.ready)
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,
),
),
),
],
),
),
),
),
),
],
)
これで、状態ごとの表示は次のようになります。
loading → 読み込み表示
ready → 操作案内
error → エラー表示と再読み込みボタン
14.10 エラーを意図的に発生させる
動作確認のため、一時的にモデルURLを存在しないものへ変更します。
static const String modelUrl =
'<https://modelviewer.dev/shared-assets/models/not-found.glb>';
アプリを完全に再起動し、次の点を確認します。
- 読み込み表示が現れる
- 読み込み失敗後にエラー画面へ変わる
- エラー内容が長い場合に省略される
- 再読み込みボタンを押すとローディング表示へ戻る
確認後は、必ず正しいURLへ戻します。
static const String modelUrl =
'<https://modelviewer.dev/shared-assets/models/Astronaut.glb>';
正しいURLへ戻したあと、再読み込みによってモデルが表示されることを確認してください。
14.11 この章の到達目標
この章の終了時点で、次の内容を確認します。
- エラー状態とエラーメッセージを区別して管理できる
- 長いエラー文字列を安全に表示できる
- 再読み込み前に状態を初期化できる
UniqueKeyでWidgetを再生成できる- ボタンから再読み込み処理を実行できる
- 正常時と異常時の両方を動作確認できる
次章では、状態ごとの表示処理を一つのメソッドへ整理し、build()をより読みやすい構造へ改善します。