Praktikum 4: Navigation & Routing

Mengelola Perpindahan Antar Halaman (Multiple Screen) di Flutter

PRAKTIKUM 4

Navigation & Routing: Multiple Screen

1.1 Tujuan

Tujuan praktikum ini yaitu mahasiswa mampu menguasai konsep Navigation dan Routing pada Flutter:

  • Membuat aplikasi yang dapat berpindah dari halaman satu ke halaman lain.
  • Membuat aplikasi yang dapat mengirim dan menerima data dari halaman lain.

1.2 Alat

  • Computer/laptop yang telah terinstall lingkungan flutter development.

1.3 Teori

Navigation dan Routing Flutter

Navigasi merupakan sebuah proses berpindah dari satu halaman (screen/page) ke halaman lain dalam sebuah aplikasi. Navigasi pada flutter menggunakan widget Navigator yang mana bekerja menggunakan konsep tumpukan (stack).

  • Halaman awal aplikasi akan berada pada posisi dasar tumpukan.
  • Ketika berpindah ke halaman baru, Flutter menggunakan perintah push, sehingga halaman baru akan berada di atas tumpukan halaman sebelumnya.
  • Ketika kembali, Flutter menggunakan perintah pop, dan halaman teratas akan dikeluarkan dari tumpukan, menampilkan halaman sebelumnya.

Routing atau Rute adalah sebuah sistem yang digunakan untuk mendefinisikan dan mengelola routes dalam aplikasi. Setiap route didefinisikan sehingga ketika akan memanggil halaman cukup dengan memanggil nama route tersebut.

Jenis Routing pada Flutter
Navigator (Anonymous Routes)

Widget Navigation menampilkan halaman dengan konsep tumpukan menggunakan method seperti push() atau pop() secara langsung.

Push():
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => const ProductDetail()),
);
Pop():
Navigator.pop(context);
Named Routes

Named Routes atau Rute Bernama mengelola route pada widget MaterialApp kemudian memanggilnya berdasarkan nama yang telah diberikan.

Definisikan Routes pada MaterialApp:
return MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const Product(),
    '/product_detail': (context) => const ProductDetail(),
  },
);
Menggunakan Named Routes:
Navigator.pushNamed(context, '/product_detail');
Jenis Method Navigation
Push dan Pop
// Push - Menambahkan halaman baru
Navigator.push(context, route);

// Pop - Kembali ke halaman sebelumnya
Navigator.pop(context);

// Pop dengan mengirim data kembali
Navigator.pop(context, 'data yang dikembalikan');
Push Replacement

Mengganti halaman saat ini dengan halaman baru (halaman saat ini dihapus dari tumpukan).

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => LoginPage()),
);
Push and Remove Until

Mendorong halaman baru dan menghapus semua halaman sebelumnya dari tumpukan.

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
  (route) => false, // Hapus semua
);
Mengirim dan Menerima Data
Mengirim Data

Data dapat dikirim melalui constructor atau melalui arguments pada named routes.

// Dengan constructor
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailPage(id: 123, name: 'Masnoer'),
  ),
);

// Dengan named routes dan arguments
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {'id': 123, 'name': 'Masnoer'},
);
Menerima Data

Data yang dikirim dapat diterima melalui constructor widget atau menggunakan `ModalRoute`.

Menerima dengan Constructor:
class DetailPage extends StatelessWidget {
  final int id;
  final String name;

  const DetailPage({super.key, required this.id, required this.name});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Detail: $name')),
      body: Text('ID: $id'),
    );
  }
}
Menerima dengan ModalRoute.of(context):
class DetailPage extends StatelessWidget {
  const DetailPage({super.key});
  @override
  Widget build(BuildContext context) {
    // Ambil arguments dari route
    final args = ModalRoute.of(context)!.settings.arguments as Map;
    
    // Akses data
    final int id = args['id'];
    final String name = args['name'];

    return Scaffold(
      // ...
    );
  }
}

1.4 Langkah-langkah

