方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【Flutter × Riverpod】外部サイト遷移処理中のローディング状態処理とボタン非活性化

この記事は約9分で読めます。

はじめに

Flutterアプリから外部サイトへ遷移させたい場合で、遷移まで時間がかかる(APIリクエストしてresponseの情報を元に遷移するなど)際の処理として

  • ローディングアイコンを表示して、遷移まではボタンを非活性化する
  • 遷移するまでの間にユーザーが画面を移動しないようにする

などの制御ニーズが発生する。

Flutter と Riverpod を利用してローディングの状態管理を行い、ボタンの非活性化と、ナビゲーションの戻るボタンの無効化を行ったので、備忘録記載。

コード全体像

Dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';

final loadingProvider = StateProvider<bool>((ref) => false);

class DataRetrievalPage extends HookConsumerWidget {
  const DataRetrievalPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isLoading = ref.watch(loadingProvider);

    return WillPopScope(
      onWillPop: () async => !isLoading,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('テスト画面'),
          leading: isLoading
              ? null
              : IconButton(
                  icon: const Icon(Icons.arrow_back),
                  onPressed: () => Navigator.of(context).pop(),
                ),
        ),
        body: const Padding(
          padding: EdgeInsets.all(16.0),
          child: _Body(),
        ),
      ),
    );
  }
}

class _Body extends ConsumerWidget {
  const _Body();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isLoading = ref.watch(loadingProvider);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '外部サイトに遷移します',
          style: TextStyle(
            fontSize: 16,
            height: 1.5,
          ),
        ),
        const SizedBox(height: 24),
        Center(
          child: Stack(
            alignment: Alignment.center,
            children: [
              ElevatedButton(
                onPressed: isLoading ? null : () async {
									  ref.read(loadingProvider.notifier).state = true;
									  try {
									    final urlString = await fetchExternalUrl();
									    final url = Uri.parse(urlString);
									    
									    if (await canLaunchUrl(url)) {
									      await launchUrl(url);
									    } else {
									      throw Exception('Could not launch $url');
									    }
									  } catch (e) {
									    ref.watch(exceptionHelperProvider).showMessage(e);
									  } finally {
									    ref.read(loadingProvider.notifier).state = false;
									  }
									},
									child: const Text('外部サイトに遷移'),
              ),
              if (isLoading)
                const Positioned(
                  child: CircularProgressIndicator(),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

ローディング状態の管理

loadingProvider という StateProvider を使用してローディングの状態を管理。

Dart
final loadingProvider = StateProvider<bool>((ref) => false);
  • これにより、ローディング状態はアプリの状態管理システム内で保持され、どのウィジェットからでも読み書きができるようになる。
  • 初期状態は false と定義し、これは「ローディングしていない」という状態を表す。

ボタンの非活性化と外部URLへのリンク処理

ElevatedButtononPressed プロパティを使って、ローディング中はボタンが押せないようにし、外部リンクを開く処理を行う。

  • isLoadingtrue の場合、onPressednull を渡すことでボタンは非活性化される。
  • ボタンが押されたとき (isLoadingfalse の場合)、非同期処理を開始し、loadingProvider の状態を true に設定してローディング状態に入る。
  • canLaunchUrl でURLが開けるかどうかを確認し、開ける場合は launchUrl を呼び出して実際にURLを開く。
  • try-catch-finally ブロックを使用して、例外が発生した場合でも、発生しなかった場合でも、最終的に finally ブロックでローディング状態を false に戻す。
Dart
onPressed: isLoading ? null : () async {
  ref.read(loadingProvider.notifier).state = true;
  try {
    // 外部リソースからURLを取得する想定のコード
    final urlString = await fetchExternalUrl();
    final url = Uri.parse(urlString);
    
    // URLを開く処理。成功したら外部ブラウザが起動し、失敗した場合は例外を投げます。
    if (await canLaunchUrl(url)) {
      await launchUrl(url);
    } else {
      throw Exception('Could not launch $url');
    }
  } catch (e) {
    // エラーハンドリング。例外が発生した場合は、それをユーザーに通知します。
    ref.watch(exceptionHelperProvider).showMessage(e);
  } finally {
    // 例外発生の有無にかかわらず、最終的にローディング状態を終了します。
    ref.read(loadingProvider.notifier).state = false;
  }
},
child: const Text('データ取得開始'),

AppBarの戻るボタンの非活性化

WillPopScope ウィジェットを使用して、ローディング中に物理的な戻るボタンを無効にする。また、AppBarleading ウィジェットもローディング状態に基づいて制御。

  • onWillPop に渡された関数は、isLoadingtrue の時に false を返すことで、ローディング中は画面の戻る操作を無効化。
  • AppBar の leading には、isLoadingtrue の時は何も表示しない (null) ことで、AppBar の戻るボタンも非活性化。
Dart
return WillPopScope(
  onWillPop: () async => !isLoading,
  child: Scaffold(
    appBar: AppBar(
      // ...
      leading: isLoading
          ? null
          : IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: () => Navigator.of(context).pop(),
            ),
    ),
    // ...
  ),
);

ローディング中の進捗表示

Stack ウィジェットを使用して、ローディング中に CircularProgressIndicator をボタンの中央に表示。

  • ローディング状態 (isLoading) が true の場合、CircularProgressIndicator が表示される。
  • Stackalignment プロパティを Alignment.center に設定することで、プログレスインジケータはボタンの中央に表示される。
Dart
Center(
  child: Stack(
    alignment: Alignment.center,
    children: [
      ElevatedButton(
        // ...
      ),
      if (isLoading)
        const Positioned(
          child: CircularProgressIndicator(),
        ),
    ],
  ),
),

結果

外部サイトに遷移というボタンをクリックしたら、ローディングが表示されて、ボタンは非活性化され、またアプリのナビゲーションバーの戻るボタンも無効化することができた。

コメント

タイトルとURLをコピーしました