Flutter Bottom Navigation

2/19/2020 • Programming

Salah satu komponen material design untuk navigasi adalah Bottom Navigation Bar, dengan komponen tersebut pengguna dapat berpindah halaman atau layar dengan mudah dan ergonomis terutama pada perangkat mobile. Bottom Navigation pada praktik terbaiknya haruslah memiliki tiga hingga lima destinasi atau tujuan. Setiap destinasi dilambangkan dengan ikon dan teks. Teks pada Bottom Navigation bersifat opsional. Untuk lebih lengkapnya tentang komponen ini bisa kunjungi halaman material design di sini.

Bottom navigation sample

Di Flutter, Bottom Navigation sudah tersedia dalam paket flutter/material.dart dengan nama widget BottomNavigationBar dan BottomNavigationBarItem sebagai item navigasi. Widget ini dapat diterapkan pada properti bottomNavigationBar pada widget Scaffold. Sebenarnya ada banyak cara penggunaan komponen ini, namun pada tulisan ini hanya akan menunjukkan salah satunya saja.

class Main extends StatefulWidget {
  
  _MainState createState() => _MainState();
}

class _MainState extends State<Main> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: /* ... */,
      bottomNavigationBar: BottomNavigationBar(
        onTap: (index) {
          /* ... */
        },
        items: <BottomNavigationBarItem>[
          /* ... */
        ],
      ),
    );
  }
}

Karena setiap kali BottomNavigationBarItem di-tap kita perlu berganti layar maka di sini kita menggunakan StatefulWidget. AFAIK, di Flutter tidak ada controller atau adapter untuk menangani pergantian layar pada Bottom Navigation. Jadi konsepnya, setiap aksi tap pada item navigasi, method onTap pada BottomNavigationBar akan triggered dengan membawa index dari item yang mendapat aksi tap. Nah, method onTap inilah yang akan kita gunakan untuk berganti layar dengan memperbarui body pada Scaffold.

class _MainState extends State<Main> {

  final List<Widget> _screens = []; // List untuk menampung layar

  int _activeScreenIndex = 0; // Indeks layar yang sedang tampil

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: _screens[_activeScreenIndex],
      bottomNavigationBar: BottomNavigationBar(
        onTap: (index) {
          // Update indeks layar yang sedang tampil
          // setiap kali menerima aksi tap
          setState(() {
            _activeScreenIndex = index;
          });
        },
        items: <BottomNavigationBarItem>[
          /* ... */
        ],
      ),
    );
  }
}

Untuk _screens kita contohkan tiga buah simple widget dan untuk items pada BottomNavigationBar tentu kita definisikan tiga item menyesuaikan widget pada _screens.

class _MainState extends State<Main> {

  // Tiga layar
  final List<Widget> _screens = [
    HomeScreen(),
    FavoriteScreen(),
    AccountScreen()
  ];

  int _activeScreenIndex = 0;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: _screens[_activeScreenIndex],
      bottomNavigationBar: BottomNavigationBar(
        onTap: (index) {
          setState(() {
            _activeScreenIndex = index;
          });
        },
        items: <BottomNavigationBarItem>[

          // Tiga item navigasi
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text('Home'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite),
            title: Text('Favorite'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.account_box),
            title: Text('Account'),
          ),
        ],
      ),
    );
  }
}

Widget untuk masing-masing layar

class HomeScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text('Welcome to Home!'),
      ),
    );
  }
}

class FavoriteScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text('Favorite things!'),
      ),
    );
  }
}

class AccountScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text('Your account!'),
      ),
    );
  }
}

Sampai di sini kita telah menerapkan Bottom Navigation, tambahkan main function

void main() => runApp(
      MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.teal,
        ),
        home: Main(),
      ),
    );

Lalu coba jalankan, seharusnya aplikasi dapat berjalan seperti berikut

Pertama Jalankan

Layar berganti sesuai dengan item yang dipilih namun item pada Bottom Navigation tidak mengubah state-nya saat menerima aksi tap, untuk mengatasi ini cukup tambahkan properti currentIndex pada widget BottomNavigationBar dan nilainya adalah _activeScreenIndex seperti ini

BottomNavigationBar(
    currentIndex: _activeScreenIndex,
    onTap: ...,
    items: ...
)

Jika dijalankan lagi seharusnya hasilnya seperti ini

Kedua Jalankan

Bekerja dengan baik. Selanjutnya kita akan mencoba menampilkan daftar teks plain sederhana pada layar favorite, cukup dengan menggunakan ListView seperti ini

class FavoriteScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Container(
      child: ListView(
        children: List<Widget>.generate(20, (index) {
          return ListTile(
            title: Text(
              'Favorite $index',
              style: TextStyle(
                fontSize: 18,
              ),
            ),
            subtitle: Text('This is description of favorite $index'),
          );
        }),
      ),
    );
  }
}

Ketika dijalankan lagi, layar favorite akan menampilkan data seperti berikut

Ketiga Jalankan

Ada experience yang kurang baik saat melakukan navigasi dari layar favorite yang menampilkan daftar data menuju ke layar account lalu kembali lagi ke layar favorite dengan daftar data yang berada di posisi awal lagi, ini menunjukkan state layar tidak tersimpan, setiap kali layar tampil selalu membuat instansi widget baru. Untuk mengatasi ini, kita dapat memanfaatkan widget PageStorage yang dapat membuat penampung widget layar supaya tidak selalu membuat instansi baru saat dibutuhkan. Widget PageStorage digunakan bersama dengan PageStorageKey sebagai key dan PageStorageBucket sebagai penampung atau penyimpan state dari masing-masing layar. Penerapan seperti ini

