TEXTBOOK SECTION / AI LEARNING

読み込み中の表示を作る

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

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

OVERVIEW

この節で学べること

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

TABLE OF CONTENTS

目次

CONTENT

ここから

この章では、3Dモデルの読み込み中に、進行中であることを利用者へ伝える表示を追加します。

3Dモデルは、通信速度やファイル容量によって表示までに時間がかかることがあります。何も表示されない状態が続くと、利用者はアプリが停止しているのか、処理が進んでいるのか判断できません。

そこで、ViewerStatus.loadingの間だけ、3Dビューアーの上へローディング表示を重ねます。

13.1 読み込み表示が必要な理由

非同期処理では、処理の開始と完了の間に待ち時間が発生します。

この待ち時間に何も案内がないと、利用者が次のように感じる可能性があります。

  • 画面が正しく開いていない
  • タッチ操作が反応していない
  • 3Dモデルの読み込みに失敗した
  • アプリが停止している

読み込み表示は、処理が進行中であることを伝えるためのフィードバックです。処理時間を短くする機能ではありませんが、利用者の不安や誤操作を減らせます。

13.2 CircularProgressIndicatorを表示する

Flutterには、読み込み中を表すCircularProgressIndicatorが用意されています。

const CircularProgressIndicator()

進捗率を指定しない場合は、処理が継続中であることを示す円形アニメーションになります。

今回は、モデルの正確な読み込み率を画面へ表示せず、読み込み中であることだけを伝えます。

色と線の太さも指定できます。

const CircularProgressIndicator(
  strokeWidth: 2.5,
  color: Color(0xFF151515),
)

strokeWidthは円形インジケーターの線の太さです。

13.3 説明文を横に配置する

インジケーターだけでは何を読み込んでいるのか分かりにくいため、説明文を追加します。

const Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    CircularProgressIndicator(
      strokeWidth: 2.5,
      color: Color(0xFF151515),
    ),
    SizedBox(width: 12),
    Text(
      '3Dモデルを読み込んでいます',
      style: TextStyle(
        fontWeight: FontWeight.w500,
      ),
    ),
  ],
)

mainAxisSizeMainAxisSize.minを指定すると、Rowは子Widgetに必要な幅だけを使用します。

SizedBoxは、インジケーターと文字の間に余白を作ります。

13.4 半透明の背景を追加する

読み込み表示を3Dビューアー上へ直接置くと、モデルや背景と重なって読みにくくなることがあります。

そこで、ローディング表示を背景付きの領域で囲みます。

DecoratedBox(
  decoration: BoxDecoration(
    color: const Color(0xEFFFFFFF),
    borderRadius: BorderRadius.circular(16),
  ),
  child: const Padding(
    padding: EdgeInsets.symmetric(
      horizontal: 18,
      vertical: 14,
    ),
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        CircularProgressIndicator(
          strokeWidth: 2.5,
          color: Color(0xFF151515),
        ),
        SizedBox(width: 12),
        Text('3Dモデルを読み込んでいます'),
      ],
    ),
  ),
)

Color(0xEFFFFFFF)は、わずかに透過した白です。背後の表示を完全には隠さず、文字の読みやすさを確保します。

13.5 ローディング用Widgetをメソッドへ分ける

build()を読みやすく保つため、読み込み表示を返すメソッドへ分離します。

