Skip to main content

Bab 2: Hello Flutter

Sekarang setelah kamu mendapatkan pengenalan singkat, kamu siap memulai pendidikan Flutter-mu. Tugas pertamamu adalah membangun sebuah aplikasi dasar dari nol, yang akan memberimu kesempatan untuk membiasakan diri dengan tools serta struktur dasar aplikasi Flutter. Kamu akan melakukan kustomisasi aplikasi dan mempelajari cara menggunakan beberapa widget populer seperti ListView dan Slider untuk memperbarui UI sebagai respons terhadap perubahan.

Membuat aplikasi sederhana akan menunjukkan betapa cepat dan mudahnya membangun aplikasi cross-platform dengan Flutter — sekaligus memberimu quick win di awal.

Pada akhir bab ini, kamu akan berhasil membangun sebuah aplikasi resep yang sederhana. Karena kamu masih berada di tahap awal mempelajari Flutter, aplikasi ini akan menyediakan daftar resep yang di-hard-code dan memungkinkan kamu menggunakan Slider untuk menghitung ulang jumlah bahan berdasarkan jumlah porsi.

Berikut tampilan aplikasi setelah nanti selesai dibuat di akhir bab:

Semua yang kamu butuhkan untuk memulai bab ini hanyalah memastikan Flutter sudah terpasang dengan benar. Jika hasil dari flutter doctor tidak menunjukkan error apa pun, berarti kamu sudah siap untuk mulai. Jika masih ada masalah, kembali ke Bab 1, Dasar-dasar Flutter, untuk menyiapkan environment-mu terlebih dahulu.

Membuat Aplikasi Baru

Ada dua cara sederhana untuk memulai aplikasi Flutter baru. Pada bab sebelumnya, kamu membuat project aplikasi baru melalui IDE. Sebagai alternatif, kamu juga bisa membuat aplikasi menggunakan perintah flutter. Pada bagian ini, kamu akan menggunakan opsi kedua.

Buka jendela terminal, lalu navigasikan ke lokasi tempat kamu ingin membuat folder baru untuk project tersebut. Sebagai contoh, kamu bisa menggunakan materi dari buku ini dan menuju ke:

flta-materials/02-hello-flutter/projects/starter/

Membuat project baru sangatlah mudah. Di terminal, jalankan perintah berikut:

flutter create recipes

Perintah ini akan membuat sebuah aplikasi baru di dalam folder baru, yang keduanya bernama recipes. Project tersebut berisi kode aplikasi, seperti yang kamu lihat pada bab sebelumnya, dengan dukungan untuk dijalankan di iOS, Android, Linux, macOS, web, dan Windows.

Buka folder recipes yang akan kita pakai dengan IDE pilihanmu.

Build dan jalankan aplikasi demo seperti pada bab sebelumnya.

Menekan tombol + akan menaikkan angka counter.

Modifkasi Demo App

Aplikasi siap pakai ini merupakan titik awal yang baik karena perintah flutter create sudah menyiapkan seluruh boilerplate yang dibutuhkan agar kamu bisa langsung mulai. Namun, ini belum benar-benar aplikasi yang kita mau. Secara harfiah, aplikasinya masih bernama MyApp, seperti yang bisa kamu lihat di bagian atas file main.dart:

