【Flutter】tutorial_coach_mark パッケージの使い方

こんにちは、株式会社Pentagonでアプリ開発をしている山田です。
https://pentagon.tokyo

今回は、tutorial_coach_markパッケージを用いたチュートリアル画面の作成について解説します。

【こんな人に読んで欲しい】

  • Flutterでチュートリアル画面を作成したい方
  • 初めてtutorial_coach_markパッケージを使う方

【この記事を読むメリット】

基本的なチュートリアルの実装方法を学べます。

【結論】

tutorial_coach_markパッケージを使えば、Flutterでの簡単なチュートリアル実装は容易なので、参考になれば幸いです。

目次

今回作成するサンプル画像

今回は以下の様なサンプルを作成します。

作成したサンプルアプリの全体のコード

以下が今回作成したサンプルアプリの全体のコードです。

import 'package:flutter/material.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';

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

class MyApp extends StatelessWidget {
 // This widget is the root of your application.
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'TutorialCoachMark デモ',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 late TutorialCoachMark tutorialCoachMark;
 List<TargetFocus> targets = <TargetFocus>[];

 GlobalKey keyButton = GlobalKey();
 GlobalKey keyButton1 = GlobalKey();
 GlobalKey keyButton2 = GlobalKey();
 GlobalKey keyButton3 = GlobalKey();
 GlobalKey keyButton4 = GlobalKey();
 GlobalKey keyButton5 = GlobalKey();

 GlobalKey keyBottomNavigation1 = GlobalKey();
 GlobalKey keyBottomNavigation2 = GlobalKey();
 GlobalKey keyBottomNavigation3 = GlobalKey();

