Flutter Effects – Membuat Nested Navigation Flow

Untuk teman-teman yang akan baru memulai belajar Flutter silahkan masuk ke artikel ini dulu ya teman-teman ➡ Aplikasi Pertamaku “Halo Semuaaa…“. Jika sudah yuk lanjut baca artikel ini…

Jangan lupa baca artikel sebelumnya ya teman-temanMembuat Download Button


Perlu diketahui, aplikasi mengumpulkan ratusan route selama operasinya berjalan. Beberapa route pasti didefinisikan sebagai top-level route, misalnya seperti, “/“, “profile”, “contact”, “social_feed”. Namun dapat teman-teman bayangkan semua route tersebut didefinisikan di dalam top-level widget Navigator, sehingga list-nya akan sangat banyak dan banyak dari route ini akan lebih baik ditangani di dalam widget lain.

Pada artikel ini, kita akan membuat sebuah alur aplikasi IoT untuk setup bola lampu nirkabel yang nantinya dapat kita kontrol melalui aplikasi. Alur setup ini terdiri dari 4 pages:

  1. Temukan bola lampu terdekat
  2. Pilih bola lampu yang ingin ditambahkan
  3. Tambahkan bola lampu
  4. Selesaikan setup

Kita dapat mengatur behavior aplikasi dari top-level widget Navigator. Namun, akan lebih masuk akal jika mendefinisikan nested widget Navigator kedua di dalam widget SetupFlow dan biarkan nested Navigator mengambil alih kepemilikan atas 4 halaman pada alur setup. Penunjukan navigasi ini memfasilitasi kontrol lokal yang lebih besar, yang umumnya lebih disukai saat mengembangkan software. Berikut langkah-langkah yang akan dibahas pada penelitian ini:

1. Mempersiapkan Navigasi

Aplikasi IoT memiliki 2 top-level screens bersamaan dengan alur setup. Perlu diketahui, pendefinisian nama route sebagai konstanta di bawah ini bertujuan agar dapat dijadikan referensi dalam kode aplikasi yang dibuat.

const routeHome = '/';
const routeSettings = '/settings';
const routePrefixDeviceSetup = '/setup/';
const routeDeviceSetupStart = '/setup/$routeDeviceSetupStartPage';
const routeDeviceSetupStartPage = 'find_devices';
const routeDeviceSetupSelectDevicePage = 'select_device';
const routeDeviceSetupConnectingPage = 'connecting';
const routeDeviceSetupFinishedPage = 'finished';

Home dan setting screens direferensikan dengan nama statis. Alur setup menggunakan dua path supaya alur setup dapat membuat nama route mereka sendiri ('/setup/' awalannya dapat diikuti dengan nama halaman tertentu). Selanjutnya dengan menggabungkan dua path, Navigator dapat menentukan nama route untuk alur setup tanpa harus mengenali semua individual pages yang terkait dengan alur setup.

Top-level Navigator tidak bertanggung jawab untuk mengidentifikasi halaman alur setup individu, oleh karena itu top-level Navigator perlu mengurai nama route yang masuk untuk mengidentifikasi awalan alur setup. Kebutuhan untuk mengurai ini berarti kita tidak menggunakan properti route dari Top-level Navigator, sebagai gantinya kita harus menyediakan fungsi untuk properti onGenerateRoute.

Cuplikan berikut mengimplementasikan onGenerateRoute untuk mengembalikan widget yang sesuai untuk masing-masing three top-level paths yang ada.

onGenerateRoute: (settings) {
 late Widget page;
 if (settings.name == routeHome) {
   page = HomeScreen();
 } else if (settings.name == routeSettings) {
   page = SettingsScreen();
 } else if (settings.name!.startsWith(routePrefixDeviceSetup)) {
   final subRoute = settings.name!.substring(
     routePrefixDeviceSetup.length,
   );
   page = SetupFlow(
     initialSetupRoute: subRoute,
   );
 } else {
   throw Exception('Unknown route: ${settings.name}');
 }

 return MaterialPageRoute<dynamic>(
   builder: (context) {
     return page;
   },
   settings: settings,
 );
},

