初めに
tutorial_coach_mark とは、以下のように、特定の Widget にフォーカスして、ユーザにアプリの操作方法を理解してもらうためのチュートリアルを作成するパッケージです。
公式ページから引用
記事の対象者
- アプリにチュートリアルを実装したい方
- 操作が難しいアプリを開発している方
- アプリに対して「操作がわかりにくい」とフィードバックがあった方
準備
パッケージの追加
まずは tutorial_coach_mark パッケージ を「 pubspeck.yaml 」に記述します。
パッケージのバージョンは、特に制約がなければ最新のバージョンで問題ありません。
dependencies:
flutter:
sdk: flutter
tutorial_coach_mark: ^1.2.4
Pub get をしてパッケージの準備は完了です。
完成イメージ
簡単な入力フォームを作成し、その入力方法を説明するためのチュートリアルを作成します。
本来であれば、入力フォームにチュートリアルを設けることはないかと思いますが、今回は一例として実装します。
全体コード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
class TutorialCoachMarkExample extends ConsumerStatefulWidget {
const TutorialCoachMarkExample({Key? key}) : super(key: key);
@override
TutorialCoachMarkExampleState createState() =>
TutorialCoachMarkExampleState();
}
class TutorialCoachMarkExampleState extends ConsumerState<TutorialCoachMarkExample> {
late TutorialCoachMark tutorialCoachMark;
List<TargetFocus> targets = [];
final GlobalKey _key = GlobalKey();
final GlobalKey _key1 = GlobalKey();
final GlobalKey _key2 = GlobalKey();
final GlobalKey _key3 = GlobalKey();
@override
void initState() {
initTargets();
WidgetsBinding.instance.addPostFrameCallback(_layout);
super.initState();
}
void _layout(_) {
Future.delayed(const Duration(milliseconds: 100));
showTutorial();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text("新規登録"),
),
body: Center(
child: Column(
children: [
const SizedBox(
height: 30,
),
Image.asset(
"assets/flutter_logo.png",
),
const SizedBox(
height: 30,
),
const Text("登録を完了させましょう!"),
const SizedBox(
height: 30,
),
SizedBox(
key: _key,
width: MediaQuery.of(context).size.width * 0.8,
child: const TextField(
decoration: InputDecoration(
border: OutlineInputBorder(), hintText: "ニックネーム"),
),
),
const SizedBox(
height: 30,
),
SizedBox(
key: _key1,
width: MediaQuery.of(context).size.width * 0.8,
child: const TextField(
decoration: InputDecoration(
border: OutlineInputBorder(), hintText: "メールアドレス"),
),
),
const SizedBox(
height: 30,
),
SizedBox(
key: _key2,
width: MediaQuery.of(context).size.width * 0.8,
child: const TextField(
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: "パスワード",
),
),
),
const SizedBox(
height: 30,
),
ElevatedButton(
key: _key3,
style: ElevatedButton.styleFrom(primary: Colors.blue[40]),
onPressed: () {},
child: const Padding(
padding: EdgeInsets.all(10.0),
child: Text("登録"),
),
),
],
),
),
);
}
void initTargets() {
targets.add(
TargetFocus(
identify: "Target 0",
keyTarget: _key,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
Text(
"ニックネームを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
],
),
)
],
shape: ShapeLightFocus.RRect,
radius: 5,
),
);
targets.add(
TargetFocus(
identify: "Target 1",
keyTarget: _key1,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text(
"メールアドレスを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
],
))
],
shape: ShapeLightFocus.RRect,
radius: 5,
),
);
targets.add(TargetFocus(
identify: "Target 2",
keyTarget: _key2,
contents: [
TargetContent(
align: ContentAlign.top,
child: Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: Text(
"パスワードを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
),
],
)),
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: Text(
"パスワードは安全に保管してください。",
style: TextStyle(color: Colors.white, fontSize: 14.0),
),
),
],
),
),
],
shape: ShapeLightFocus.RRect,
radius: 5,
));
targets.add(TargetFocus(
identify: "Target 3",
keyTarget: _key3,
// color: Colors.red,
contents: [
TargetContent(
align: ContentAlign.top,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(10.0),
child: Image.asset(
"assets/icon_flutter.png",
fit: BoxFit.contain,
height: 120,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 20.0),
child: Text(
"三つの項目が入力できたらこのボタンを押して登録完了です!",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
),
],
))
],
shape: ShapeLightFocus.RRect,
radius: 10
));
}
void showTutorial() {
tutorialCoachMark = TutorialCoachMark(
targets: targets,
textSkip: "SKIP",
paddingFocus: 10,
opacityShadow: 0.8,
)..show(context: context);
}
}
実装
それぞれ解説していきます。なお、今回は tutorial_coach_mark の使い方を解説することがこの記事の趣旨であるため、フォームの実装方法については解説しません。
List<TargetFocus> targets = [];
final GlobalKey _key = GlobalKey();
final GlobalKey _key1 = GlobalKey();
final GlobalKey _key2 = GlobalKey();
final GlobalKey _key3 = GlobalKey();
このコードでは TargetFocus
のリストを targets
という変数に代入し、初期値を空のリストにしています。
また、 _key
から _key4
までの変数にグローバルキーを代入しています。このグローバルキーはチュートリアルでどの要素にフォーカスするかを指定する時に必要になります。
@override
void initState() {
initTargets();
WidgetsBinding.instance.addPostFrameCallback(_layout);
super.initState();
}
このコードでは initState()
でページの初期状態を指定しています。
initTargets()
は、先ほど作成した TargetFocus
の空のリストに TargetFocus
を追加していく処理になります。詳しくは後述します。
WidgetsBinding.instance.addPostFrameCallback()
この関数は全ての Widget のビルドが終わった時に実行する処理を記述するための関数です。
そしてこの関数は最初にページが読み込まれたとき、一度しか発火しません。
したがって、ページが読み込まれた瞬間にチュートリアルを実行する必要がある際に最適なメソッドと言えます。
上のコードでは、addPostFrameCallback()
関数の引数に _layout
が入れられています。
この _layout
は自作のメソッドで、チュートリアルを表示させるメソッドが含まれています。
void _layout(_) {
Future.delayed(const Duration(milliseconds: 100));
showTutorial();
}
このコードは先述した _layout()
の内容を記述したコードです。
Future.delayed()
メソッドではチュートリアルを表示させる時間を遅らせています。
showTutorial()
メソッドは、チュートリアルを表示させるメソッドで、全ての Widget のビルドが完了し、_layout()
メソッドが実行された時に実行されます。
詳しい内容は後述します。
SizedBox(
key: _key,
width: MediaQuery.of(context).size.width * 0.8,
child: const TextField(
decoration: InputDecoration(
border: OutlineInputBorder(), hintText: "ニックネーム"
),
),
),
このコードでは SizedBox
の子要素に TextField
を指定することで大きさを調整して表示させています。
key: _key,
このようにすることで、 SizedBox
にグローバルキーが割り当てられます。
このキーがあることで、それぞれのチュートリアルの場面で異なる Widget にフォーカスを当てたり、表示させる内容を変更したりすることができるようになります。
このコード以下の SizedBox
にそれぞれ別のグローバルキーを割り当てることで、独自の内容を表示させています。グローバルキーの割り当ては全く同じなので、その他の要素に関しては省略します。
void initTargets() {
targets.add(
TargetFocus(
identify: "Target 0",
keyTarget: _key,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
Text(
"ニックネームを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
],
),
)
],
shape: ShapeLightFocus.RRect,
radius: 5,
),
);
このコードでは targts
のリストに TargetFocus
を追加しています。
以下で詳しく解説していきます。
identify: "Target 0",
identify
は TargetFocus
がどの要素に当てられているものか、何を説明するためのものかなど、 TargetFocus
の説明などを String 型で指定するためのプロパティです。
公式ページでも使用方法が厳しく限定されているわけではないため、 TargetFocus
の説明を簡潔にまとめれば自分にもわかりやすくなります。
keyTarget: _key,
このコードでフォーカスさせたい要素のグローバルキーを指定しています。
keyTarget
がないとフォーカスする場所がないため、エラーになります。
contents: [
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
Text(
"ニックネームを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
],
),
)
],
contents
ではチュートリアルで、特定の Widget にフォーカスが当たった際に表示される要素を指定しています。
表示させる要素は TargetContent()
で囲む必要があります。
align
でコンテンツを表示させる位置を指定します。
child
で表示させる Widget を指定します。上のコードではテキストを表示させています。
テキスト以外にも画像などを表示させることも可能です。
shape: ShapeLightFocus.RRect,
radius: 5,
このコードではフォーカスの形を指定しています。
上のようにすると四角のフォーカスが当たります。
shape: ShapeLightFocus.Circle,
このようにすると円形のフォーカスが当たります。
radius
では、フォーカスを四角にしたときのみ、その角を調整することができます。
void initTargets() {
targets.add(
TargetFocus(
identify: "Target 0",
keyTarget: _key,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const <Widget>[
Text(
"ニックネームを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
],
),
)
],
shape: ShapeLightFocus.RRect,
radius: 5,
+ color: Colors.red,
),
);
color
を指定すると、以下のように特定のフォーカスだけ背景色を変更することができます。
targets.add(TargetFocus(
// identify: "Target 2",
keyTarget: _key2,
contents: [
TargetContent(
align: ContentAlign.top,
child: Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: Text(
"パスワードを入力してください。",
style: TextStyle(color: Colors.white, fontSize: 18.0),
),
),
],
)),
TargetContent(
align: ContentAlign.bottom,
child: Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: Text(
"パスワードは安全に保管してください。",
style: TextStyle(color: Colors.white, fontSize: 14.0),
),
),
],
),
),
],
shape: ShapeLightFocus.RRect,
radius: 5,
+ focusAnimationDuration: Duration(seconds: 2),
));
focusAnimationDuration
の Duration
を2秒に指定すると、2秒かけてフォーカスするようになるため、他の要素へのフォーカスと比較して以下のようにフォーカスする際の速度が遅くなります。
パスワードの TextField にフォーカスする際にのみフォーカスの速度が遅くなっているのが分かるかと思います。
void showTutorial() {
tutorialCoachMark = TutorialCoachMark(
targets: targets,
textSkip: "SKIP",
paddingFocus: 10,
opacityShadow: 0.8,
)..show(context: context);
}
このコードはチュートリアルを表示させるためのメソッドを記述しています。
targets: targets,
この記述で、チュートリアルのターゲットに TargetFocus
のリストが代入された targets
が指定されています。
textSkip: "SKIP",
この記述で、スキップボタンのテキストを変更しています。
なお、何も指定していないデフォルトの状態では「SKIP」というテキストになります。
paddingFocus: 10,
この記述で特定の要素にフォーカスした際のその要素との空白を指定しています。以下の画像では薄い赤色の部分になります。
opacityShadow: 0.8,
この記述でフォーカスされている部分以外の透明度を指定しています。
この値を変更すると以下のようになります。
opacityShadow: 0.3,

TutorialCoachMark()
には show()
メソッドがあり、これを実行することでチュートリアルが表示されます。
以上です。
あとがき
最後まで読んでいただきありがとうございました。
参考にしていただければ幸いです。
誤っている箇所があればご指摘いただければ幸いです。
参考にしたサイト