class MyApp extends StatelessWidget {

Baris ini mendefinisikan sebuah class Dart baru bernama MyApp yang meng-extends — atau mewarisi — StatelessWidget. Di Flutter, hampir semua yang membentuk user interface adalah sebuah Widget. Sebuah StatelessWidget tidak akan mengubah apapun setelah aplikasi di build. Kamu akan mempelajari lebih banyak tentang widget dan state pada bagian selanjutnya. Untuk sekarang, anggap saja MyApp sebagai wadah utama aplikasi.

Karena kamu sedang membangun aplikasi resep, tentu kamu tidak ingin class utama bernama MyApp — kamu ingin menamainya RecipesApp.

Meskipun kamu bisa menggantinya secara manual di beberapa tempat, kamu bisa mengurangi risiko kesalahan copy-paste atau typo dengan menggunakan fitur rename di IDE. Fitur ini memungkinkan kamu mengganti nama sebuah simbol di tempat definisinya sekaligus di semua tempat yang menggunakannya.

Di Android Studio, kamu bisa menggunakan menu Refactor ▸ Rename atau melalui menu klik kanan.

Klik MyApp pada baris class MyApp ..., lalu pilih salah satu opsi refactor tersebut.

Pada pop-up yang muncul, ganti nama MyApp menjadi RecipesApp, lalu klik tombol Refactor.

Kode akhirnya akan terlihat seperti berikut:

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

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

Fungsi main() adalah entry point dari aplikasi ketika pertama kali dijalankan. Fungsi ini selalu menjadi titik awal eksekusi kode Dart.

Sementara itu, runApp() memberi tahu Flutter widget tingkat teratas (top-level widget) mana yang harus dijalankan sebagai root dari aplikasi. Dalam kasus ini, widget tersebut adalah RecipesApp.

Perlu diperhatikan bahwa hot reload tidak akan menerapkan perubahan kode seperti ini (perubahan pada struktur root aplikasi). Agar perubahan benar-benar dijalankan, kamu harus melakukan hot restart.

Hot restart akan menghentikan aplikasi sementara, lalu menjalankannya kembali dengan kode terbaru.

note

Seperti disebutkan pada Bab 1, saat kamu menyimpan perubahan, hot reload akan berjalan secara otomatis dan memperbarui UI. Jika hal ini tidak terjadi, periksa pengaturan Flutter di IDE kamu untuk memastikan fitur tersebut aktif. Jika kamu tidak ingin hot reload berjalan otomatis saat menyimpan perubahan, kamu bisa menjalankannya secara manual. Pintasan untuk Android Studio adalah **Option-Command-**.

Dengan hot reload, kamu dapat dengan cepat melihat efek dari perubahan kode dan status aplikasi akan tetap dipertahankan. Misalnya, jika pengguna berada dalam status “sudah login” sebelum kode diubah, hot reload akan mempertahankan status tersebut sehingga kamu tidak perlu login ulang untuk menguji perubahan.

Jika kamu melakukan perubahan yang signifikan, seperti menambahkan properti baru pada sebuah state atau mengubah fungsi main() seperti pada contoh di atas, maka kamu perlu melakukan hot restart agar perubahan baru tersebut terdeteksi dan disertakan dalam build yang baru.

Untuk perubahan yang lebih besar lagi, seperti menambahkan dependensi atau aset, kamu perlu melakukan full build dan menjalankan ulang aplikasi.

Dalam contoh kasus ini kita tidak akan melihat perubahan apapun di UI.

Styling Aplikasi

Selanjutnya, kita akan mengubah tampilan widget di aplikasi ini.

Ubah isi method build() di dalam RecipesApp dengan:

// 1
@override
Widget build(BuildContext context) {
// 2
final ThemeData theme = ThemeData();
// 3
return MaterialApp(
// 4
title: 'Recipe Calculator',
// 5
theme: theme.copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.greenAccent,
),
),
// 6
home: const MyHomePage(
title: 'Recipe Calculator',
),
);
}

Kode di atas akan mengubah tampilan aplikasi:

  1. Metode build() pada sebuah widget adalah titik awal untuk menyusun widget-widget lain menjadi sebuah widget baru. Anotasi @override memberi tahu Dart analyzer bahwa metode ini dimaksudkan untuk menggantikan metode bawaan dari StatelessWidget (jika kamu tidak memahami kalimat ini tidak apa-apa, untuk saat ini pahami bahwa kode ini wajib ada disetiap method build()).
  2. Sebuah tema menentukan aspek visual seperti warna. ThemeData default akan menampilkan standar bawaan Material.
  3. MaterialApp menggunakan Material Design dan merupakan widget yang akan disertakan di dalam RecipesApp.
  4. Nama aplikasi yang ada di title dipakai sistem untuk mengidentifikasi aplikasi. UI tidak akan menampilkan nama ini.
  5. Dengan menyalin tema dan mengganti skema warna dengan skema khusus, kamu sudah mengubah warna aplikasi. Di sini, dengan menggunakan konstruktor khusus fromSeed, kita menghasilkan variasi warna dan tone yang digunakan ThemeData untuk menata widget sesuai spesifikasi Material Design.
  6. Widget ini masih menggunakan widget MyHomePage yang sama seperti sebelumnya, tetapi sekarang kita telah mengubah nama aplikasi dan menampilkannya di perangkat.

Ketika aplikasi di jalankan ulang, kita akan melihat widget yang sama namun dengan style yang cukup berbeda.

Kita telah mengambil langkah pertama dalam personalisasi aplikasi sehingga aplikasi contoh yang di-generate oleh Flutter perlahan-lahan berubah menjadi aplikasi yang kita mau dengan mengubah isi MaterialApp. Kita akan bersihkan sisa kode lain di bagian berikutnya.

Membersihkan Aplikasi dari Kode Contoh

Kamu sudah mengubah tema aplikasi, tetapi tampilannya masih menampilkan demo counter. Langkah berikutnya adalah membersihkan sisa kode tersebut. Untuk memulainya, ganti class MyHomePageState yang ada dengan:

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// 1
return Scaffold(
// 2
appBar: AppBar(
title: Text(widget.title),
),
// 3
body: SafeArea(
// TODO: Ganti child: Container()
// 4
child: Container(),
),
);
}

// TODO: Tambahkan buildRecipeCard() disini
}