final PageStorageBucket _bucket = PageStorageBucket(); // Dekralasi PageStorageBucket
final List<Widget> _screens = [
  // Gunakan PageStorageKey pada masing-masing widget layar
  HomeScreen(key: PageStorageKey('key-home')),
  FavoriteScreen(key: PageStorageKey('key-favorite')),
  AccountScreen(key: PageStorageKey('key-account'))
];


Widget build(BuildContext context) {
  return Scaffold(
    appBar: /* ... */,
    body: PageStorage( // Terapkan PageStorage di body
      child: _screens[_activeScreenIndex],
      bucket: _bucket,
    ),
    bottomNavigationBar: /* ... */
  );
}

Pada widget layar kita menggunakan PageStorageKey maka kita perlu membuat constructor dengan parameter Key supaya dapat di-pass ke super constructor widget layar. Super class widget layar adalah widget, setiap widget memiliki properti key, key ini berfungsi untuk mengontrol bagaimana satu widget menggantikan widget lain di widget tree.

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key key}) : super(key: key);

  
  Widget build(BuildContext context) { /* ... */ }
}

class FavoriteScreen extends StatelessWidget {
  const FavoriteScreen({Key key}) : super(key: key);

  
  Widget build(BuildContext context) { /* ... */ }
}

class AccountScreen extends StatelessWidget {
  const AccountScreen({Key key}) : super(key: key);

  
  Widget build(BuildContext context) { /* ... */ }
}

Lalu coba jalankan lagi, maka seharusnya sesuai ekspektasi

Keempat Jalankan

Supaya kode lebih rapi, kita dapat membuat class model untuk menampung properti layar dan item navigasi, misal di sini saya beri nama class-nya PageItem seperti ini

class PageItem {
  final String title;
  final IconData icon;
  final Widget screen;

  PageItem(this.title, this.icon, this.screen);
}

Lalu kita sesuaikan kode pada class _MainState sehingga menjadi seperti ini

class _MainState extends State<Main> {
  final PageStorageBucket _bucket = PageStorageBucket();
  final List<PageItem> _items = [
    PageItem('Home', Icons.home, HomeScreen(key: PageStorageKey('key--home'))),
    PageItem('Favorite', Icons.favorite, FavoriteScreen(key: PageStorageKey('key--favorite'))),
    PageItem('Account', Icons.account_box, AccountScreen(key: PageStorageKey('key--account'))),
  ];

  int _activeScreenIndex = 0;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_items[_activeScreenIndex].title),
      ),
      body: PageStorage(
        child: _items[_activeScreenIndex].screen,
        bucket: _bucket,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _activeScreenIndex,
        onTap: (index) {
          setState(() {
            _activeScreenIndex = index;
          });
        },
        items: _items.map((item) {
          return BottomNavigationBarItem(
            title: Text(item.title),
            icon: Icon(item.icon),
          );
        }).toList(),
      ),
    );
  }
}

Selain lebih rapi, kita juga akan lebih mudah saat menambah widget layar baru. Cukup dengan menambahkan PageItem pada list _items, layar dan item navigasi akan menyesuaikan. Selain itu kita membuat judul pada AppBar juga menjadi dinamis sesuai layar yang tampil.

Kelima Jalankan

Dan kode selengkapnya dapat dilihat di bawah ini.

Keseluruhan kode
import 'package:flutter/material.dart';

void main() => runApp(
      MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.teal,
        ),
        home: Main(),
      ),
    );

class Main extends StatefulWidget {
  
  _MainState createState() => _MainState();
}

class _MainState extends State<Main> {
  final PageStorageBucket _bucket = PageStorageBucket();
  final List<PageItem> _items = [
    PageItem('Home', Icons.home, HomeScreen(key: PageStorageKey('key--home'))),
    PageItem('Favorite', Icons.favorite, FavoriteScreen(key: PageStorageKey('key--favorite'))),
    PageItem('Account', Icons.account_box, AccountScreen(key: PageStorageKey('key--account'))),
  ];

  int _activeScreenIndex = 0;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_items[_activeScreenIndex].title),
      ),
      body: PageStorage(
        child: _items[_activeScreenIndex].screen,
        bucket: _bucket,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _activeScreenIndex,
        onTap: (index) {
          setState(() {
            _activeScreenIndex = index;
          });
        },
        items: _items.map((item) {
          return BottomNavigationBarItem(
            title: Text(item.title),
            icon: Icon(item.icon),
          );
        }).toList(),
      ),
    );
  }
}

class PageItem {
  final String title;
  final IconData icon;
  final Widget screen;

  PageItem(this.title, this.icon, this.screen);
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text('Welcome to Home!'),
      ),
    );
  }
}

class FavoriteScreen extends StatelessWidget {
  const FavoriteScreen({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      child: ListView(
        children: List<Widget>.generate(20, (index) {
          return ListTile(
            title: Text(
              'Favorite $index',
              style: TextStyle(
                fontSize: 18,
              ),
            ),
            subtitle: Text('This is description of favorite $index'),
          );
        }),
      ),
    );
  }
}

class AccountScreen extends StatelessWidget {
  const AccountScreen({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text('Your account!'),
      ),
    );
  }
}

Seperti yang sebelumnya saya singgung, ada banyak cara penerapan Bottom Navigation pada Flutter dan ini hanya salah satunya. Di Flutter, everything is widget, jadi sangat fleksibel.

Demikian, semoga bermanfaat.