🍏

Cupertino widgets②

2024/12/30に公開

iOSのローディングUIを再現する

CupertinoActivityIndicator classを使用するとiPhoneでよくみるあのローディングを再現できます。

ローディング・インジケーターと呼ばれているものですかね。

iOSのローディングインジケーター(CupertinoActivityIndicator)

iOSのローディングインジケーター(CupertinoActivityIndicator)の一般的な使用例をいくつか実装してみましょう。よくあるケースとして:

  1. API通信時のローディング
  2. 画像の読み込み
  3. ページ遷移時のローディング
  4. プルトゥリフレッシュ

https://youtube.com/shorts/eVvRF_ovIIE

example

main.dart
import 'dart:async';
import 'package:flutter/cupertino.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const CupertinoApp(
      theme: CupertinoThemeData(brightness: Brightness.light),
      home: MyHomePage(),
    );
  }
}

// ホーム画面
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('ローディング例'),
      ),
      child: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            // API通信例
            const APILoadingExample(),
            const SizedBox(height: 20),
            // 画像読み込み例
            const ImageLoadingExample(),
            const SizedBox(height: 20),
            // ページ遷移ボタン
            CupertinoButton.filled(
              onPressed: () {
                Navigator.push(
                  context,
                  CupertinoPageRoute(
                    builder: (context) => const DetailPage(),
                  ),
                );
              },
              child: const Text('詳細ページへ'),
            ),
            const SizedBox(height: 20),
            // プルトゥリフレッシュ例
            const Text(
              '↓ プルトゥリフレッシュを試してください',
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

// API通信のローディング例
class APILoadingExample extends StatefulWidget {
  const APILoadingExample({super.key});

  
  State<APILoadingExample> createState() => _APILoadingExampleState();
}

class _APILoadingExampleState extends State<APILoadingExample> {
  bool _isLoading = false;
  String _data = '未取得';

  Future<void> _fetchData() async {
    setState(() {
      _isLoading = true;
    });

    // APIリクエストのシミュレーション
    await Future.delayed(const Duration(seconds: 2));

    setState(() {
      _isLoading = false;
      _data = '更新されたデータ: ${DateTime.now().toString()}';
    });
  }

  
  Widget build(BuildContext context) {
    return CupertinoButton.filled(
      onPressed: _isLoading ? null : _fetchData,
      child: _isLoading
          ? const CupertinoActivityIndicator(color: CupertinoColors.white)
          : Text('データを取得: $_data'),
    );
  }
}

// 画像読み込みのローディング例
class ImageLoadingExample extends StatefulWidget {
  const ImageLoadingExample({super.key});

  
  State<ImageLoadingExample> createState() => _ImageLoadingExampleState();
}

class _ImageLoadingExampleState extends State<ImageLoadingExample> {
  bool _isLoading = true;

  
  void initState() {
    super.initState();
    _loadImage();
  }

  Future<void> _loadImage() async {
    // 画像読み込みのシミュレーション
    await Future.delayed(const Duration(seconds: 2));
    if (mounted) {
      setState(() {
        _isLoading = false;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Container(
      height: 200,
      decoration: BoxDecoration(
        color: CupertinoColors.systemGrey6,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(
        child: _isLoading
            ? const CupertinoActivityIndicator(radius: 15)
            : const Icon(
                CupertinoIcons.photo,
                size: 50,
                color: CupertinoColors.systemGrey,
              ),
      ),
    );
  }
}

// 詳細ページ(ページ遷移時のローディング例)
class DetailPage extends StatefulWidget {
  const DetailPage({super.key});

  
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  bool _isLoading = true;
  List<String> _items = [];

  
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    // データ読み込みのシミュレーション
    await Future.delayed(const Duration(seconds: 2));
    if (mounted) {
      setState(() {
        _isLoading = false;
        _items = List.generate(20, (index) => 'アイテム ${index + 1}');
      });
    }
  }

  
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('詳細ページ'),
      ),
      child: SafeArea(
        child: _isLoading
            ? const Center(
                child: CupertinoActivityIndicator(radius: 15),
              )
            : CustomScrollView(
                physics: const AlwaysScrollableScrollPhysics(),
                slivers: [
                  CupertinoSliverRefreshControl(
                    onRefresh: _loadData,
                  ),
                  SliverList(
                    delegate: SliverChildBuilderDelegate(
                      (context, index) => Container(
                        padding: const EdgeInsets.all(16),
                        decoration: BoxDecoration(
                          border: Border(
                            bottom: BorderSide(
                              color: CupertinoColors.systemGrey5,
                            ),
                          ),
                        ),
                        child: Text(_items[index]),
                      ),
                      childCount: _items.length,
                    ),
                  ),
                ],
              ),
      ),
    );
  }
}

このサンプルでは、以下の一般的なローディングパターンを実装しています:

  1. APIリクエスト時のローディング
  • ボタン内にインジケーターを表示
  • ボタンを無効化してダブルタップを防止
  • データ取得完了後に結果を表示
  1. 画像読み込み時のローディング
  • プレースホルダー領域にインジケーターを表示
  • 読み込み完了後にアイコンを表示
  • エラー時の代替表示
  1. ページ遷移時のローディング
  • 画面中央にインジケーターを表示
  • データ読み込み完了後にコンテンツを表示
  • プルトゥリフレッシュ機能
  1. プルトゥリフレッシュ
  • CupertinoSliverRefreshControlを使用
  • iOS標準のプルトゥリフレッシュ動作
  • リフレッシュ時にデータを再読み込み

使用時の注意点:

  1. インジケーターのサイズ
CupertinoActivityIndicator(
  radius: 15,  // デフォルトは10
  color: CupertinoColors.white,  // 色の変更も可能
)
  1. 状態管理
bool _isLoading = true;  // ローディング状態の管理
// メモリリーク防止
if (mounted) {
  setState(() {
    _isLoading = false;
  });
}
  1. エラーハンドリング
try {
  // データ取得処理
} catch (e) {
  // エラー処理
} finally {
  setState(() {
    _isLoading = false;
  });
}

このように、iOSのローディングインジケーターは様々なユースケースで使用され、ユーザーに処理中であることを適切に伝えることができます。

Discussion