Sekilas tentang apa yang dilakukan contoh di atas:

  1. Scaffold menyediakan struktur awal untuk sebuah layar atau halaman. Dalam kasus ini, kamu menggunakan dua properti.

  2. AppBar menerima properti title dengan menggunakan widget Text yang nilainya diberi dari home: MyHomePage(title: 'Recipe Calculator') pada langkah sebelumnya.

  3. body menggunakan SafeArea, yang menjaga aplikasi agar tidak terlalu dekat dengan elemen antarmuka sistem operasi seperti notch atau area interaktif seperti Home Indicator di bagian bawah beberapa layar iOS atau menu navigasi di Android.

  4. SafeArea memiliki sebuah child widget, yaitu widget Container yang masih kosong.

note

Beberapa widget seperti AppBar juga bisa menerima properti tampilan kustom. Pada template proyek bawaan Flutter, AppBar memiliki properti backgroundColor yang diset ke inversePrimary di _MyHomePageState. Pada kasus ini, kamu menghapus pengaturan tampilan kustom tersebut, sehingga warna AppBar ikut berubah.

Setelah hot reload, kita akan dapatkan tampilan yang sudah bersih:

Membangun List Resep

Aplikasi resep yang kosong tentu tidak terlalu berguna. Aplikasi ini seharusnya memiliki daftar resep yang rapi dan nyaman untuk kamu scroll. Namun, sebelum kamu bisa menampilkannya, kamu perlu menyiapkan data yang akan digunakan untuk mengisi tampilan UI.

Membuat Data Model

Kamu akan menggunakan Recipe sebagai struktur data utama untuk resep di aplikasi ini.

Buat file Dart baru di dalam folder lib dengan nama recipe.dart.
Tambahkan class berikut ke dalam file tersebut:

class Recipe {
String label;
String imageUrl;
// TODO: Tambahkan bahan (ingredients) dan porsi (servings)

Recipe(
this.label,
this.imageUrl,
);

// TODO: Tambahkan List<Recipe> di sini
}

// TODO: Tambahkan kelas Ingredient di sini

Ini adalah awal dari sebuah model Recipe dengan properti label dan image.

Kamu juga perlu menyediakan data agar aplikasi punya sesuatu untuk ditampilkan. Pada aplikasi yang lebih lengkap, data ini biasanya dimuat dari database lokal atau API berbasis JSON. Namun, demi kesederhanaan saat kamu mulai belajar Flutter, pada chapter ini kita akan menggunakan data yang di-hard-code.

Tambahkan properti berikut ke dalam Recipe dengan mengganti komentar
// TODO: Add List<Recipe> here dengan kode:

static List<Recipe> samples = [
Recipe(
'Spaghetti and Meatballs',
'assets/2126711929_ef763de2b3_w.jpg',
),
Recipe(
'Tomato Soup',
'assets/27729023535_a57606c1be.jpg',
),
Recipe(
'Grilled Cheese',
'assets/3187380632_5056654a19_b.jpg',
),
Recipe(
'Chocolate Chip Cookies',
'assets/15992102771_b92f4cc00a_b.jpg',
),
Recipe(
'Taco Salad',
'assets/8533381643_a31a99e8a6_c.jpg',
),
Recipe(
'Hawaiian Pizza',
'assets/15452035777_294cefced5_c.jpg',
),
];

Ini adalah daftar resep yang di-hard-code. Kamu akan menambahkan detail lebih lanjut nanti, tetapi untuk saat ini isinya hanya berupa daftar nama dan gambar.

note

List adalah kumpulan item yang berurutan dan dalam beberapa bahasa pemrograman, ia disebut array. Indeks pada List dimulai dari 0.

Kamu sudah membuat sebuah List berisi nama resep dan gambar, tetapi proyekmu belum memiliki file gambarnya. Untuk menambahkannya, buka Finder lalu salin folder assets dari level teratas folder 02-hello-flutter yang ada di materi buku. Setelah itu, tempelkan folder tersebut ke dalam struktur folder proyekmu. Jika sudah benar, posisinya akan sejajar dengan folder lib.

Dengan begitu, aplikasi akan bisa menemukan gambar-gambar tersebut saat dijalankan.

Kamu juga akan melihat bahwa setelah melakukan copy-paste melalui Finder, folder dan gambar tersebut otomatis muncul di daftar proyek Android Studio.

Namun, hanya menambahkan assets ke dalam proyek belum akan menampilkannya di aplikasi. Untuk memberi tahu aplikasi agar menyertakan assets tersebut, buka file pubspec.yaml di folder root proyek recipes.

Di bawah komentar `# To add assets to your application...```, tambahkan baris-baris berikut:

assets:
- assets/

Baris-baris ini menentukan bahwa assets/ adalah sebuah folder aset dan harus disertakan bersama aplikasi. Pastikan baris pertama di sini sejajar (indentasiny alias tab-nya sama) dengan baris uses-material-design: true yang ada di atasnya.

Setelah kamu memodifikasi pubspec.yaml, IDE kamu mungkin akan menampilkan notifikasi untuk mengambil ulang dependensi proyek.

