はじめに
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へのリンク処理
ElevatedButton の onPressed プロパティを使って、ローディング中はボタンが押せないようにし、外部リンクを開く処理を行う。
isLoadingがtrueの場合、onPressedにnullを渡すことでボタンは非活性化される。- ボタンが押されたとき (
isLoadingがfalseの場合)、非同期処理を開始し、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 ウィジェットを使用して、ローディング中に物理的な戻るボタンを無効にする。また、AppBar の leading ウィジェットもローディング状態に基づいて制御。
onWillPopに渡された関数は、isLoadingがtrueの時にfalseを返すことで、ローディング中は画面の戻る操作を無効化。- AppBar の
leadingには、isLoadingがtrueの時は何も表示しない (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が表示される。 StackのalignmentプロパティをAlignment.centerに設定することで、プログレスインジケータはボタンの中央に表示される。
Dart
Center(
child: Stack(
alignment: Alignment.center,
children: [
ElevatedButton(
// ...
),
if (isLoading)
const Positioned(
child: CircularProgressIndicator(),
),
],
),
),結果
外部サイトに遷移というボタンをクリックしたら、ローディングが表示されて、ボタンは非活性化され、またアプリのナビゲーションバーの戻るボタンも無効化することができた。



コメント