Membuat Tour Guide Aplikasi Flutter Menggunakan Tutorial Coach Mark

Membuat Tour Guide Aplikasi Flutter Menggunakan Tutorial Coach Mark

sobatcoding.com - Tutorial cara membuat tour guide aplikasi menggunakan tutorial coach mark pada flutter

Pernahkah kalian menjumpai aplikasi android dimana saat kita masuk ke aplikasi tersebut muncul sorotan atau highlight ke salah satu menu sehingga kita tahu fungsi dari menu-menu tersebut? Bagaimana cara membuatnya?

Flutter tour sobatcoding.com

Artikel kali ini kita akan mencoba belajar implementasi fitur seperti apps tour mengenai fitur atau menu menu apa yang ada di aplikasi andorid yang kita bangun. Fungsinya adalah meperkenalkan kepada user baru tentang menu dan keterangan atau cara penggunaan menu tersebut.

Environtment

Pada contoh kali ini, admin menggunakan flutter versi 3.10.6. Adapun dependencies yang digunakan adalah iconsax, shared_preferences dan tutorial_coach_mark

Install Package

Package yang digunakan adalah package tutorial_coah_mark dan kalian bisa install dengan cara command berikut

flutter pub add iconsax
flutter pub add tutorial_coach_mark
flutter pub add shared_preferences

Tambahkan Asset

Tambahkan asset image yang kalian punya, kemudian buka file pubpsec.yaml dan setting seperti ini

# To add assets to your application, add an assets section, like this:
  assets:
    - assets/img/
  #   - images/a_dot_ham.jpeg

Fungsinya adalah semua asset image yang ada di folder assets/img nanti bisa kita akses di dalam widget

 

Implementasi Package

Untuk implementasi pertama kita buat dahulu halaman dua buah halaman. Halaman SplashScreen dan halaman utama yang akan kita jadikan sebagai contoh.

Untuk halaman SplashScreen sederhana kalian bisa buat seperti contoh berikut

import 'package:flutter/material.dart';
import 'package:flutter_tour/home_screen.dart';

class SplashScreen extends StatefulWidget {
  const SplashScreen({super.key});

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  @override
  void initState() {
    super.initState();

    goTohomePage();
  }

  @override
  void dispose() {
    super.dispose();
  }

  void goTohomePage() async {
    await Future.delayed(const Duration(seconds: 5), () async {
      final navigator = Navigator.of(context);
      navigator.pushReplacement(
          MaterialPageRoute(builder: (context) => const HomePage()));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        padding: const EdgeInsets.all(30),
        height: double.maxFinite,
        width: double.maxFinite,
        child: Center(
            child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Image.asset(
              "assets/img/logo.png",
              width: 64,
            ),
            const SizedBox(
              height: 10,
            ),
            const Text("sobatcoding.com", style: TextStyle(fontWeight: FontWeight.w600),),
            const SizedBox(height: 20),
            Container(
              margin: const EdgeInsets.all(0),
              height: 40,
              child: const Center(
                child: CircularProgressIndicator(
                  color: Colors.grey,
                ),
              ),
            ),
            const SizedBox(height: 10),
            const Text("Please wait ..."),
          ],
        )),
      ),
    );
  }
}

Kalau kalian lihat dalam kode ini

void goTohomePage() async {
    await Future.delayed(const Duration(seconds: 5), () async {
      final navigator = Navigator.of(context);
      navigator.pushReplacement(
          MaterialPageRoute(builder: (context) => const HomePage()));
    });
  }

Artinya user akan berada di halaman SplashScreen bertahan atau delay selama 5 detik dan 5 setelahnya, navigasi akan menuju halaman utama HomePage