Hal ini terjadi karena pubspec.yaml berfungsi sebagai manifest aplikasi. Jadi, ketika kamu mengubahnya, kamu juga perlu memberi tahu VM Dart bahwa ada perubahan dan semua kode yang dibundel perlu diperbarui. Perlu diperhatikan, perubahan seperti ini mengharuskan kamu melakukan restart penuh aplikasi, bukan sekadar hot reload.

Menampilkan List

Setelah datanya siap dipakai, selanjutnya kita perlu tempat untuk menampilkan data-data itu.

Kembali ke main.dart, kita perlu mengimpor data file tadi. Tambahkan kode berikut di atas file di bawah baris import lainnya:

import 'recipe.dart';

Selanjutnya di _MyHomePageState cari dan ubah // TODO: Ganti Child Container() dan dua baris di bawahnya dengan:

// 4
child: ListView.builder(
// 5
itemCount: Recipe.samples.length,
// 6
itemBuilder: (BuildContext context, int index) {
// 7
// TODO: Update untuk menggunakan Recipe card
return Text(Recipe.samples[index].label);
},
),

Kode di atas tujuannya:

  1. Membuat sebuah list dengan ListView.

  2. Menentukan banyaknya data di dalam list lewat itemCount. Pada contoh kasus ini, length adalah jumlah dari setiap objek yang ada di dalam Recipe.samples.

  3. Setiap baris di dalam list diproses oleh itemBuilder.

  4. Widget Text menampilkan nama dari setiap resep.

Lakukan hot reload sekarang dan kamu akan lihat tampilan berikut:

Datanya sudah ada tinggal kita tampilkan dalam tampilan yang lebih menarik. :]

Menampilakn Data dengan Card

Keren, sekarang kamu sudah menampilkan list dengan data, tapi ini masih belum bisa dibilang sebuah aplikasi. Supaya tampilannya lebih menarik, langkah selanjutnya adalah menambahkan gambar yang mendampingi judul resep.

Untuk melakukannya, kamu akan menggunakan Card. Dalam Material Design, Card mendefinisikan sebuah area pada UI tempat kamu menata informasi yang saling berkaitan tentang satu entitas tertentu.
Sebagai contoh, Card pada aplikasi musik bisa berisi judul album, nama artis, tanggal rilis, dilengkapi dengan gambar sampul album dan mungkin juga kontrol untuk memberi rating bintang.

Card resep kamu akan menampilkan label resep dan gambar resep. Struktur widget tree-nya akan memiliki susunan seperti berikut.

Di main.dart, pada bagian paling bawah kelas _MyHomePageState, buat sebuah custom widget dengan mengganti komentar // TODO: Tambahkan buildRecipeCard() disini dengan implementasi yang sesuai.

Widget buildRecipeCard(Recipe recipe) {
// 1
return Card(
// 2
child: Column(
// 3
children: <Widget>[
// 4
Image(image: AssetImage(recipe.imageUrl)),
// 5
Text(recipe.label),
],
),
);
}

Berikut cara mendefinisikan custom Card widget yang baru:

  1. Kamu mengembalikan sebuah Card dari buildRecipeCard().

  2. Properti child dari Card adalah sebuah Column. Column adalah widget yang mendefinisikan tata letak secara vertikal.

  3. Column memiliki dua child.

  4. Child pertama adalah widget Image. AssetImage menunjukkan bahwa gambar diambil dari local asset bundle yang sudah didefinisikan di pubspec.yaml.

  5. Child kedua adalah widget Text. Widget ini akan berisi nilai recipe.label.

Untuk menggunakan Card tersebut, buka _MyHomePageState lalu ganti komentar // TODO: Update untuk menggunakan Recipe card beserta baris return di bawahnya dengan kode berikut:

// TODO: Tambahkan GestureDetector
return buildRecipeCard(Recipe.samples[index]);

Kode ini menginstruksikan itemBuilder agar menggunakan custom Card widget yang sudah dibuat untuk setiap resep yang ada di dalam daftar samples.

Lakukan hot restart pada aplikasi untuk melihat kartu berisi gambar dan teks muncul di layar.

Perhatikan bahwa Card secara default tidak berbentuk kotak datar dengan sudut tajam. Material Design menyediakan radius sudut standar dan bayangan (drop shadow) bawaan untuk Card.

Melihat Widget Tree

Sekarang adalah waktu yang tepat untuk memikirkan widget tree dari keseluruhan aplikasi. Kamu ingat bahwa semuanya dimulai dari RecipesApp di dalam main()?

RecipesApp membangun sebuah MaterialApp, yang kemudian menggunakan MyHomePage sebagai home. MyHomePage membangun sebuah Scaffold dengan AppBar dan ListView. Lalu kamu memperbarui builder pada ListView agar membuat sebuah Card untuk setiap item.

Memikirkan widget tree membantu kamu memahami aplikasi ketika layout menjadi lebih kompleks dan saat kamu mulai menambahkan interaktivitas. Untungnya, kamu tidak perlu mencatat diagram widget tree secara manual.

