Terakhir diperbarui: 27 Agustus 2026
Setelah menyelesaikan codelab ini, mahasiswa mampu:
StatelessWidget, StatefulWidget, Container, Row, Column, dan Expanded.Pada pendekatan deklaratif, kode mendeskripsikan tampilan berdasarkan state saat ini. Ketika state berubah, Flutter membangun ulang bagian UI yang relevan. Developer berfokus pada hubungan data dan tampilan, bukan mengubah setiap elemen UI secara manual.
Widget build(BuildContext context) {
return Text('Halo, $nama');
}
StatelessWidget cocok untuk UI yang output-nya ditentukan oleh konfigurasi dari parent.StatefulWidget memiliki objek State untuk data yang dapat berubah selama lifecycle.Container menggabungkan ukuran, padding, margin, decoration, dan child.Row menyusun child secara horizontal, sedangkan Column secara vertikal.Expanded membagi ruang yang tersedia di dalam Row atau Column.MaterialApp dan widget Material 3 cocok untuk pola desain Android dan lintas platform. CupertinoApp serta widget Cupertino menyediakan tampilan yang mengikuti pola iOS. Pilih komponen berdasarkan kebutuhan pengalaman pengguna, bukan sekadar nama platform.
Layout responsif menyesuaikan struktur UI dengan ruang yang tersedia. Gunakan LayoutBuilder atau MediaQuery untuk membaca ukuran, lalu ubah jumlah kolom atau arah layout. Hindari ukuran piksel tetap untuk elemen yang harus beradaptasi.
Theme menyimpan warna, tipografi, dan bentuk komponen secara terpusat. Sediakan theme dan darkTheme pada MaterialApp. Untuk aksesibilitas, gunakan kontras yang cukup, label yang bermakna, ukuran teks yang terbaca, dan jangan menyampaikan informasi hanya melalui warna.
Sebelum mulai, amati target codelab ini: dashboard mahasiswa yang menampilkan satu kolom pada layar sempit dan dua kolom pada layar lebar, dengan tema terang dan gelap yang konsisten.
Sebelum dashboard responsif, latih dulu widget dasar dengan membuat kartu profil sederhana. Buat project baru atau ganti sementara isi lib/main.dart:
import 'package:flutter/material.dart';
void main() => runApp(const ProfileApp());
class ProfileApp extends StatelessWidget {
const ProfileApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(child: ProfileCard()),
),
);
}
}
class ProfileCard extends StatelessWidget {
const ProfileCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 320,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const CircleAvatar(child: Icon(Icons.person)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Nama Mahasiswa',
style: TextStyle(fontWeight: FontWeight.bold)),
Text('...ketik nama Anda di sini...'),
],
),
),
],
),
const SizedBox(height: 12),
const Row(children: [
Expanded(child: Text('NIM')),
Text('...ketik NIM Anda di sini...'),
]),
const Row(children: [
Expanded(child: Text('Kelas')),
Text('...ketik kelas Anda di sini...'),
]),
],
),
);
}
}
Expanded pada baris nama, lalu amati peringatan overflow atau perilaku layout-nya; kembalikan setelah itu.mainAxisSize: MainAxisSize.min menjadi nilai default dan amati perubahan tinggi kartu.Email) menggunakan pola Row + Expanded yang sama.flutter create responsive_dashboard
cd responsive_dashboard
flutter run
Buka lib/main.dart. Buat aplikasi profil sederhana berikut, lalu jalankan pada emulator atau perangkat fisik.
import 'package:flutter/material.dart';
void main() => runApp(const DashboardApp());
class DashboardApp extends StatelessWidget {
const DashboardApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark, colorSchemeSeed: Colors.indigo),
themeMode: ThemeMode.system,
home: const DashboardPage(),
);
}
}
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Student Dashboard')),
body: LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth >= 700 ? 2 : 1;
return GridView.count(
padding: const EdgeInsets.all(16),
crossAxisCount: columns,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2.6,
children: const [
DashboardCard(title: 'Assignments', value: '8'),
DashboardCard(title: 'Attendance', value: '92%'),
DashboardCard(title: 'Portfolio', value: 'Ready'),
DashboardCard(title: 'Current week', value: '02'),
],
);
},
),
);
}
}
class DashboardCard extends StatelessWidget {
const DashboardCard({required this.title, required this.value, super.key});
final String title;
final String value;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(children: [
Expanded(child: Text(title)),
Text(value, style: Theme.of(context).textTheme.headlineSmall),
]),
),
);
}
}
Sejauh ini dashboard masih StatelessWidget. Ubah DashboardApp menjadi StatefulWidget dan tambahkan CupertinoSwitch (widget Cupertino) pada AppBar untuk mengganti tema secara manual — sekaligus membedakan komponen Material dan Cupertino secara langsung:
class DashboardApp extends StatefulWidget {
const DashboardApp({super.key});
@override
State<DashboardApp> createState() => _DashboardAppState();
}
class _DashboardAppState extends State<DashboardApp> {
bool isDark = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark, colorSchemeSeed: Colors.indigo),
themeMode: isDark ? ThemeMode.dark : ThemeMode.light,
home: DashboardPage(
isDark: isDark,
onDarkChanged: (value) => setState(() => isDark = value),
),
);
}
}
Sesuaikan DashboardPage agar menerima state dan callback:
class DashboardPage extends StatelessWidget {
const DashboardPage({
required this.isDark,
required this.onDarkChanged,
super.key,
});
final bool isDark;
final ValueChanged<bool> onDarkChanged;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Student Dashboard'),
actions: [
Row(
children: [
Icon(isDark ? Icons.dark_mode : Icons.light_mode),
const SizedBox(width: 4),
CupertinoSwitch(
value: isDark,
onChanged: onDarkChanged,
),
const SizedBox(width: 12),
],
),
],
),
body: LayoutBuilder(
// ... kode GridView sebelumnya, tidak berubah
),
);
}
}
700 menjadi nilai lain dan amati perubahan jumlah kolom.themeMode menjadi ThemeMode.dark, lalu kembalikan ke ThemeMode.system.Semantics atau label yang bermakna pada elemen yang penting bagi screen reader.Kembangkan dashboard menjadi halaman Academic Overview dengan ketentuan:
Row, Column, Expanded, dan Container.CupertinoSwitch atau Switch.adaptive).screenshots/.Setelah implementasi mandiri selesai, gunakan AI hanya untuk membandingkan dua alternatif tata letak. Kerjakan challenge berikut:
GridView dan versi LayoutBuilder + Column. Jelaskan trade-off responsif dan aksesibilitasnya."Expanded justru menyebabkan overflow di dalam Row, beri contoh kode yang gagal dan perbaikannya."Kriteria lolos: saran AI yang Anda pakai benar-benar berjalan, responsif, tidak menurunkan aksesibilitas, dan Anda mampu menjelaskan setiap keputusannya saat code review, bukan sekadar menyalin output AI.
Setelah tugas utama berjalan, rapikan kode Anda:
InfoCard) yang menerima title dan value, sehingga tidak ada duplikasi widget.Theme.of(context) agar mengikuti tema terang/gelap secara otomatis.const kWideBreakpoint = 700;) agar hanya didefinisikan satu kali.flutter analyze dan pastikan tidak ada error maupun warning baru.Tambahkan widget test di folder test/ untuk memverifikasi perilaku responsif. Override ukuran layar menggunakan tester.view:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:responsive_dashboard/main.dart';
void main() {
testWidgets('Dashboard satu kolom di layar sempit', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(const DashboardApp());
final width = tester.getSize(find.byType(Card)).width;
expect(width, lessThan(700));
});
testWidgets('Dashboard dua kolom di layar lebar', (tester) async {
tester.view.physicalSize = const Size(1200, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(const DashboardApp());
final width = tester.getSize(find.byType(Card)).width;
expect(width, greaterThan(500));
});
}
Jalankan dengan flutter test. Kedua test harus lulus sebelum tugas dikumpulkan. Simpan hasil test pada folder test/ di folder tugas minggu ini.
flutter analyze tidak menghasilkan error.flutter test lulus semua widget test responsif.test/, dan README sudah tersimpan pada folder tugas Week 2.Expanded membantu dan kapan penggunaannya justru menghasilkan layout error?