Praktikum 1: Multiple Screen
  1. Buat file dart baru.
  2. Buat class menggunakan stateless widget dengan nama MyNav.
  3. Inisialisasi route pada MaterialApp.
  4. Buat class Product dan ProductDetail.
Kode Program Lengkap
import 'package:flutter/material.dart';

void main() => runApp(const MyNav());

class MyNav extends StatelessWidget {
  const MyNav({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => const Product(),
        '/product_detail': (context) => const ProductDetail(),
      },
    );
  }
}

class Product extends StatelessWidget {
  const Product({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(context, '/product_detail');
          },
          child: const Text('Go to Product Detail'),
        ),
      ),
    );
  }
}

class ProductDetail extends StatelessWidget {
  const ProductDetail({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product Detail'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('Back to Product'),
        ),
      ),
    );
  }
}
Praktikum 2: Mengirim dan Menerima Data
1. Inisialisasi Route pada MaterialApp
return MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HomePage(),
    '/product': (context) => const MyProduct(),
  },
);
2. Pindah ke Halaman MyProfile (mengirim via constructor)

Pada class HomePage, tambahkan tombol ini:

ElevatedButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) =>
            const MyProfile(id: 1, name: 'Masnoer'),
      ),
    );
  },
  child: const Text('Profile'),
),
3. Menerima Data pada MyProfile
class MyProfile extends StatelessWidget {
  const MyProfile({super.key, required this.id, required this.name});
  final int id;
  final String name;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Profile'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ID: $id'),
            Text('Name: $name'),
          ],
        ),
      ),
    );
  }
}
4. Pindah ke Halaman MyProduct (mengirim via arguments)

Pada class HomePage, tambahkan tombol ini:

ElevatedButton(
  onPressed: () {
    Navigator.pushNamed(
      context,
      '/product',
      arguments: {'id': 101, 'name': 'Laptop'},
    );
  },
  child: const Text('Product'),
),
5. Menerima Data pada MyProduct
class MyProduct extends StatelessWidget {
  const MyProduct({super.key});
  
  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as Map?;
    final int id = args?['id'] ?? 0;
    final String name = args?['name'] ?? 'Unknown';
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('Product'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Product ID: $id'),
            Text('Product Name: $name'),
          ],
        ),
      ),
    );
  }
}

1.5 Latihan/Tugas

Tugas 1: Halaman Login dan Halaman Utama

Pada tugas ini, kita akan membuat alur login sederhana. Pengguna akan memasukkan username dan password di halaman login, kemudian data tersebut akan dikirim dan ditampilkan di halaman utama setelah tombol login ditekan.

Langkah Pengerjaan:
  1. Membuat Halaman Login (`login_page.dart`):
    • Gunakan `StatefulWidget` untuk mengelola input dari `TextEditingController`.
    • Buat dua `TextField` untuk username dan password.
    • Buat sebuah `ElevatedButton` yang saat ditekan akan memanggil `Navigator.pushReplacement`. Kenapa `pushReplacement`? Agar pengguna tidak bisa kembali ke halaman login setelah berhasil masuk.
    • Kirim data dari `TextEditingController` melalui arguments dari `pushReplacementNamed`.
  2. Membuat Halaman Utama (`home_page.dart`):
    • Buat `StatelessWidget` sederhana.
    • Di dalam method `build`, ambil data yang dikirim dari halaman login menggunakan `ModalRoute.of(context)!.settings.arguments`.
    • Tampilkan data username dan password yang telah diterima menggunakan widget `Text`.
  3. Mengatur Rute (`main.dart`):
    • Di dalam `MaterialApp`, definisikan `initialRoute` ke `'/login'`.
    • Daftarkan kedua halaman (`LoginPage` dan `HomePage`) di dalam properti `routes`.
Kode Program Lengkap:

Berikut adalah kode untuk ketiga file yang dibutuhkan.