Di Android Studio, buka Flutter Inspector dari menu View ▸ Tool Windows ▸ Flutter Inspector saat aplikasi dijalankan. Menu ini akan menampilkan UI debugging tool yang powerful.

Tampilan ini menunjukkan semua widget yang ada di layar dan bagaimana mereka disusun. Saat kamu melakukan scroll, kamu bisa me-refresh tree tersebut. Kamu mungkin akan melihat jumlah card berubah. Itu karena List tidak menyimpan semua item di memori sekaligus, demi meningkatkan performa. Kamu akan mempelajari lebih lanjut tentang cara kerjanya di Bab 4, "Memahami Cara Kerja Widget".

Memoles Tampilan Card

Card bawaan terlihat cukup oke, tapi masih bisa dibuat jauh lebih menarik. Dengan menambahkan beberapa sentuhan ekstra, kamu bisa mempercantik tampilan card. Caranya antara lain dengan membungkus widget menggunakan widget layout seperti Padding atau dengan menentukan parameter styling tambahan.

Mulai dengan mengganti buildRecipeCard() dengan:

Widget buildRecipeCard(Recipe recipe) {
return Card(
// 1
elevation: 2.0,
// 2
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
// 3
child: Padding(
padding: const EdgeInsets.all(16.0),
// 4
child: Column(
children: <Widget>[
Image(image: AssetImage(recipe.imageUrl)),
// 5
const SizedBox(
height: 14.0,
),
// 6
Text(
recipe.label,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w700,
fontFamily: 'Palatino',
),
)
],
),
),
);
}

Ada beberapa perubahan yang perlu diperhatikan:

  1. Nilai elevation suatu card menentukan seberapa "tinggI" card dari dasar layar, sehingga mempengaruhi shadow-nya.

  2. Parameter shape mempengaruhi bentuk dari card (dari istilah bahasa inggris shape yang berarti bentuk). Kode ini menentukan bahwa bentuknya adalah kotak dengan ujung yang membulat (rounded rectangle) dengan masing-masing ujung memiliki radius 10.0.

  3. Padding memberikan jarak sebesar nilai yang diberikan di dalam child-nya.

  4. Child dari padding adalah Colum yang sama seperti sebelumnya yang berisi gambar dan teks.

  5. Diantara gambar dan teks ada SizedBox. Widget ini adalah kotak kosong dengan tujuan memberikan jarak diantara dua widget.

  6. Kita bisa mengubah tampilan widget Text dengan bojek style. Dalam kasus ini, kita menggunakan font Palatino dengan ukuran 20.0 dan weight w700 (bold).

Hot reload dan kamu akan melihat list yang lebih cakep.

Kamu bisa bereksperimen dengan nilai-nilai ini untuk mendapatkan tampilan daftar yang terasa “pas” menurutmu. Dengan hot reload, kamu bisa dengan mudah melakukan perubahan dan langsung melihat hasilnya pada aplikasi yang sedang berjalan.

Dengan menggunakan Widget Inspector, kamu akan melihat widget Padding dan SizedBox yang ditambahkan. Saat kamu memilih sebuah widget, misalnya SizedBox, panel terpisah akan menampilkan semua properti widget tersebut secara real-time, termasuk properti yang kamu atur secara eksplisit serta yang diwarisi atau disetel secara default.

Memilih sebuah widget juga akan menunjukkan dimana kode untuk widget tersebut.

tip

Kamu mungkin perlu klik tombol Refresh Tree yang ada di pojok kanan atas untuk memuat ulang struktur di dalam inspektor. Lihat Bab 4, "Memahami Cara Kerja Widget" untuk lebih detail.

Menambahkan Halaman Detail Resep

Sekarang kita sudah punya list yang cukup menarik, tapi masih belum interaktif. Yang akan membuat aplikasi ini lebih menarik adalah bila user bisa memilih salah satu resep dan melihat informasi lebih detail tentang resep tersebut. Kita akan mulai mengimplementasi fitur untuk mendeteksi sentuhan pada sebuah card.

Di dalam _MyHomePageState, cari // TODO: Tambahkan GestureDetector lalu ganti perintah return di bawahnya dengan kode berikut:

// 7
return GestureDetector(
// 8
onTap: () {
// 9
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
// 10
// TODO: Timpa dengan return RecipeDetail()
return Text('Detail page');
},
),
);
},
// 11
child: buildRecipeCard(Recipe.samples[index]),
);

Kode tersebut menambahkan ebberapa widget dan beberapa konsep baru. Mari kita bahas baris per baris:

  1. Memperkenalkan widget GestureDetector yang, sesuai namanya, berfungsi untuk mendeteksi gesture (sentuhan).
  2. Mengimplementasikan fungsi onTap(), yaitu callback yang akan dipanggil ketika widget disentuh.
  3. Widget Navigator mengelola stack dari setiap halaman. Dengan memanggil push() menggunakan MaterialPageRoute, kamu akan menambahkan (push) halaman Material baru ke dalam stack tersebut. Navigasi ini akan dibahas lebih mendalam di Bagian III, "Navigasi Antar Halaman".
  4. Properti builder digunakan untuk membuat widget halaman tujuan.
  5. Properti child pada GestureDetector menentukan area di mana gesture akan terdeteksi (dalam contoh ini berarti setiap card resep).