Selanjutnya buatlah halaman utama berisi header, produk dan bottom navigation dengan tampilan seperti berikut

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

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<dynamic> items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  Widget header() {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.only(left: 20, top: 10, right: 20, bottom: 10),
      color: Colors.green.shade400,
      child: Row(
        key: profileKey,
        mainAxisAlignment: MainAxisAlignment.start,
        children: const [
          CircleAvatar(
              radius: 24,
              backgroundColor: Colors.white,
              child: CircleAvatar(
                radius: 20,
                backgroundImage: AssetImage("assets/img/logo.png"),
              )),
          SizedBox(width: 10),
          Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                "Administrator",
                style: TextStyle(fontWeight: FontWeight.w600),
              ),
              Text(
                "admin@sobatcoding.com",
                style: TextStyle(fontSize: 12, color: Colors.white),
              )
            ],
          )
        ],
      ),
    );
  }

  Widget itemProduk(i) {
    return InkWell(
      onTap: () {
        null;
      },
      child: Card(
        key: i == 1 ? productKey : null,
        elevation: 3.0,
        color: Colors.white,
        surfaceTintColor: Colors.white,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Image.asset("assets/img/shoe.png"),
            ),
            const SizedBox(height: 5),
            Expanded(
              child: Text(
                "Produk $i",
                style:
                    const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
              ),
            )
          ],
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();

  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        height: double.infinity,
        color: Colors.grey.shade50,
        child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [
          header(),
          Expanded(
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  children: [
                    GridView.count(
                        crossAxisCount: 2,
                        shrinkWrap: true,
                        children: items.map((e) => itemProduk(e)).toList())
                  ],
                ),
              ),
            ),
          ),
        ]),
      ),
      bottomNavigationBar: BottomNavigationBar(
          currentIndex: 0,
          selectedItemColor: Colors.green.shade400,
          selectedLabelStyle: const TextStyle(fontSize: 14),
          unselectedLabelStyle: const TextStyle(fontSize: 12),
          backgroundColor: Colors.white,
          type: BottomNavigationBarType.fixed,
          items: [
            BottomNavigationBarItem(
              icon: Container(
                margin: const EdgeInsets.only(
                  top: 10,
                  bottom: 10,
                ),
                child: const Icon(Iconsax.direct_normal),
              ),
              label: 'Beranda',
            ),
            BottomNavigationBarItem(
              icon: Container(
                margin: const EdgeInsets.only(
                  top: 10,
                  bottom: 10,
                ),
                child: const Icon(Iconsax.bag),
              ),
              label: 'Keranjang',
            ),
            BottomNavigationBarItem(
              icon: Container(
                margin: const EdgeInsets.only(
                  top: 10,
                  bottom: 10,
                ),
                child: const Icon(Iconsax.profile_circle),
              ),
              label: 'Profile',
            ),
          ]),
    );
  }
}

Buka file main.dart , arahkan untuk default route ke halaman SplashScreen

return MaterialApp(
      title: 'Tour Guide Flutter Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SplashScreen(),
    );

 

 

Membuat Fitur Tour

Buatlah sebuah widget bernama CoachMarkDescription dan masukkan kode berikut

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

class CoachMarkDescription extends StatefulWidget {
  const CoachMarkDescription(
      {super.key,
      required this.tutorialCoachMark,
      required this.text});

  final String text;
  final TutorialCoachMark tutorialCoachMark;

  @override
  State<CoachMarkDescription> createState() => _CoachMarkDescriptionState();
}

class _CoachMarkDescriptionState extends State<CoachMarkDescription> {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(15),
      decoration: BoxDecoration(
          color: Colors.white, borderRadius: BorderRadius.circular(10)),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.only(top: 10.0),
            child: Text(
              widget.text,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
          const SizedBox(height: 16),
          Visibility(
            visible: true,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                    onPressed: () {
                      widget.tutorialCoachMark.skip();
                    },
                    style: TextButton.styleFrom(
                        foregroundColor: Colors.lightBlue.shade700,
                        backgroundColor: Colors.white,
                        elevation: 2.0),
                    child: const Text("SKIP")),
                const SizedBox(
                  width: 16,
                ),
                ElevatedButton(
                  onPressed: () {
                    widget.tutorialCoachMark.next();
                  },
                  style: ElevatedButton.styleFrom(
                    foregroundColor: Colors.white,
                    backgroundColor: Colors.lightBlue.shade700,
                  ),
                  child: const Text("NEXT"),
                )
              ],
            ),
          )
        ],
      ),
    );
  }
}

Selanjutnya kita buat Global Key yang digunakan untuk indexing widget mana yang akan kita highlight.

GlobalKey profileKey = GlobalKey();
GlobalKey productKey = GlobalKey();
GlobalKey chartKey = GlobalKey();

Kita buat contoh ada 3 buah key masing-masing bernama profileKey, productKey dan chartKety.

Selanjutnya buatlah function untuk generate Tutorial