Perhatikan bahwa route home dan settings dicocokkan dengan nama route yang tepat. Namun, kondisi setup flow route hanya memeriksa prefiks. Jika nama route berisi setup flow prefiks, maka nama route lainnya akan diabaikan dan diteruskan ke widget SetupFlow untuk diproses. Pemisahan nama route inilah yang memungkinkan top-level Navigator menjadi agnostik terhadap berbagai subroute dalam alur setup.

Kemudian buat widget stateful bernama SetupFlow untuk menerima nama route.

class SetupFlow extends StatefulWidget {
 const SetupFlow({
   Key? key,
   required this.setupPageRoute,
 }) : super(key: key);

 final String setupPageRoute;

 @override
 SetupFlowState createState() => SetupFlowState();
}

class SetupFlowState extends State<SetupFlow> {
  //...
}

2. Menampilkan App Bar untuk Alur Setup

Pada langkah ini kita akan mengatur app bar akan tetap muncul di semua halaman alur setup. Return widget Scaffold dari widget’s SetupFlow pada method build() dan sertakan widget AppBar yang diinginkan.

@override
Widget build(BuildContext context) {
 return Scaffold(
   appBar: _buildFlowAppBar(),
   body: SizedBox(),
 );
}

PreferredSizeWidget _buildFlowAppBar() {
 return AppBar(
   title: Text('Bulb Setup'),
 );
}

Selanjutnya app bar menampilkan ikon kembali dan keluar dari alur setup saat ikon kembali ditekan. Namun, jika pengguna keluar dari alur setup maka semua setup yang telah dilakukan hilang. Oleh karena itu ketika pengguna menekan ikon penggali, aplikasi akan melakukan konfirmasi apakah pengguna benar ingin keluar dari alur setup.

Future<void> _onExitPressed() async {
 final isConfirmed = await _isExitDesired();

 if (isConfirmed && mounted) {
   _exitSetup();
 }
}

Future<bool> _isExitDesired() async {
 return await showDialog<bool>(
         context: context,
         builder: (context) {
           return AlertDialog(
             title: Text('Are you sure?'),
             content: Text('If you exit device setup, your progress will be lost.'),
             actions: [
               TextButton(
                 onPressed: () {
                   Navigator.of(context).pop(true);
                 },
                 child: Text('Leave'),
               ),
               TextButton(
                 onPressed: () {
                   Navigator.of(context).pop(false);
                 },
                 child: Text('Stay'),
               ),
             ],
           );
         }) ??
     false;
}

void _exitSetup() {
 Navigator.of(context).pop();
}

@override
Widget build(BuildContext context) {
 return WillPopScope(
   onWillPop: _isExitDesired,
   child: Scaffold(
     appBar: _buildFlowAppBar(),
     body: SizedBox(),
   ),
 );
}

PreferredSizeWidget _buildFlowAppBar() {
 return AppBar(
   leading: IconButton(
     onPressed: _onExitPressed,
     icon: Icon(Icons.chevron_left),
   ),
   title: Text('Bulb Setup'),
 );
}

Jadi ketika user menekan ikon kembali pada app bar atau menekan tombol kembali pada Android, maka akan muncul pesan peringatan untuk mengkonfirmasi bahwa apakah benar pengguna ingin keluar dari alur setup. Jika pengguna menekan Leave, alur setup akan muncul dengan sendirinya dari top-level navigation stack. Kemudian jika pengguna menekan Stay, maka semua tindakan menuju keluar alur setup diabaikan.

Teman-teman mungkin memperhatikan kalau Navigator.pop() dipanggil oleh kedua tombol Leave dan Stay, padahal sebenarnya action pop() yang memunculkan pesan peringatan dari navigation stack dan bukan dari alur setup.


3. Membuat Nested Routes

Seperti yang teman-teman ketahui tugas dari alur setup adalah menampilkan halaman yang sesuai dengan alur tersebut. Dalam hal ini kita tambahkan widget Navigator ke SetupFlow dan implementasikan properti onGenerateRoute.

final _navigatorKey = GlobalKey<NavigatorState>();

void _onDiscoveryComplete() {
 _navigatorKey.currentState!.pushNamed(
   routeDeviceSetupSelectDevicePage,
 );
}