Lakukan hot reload dan setiap card di sentuh maka kita akan melihat halaman hitam dari Detail page.

Membuat Halaman Baru

Halaman hitam yang kita lihat hanyalah placeholder. Bukan hanya karena tampilannya jelek, tapi halaman ini tidak memiliki semua yang umumnya ada, misalkan iOS yang tidak punya tombol back akan membuat user "yangkut" di sini. Tapi tenang saja, kita akan perbaiki semua!

Dalam folder lib, buat file Dart baru dengan nama recipe_detail.dart.

Sekarang, tambahkan kode berikut ke dalam file tadi, acuhkan tanda merah yang muncul:

import 'package:flutter/material.dart';
import 'recipe.dart';
class RecipeDetail extends StatefulWidget {
final Recipe recipe;
const RecipeDetail({
Key? key,
required this.recipe,
}) : super(key: key);

@override
State<RecipeDetail> createState() {
return _RecipeDetailState();
}
}
// TODO: Tambahkan _RecipeDetailState di sini

Kode tersebut akan membuat StatefulWidget baru yang memiliki initializer untuk menerima detail Recipe yang akan ditampilkan. Widget ini dibuat sebagai StatefulWidget karena nanti kamu akan menambahkan beberapa state interaktif ke halaman ini.

Kamu membutuhkan _RecipeDetailState untuk membangun widget tersebut, jadi ganti // _RecipeDetailState di sini dengan:

class _RecipeDetailState extends State<RecipeDetail> {
// TODO: Tambahkan _sliderVal di sini
@override
Widget build(BuildContext context) {
// 1
return Scaffold(
appBar: AppBar(title: Text(widget.recipe.label)),
// 2
body: SafeArea(
// 3
child: Column(
children: <Widget>[
// 4
SizedBox(
height: 300,
width: double.infinity,
child: Image(image: AssetImage(widget.recipe.imageUrl)),
),
// 5
const SizedBox(height: 4),
// 6
Text(widget.recipe.label, style: const TextStyle(fontSize: 18)),
// TODO: Tambahkan Expanded
// TODO: Tambahkan Slider() di sini
],
),
),
);
}
}

Body dari widget ini sama seperti yang sudah kamu lihat sebelumnya. Berikut beberapa hal yang perlu diperhatikan:

  1. Scaffold mendefinisikan struktur umum dari halaman.
  2. Di dalam body, terdapat sebuah SafeArea, sebuah Column dengan beberapa child berupa SizedBox dan Text.
  3. SafeArea menjaga aplikasi agar tidak terlalu dekat dengan antarmuka sistem operasi, seperti notch atau area interaktif di sebagian besar iPhone.
  4. Satu hal baru adalah SizedBox yang membungkus Image, yang mendefinisikan batas ukuran yang dapat disesuaikan untuk gambar. Di sini, tingginya ditetapkan tetap 300, tetapi lebarnya akan menyesuaikan agar sesuai dengan rasio. Satuan ukuran di Flutter adalah logical pixels.
  5. Terdapat sebuah SizedBox yang berfungsi sebagai spacer.
  6. Text untuk label memiliki style yang sedikit berbeda dibandingkan Card utama, untuk menunjukkan seberapa besar tingkat kustomisasi yang tersedia.

Selanjutnya, kembali ke main.dart dan tambahkan baris berikut di bagian atas file:

import 'recipe_detail.dart';

Lalu cari // TODO: Timpa dengan return RecipeDetail(), kemudian ganti dengan perintah return:

return RecipeDetail(recipe: Recipe.samples[index]);

Lakukan hot restart dengan memilih Run ▸ Flutter Hot Restart dari menu untuk mengembalikan state aplikasi ke posisi awal. Mengetuk sebuah kartu resep sekarang akan menampilkan halaman RecipeDetail.

tip

Kamu perlu menggunakan hot restart sekarang karena hot reload tidak akan mengubah UI setelah state nya di modifikasi.

Karena sekarang kita punya Scaffold dengan sebuah appBar, Flutter secara otomatis akan memberikan tombol kembali untuk membawa pengguna kembali ke halaman sebelumnya.

Menambahkan Bahan-bahan

Untuk menyelesaikan halaman detail, kamu akan perlu menampilkan informais tambahan di kelas Recipe. Sebelum melakukannya, kita perlu menambah daftar bahan-bahan ke resep tersebut.

Buka recipe.dart lalu timpa //TODO: Tambahkan kelas Ingredient di ini dengan kode sebagai berikut:

class Ingredient {
double quantity;
String measure;
String name;
Ingredient(
this.quantity,
this.measure,
this.name,
);
}

