TEXTBOOK SECTION / AI LEARNING

エラー画面と再読み込みを実装する

Flutterアプリケーション開発概論の「3Dモデルと描画」より、エラー画面と再読み込みを実装するを解説。生成AI、AI活用、DX、業務改善を実践しながら学べるオンライン教材です。

143Dモデルと描画Flutter / iOS / Android / MacOS / Windows / 基礎から学ぶ / 開発 / アプリ開発

OVERVIEW

この節で学べること

概要を表示する
項目内容
教材名Flutterアプリケーション開発概論
3Dモデルと描画
エラー画面と再読み込みを実装する
カテゴリFlutter / iOS / Android / MacOS / Windows / 基礎から学ぶ / 開発 / アプリ開発
学習内容生成AI、AI活用、DX、業務改善を実践しながら理解するための教材です。

TABLE OF CONTENTS

目次

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,
          ),
        ],
      ),
    ),
  );
}

mainAxisSizeMainAxisSize.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つを同時に更新します。

  1. 状態をloadingへ戻す
  2. 以前のエラーメッセージを削除する
  3. 新しい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()をより読みやすい構造へ改善します。

https://app.notion.com/p/3a0b4ab5d13680569c3fd8a4b174d779?v=2c7b4ab5d136802d9a64000c46ee24e8&source=copy_link

FAQ

よくある質問

エラー画面と再読み込みを実装するは医療関係者向けだけの内容ですか。
医療分野の例が含まれる場合もありますが、医療関係者だけに限定した内容ではありません。生成AI、AI活用、DX、業務改善、プロトタイプ開発など、一般的なAI学習の事例として読める内容です。
AI初心者でも読めますか。
はい。AIをこれから学ぶ方、数学が苦手な方、仕事でAIを使いたい方にも読み進めやすいように、教材の章と節の流れに沿って整理しています。
サムネイル画像は必ず表示されますか。
はい。教材にcoverUrlが設定されている場合はその画像を表示し、未設定の場合は代替サムネイル画像を表示します。
Flutterアプリケーション開発概論のほかの章も読めますか。
はい。教材トップから章立てを確認でき、前後の節へもページ下部のナビゲーションから移動できます。