/// 3Dモデル読み込み中の案内を構築します。
///
/// 入力: なし
/// 出力: ローディング表示を含む[Widget]
Widget _buildLoadingOverlay() {
  return Center(
    child: DecoratedBox(
      decoration: BoxDecoration(
        color: const Color(0xEFFFFFFF),
        borderRadius: BorderRadius.circular(16),
      ),
      child: const Padding(
        padding: EdgeInsets.symmetric(
          horizontal: 18,
          vertical: 14,
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(
              width: 22,
              height: 22,
              child: CircularProgressIndicator(
                strokeWidth: 2.5,
                color: Color(0xFF151515),
              ),
            ),
            SizedBox(width: 12),
            Text(
              '3Dモデルを読み込んでいます',
              style: TextStyle(
                fontSize: 14,
                fontWeight: FontWeight.w500,
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

SizedBoxでインジケーターの大きさを固定すると、表示が大きくなりすぎるのを防げます。

13.6 Stackの中へ読み込み表示を追加する

第9章で作成したStackへ、ローディング表示を追加します。

Stack(
  fit: StackFit.expand,
  children: [
    Flutter3DViewer(
      controller: _controller,
      src: ModelSource.modelUrl,
      enableTouch: true,
      activeGestureInterceptor: true,
      progressBarColor: const Color(0xFF151515),
      onLoad: _handleModelLoaded,
      onError: _handleModelError,
    ),
    if (_status == ViewerStatus.loading)
      _buildLoadingOverlay(),
    const Positioned(
      left: 16,
      right: 16,
      bottom: 16,
      child: IgnorePointer(
        child: Text('操作案内'),
      ),
    ),
  ],
)

Dartのコレクション内ifを使用すると、条件を満たす場合だけWidgetをchildrenへ追加できます。

if (_status == ViewerStatus.loading)

状態がloadingの間だけローディング表示が追加され、readyerrorへ変わると自動的に取り除かれます。

13.7 操作案内との重なりを考える

現在のStackには、ローディング表示と操作案内の両方があります。

読み込み中はまだ3Dモデルを操作できないため、操作案内を表示し続ける必要はありません。そこで、操作案内もreadyのときだけ表示するように変更します。

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

これにより、状態ごとに必要な案内だけが表示されます。

13.8 この章で更新する主要部分

_ModelViewerScreenStateへ、ローディング表示のメソッドを追加します。

/// 3Dモデル読み込み中の案内を構築します。
///
/// 入力: なし
/// 出力: ローディング表示を含む[Widget]
Widget _buildLoadingOverlay() {
  return Center(
    child: DecoratedBox(
      decoration: BoxDecoration(
        color: const Color(0xEFFFFFFF),
        borderRadius: BorderRadius.circular(16),
      ),
      child: const Padding(
        padding: EdgeInsets.symmetric(
          horizontal: 18,
          vertical: 14,
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(
              width: 22,
              height: 22,
              child: CircularProgressIndicator(
                strokeWidth: 2.5,
                color: Color(0xFF151515),
              ),
            ),
            SizedBox(width: 12),
            Flexible(
              child: Text(
                '3Dモデルを読み込んでいます',
                style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

Stackは次のように更新します。

Stack(
  fit: StackFit.expand,
  children: [
    Flutter3DViewer(
      controller: _controller,
      src: ModelSource.modelUrl,
      enableTouch: true,
      activeGestureInterceptor: true,
      progressBarColor: const Color(0xFF151515),
      onProgress: (double progressValue) {
        debugPrint('Loading progress: $progressValue');
      },
      onLoad: _handleModelLoaded,
      onError: _handleModelError,
    ),
    if (_status == ViewerStatus.loading)
      _buildLoadingOverlay(),
    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,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
  ],
)

13.9 動作確認

アプリを完全に停止してから再起動し、次の順序で表示が変化することを確認します。

画面を開く
  ↓
読み込み表示が現れる
  ↓
3Dモデルの読み込みが完了する
  ↓
読み込み表示が消える
  ↓
操作案内が表示される

モデルが端末や内部処理によってキャッシュされている場合、読み込み表示が短時間しか見えないことがあります。その場合でも、状態がloadingからreadyへ切り替わっていれば正常です。

13.10 この章の到達目標

この章の終了時点で、次の内容を確認します。

  • 読み込み表示が必要な理由を説明できる
  • CircularProgressIndicatorを配置できる
  • Stackを使って3Dビューアーの上へ表示を重ねられる
  • コレクション内ifでWidgetの表示条件を指定できる
  • loadingの間だけローディング表示を出せる
  • readyになった後だけ操作案内を表示できる
  • 状態に応じて不要なWidgetを取り除ける

次章では、3Dモデルの読み込みに失敗した場合のエラー表示と、再読み込みボタンを実装します。

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

FAQ

よくある質問

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