Kelas ini adalah wadah sederhana untuk data bahan (ingredient). Ia memiliki nama, satuan ukuran (measure) — seperti cup atau sendok makan (tablespoon) — dan jumlah.

Di bagian atas kelas Recipe, ganti // TODO: Tambahkan bahan (ingredients) dan porsi (servings) dengan baris berikut, abaikan garis merah bergelombang yang mungkin muncul:

int servings;
List<Ingredient> ingredients;

Kode di atas menambahkan properti servings sebagai angka yang menyatakan jumlah porsi yang bisa dibuat beserta ingredients sebagai sebuah List yang menandakan ada lebih dari satu ingredient (bahan).

Untuk menggunakannya, buka lagi kelas Recipe kemudian ubah konstruktornya dari:

Recipe(
this.label,
this.imageUrl,
);

menjadi:

Recipe(
this.label,
this.imageUrl,
this.servings,
this.ingredients,
);

Kamu mungkin akan melihat gelombang merah di bawah beberapa baris kode karena data untuk bahan dan jumlah porsi tadi belum ada.

Untuk menambahkan bahan dan jumlah porsi agar properti servings dan ingredients terisi, perbarui samples menjadi:

static List<Recipe> samples = [
Recipe(
'Spaghetti and Meatballs',
'assets/2126711929_ef763de2b3_w.jpg',
4,
[
Ingredient(
1,
'box',
'Spaghetti',
),
Ingredient(
4,
'',
'Frozen Meatballs',
),
Ingredient(
0.5,
'jar',
'sauce',
),
],
),
Recipe(
'Tomato Soup',
'assets/27729023535_a57606c1be.jpg',
2,
[
Ingredient(
1,
'can',
'Tomato Soup',
),
],
),
Recipe(
'Grilled Cheese',
'assets/3187380632_5056654a19_b.jpg',
1,
[
Ingredient(
2,
'slices',
'Cheese',
),
Ingredient(
2,
'slices',
'Bread',
),
],
),
Recipe(
'Chocolate Chip Cookies',
'assets/15992102771_b92f4cc00a_b.jpg',
24,
[
Ingredient(
4,
'cups',
'flour',
),
Ingredient(
2,
'cups',
'sugar',
),
Ingredient(
0.5,
'cups',
'chocolate chips',
),
],
),
Recipe(
'Taco Salad',
'assets/8533381643_a31a99e8a6_c.jpg',
1,
[
Ingredient(
4,
'oz',
'nachos',
),
Ingredient(
3,
'oz',
'taco meat',
),
Ingredient(
0.5,
'cup',
'cheese',
),
Ingredient(
0.25,
'cup',
'chopped tomatoes',
),
],
),
Recipe(
'Hawaiian Pizza',
'assets/15452035777_294cefced5_c.jpg',
4,
[
Ingredient(
1,
'item',
'pizza',
),
Ingredient(
1,
'cup',
'pineapple',
),
Ingredient(
8,
'oz',
'ham',
),
],
),
];

Kode di atas akan mengiri data untuk dua properti tambahan tadi. Tolong jangan diikuti ya karena resep ini hanya contoh. :]

Lakukan hot reload sekarang. Tidak akan ada perubahan UI dulu, tapi pastikan build berjalan dengan sukses.

Menampilkan Bahan-Bahan

Sebuah resep tidak akan lengkap tanpa bahannya. Sekarang kita siap untuk menambahkan widget baru untuk menampilkan bahan-bahan tadi.

Buka recipe_detail.dart lalu ganti // TODO: Tambahkan Expanded dengan:

// 7
Expanded(
// 8
child: ListView.builder(
padding: const EdgeInsets.all(7.0),
itemCount: widget.recipe.ingredients.length,
itemBuilder: (BuildContext context, int index) {
final ingredient = widget.recipe.ingredients[index];
// 9
// TODO: Tambahkan ingredient.quantity
return Text('${ingredient.quantity} ${ingredient.measure} ${ingredient.name}');
},
),
),

Kode ini menambahkan:

  1. Widget Expanded yang akan mengembang untuk mengisi ruang di dalam sebuah Column. Dengan cara ini, daftar ingredient akan memakai ruang kosong yang tidak ditempati oleh widget lain.
  2. ListView diatur agak memiliki satu baris untuk setiap ingredient.
  3. Text yang menggunakan string interpolation untuk mengisi sebuah teks dengan nilai yang diisi saat runtime. Kamu menggunakan sintaks ${perintah} di dalam string literal untuk menandainya.

Lakukan hot restart dengan memilih Run ▸ Flutter Hot Restart lalu buka halaman detail untuk melihat daftar ingredients.

Kerja bagus, layar sekarang menampilkan nama resep dan daftar ingredients. Selanjutnya, kamu akan menambahkan sebuah fitur untuk membuatnya menjadi interaktif.

Mengatur Jumlah porsi dengan Slider

