対象者
- Flutterを用いたモバイルアプリの開発経験があり、ユーザー体験を向上させるための新しい方法を求めている方。
- ProgressIndicatorの基本的な使用方法やカスタマイズ方法、そしてそのコントロールについて学びたい方。
- 一般的なFlutterのベストプラクティスを学び、自身の開発スキルを向上させたい方。
はじめに
アプリ開発では時間の掛かる処理があり、ユーザーにロード時間を通知する必要があります。これはProgressIndicator を使用して実現できます。これらの小さな要素がユーザー体験を大きく向上させます。この記事では、LinearProgressIndicatorとCircularProgressIndicatorについて学びます。
LinearProgressIndicatorとCircularProgressIndicatorは、日本語で「線形進行状況表示器」と「円形進行状況表示器」という意味です。プログラムの世界では一般的に進行状況をユーザーに視覚的に示すウィジェットを指します。
Flutterにおいては、タスクの進行状況を表示するウィジェットです。そのため、長時間かかるタスクや、終了時間が予測できないタスクが実行中であることをユーザーに示すことができます。
実際のアプリとしては、データのダウンロードやアップロード、ファイルの読み込みなどのケースにLinearProgressIndicatorやCircularProgressIndicatorを用いて進行状況を表示することで、ユーザー体験を向上させることができます。
棒で進行状況を示すLinearProgressIndicatoと円形のCircularProgressIndicatorを、それぞれの特性とカスタマイズ方法について解説します。また、見た目だけでなく、それらをどのように制御し、さまざまな状況で最適なユーザーエクスペリエンスを提供するかについても触れます。
実際のコードを交えながら、一緒に学んでいきましょう。この記事を読むことで、FlutterにおけるProgressIndicatorの完全な理解と効果的な使用法を身につけることができます。
LinearProgressIndicatorについて
概要と使用方法
LinearProgressIndicator
はFlutterのMaterialコンポーネントで、進行状況を視覚化するために使用されます。画面上に水平な進行バーとして表示され、タスクの進行状況を示すための簡易的で直感的な手段です。例えば、ファイルのアップロード中やデータのダウンロード中など、ユーザーが待ち時間を理解できるようにするために使用されます。
基本的な使用方法は、LinearProgressIndicator
ウィジェットをbuild
メソッド内に配置することです。指定したvalue
プロパティによって、進行バーの進行状況が制御されます。
以下に、基本的な使用例を示します。
LinearProgressIndicator(
value: _progress, // _progressは進行状況を制御するための変数です
);
ボーダー/角の半径の追加
LinearProgressIndicator
はデフォルトでは四角形ですが、角を丸くすることで見た目を変更することも可能です。これは、LinearProgressIndicator
をContainer
やClipRRect
ウィジェットと組み合わせることで実現します。
具体的には、LinearProgressIndicator
をContainer
ウィジェットで囲み、そのContainer
にBoxDecoration
を適用します。BoxDecoration
のborderRadius
プロパティを使用して、角の半径を指定します。これにより、プログレスバーの角が丸くなります。
以下に、角の半径を追加する使用例を示します。
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: _progress, // _progressは進行状況を制御するための変数です
),
);
これにより、角の丸いLinearProgressIndicator
が作成できます。見た目をカスタマイズすることで、アプリのデザインと一致させることが可能です。また、使用者の目を引くためにも有効な手段と言えるでしょう。
幅、アニメーション、ラインの高さの変更
FlutterのLinearProgressIndicator
は幅、アニメーション、そしてラインの高さといった要素を自由にカスタマイズすることができます。これにより、アプリケーションのデザインに合わせて進行状況のバーを調整できるので、ユーザビリティの向上に大いに役立ちます。
まず、LinearProgressIndicator
の幅はその親ウィジェットに依存します。つまり、Container
ウィジェット等を使って親ウィジェットのサイズを制御することで、LinearProgressIndicator
の幅も同時に変更されます。
また、ラインの高さはLinearProgressIndicator
のminHeight
属性を使って変更することが可能です。minHeight
属性に指定した数値がそのままラインの高さとなります。
さらに、LinearProgressIndicator
のアニメーションはvalueColor
プロパティを用いてカスタマイズできます。valueColor
プロパティにAlwaysStoppedAnimation<Color>
を指定し、進行状況のバーが変化する色を定義します。
以下に、これらのカスタマイズを行うサンプルコードを示します。
Container(
width: 200.0, // LinearProgressIndicatorの幅を制御します
child: LinearProgressIndicator(
minHeight: 10.0, // ラインの高さを指定します
value: _progress, // 進行状況を制御します
valueColor: AlwaysStoppedAnimation<Color>(Colors.red), // アニメーションの色を制御します
),
);
このように、FlutterのLinearProgressIndicator
では様々な要素をカスタマイズして、ユーザ体験を向上させることが可能です。
親ウィジェットにフィットしない問題
ある状況下で、LinearProgressIndicator
が親ウィジェットにフィットしないという問題が発生することがあります。これは主に、親ウィジェットのサイズが未定義であったり、制約が存在しないときに起こります。
Flutterでは、ウィジェットが描画されるときにはその大きさや位置が確定している必要があります。そのため、ウィジェットのサイズが未定義の場合、エラーが発生することがあります。
この問題を解決するには、親ウィジェットのサイズを明示的に定義するか、または親ウィジェットに制約を与えることが一般的な解決策となります。
例えば、以下のようにContainer
ウィジェットを使用して親ウィジェットの幅を明示的に指定することができます。
SizedBox(
width: 200.0, // LinearProgressIndicatorの幅を明示的に指定します
child: LinearProgressIndicator(
value: _progress, // 進行状況を制御します
),
);
このように、親ウィジェットのサイズを明示的に指定することで、LinearProgressIndicator
が正しく描画されるようになります。これにより、ユーザが進行状況を正確に把握できるようになります。
CircularProgressIndicatorについて
概要と使用方法
CircularProgressIndicator
はFlutterの中でも特によく利用されるウィジェットの一つです。このウィジェットは一般的には、アプリケーションがバックグラウンドで処理を行っているときに、ユーザにその進行状況を視覚的に示すために使われます。CircularProgressIndicator
は、その名の通り、円形のプログレスバーとして表示されます。
基本的な使用方法は非常にシンプルで、以下のようにCircularProgressIndicator
ウィジェットを作成するだけです。
CircularProgressIndicator();
この状態でウィジェットを作成すると、無限のアニメーション(スピニング)が表示されます。進行状況をユーザに示すために、value
属性を利用して0.0から1.0までの値を指定することも可能です。
CircularProgressIndicator(
value: _progress,
);
ここで、_progress
は進行状況を示す0.0から1.0までの値です。_progress
の値を更新することで、ユーザに進行状況を反映させることができます。
サイズの調整
CircularProgressIndicator
のサイズは、SizedBox
ウィジェットを使用して調整することができます。具体的には、SizedBox
ウィジェットのheight
とwidth
属性を指定することで、CircularProgressIndicator
のサイズを自由に設定することが可能です。
またstrokeWidth
を使うことで、円形の太さを調整することができます。
以下に、サイズを調整する例を示します。
SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator( strokeWidth: 10.0),
);
このコードは、高さと幅が共に50ピクセルのCircularProgressIndicator
を作成します。
色の変更
CircularProgressIndicator
の色はvalueColor
属性を使用して変更することができます。具体的には、valueColor
属性にAlwaysStoppedAnimation<Color>
ウィジェットを指定し、その中で色を定義します。
以下に、色を変更する例を示します。
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="d" data-joplin-source-open="```d
" data-joplin-source-close="
```">CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
);
このコードは、赤色のCircularProgressIndicator
を作成します。
以上、CircularProgressIndicator
の基本的な使用方法からサイズと色の調整までを解説しました。これらの設定を適切に用いることで、アプリケーションの見た目や使い勝手を向上させることが可能です。
応用
特定の期間だけ表示する方法
Flutterでは、特定の期間だけCircularProgressIndicator
を表示することが可能です。これはFuture.delayed
メソッドとsetState
メソッドを組み合わせて行います。以下に具体的な例を示します。
bool _isLoading = true;
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 2)).then((value) => setState(() {
_isLoading = false;
}));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _isLoading ? CircularProgressIndicator() : Text('Loading Finished!'),
),
);
}
この例では、アプリケーションが起動した時に2秒間だけCircularProgressIndicator
を表示し、その後にテキストメッセージを表示します。Future.delayed
を用いて非同期に2秒間の遅延を作り、その後setState
で_isLoading
をfalse
にセットします。これにより、ウィジェットが再描画され、CircularProgressIndicator
の代わりにテキストが表示されます。
決定的と非決定的なProgress Indicatorの違い
Progress Indicatorには大きく分けて2つのタイプがあります。それが「決定的なProgress Indicator」(Determinate Progress Indicator)と「非決定的なProgress Indicator」(Indeterminate Progress Indicator)です。
決定的なProgress Indicatorは、処理が完了するまでの進行状況を具体的なパーセンテージで表示するものです。つまり、処理全体が100%であるとき、処理が50%完了すればProgress Indicatorも50%の位置になります。このタイプのIndicatorは、処理の全体の長さや進行状況をあらかじめ知ることができる場合に適しています。
一方、非決定的なProgress Indicatorは、処理が進行中であることを示すためだけのもので、具体的な進行状況は示しません。このタイプのIndicatorは、処理の全体の長さや進行状況をあらかじめ知ることができない場合に適しています。
FlutterのCircularProgressIndicator
では、value
属性に具体的な値を指定することで決定的なIndicatorとして機能します。一方、value
属性を指定しない場合は非決定的なIndicatorとして機能します。
以上が、特定の期間だけProgress Indicatorを表示する方法と、決定的と非決定的なProgress Indicatorの違いについての解説です。これらを理解し、適切な状況で適切なタイプのIndicatorを使用することで、ユーザ体験を向上させることが可能です。
Timerを使用した進行状況の表示
アプリケーションで進行状況を表示する場合、一般的な方法の1つは、Timer
を用いて進行状況を逐次更新することです。この方法は特に、ファイルのダウンロード進行状況など、一定間隔で進行状況を更新する必要がある場合に有用です。
以下に、Timer
を用いてLinearProgressIndicator
の進行状況を更新する具体的な例を示します。
class _MyHomePageState extends State<MyHomePage> {
var _progressByTimer = 0.0;
@override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (Timer timer) {
setState(() {
_progressByTimer += 0.25;
if (1.0 <= _progressByTimer) {
timer.cancel();
}
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Linear Progress Indicator"),
),
body: Center(
child: LinearProgressIndicator(value: _progressByTimer),
),
);
}
}
このコードでは、initState
メソッド内でTimer.periodic
を用いて1秒ごとに進行状況を25%ずつ増加させ、LinearProgressIndicator
に反映させています。また、進行状況が100%に達したらタイマーをキャンセルします。
SemanticsLabelとSemanticsValueのアクセシビリティ機能
FlutterのProgressIndicator
ウィジェットには、視覚障害を持つユーザー向けにアクセシビリティ機能を提供するsemanticsLabel
とsemanticsValue
の2つのプロパティがあります。
semanticsLabel
はスクリーンリーダーがIndicatorを読み上げる際の説明を指定します。semanticsValue
はIndicatorの現在の値を表す文字列を指定します。これらのプロパティを使用することで、視覚障害を持つユーザーでもIndicatorの進行状況を理解することが可能になります。
以下に、これらのプロパティを用いた例を示します。
LinearProgressIndicator(
value: progress,
semanticsLabel: 'Linear progress indicator',
semanticsValue: '${(progress * 100).round()}%',
)
このコードでは、semanticsLabel
には「Linear progress indicator」、semanticsValue
には進行状況のパーセンテージが指定されています。これにより、スクリーンリーダーは「Linear progress indicator, 50%」のようにIndicatorを読み上げます。
Indicatorの表示制御
ProgressIndicatorの表示は、通常、処理が進行中であることをユーザーに伝えるために使用されます。具体的には、ダウンロード、アップロード、大量のデータの読み込みなどの進行中の処理を示す場合に便利です。一方で、あまりに長い時間、ProgressIndicatorが表示され続けると、ユーザー体験に悪影響を及ぼす可能性があります。したがって、適切な表示制御が重要となります。
実際の使用例を以下に示します。
bool _isLoading = false;
void loadData() async {
setState(() {
_isLoading = true;
});
// ここに時間のかかる処理を記述します。
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Progress Indicator'),
),
body: Center(
child: _isLoading
? CircularProgressIndicator()
: Text('No loading'),
),
);
}
この例では、_isLoading
という状態変数を使って、時間のかかる処理の前後でProgressIndicatorの表示/非表示を切り替えています。このように、適切な表示制御を行うことで、ユーザーに対して適切な情報を提供し、良好なユーザー体験を維持することが可能となります。
色を随時変えるようにする
valueColorでAlwaysStoppedAnimationを使ってきたので、他のアニメーションクラスを使うとどうなるかテストしました。随時色が変化し続けるようになりました。
var _progressByTimer = 0.0;
late final _animationController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
final _colorTween = ColorTween(begin: Colors.blue, end: Colors.red);
@override
void initState() {
super.initState();
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return LinearProgressIndicator(
value: _progressByTimer,
valueColor: _colorTween.animate(_animationController),
);
}),
Q&A
Q1: LinearProgressIndicator
とCircularProgressIndicator
の主な違いは何ですか?
A: LinearProgressIndicator
とCircularProgressIndicator
はどちらもタスクの進行状況を表示するためのFlutterのウィジェットですが、その表示の形状が異なります。LinearProgressIndicator
は進行状況を線形に、CircularProgressIndicator
は円形に表示します。
Q2: 決定的なProgress Indicatorと非決定的なProgress Indicatorの違いは何ですか?
A: 決定的なProgress Indicatorは、明確な進行状況(例えばダウンロードのパーセンテージなど)を表示します。一方、非決定的なProgress Indicatorは進行中であることを示すだけで、具体的な進行状況は表示しません。
Q3: SemanticsLabel
とSemanticsValue
は何のために使用するのですか?
A: SemanticsLabel
とSemanticsValue
は、アクセシビリティ機能を提供するために使用されます。特に視覚障害のあるユーザーに対して、進行状況を伝えるために役立ちます。
まとめ
ウェブサイトやアプリケーションのユーザーエクスペリエンスを高めるために、タスクの進行状況をユーザーに示すのが重要です。これを達成するために、FlutterはLinearProgressIndicator
とCircularProgressIndicator
という二つの素晴らしいウィジェットを提供しています。
LinearProgressIndicatorは、一連のタスクが完了するまでの進行状況を線形に表示します。色、幅、角の半径を自由に設定でき、さらにはアニメーションやラインの高さもカスタマイズ可能です。しかし、親ウィジェットのサイズにフィットしないという問題が発生することがありますが、これは適切な制約と配置を行うことで解決できます。一方、CircularProgressIndicatorは、進行中のタスクを円形に表示します。これもサイズや色を調整可能で、特定の期間だけ表示することも可能です。
また、決定的なProgress Indicatorと非決定的なProgress Indicatorの違いについても理解しました。決定的なものは明確な進行状況(例:ダウンロードのパーセンテージ)を表示し、非決定的なものは進行中であることを示すだけです。
さらに、進行状況の表示にはTimer
を使用した例を学びました。これは一定時間後に特定のアクションをトリガーするのに役立ちます。さらに、SemanticsLabel
とSemanticsValue
を使ったアクセシビリティ機能についても学びました。これらは、視覚障害のあるユーザーに進行状況を伝えるのに役立ちます。
最後に、ProgressIndicatorの動作について学びました。具体的には、Indicatorの表示制御、継続的なスピニング行動、そして停止に必要な特定の呼び出しについて学びました。これらの知識により、ユーザーに最高のエクスペリエンスを提供できるようになりました。
全体を通して、ProgressIndicatorはアプリケーションのユーザビリティを向上させ、ユーザーに安心感を与える重要な要素であると理解しました。
参考
- CircularProgressIndicator class (Flutter Widget of the Week)
- CircularProgressIndicator class – material library
- How to show CircularProgressIndicator for 3 seconds in flutter
ソース(main.dartにコピペして動作確認用)
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
var _progress = 0.8;
var _isLoading = true;
var _progressByTimer = 0.0;
late final _animationController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
final _colorTween = ColorTween(begin: Colors.blue, end: Colors.red);
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 2)).then((value) => setState(() {
_isLoading = false;
}));
Timer.periodic(Duration(seconds: 1), (Timer timer) {
setState(() {
_progressByTimer += 0.25;
if (1.0 <= _progressByTimer) {
timer.cancel();
}
});
});
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
const SizedBox(height: 16),
const LinearProgressIndicator(),
const SizedBox(height: 16),
const LinearProgressIndicator(
color: Colors.amber, backgroundColor: Colors.grey),
const SizedBox(height: 16),
const LinearProgressIndicator(
color: Colors.amber,
backgroundColor: Colors.grey,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.red), // アニメーションの色を制御します
),
const SizedBox(height: 16),
const CircularProgressIndicator(
color: Colors.amber,
backgroundColor: Colors.grey,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.red), // アニメーションの色を制御します
),
const SizedBox(height: 16),
const LinearProgressIndicator(
value: 0.1,
color: Colors.amber,
backgroundColor: Colors.grey,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.red), // アニメーションの色を制御します
),
const SizedBox(height: 16),
const LinearProgressIndicator(
value: 0.1,
color: Colors.amber,
backgroundColor: Colors.grey,
),
const SizedBox(height: 16),
LinearProgressIndicator(
value: _progress, // _progressは進行状況を制御するための変数です
),
const SizedBox(height: 16),
SizedBox(
width: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
minHeight: 16.0, // ラインの高さを指定します
value: _progress, // 進行状況を制御します
valueColor: AlwaysStoppedAnimation<Color>(
Colors.red), // アニメーションの色を制御します
),
),
),
const SizedBox(height: 16),
CircularProgressIndicator(),
const SizedBox(height: 16),
CircularProgressIndicator(value: _progress),
SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
strokeWidth: 10.0,
),
),
_isLoading ? CircularProgressIndicator() : Text('Loading Finished!'),
LinearProgressIndicator(value: _progressByTimer),
const SizedBox(height: 16),
LinearProgressIndicator(
value: _progress,
semanticsLabel: 'Linear progress indicator',
semanticsValue: '${(_progress * 100).round()}%',
),
const SizedBox(height: 16),
AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return LinearProgressIndicator(
value: _progressByTimer,
valueColor: _colorTween.animate(_animationController),
);
}),
],
),
);
}
}