File: `main.dart`
import 'package:flutter/material.dart';
import 'login_page.dart';
import 'home_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Login App',
      initialRoute: '/login', // Halaman awal adalah login
      routes: {
        '/login': (context) => const LoginPage(),
        '/home': (context) => const HomePage(),
      },
    );
  }
}
File: `login_page.dart`
import 'package:flutter/material.dart';

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

  @override
  State createState() => _LoginPageState();
}

class _LoginPageState extends State {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

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

  void _login() {
    // Pindah ke halaman home dan kirim data
    Navigator.pushReplacementNamed(
      context,
      '/home',
      arguments: {
        'username': _usernameController.text,
        'password': _passwordController.text,
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Login'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(
                labelText: 'Username',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 20),
            TextField(
              controller: _passwordController,
              obscureText: true, // Untuk menyembunyikan password
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: _login,
              child: const Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}
File: `home_page.dart`
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    // Terima data yang dikirim dari halaman login
    final args = ModalRoute.of(context)!.settings.arguments as Map?;
    final String username = args?['username'] ?? 'Tidak ada username';
    final String password = args?['password'] ?? 'Tidak ada password';

    return Scaffold(
      appBar: AppBar(
        title: const Text('Halaman Utama'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Selamat Datang!',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            Text('Username: $username'),
            Text('Password: $password'),
          ],
        ),
      ),
    );
  }
}

Tugas 2: Contoh Navigasi dengan Bottom Navigation Bar

Widget `BottomNavigationBar` adalah salah satu implementasi navigasi yang paling umum. Widget ini memungkinkan pengguna berpindah antar beberapa halaman utama (biasanya 3-5 halaman) dengan mudah dari bagian bawah layar.

Langkah Pengerjaan:
  1. Buat sebuah `StatefulWidget` sebagai halaman utama yang akan menampung `BottomNavigationBar`.
  2. Buat sebuah `List` yang berisi widget-widget untuk setiap halaman (misalnya Halaman Beranda, Pencarian, dan Profil).
  3. Buat sebuah variabel `_selectedIndex` untuk melacak tab mana yang sedang aktif.
  4. Gunakan `Scaffold` dan pada properti `body`, tampilkan widget dari `List` berdasarkan `_selectedIndex`.
  5. Pada properti `bottomNavigationBar`, buat sebuah `BottomNavigationBar` dengan `items` (daftar `BottomNavigationBarItem`).
  6. Atur `currentIndex` ke `_selectedIndex` dan implementasikan method `onTap` untuk mengubah nilai `_selectedIndex` di dalam `setState`.
Kode Program Lengkap:

Kode ini digabungkan dalam satu file `main.dart` untuk kemudahan.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Bottom Nav Example',
      home: MainScreen(), // Halaman utama kita
    );
  }
}

// Halaman-halaman dummy untuk ditampilkan
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('Halaman Beranda', style: TextStyle(fontSize: 24)));
  }
}

class SearchScreen extends StatelessWidget {
  const SearchScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('Halaman Pencarian', style: TextStyle(fontSize: 24)));
  }
}

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('Halaman Profil', style: TextStyle(fontSize: 24)));
  }
}


// Widget utama yang menampung BottomNavigationBar
class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State createState() => _MainScreenState();
}

class _MainScreenState extends State {
  int _selectedIndex = 0; // Indeks tab yang aktif

  // Daftar halaman yang akan ditampilkan
  static const List<Widget> _widgetOptions = <Widget>[
    HomeScreen(),
    SearchScreen(),
    ProfileScreen(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index; // Update state saat item di-tap
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Contoh Bottom Navigation Bar'),
      ),
      body: _widgetOptions.elementAt(_selectedIndex), // Tampilkan halaman sesuai indeks
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Beranda',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Cari',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profil',
          ),
        ],
        currentIndex: _selectedIndex, // Item yang sedang aktif
        selectedItemColor: Colors.amber[800], // Warna item aktif
        onTap: _onItemTapped, // Method yang dipanggil saat item di-tap
      ),
    );
  }
}

Kembali ke Daftar Laporan