Saat ini kamu sedang menampilkan bahan-bahan untuk satu porsi yang disarankan. Akan jauh lebih baik jika kamu bisa mengubah jumlah porsi yang diinginkan dan jumlah bahan-bahan pun akan diperbarui secara otomatis.

Kamu akan melakukannya dengan menambahkan sebuah widget Slider agar pengguna dapat menyesuaikan jumlah porsinya.

Pertama, buat sebuah variabel untuk menyimpan nilai slider. Masih di recipe_detail.dart, ganti // TODO: Tambahkan _sliderVal di sini dengan:

int _sliderVal = 1;

Sekarang cari // TODO: Tambahkan Slider() di sini dan ganti dengan kode berikut:

Slider(
// 10
min: 1,
max: 10,
divisions: 9,
// 11
label: '${_sliderVal * widget.recipe.servings} servings',
// 12
value: _sliderVal.toDouble(),
// 13
onChanged: (newValue) {
setState(() {
_sliderVal = newValue.round();
});
},
// 14
activeColor: Colors.green,
inactiveColor: Colors.black,
),

Slider menampilkan sebuah lingkaran yang bisa diseret untuk mengubah sebuah nilai. Berikut cara kerjanya:

  1. Kamu menggunakan min, max, dan divisions untuk menentukan bagaimana slider bergerak. Dalam hal ini, ia bergerak antara nilai 1 dan 10, dengan sepuluh titik berhenti yang terpisah. Artinya, nilainya hanya bisa berupa 1, 2, 3, 4, 5, 6, 7, 8, 9, atau 10.
  2. label akan diperbarui saat _sliderVal berubah dan menampilkan jumlah servings (porsi) yang sudah disesuaikan.
  3. Slider bekerja dengan nilai bertipe double, jadi baris ini mengonversi variabel bertipe int.
  4. Saat slider berubah, kita panggil method round() untuk mengubah nilai double menjadi int lalu menyimpannya di _sliderVal.
  5. Baris ini mengatur warna slider agar sesuai dengan "brand". Nilai activeColor warna untuk bagian diantara nilai minimum dan lingkaran, sementara inactiveColor untuk bagian lainnya.

Hot reload, atur slider dan perhatikan nilai yang ditampilkan oleh indikatornya.

Memperbarui Resep

Meski angka slider sudah mengikuti posisinya, tapi untuk saat ini ia belum mepengaruhi jumlah-jumlah bahan.

Untuk melakukannya, kamu hanya perlu mengubah pernyataan return pada itemBuilder di Expanded untuk ingredients agar menyertakan nilai _sliderVal saat ini sebagai faktor pengali untuk setiap ingredient.

Timpa // TODO Tambahkan ingredient.quantity dengan perintah return di bawahnya dengan kode yang baru:

return Text('${ingredient.quantity * _sliderVal} '
'${ingredient.measure} '
'${ingredient.name}');

Setelah hot reload, kamu akan melihat jumlah porsi resep ini akan berubah setiap mengubah slider.

Itu saja! Sekarang kamu telah membangun sebuah aplikasi Flutter yang keren dan interaktif yang bekerja dengan cara yang sama di berbagai perangkat.

Pada beberapa bagian berikutnya, kamu akan terus mengeksplorasi bagaimana widget dan state bekerja. Kamu juga akan mempelajari fungsi-fungsi penting seperti networking.

Poin-poin Penting

  • Membangun aplikasi baru dengan flutter create.
  • Menggunakan widget untuk menyusun layout sebuah halaman.
  • Menggunakan parameter widget untuk styling.
  • Widget MaterialApp menentukan aplikasi, dan Scaffold menentukan struktur tingkat atas dari sebuah halaman.
  • State memungkinkan widget menjadi interaktif.
  • Saat state berubah, biasanya kamu perlu melakukan hot restart aplikasi, bukan hot reload. Dalam beberapa kasus, kamu juga perlu membangun ulang dan me-restart aplikasi sepenuhnya.

Langkah Selanjutnya

Selamat, kamu telah menulis aplikasi pertamamu!

Untuk mendapatkan gambaran tentang semua opsi widget yang tersedia, dokumentasi (https://api.flutter.dev/) seharusnya menjadi titik awalmu. Secara khusus, Material library (https://api.flutter.dev/flutter/material/material-library.html) dan Widgets catalog (https://docs.flutter.dev/development/ui/widgets) akan mencakup sebagian besar hal yang bisa kamu tampilkan di layar. Halaman-halaman tersebut mencantumkan semua parameter, dan sering kali memiliki bagian interaktif di browser tempat kamu bisa bereksperimen.

Bab 3, Widget Dasar, sepenuhnya membahas penggunaan widget, dan Bab 4, Memahami Cara Kerja Widget, membahas lebih dalam teori di balik widget. Bab-bab selanjutnya akan membahas lebih dalam konsep-konsep lain yang diperkenalkan secara singkat di bab ini.