 @override
 void initState() {
   Future.delayed(Duration.zero, showTutorial);
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       actions: <Widget>[
         IconButton(
           // key: keyButton1,
           icon: Icon(Icons.add),
           onPressed: () {},
         ),
         PopupMenuButton(
           key: keyButton1,
           icon: Icon(Icons.view_list, color: Colors.white),
           itemBuilder: (context) => [
             PopupMenuItem(
               child: Text("選択肢1"),
             ),
             PopupMenuItem(
               child: Text("選択肢2"),
             ),
             PopupMenuItem(
               child: Text("選択肢3"),
             ),
           ],
         )
       ],
     ),
     body: Container(
       color: Colors.white,
       child: Stack(
         children: <Widget>[
           Padding(
             padding: const EdgeInsets.only(top: 100.0),
             child: Align(
               alignment: Alignment.topCenter,
               child: Container(
                 key: keyButton,
                 color: Colors.blue,
                 height: 100,
                 width: MediaQuery.of(context).size.width - 50,
                 child: Align(
                   alignment: Alignment.center,
                   child: ElevatedButton(
                     child: Icon(Icons.remove_red_eye),
                     onPressed: () {
                       showTutorial();
                     },
                   ),
                 ),
               ),
             ),
           ),
           Align(
             alignment: Alignment.center,
             child: SizedBox(
               width: 50,
               height: 50,
               child: ElevatedButton(
                 key: keyButton2,
                 onPressed: () {},
                 child: Container(),
               ),
             ),
           ),
           Align(
             alignment: Alignment.bottomCenter,
             child: Padding(
               padding: const EdgeInsets.all(50.0),
               child: SizedBox(
                 width: 50,
                 height: 50,
                 child: ElevatedButton(
                   key: keyButton3,
                   onPressed: () {},
                   child: Container(),
                 ),
               ),
             ),
           ),
           Align(
             alignment: Alignment.centerRight,
             child: Padding(
               padding: const EdgeInsets.all(50.0),
               child: SizedBox(
                 width: 50,
                 height: 50,
                 child: ElevatedButton(
                   key: keyButton4,
                   onPressed: () {},
                   child: Container(),
                 ),
               ),
             ),
           ),
           Align(
             alignment: Alignment.centerLeft,
             child: Padding(
               padding: const EdgeInsets.all(50.0),
               child: SizedBox(
                 width: 50,
                 height: 50,
                 child: ElevatedButton(
                   key: keyButton5,
                   onPressed: () {},
                   child: Container(),
                 ),
               ),
             ),
           )
         ],
       ),
     ),
     bottomNavigationBar: Stack(
       children: [
         Container(
           height: 50,
           child: Row(
             children: [
               Expanded(
                   child: Center(
                 child: SizedBox(
                   key: keyBottomNavigation1,
                   height: 40,
                   width: 40,
                 ),
               )),
               Expanded(
                   child: Center(
                 child: SizedBox(
                   key: keyBottomNavigation2,
                   height: 40,
                   width: 40,
                 ),
               )),
               Expanded(
                 child: Center(
                   child: SizedBox(
                     key: keyBottomNavigation3,
                     height: 40,
                     width: 40,
                   ),
                 ),
               ),
             ],
           ),
         ),
         BottomNavigationBar(
           items: const <BottomNavigationBarItem>[
             BottomNavigationBarItem(
               icon: Icon(Icons.home),
               label: 'Home',
             ),
             BottomNavigationBarItem(
               icon: Icon(Icons.business),
               label: 'Business',
             ),
             BottomNavigationBarItem(
               icon: Icon(Icons.school),
               label: 'School',
             ),
           ],
           // currentIndex: _selectedIndex,
           selectedItemColor: Colors.amber[800],
           onTap: (index) {},
         ),
       ],
     ),
   );
 }

 void showTutorial() {
   initTargets();
   tutorialCoachMark = TutorialCoachMark(
     context,
     targets: targets,
     colorShadow: Colors.red,
     textSkip: "スキップ",
     paddingFocus: 10,
     opacityShadow: 0.8,
     onFinish: () {
       print("終了");
     },
     onClickTarget: (target) {
       print('onClickTarget: $target');
     },
     onSkip: () {
       print("スキップ");
     },
     onClickOverlay: (target) {
       print('onClickOverlay: $target');
     },
   )..show();
 }

 void initTargets() {
   targets.clear();
   targets.add(
     TargetFocus(
       identify: "keyBottomNavigation1",
       keyTarget: keyBottomNavigation1,
       alignSkip: Alignment.topRight,
       contents: [
         TargetContent(
           align: ContentAlign.top,
           builder: (context, controller) {
             return Container(
               child: Column(
                 mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(
                     "1番目の選択肢。",
                     style: TextStyle(
                       color: Colors.white,
                     ),
                   ),
                 ],
               ),
             );
           },
         ),
       ],
     ),
   );

   targets.add(
     TargetFocus(
       identify: "keyBottomNavigation2",
       keyTarget: keyBottomNavigation2,
       alignSkip: Alignment.topRight,
       contents: [
         TargetContent(
           align: ContentAlign.top,
           builder: (context, controller) {
             return Container(
               child: Column(
                 mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(
                     "2番目の選択肢",
                     style: TextStyle(
                       color: Colors.white,
                     ),
                   ),
                 ],
               ),
             );
           },
         ),
       ],
     ),
   );

   targets.add(
     TargetFocus(
       identify: "keyBottomNavigation3",
       keyTarget: keyBottomNavigation3,
       alignSkip: Alignment.topRight,
       contents: [
         TargetContent(
           align: ContentAlign.top,
           builder: (context, controller) {
             return Container(
               child: Column(
                 mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(
                     "3番目の選択肢",
                     style: TextStyle(
                       color: Colors.white,
                     ),
                   ),
                 ],
               ),
             );
           },
         ),
       ],
     ),
   );
   targets.add(
     TargetFocus(
       identify: "Target 0",
       keyTarget: keyButton1,
       contents: [
         TargetContent(
           align: ContentAlign.bottom,
           builder: (context, controller) {
             return Container(
               child: Column(
                 mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(
                     "4番目の選択肢",
                     style: TextStyle(
                         fontWeight: FontWeight.bold,
                         color: Colors.white,
                         fontSize: 20.0),
                   ),
                   Padding(
                     padding: const EdgeInsets.only(top: 10.0),
                     child: Text(
                       "Columnでテキストを増やすことができる。",
                       style: TextStyle(color: Colors.white),
                     ),
                   ),
                 ],
               ),
             );
           },
         ),
       ],
     ),
   );
   targets.add(
     TargetFocus(
       identify: "Target 1",
       keyTarget: keyButton,
       color: Colors.purple,
       contents: [
         TargetContent(
           align: ContentAlign.bottom,
           builder: (context, controller) {
             return Container(
               child: Column(
                 mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(
                     "5番目の選択肢",
                     style: TextStyle(
                       fontWeight: FontWeight.bold,
                       color: Colors.white,
                       fontSize: 20.0,
                     ),
                   ),
                   Padding(
                     padding: const EdgeInsets.only(top: 10.0),
                     child: Text(
                       "背景の色を変更することができます。",
                       style: TextStyle(color: Colors.white),
                     ),
                   ),
                   ElevatedButton(
                     onPressed: () {
                       controller.previous();
                     },
                     child: Icon(Icons.chevron_left),
                   ),
                 ],
               ),
             );
           },
         )
       ],
       shape: ShapeLightFocus.RRect,
       radius: 5,
     ),
   );
   targets.add(
     TargetFocus(
       identify: "Target 2",
       keyTarget: keyButton4,
       contents: [
         TargetContent(
           align: ContentAlign.left,
           child: Container(
             child: Column(
               mainAxisSize: MainAxisSize.min,
               crossAxisAlignment: CrossAxisAlignment.start,
               children: <Widget>[
                 Text(
                   "7番目の選択肢",
                   style: TextStyle(
                       fontWeight: FontWeight.bold,
                       color: Colors.white,
                       fontSize: 20.0),
                 ),
                 Padding(
                   padding: const EdgeInsets.only(top: 10.0),
                   child: Text(
                     "左側にコンテンツを置く。",
                     style: TextStyle(color: Colors.white),
                   ),
                 )
               ],
             ),
           ),
         ),
         TargetContent(
             align: ContentAlign.top,
             child: Container(
               child: Column(
                 mainAxisSize: MainAxisSize.min,
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(
                     "6番目の選択肢",
                     style: TextStyle(
                         fontWeight: FontWeight.bold,
                         color: Colors.white,
                         fontSize: 20.0),
                   ),
                   Padding(
                     padding: const EdgeInsets.only(top: 10.0),
                     child: Text(
                       "align で Top を指定しています。",
                       style: TextStyle(color: Colors.white),
                     ),
                   )
                 ],
               ),
             ))
       ],
       shape: ShapeLightFocus.RRect,
     ),
   );
   targets.add(TargetFocus(
     identify: "Target 3",
     keyTarget: keyButton5,
     contents: [
       TargetContent(
           align: ContentAlign.right,
           child: Container(
             child: Column(
               mainAxisSize: MainAxisSize.min,
               crossAxisAlignment: CrossAxisAlignment.start,
               children: <Widget>[
                 Text(
                   "8番目の選択肢",
                   style: TextStyle(
                       fontWeight: FontWeight.bold,
                       color: Colors.white,
                       fontSize: 20.0),
                 ),
                 Padding(
                   padding: const EdgeInsets.only(top: 10.0),
                   child: Text(
                     "right で対象の右に置く",
                     style: TextStyle(color: Colors.white),
                   ),
                 )
               ],
             ),
           ))
     ],
     shape: ShapeLightFocus.RRect,
   ));
   targets.add(TargetFocus(
     identify: "Target 4",
     keyTarget: keyButton3,
     contents: [
       TargetContent(
         align: ContentAlign.top,
         child: Container(
           child: Column(
             children: <Widget>[
               InkWell(
                 onTap: () {
                   tutorialCoachMark.previous();
                 },
                 child: Padding(
                   padding: const EdgeInsets.all(10.0),
                   child: Image.network(
                     "https://juststickers.in/wp-content/uploads/2019/01/flutter.png",
                     height: 200,
                   ),
                 ),
               ),
               Padding(
                 padding: const EdgeInsets.only(bottom: 20.0),
                 child: Text(
                   "9番目の選択肢",
                   style: TextStyle(
                       color: Colors.white,
                       fontWeight: FontWeight.bold,
                       fontSize: 20.0),
                 ),
               ),
               Text(
                 "画像の挿入も可能です。",
                 style: TextStyle(color: Colors.white),
               ),
             ],
           ),
         ),
       ),
     ],
     shape: ShapeLightFocus.Circle,
   ));
   targets.add(TargetFocus(
     identify: "Target 5",
     keyTarget: keyButton2,
     contents: [
       TargetContent(
         align: ContentAlign.top,
         child: Container(
           child: Column(
             mainAxisSize: MainAxisSize.min,
             children: <Widget>[
               Padding(
                 padding: const EdgeInsets.only(bottom: 20.0),
                 child: Text(
                   "10番目の選択肢",
                   style: TextStyle(
                       color: Colors.white,
                       fontWeight: FontWeight.bold,
                       fontSize: 20.0),
                 ),
               ),
               Text(
                 "TOPで対象の上に置く",
                 style: TextStyle(color: Colors.white),
               ),
             ],
           ),
         ),
       ),
       TargetContent(
           align: ContentAlign.bottom,
           child: Column(
             mainAxisSize: MainAxisSize.min,
             children: <Widget>[
               Padding(
                 padding: const EdgeInsets.only(bottom: 20.0),
                 child: Text(
                   "11番目の選択肢",
                   style: TextStyle(
                       color: Colors.white,
                       fontWeight: FontWeight.bold,
                       fontSize: 20.0),
                 ),
               ),
               Container(
                 child: Text(
                   "BOTTOMで対象の下に置く",
                   style: TextStyle(color: Colors.white),
                 ),
               ),
             ],
           ))
     ],
     shape: ShapeLightFocus.Circle,
   ));
 }
}