void showTutorial() {
    initTargets();
    tutorialCoachMark = TutorialCoachMark(
        targets: targets,
        pulseEnable: false,
        onFinish: () {
          //
        })
      ..show(context: context);
  }

  void initTargets() {
    targets = [
      TargetFocus(
          identify: "Profile",
          keyTarget: profileKey,
          enableOverlayTab: true,
          shape: ShapeLightFocus.RRect,
          contents: [
            TargetContent(
                align: ContentAlign.bottom,
                builder: (context, controller) {
                  return CoachMarkDescription(
                      tutorialCoachMark: tutorialCoachMark!,
                      text: "Ini adalah informasi profile Anda");
                })
          ]),
      TargetFocus(
          identify: "Product",
          keyTarget: productKey,
          enableOverlayTab: true,
          shape: ShapeLightFocus.RRect,
          contents: [
            TargetContent(
                align: ContentAlign.bottom,
                builder: (context, controller) {
                  return CoachMarkDescription(
                      tutorialCoachMark: tutorialCoachMark!,
                      text:
                          "Pilih produk sepatu yang Anda inginkan dengan sekali klik");
                })
          ]),
      TargetFocus(
          identify: "Chart",
          keyTarget: chartKey,
          enableOverlayTab: true,
          shape: ShapeLightFocus.Circle,
          contents: [
            TargetContent(
                align: ContentAlign.top,
                builder: (context, controller) {
                  return CoachMarkDescription(
                      tutorialCoachMark: tutorialCoachMark!,
                      text:
                          "Semua paket yang kamu tambahkan ada di dalam keranjang ini dan siap untuk di cekout");
                })
          ]),
    ];
  }

Kita set untuk key diatas ke dalam beberapa widget yang tersedia

Header

untuk header kita set key profileKey di dalam widget Row

Widget header() {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.only(left: 20, top: 10, right: 20, bottom: 10),
      color: Colors.green.shade400,
      child: Row(
        key: profileKey,
        ...
  }

Produk

untuk produk kita set key productKey di dalam widget Card elemen pertama

Widget itemProduk(i) {
    return InkWell(
      onTap: () {
        null;
      },
      child: Card(
        key: i == 1 ? productKey : null,
        
        ...
    );
  }

BottomNavigationBar

untuk bottomNavigationBar kita set key chartKey di dalam salah satu item widget BottomNavigationBar

bottomNavigationBar: BottomNavigationBar(
            ...
            BottomNavigationBarItem(
              icon: Container(
                key: chartKey,
                margin: const EdgeInsets.only(
                  top: 10,
                  bottom: 10,
                ),
                child: const Icon(Iconsax.bag),
              ),
              label: 'Keranjang',
            ),
            
            ...
          ])

 

Langsung kita jalankan hasilnya kurang lebih seperti berikut

 

Bagaimana jika kita ingin tour apps hanya muncul satu kali saja saat pertama kali user mengakses ke aplikasi?

Caranya sangat mudah kita tinggal tambahkan semacam session yang kita simpan saat tour apps selesai

Kita buat file bernama CoachMarkPreferences.dart dan masukkan kode berikut

import 'package:shared_preferences/shared_preferences.dart';

class CoachMarkPreferences {
 
  //fungsi untuk menyimpan session tour
  static Future<void> saveSession() async {
    final SharedPreferences pref = await SharedPreferences.getInstance();

    pref.setBool("watchedTour", true);

  }
 
  //fungsi untuk mengecek apakah session tour sudah disimpan
  static Future<bool> watchedTour() async {
    final SharedPreferences pref = await SharedPreferences.getInstance();

    bool watchedTour = pref.getBool("watchedTour") ?? false;
    return watchedTour;
  }

}

Kemudian implementasikan di halaman widget homePage, dibagian initState kita modifikasi seperti berikut ini

@override
  void initState() {
    super.initState();

    Future.delayed(const Duration(seconds: 1), () async {
     
     //kita cek apakah ada session yang sudah pernah disimpan atau belum
      bool tour = await CoachMarkPreferences.watchedTour();
     //jika tidak ada apps tour akan muncul
      if (!tour) {
        showTutorial();
      }
    });
  }

Kemudian di fungsi showTutorial kita masukkan fungsi simpan session di dalam event onFinish:

void showTutorial() {
    initTargets();
    tutorialCoachMark = TutorialCoachMark(
        targets: targets,
        pulseEnable: false,
        onFinish: () {
         //kita tambahakan fungsi simpan session saat tour app berakhir atau finish
          CoachMarkPreferences.saveSession();
        })
      ..show(context: context);
  }

 

Demikian tutorial kali ini semoga bermanfaat.