void _onDeviceSelected(String deviceId) {
 _navigatorKey.currentState!.pushNamed(
   routeDeviceSetupConnectingPage,
 );
}

void _onConnectionEstablished() {
 _navigatorKey.currentState!.pushNamed(
   routeDeviceSetupFinishedPage,
 );
}

@override
Widget build(BuildContext context) {
 return WillPopScope(
   onWillPop: _isExitDesired,
   child: Scaffold(
     appBar: _buildFlowAppBar(),
     body: Navigator(
       key: _navigatorKey,
       initialRoute: widget.setupPageRoute,
       onGenerateRoute: _onGenerateRoute,
     ),
   ),
 );
}

Route _onGenerateRoute(RouteSettings settings) {
 late Widget page;
 switch (settings.name) {
   case routeDeviceSetupStartPage:
     page = WaitingPage(
       message: 'Searching for nearby bulb...',
       onWaitComplete: _onDiscoveryComplete,
     );
     break;
   case routeDeviceSetupSelectDevicePage:
     page = SelectDevicePage(
       onDeviceSelected: _onDeviceSelected,
     );
     break;
   case routeDeviceSetupConnectingPage:
     page = WaitingPage(
       message: 'Connecting...',
       onWaitComplete: _onConnectionEstablished,
     );
     break;
   case routeDeviceSetupFinishedPage:
     page = FinishedPage(
       onFinishPressed: _exitSetup,
     );
     break;
 }

 return MaterialPageRoute<dynamic>(
   builder: (context) {
     return page;
   },
   settings: settings,
 );
}

Function _onGenerateRoute berfungsi sama seperti top-level NavigatorObjek RouteSettings diteruskan ke fungsi _onGenerateRoute yang menyertakan routes name. Berdasarkan nama route tersebut, satu dari empat halaman alur akan di-returned.

Halaman pertama disebut find_devices, menampilkan animasi pencarian jaringan dengan jeda beberapa detik. Setelah itu melakukan callback _onDiscoveryComplete sehingga alur setup mengetahui bahwa pencarian perangkat telah selesai dan halaman untuk memilih perangkat harus ditampilkan. Oleh karena itu, _onDiscoveryComplete, _navigatorKey menginstruksikan nested Navigator untuk menampilkan halaman select_device.

Selanjutnya halaman select_device meminta pengguna untuk memilih perangkat dari daftar perangkat yang tersedia. Dalam artikel ini, hanya satu perangkat yang ditampilkan ke pengguna, lalu saat pengguna mengetuk perangkat yang ada di daftar perangkat maka callback onDeviceSelected dipanggil. Saat alur setup mengetahui perangkat yang dipilih, sehingga halaman connecting harus ditampilkan. Dalam hal ini, _onDeviceSelected dan  _navigatorKey menginstruksikan nested Navigator untuk membuka halaman ”connecting”.

Kemudian untuk halaman connecting, cara kerjanya hampir sama seperti halaman find_devices. Halaman connecting menunggu beberapa saat kemdian memanggil callback _onConnectionEstablished. Alur setup akan mengenali hal tersebut ketika koneksi terjadi dan kemudian halaman final harus ditampilkan. Oleh karena itu, _onConnectionEstablished dan _navigatorKey menginstruksikan nested Navigator untuk membuka halaman finished.

Halaman finished memberikan pengguna tombol Finish, ketika pengguna mengetuk tombol tersebut callback _exitSetup dipanggil sehingga menampilkan seluruh alur setup dari  top-level Navigator stack dan membawa pengguna ke layar utama aplikasi.


Berikut cuplikan kode dan simulasinya, jika teman-teman menggunakan VSCode jalankan projectnya dengan menekan F5, klik hot reload (⚡) atau klik tombol ▶, berikut tampilannya:


Jika ada pertanyaan silahkan komen dan jika artikel ini dirasa bermanfaat, jangan lupa like dan sharenya ya teman-teman. ??????? Sampai bertemu di artikel selanjutnya.
Terima Kasih, Assalamu’alaykum… Salam KODINGINDONESIA

Referensi : https://flutter.dev//

Anton Prafanto

Konten developer kodingindonesia.com & staf pengajar tetap di Universitas Mulawarman Samarinda

all author posts