コードの解説

ここで、各コードの解説を行います。

ベースになる画面について

Widget build(BuildContext context) {}にベースとなる画面が作成されています。
ここで大切なのは、チュートリアルでベースとなる画面を使用する際、各Widgetに key: を指定することです。
この key: を用いてチュートリアルを表示させます。

例えば、サンプルでは以下の様に指定しています。

        Align(
             alignment: Alignment.center,
             child: SizedBox(
               width: 50,
               height: 50,
               child: ElevatedButton(
                 key: keyButton2,
                 onPressed: () {},
                 child: Container(),
               ),
             ),
           ),

チュートリアル表示について

keyButton2 を用いた部分を用いて解説していきます。

targets.add(TargetFocus(
   identify: "Target 5",
   keyTarget: keyButton2,
   contents: [
     TargetContent(
       align: ContentAlign.top,
       child: Container(
         child: Column(
           mainAxisSize: MainAxisSize.min,
           children: <Widget>[
             Padding(
               padding: const EdgeInsets.only(bottom: 20.0),
               child: Text(
                 "10番目の選択肢",
                 style: TextStyle(
                     color: Colors.white,
                     fontWeight: FontWeight.bold,
                     fontSize: 20.0),
               ),
               ),
             Text(
               "TOPで対象の上に置く",
               style: TextStyle(color: Colors.white),
             ),
           ],
         ),
       ),
     ),
     TargetContent(
         align: ContentAlign.bottom,
         child: Column(
           mainAxisSize: MainAxisSize.min,
           children: <Widget>[
             Padding(
               padding: const EdgeInsets.only(bottom: 20.0),
               child: Text(
                 "11番目の選択肢",
                 style: TextStyle(
                     color: Colors.white,
                     fontWeight: FontWeight.bold,
                     fontSize: 20.0),
               ),
             ),
             Container(
               child: Text(
                 "BOTTOMで対象の下に置く",
                 style: TextStyle(color: Colors.white),
               ),
             ),
           ],
         ))
   ],
   shape: ShapeLightFocus.Circle,
 ));
}

まず、

List<TargetFocus> targets = <TargetFocus>[]; 

で初期化を行います。
その後、targets.add でチュートリアルの動作を追加しましょう。

TargetFocus() でサンプルの様にフォーカスした表示になります。そして、TargetContent()で文字の色や位置を指定可能です。

shape: ShapeLightFocus.Circle, では、どの様な形でFocusするかを指定出来ます。ここでは丸くフォーカスする様にしました。

まとめ

今回は、tutorial_coach_mark パッケージの導入について解説しました。チュートリアルが比較的容易に導入ができることを理解できたと思います。
チュートリアルを導入したい方はこの記事を参考にしてみてはいかがでしょうか?

採用情報はこちら
目次