Setelah pelajaran ini kamu bisa menelusuri layar katalog yang memadukan JSON ke model, FutureBuilder, pencarian dengan setState, dan daftar favorit di penyimpanan lokal.
±17 menit
4 latihan
Diperiksa 28 Okt 2026
Latihan di pelajaran ini butuh akun gratis. Progres, kode, dan catatanmu hanya tersimpan di browser ini. Masuk untuk menyimpannya
Materi
Kedai kopi di Malang punya API katalog sederhana, dan kamu memeriksa layar katalog yang ditulis rekanmu sebelum aplikasinya dipakai kasir. olahKatalog mengubah body JSON menjadi daftar Produk yang tersedia. ambilKatalog memeriksa statusCode sebelum mengurai. _KatalogState membuat Future di initState, menyaring hasil sesuai kata kunci pencarian, dan menyimpan nama produk favorit di shared_preferences.
// Produk dan Produk.fromJson seperti di pelajaran HTTP dan JSON.
List<Produk> olahKatalog(String body) {
final data = jsonDecode(body) as List<dynamic>;
return data
.map((e) => Produk.fromJson(e as Map<String, dynamic>))
.where((p) => p.tersedia)
.toList();
}
Future<List<Produk>> ambilKatalog() async {
final respons = await http.get(Uri.parse('https://api.kedai.example/katalog'));
if (respons.statusCode != 200) {
throw Exception('Gagal memuat katalog (${respons.statusCode})');
}
return olahKatalog(respons.body);
}
class _KatalogState extends State<Katalog> {
late Future<List<Produk>> _katalog;
String _cari = '';
Set<String> _favorit = {};
@override
void initState() {
super.initState();
_katalog = ambilKatalog();
}
Future<void> _ketukFavorit(String nama) async {
final prefs = await SharedPreferences.getInstance();
final favorit = prefs.getStringList('favorit') ?? <String>[];
if (favorit.contains(nama)) {
favorit.remove(nama);
} else {
favorit.add(nama);
}
await prefs.setStringList('favorit', favorit);
setState(() => _favorit = favorit.toSet());
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(onChanged: (teks) => setState(() => _cari = teks)),
Expanded(
child: FutureBuilder<List<Produk>>(
future: _katalog,
builder: (context, snapshot) {
if (snapshot.hasData) {
final tampil = snapshot.data!
.where((p) => p.nama.toLowerCase().contains(_cari.toLowerCase()))
.toList();
return ListView(
children: [
for (final p in tampil)
ListTile(
title: Text(p.nama),
subtitle: Text('Rp${p.harga}'),
trailing: IconButton(
icon: Icon(_favorit.contains(p.nama) ? Icons.favorite : Icons.favorite_border),
onPressed: () => _ketukFavorit(p.nama),
),
),
],
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const Center(child: CircularProgressIndicator());
},
),
),
],
);
}
}
Contoh body JSON dari server ada di soal pertama. Untuk soal lain, telusuri jalur datanya: dari server ke Future, dari Future ke snapshot, dan dari ketukan pengguna ke setState atau ke penyimpanan. Setiap soal cukup dijawab dari kode di atas dan perilaku yang dibahas di modul ini. Dua kebiasaan dari pelajaran sebelumnya dipakai di sini: Future dibuat sekali di initState, dan respons selain 200 dilempar sebagai Exception sehingga muncul lewat snapshot.hasError.
Latihan
0/4 lulus
Misi akhir modul: luluskan minimal 4 dari 4 tugas. Jawaban diperiksa di server. Lulus berarti kamu memahami contohnya, bukan penilaian seluruh pekerjaanmu.
Server membalas kode 500 dengan body berupa halaman HTML. Apa yang tampil di layar katalog?
// Produk dan Produk.fromJson seperti di pelajaran HTTP dan JSON.
List<Produk> olahKatalog(String body) {
final data = jsonDecode(body) as List<dynamic>;
return data
.map((e) => Produk.fromJson(e as Map<String, dynamic>))
.where((p) => p.tersedia)
.toList();
}
Future<List<Produk>> ambilKatalog() async {
final respons = await http.get(Uri.parse('https://api.kedai.example/katalog'));
if (respons.statusCode != 200) {
throw Exception('Gagal memuat katalog (${respons.statusCode})');
}
return olahKatalog(respons.body);
}
class _KatalogState extends State<Katalog> {
late Future<List<Produk>> _katalog;
String _cari = '';
Set<String> _favorit = {};
@override
void initState() {
super.initState();
_katalog = ambilKatalog();
}
Future<void> _ketukFavorit(String nama) async {
final prefs = await SharedPreferences.getInstance();
final favorit = prefs.getStringList('favorit') ?? <String>[];
if (favorit.contains(nama)) {
favorit.remove(nama);
} else {
favorit.add(nama);
}
await prefs.setStringList('favorit', favorit);
setState(() => _favorit = favorit.toSet());
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(onChanged: (teks) => setState(() => _cari = teks)),
Expanded(
child: FutureBuilder<List<Produk>>(
future: _katalog,
builder: (context, snapshot) {
if (snapshot.hasData) {
final tampil = snapshot.data!
.where((p) => p.nama.toLowerCase().contains(_cari.toLowerCase()))
.toList();
return ListView(
children: [
for (final p in tampil)
ListTile(
title: Text(p.nama),
subtitle: Text('Rp${p.harga}'),
trailing: IconButton(
icon: Icon(_favorit.contains(p.nama) ? Icons.favorite : Icons.favorite_border),
onPressed: () => _ketukFavorit(p.nama),
),
),
],
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const Center(child: CircularProgressIndicator());
},
),
),
],
);
}
}
Latihan 3 dari 4
Di aplikasi yang baru dipasang, pengguna mengetuk ikon favorit pada Kopi Susu, Roti Bakar, Kopi Susu, lalu Es Jeruk. Apa isi daftar favorit yang tersimpan?
Layar katalog dibuka, data berhasil dimuat, lalu pengguna mengetik teh satu huruf demi satu huruf di kolom pencarian. Berapa kali ambilKatalog() dipanggil sejak layar dibuka?
// Produk dan Produk.fromJson seperti di pelajaran HTTP dan JSON.
List<Produk> olahKatalog(String body) {
final data = jsonDecode(body) as List<dynamic>;
return data
.map((e) => Produk.fromJson(e as Map<String, dynamic>))
.where((p) => p.tersedia)
.toList();
}
Future<List<Produk>> ambilKatalog() async {
final respons = await http.get(Uri.parse('https://api.kedai.example/katalog'));
if (respons.statusCode != 200) {
throw Exception('Gagal memuat katalog (${respons.statusCode})');
}
return olahKatalog(respons.body);
}
class _KatalogState extends State<Katalog> {
late Future<List<Produk>> _katalog;
String _cari = '';
Set<String> _favorit = {};
@override
void initState() {
super.initState();
_katalog = ambilKatalog();
}
Future<void> _ketukFavorit(String nama) async {
final prefs = await SharedPreferences.getInstance();
final favorit = prefs.getStringList('favorit') ?? <String>[];
if (favorit.contains(nama)) {
favorit.remove(nama);
} else {
favorit.add(nama);
}
await prefs.setStringList('favorit', favorit);
setState(() => _favorit = favorit.toSet());
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(onChanged: (teks) => setState(() => _cari = teks)),
Expanded(
child: FutureBuilder<List<Produk>>(
future: _katalog,
builder: (context, snapshot) {
if (snapshot.hasData) {
final tampil = snapshot.data!
.where((p) => p.nama.toLowerCase().contains(_cari.toLowerCase()))
.toList();
return ListView(
children: [
for (final p in tampil)
ListTile(
title: Text(p.nama),
subtitle: Text('Rp${p.harga}'),
trailing: IconButton(
icon: Icon(_favorit.contains(p.nama) ? Icons.favorite : Icons.favorite_border),
onPressed: () => _ketukFavorit(p.nama),
),
),
],
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const Center(child: CircularProgressIndicator());
},
),
),
],
);
}
}