Lompat ke konten utama
←

Flutter Dasar: Dart, Widget, dan State

Pelajaran 18 dari 18

Data dari internet dan penyimpanan lokal

Misi: layar katalog dari API

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.

Latihan 1 dari 4

Apa yang dicetak program ini?

const contohBody = '''
[
  {"id": 11, "nama": "Kopi Susu", "harga": 18000, "tersedia": true},
  {"id": 12, "nama": "Teh Tarik", "harga": 15000, "tersedia": false},
  {"id": 13, "nama": "Roti Bakar", "harga": 22000, "tersedia": true},
  {"id": 14, "nama": "Es Jeruk", "harga": 12000, "tersedia": true}
]
''';

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();
}

void main() {
  final katalog = olahKatalog(contohBody);
  print(katalog.length);
  print(katalog.map((p) => p.nama).join(', '));
}
Pilih satu jawaban

Latihan 2 dari 4

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());
            },
          ),
        ),
      ],
    );
  }
}
Pilih satu jawaban

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?

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());
}
Pilih satu jawaban

Latihan 4 dari 4

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());
            },
          ),
        ),
      ],
    );
  }
}
Pilih satu jawaban

Catatanmu

Memuat catatan…

tersimpan otomatis, ikut tercetak · 0/4.000

Lanjutkan ke