ViewModel Bersama Lintas Fragmen

1. Sebelum memulai

Anda telah mempelajari cara menggunakan aktivitas, fragmen, intent, data binding, komponen navigasi, dan dasar komponen arsitektur. Dalam codelab ini, Anda akan memadukan semuanya dan menggunakan sampel lanjutan, yaitu aplikasi pemesanan cupcake.

Anda akan mempelajari cara menggunakan ViewModel bersama untuk berbagi data antarfragmen aktivitas yang sama dan konsep baru seperti transformasi LiveData.

Prasyarat

  • Terbiasa membaca dan memahami tata letak Android dalam XML
  • Terbiasa dengan dasar-dasar Komponen Jetpack Navigation
  • Dapat membuat grafik navigasi dengan tujuan fragmen di aplikasi
  • Sebelumnya pernah menggunakan fragmen di dalam aktivitas
  • Dapat membuat ViewModel untuk menyimpan data aplikasi
  • Dapat menggunakan data binding dengan LiveData agar UI selalu terbarui menurut data aplikasi di ViewModel

Yang akan Anda pelajari

  • Cara menerapkan praktik arsitektur aplikasi yang direkomendasikan di dalam kasus penggunaan yang lebih lanjut
  • Cara menggunakan ViewModel bersama lintas fragmen dalam suatu aktivitas
  • Cara menerapkan transformasi LiveData

Yang akan Anda buat

  • Aplikasi Cupcake yang menampilkan alur pemesanan cupcake, yang memungkinkan pengguna memilih rasa, jumlah, dan tanggal pengambilan cupcake.

Yang akan Anda butuhkan

  • Komputer yang dilengkapi Android Studio.
  • Kode awal untuk aplikasi Cupcake.

2. Ringkasan aplikasi awal

Ringkasan aplikasi Cupcake

Aplikasi Cupcake menunjukkan cara mendesain dan menerapkan aplikasi pemesanan online. Di akhir tahap ini, Anda akan menyelesaikan aplikasi Cupcake dengan tampilan berikut. Pengguna dapat memilih jumlah, rasa, dan opsi lainnya untuk pemesanan cupcake.

732881cfc463695d.png

Mendownload kode awal untuk codelab ini

Codelab ini menyediakan kode awal yang dapat Anda lanjutkan dengan membuat fitur yang diajarkan dalam codelab ini. Kode awal berisi kode yang tidak asing bagi Anda dari codelab sebelumnya.

Jika Anda mendownload kode awal dari GitHub, perhatikan bahwa nama folder project ini adalah android-basics-kotlin-cupcake-app-starter. Pilih folder ini saat Anda membuka project di Android Studio.

Untuk mendapatkan kode codelab ini dan membukanya di Android Studio, lakukan hal berikut.

Mendapatkan kode

  1. Klik URL yang diberikan. Tindakan ini akan membuka halaman GitHub project di browser.
  2. Di halaman GitHub project, klik tombol Code yang akan menampilkan dialog.

5b0a76c50478a73f.png

  1. Di dialog, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
  2. Temukan file di komputer Anda (mungkin di folder Downloads).
  3. Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.

Membuka project di Android Studio

  1. Mulai Android Studio.
  2. Di jendela Welcome to Android Studio, klik Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > New > Import Project.

21f3eec988dcfbe9.png

  1. Di dialog Import Project, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
  2. Klik dua kali pada folder project tersebut.
  3. Tunggu Android Studio membuka project.
  4. Klik tombol Run 11c34fc5e516fb1c.png untuk membangun dan menjalankan aplikasi. Pastikan aplikasi dibangun seperti yang diharapkan.
  5. Cari file project di jendela alat Project untuk melihat cara aplikasi disiapkan.

Panduan kode awal

  1. Buka project yang telah didownload di Android Studio. Nama folder project adalah android-basics-kotlin-cupcake-app-starter. Kemudian jalankan aplikasi.
  2. Eksplorasi file untuk memahami kode awal. Untuk file tata letak, Anda dapat menggunakan opsi Split di sudut kanan atas untuk melihat pratinjau tata letak dan XML secara bersamaan.
  3. Saat mengompilasi dan menjalankan aplikasi, Anda akan melihat aplikasi tersebut tidak lengkap. Tombolnya tidak memiliki banyak fungsi (kecuali untuk menampilkan pesan Toast) dan Anda tidak dapat membuka fragmen lain.

Berikut ini adalah panduan file penting dalam project.

MainActivity:

MainActivity memiliki kode yang mirip dengan kode default yang dihasilkan, yang menetapkan tampilan konten aktivitas sebagai activity_main.xml. Kode ini menggunakan konstruktor AppCompatActivity(@LayoutRes int contentLayoutId) berparameter, yang menerima tata letak yang akan di-inflate sebagai bagian dari super.onCreate(savedInstanceState).

Kode di class MainActivity

class MainActivity : AppCompatActivity(R.layout.activity_main)

sama dengan kode berikut yang menggunakan konstruktor AppCompatActivity default:

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

Tata letak (folder res/layout):

Folder resource layout berisi file aktivitas dan file tata letak fragmen. Ini adalah file tata letak sederhana, dan XML-nya mirip dengan codelab sebelumnya.

  • fragment_start.xml adalah layar pertama yang ditampilkan di aplikasi. Layar ini memiliki gambar cupcake dan tiga tombol untuk memilih jumlah cupcake untuk dipesan: satu cupcake, enam cupcake, dan dua belas cupcake.
  • fragment_flavor.xml menampilkan daftar rasa cupcake sebagai opsi tombol pilihan dengan tombol Next.
  • fragment_pickup.xml memberikan opsi untuk memilih hari pengambilan dan tombol Next untuk membuka layar ringkasan.
  • fragment_summary.xml menampilkan ringkasan detail pesanan seperti jumlah, rasa, dan tombol untuk mengirim pesanan ke aplikasi lain.

Class fragmen:

  • StartFragment.kt adalah layar pertama yang ditampilkan di aplikasi. Class ini berisi kode view binding dan pemroses klik untuk ketiga tombol.
  • Class FlavorFragment.kt, PickupFragment.kt, dan SummaryFragment.kt sebagian besar berisi kode boilerplate dan pemroses klik untuk tombol Next atau Send Order to Another App, yang menampilkan pesan toast.

Resource (folder res):

  • Folder drawable berisi aset cupcake untuk layar pertama, serta file ikon peluncur.
  • navigation/nav_graph.xml berisi empat tujuan fragmen (startFragment, flavorFragment, pickupFragment, dan summaryFragment) tanpa Actions, yang akan Anda tentukan nanti di codelab ini.
  • Folder values berisi warna, dimensi, string, gaya, dan tema yang digunakan untuk menyesuaikan tema aplikasi. Anda seharusnya sudah memahami jenis resource ini dari codelab sebelumnya.

3. Melengkapi Grafik Navigasi

Dalam tugas ini, Anda akan menghubungkan layar aplikasi Cupcake dan menyelesaikan penerapan navigasi yang tepat di dalam aplikasi.

Apakah Anda ingat apa yang kita perlukan untuk menggunakan komponen Navigasi? Ikuti panduan ini guna mengingat kembali tentang cara menyiapkan project dan aplikasi Anda untuk:

  • Menyertakan library Jetpack Navigation
  • Menambahkan NavHost ke aktivitas
  • Membuat grafik navigasi
  • Menambahkan tujuan fragmen ke grafik navigasi

Menghubungkan tujuan di grafik navigasi

  1. Di Android Studio, di jendela Project, buka file res > navigation > nav_graph.xml. Beralihlah ke tab Design, jika belum dipilih.

28c2c94eb97e2f0.png

  1. Tindakan ini akan membuka Navigation Editor untuk memvisualisasikan grafik navigasi di aplikasi. Anda akan melihat keempat fragmen yang sudah ada di aplikasi.

fdce89b318218ea6.png

  1. Hubungkan tujuan fragmen di grafik navigasi. Buat tindakan dari startFragment ke flavorFragment, koneksi dari flavorFragment ke pickupFragment, dan koneksi dari pickupFragment ke summaryFragment. Ikuti langkah berikutnya jika Anda memerlukan petunjuk lebih detail.
  2. Arahkan kursor ke startFragment sampai Anda melihat batas abu-abu di sekitar fragmen dan lingkaran abu-abu muncul di tengah tepi kanan fragmen. Klik lingkaran tersebut dan tarik ke flavorFragment, lalu lepaskan mouse.

d014c1b710c1088d.png

  1. Tanda panah di antara kedua fragmen tersebut menunjukkan koneksi yang berhasil, yang berarti Anda akan dapat berpindah dari startFragment ke flavorFragment. Tindakan ini disebut tindakan Navigasi, yang telah Anda pelajari di codelab sebelumnya.

65c7d993b98c9dea.png

  1. Demikian pula, tambahkan tindakan navigasi dari flavorFragment ke pickupFragment dan dari pickupFragment ke summaryFragment. Setelah Anda selesai membuat tindakan navigasi, grafik navigasi lengkapnya harus tampak seperti berikut.

724eb8992a1a9381.png

  1. Ketiga tindakan baru yang Anda buat ini juga akan muncul di panel Component Tree.

e4ee54469f5ff1a4.png

  1. Saat menentukan grafik navigasi, Anda juga ingin menentukan tujuan awal. Saat ini Anda dapat melihat bahwa startFragment memiliki ikon rumah kecil di sampingnya.

739d4ddac561c478.png

Itu menunjukkan bahwa startFragment akan menjadi fragmen pertama untuk ditampilkan di NavHost. Tetapkan ini sebagai perilaku yang diinginkan aplikasi. Untuk referensi mendatang, Anda dapat mengubah tujuan awal kapan saja dengan mengklik kanan pada fragmen dan memilih opsi menu Set as Start Destination.

bf3cfa7841476892.png

Selanjutnya, Anda akan menambahkan kode untuk berpindah dari startFragment ke flavorFragment dengan mengetuk tombol di fragmen pertama, bukan menampilkan pesan Toast. Di bawah ini adalah referensi tata letak fragmen awal. Anda akan meneruskan jumlah cupcake ke fragmen rasa di tugas berikutnya.

867d8e4c72078f76.png

  1. Di jendela Project, buka file Kotlin di app > java > com.example.cupcake > StartFragment.
  2. Di metode onViewCreated(), perhatikan bahwa pemroses klik ditetapkan pada ketiga tombol. Saat setiap tombol diketuk, metode orderCupcake() dipanggil dengan jumlah cupcake (1, 6, atau 12 cupcake) sebagai parameternya.

Kode referensi:

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. Di metode orderCupcake(), ganti kode yang menampilkan pesan toast dengan kode untuk membuka fragmen rasa. Dapatkan NavController menggunakan metode findNavController() dan panggil navigate() pada metode tersebut, yang meneruskan ID tindakan, R.id.action_startFragment_to_flavorFragment. Pastikan ID tindakan ini cocok dengan tindakan yang dideklarasikan di nav_graph.xml. Anda

Ganti

fun orderCupcake(quantity: Int) {
    Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}

dengan

fun orderCupcake(quantity: Int) {
   findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Tambahkan Impor import androidx.navigation.fragment.findNavController atau Anda dapat memilih dari opsi yang disediakan oleh Android Studio.

2a087f53a77765a6.png

Menambahkan Navigasi ke fragmen rasa dan pengambilan

Mirip dengan tugas sebelumnya, dalam tugas ini Anda akan menambahkan navigasi ke fragmen lain: fragmen rasa dan pengambilan.

3b351067bf4926b7.png

  1. Buka app > java > com.example.cupcake > FlavorFragment.kt. Perhatikan bahwa metode yang dipanggil di dalam pemroses klik tombol Next adalah metode goToNextScreen().
  2. Di FlavorFragment.kt, di dalam metode goToNextScreen(), ganti kode yang menampilkan toast untuk membuka fragmen pengambilan. Gunakan ID tindakan, R.id.action_flavorFragment_to_pickupFragment, dan pastikan ID ini cocok dengan tindakan yang dideklarasikan di nav_graph.xml. Anda
fun goToNextScreen() {
    findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}

Jangan lupa lakukan import androidx.navigation.fragment.findNavController.

  1. Demikian pula di PickupFragment.kt, di dalam metode goToNextScreen(), ganti kode yang ada untuk membuka fragmen ringkasan.
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

Impor androidx.navigation.fragment.findNavController.

  1. Jalankan aplikasi. Pastikan tombolnya berfungsi untuk berpindah dari layar ke layar. Informasi yang ditampilkan pada setiap fragmen mungkin tidak lengkap, tetapi jangan khawatir, Anda akan mengisi fragmen tersebut dengan data yang benar di langkah berikutnya.

96b33bf7a5bd8050.png

Memperbarui judul di panel aplikasi

Saat Anda menjelajahi aplikasi, perhatikan judul dalam panel aplikasi. Judul ini selalu ditampilkan sebagai Cupcake.

Untuk memperbaiki pengalaman pengguna, beri judul sesuai dengan fungsi fragmen yang sedang aktif.

Ubah judul di panel aplikasi (juga dikenal sebagai panel tindakan) untuk setiap fragmen menggunakan NavController dan tampilkan tombol Up (←).

b7657cdc50cfeab0.png

  1. Di MainActivity.kt, ganti metode onCreate() untuk menyiapkan pengontrol navigasi. Dapatkan instance NavController dari NavHostFragment.
  2. Buat panggilan ke setupActionBarWithNavController(navController) yang meneruskan instance NavController. Tindakan ini akan melakukan hal berikut: Menampilkan judul di panel aplikasi berdasarkan label tujuan, dan menampilkan tombol Up kapan pun Anda tidak berada di tujuan teratas.
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. Tambahkan impor yang diperlukan saat diminta oleh Android Studio.
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. Tentukan judul panel aplikasi untuk setiap fragmen. Buka navigation/nav_graph.xml dan beralih ke tab Code.
  2. Di nav_graph.xml, ubah atribut android:label untuk setiap tujuan fragmen. Gunakan resource string berikut yang telah dideklarasikan di aplikasi awal.

Untuk fragmen awal, gunakan @string/app_name dengan nilai Cupcake.

Untuk fragmen rasa, gunakan @string/choose_flavor dengan nilai Choose Flavor.

Untuk fragmen pengambilan, gunakan @string/choose_pickup_date dengan nilai Choose Pickup Date.

Untuk fragmen ringkasan, gunakan @string/order_summary dengan nilai Order Summary.

<navigation ...>
    <fragment
        android:id="@+id/startFragment"
        ...
        android:label="@string/app_name" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/flavorFragment"
        ...
        android:label="@string/choose_flavor" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment"
        ...
        android:label="@string/choose_pickup_date" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment"
        ...
        android:label="@string/order_summary" ... />
</navigation>
  1. Jalankan aplikasi. Perhatikan bahwa judul di panel aplikasi berubah saat Anda berpindah ke setiap tujuan fragmen. Perhatikan juga bahwa tombol Up (panah ←) kini muncul di panel aplikasi. Jika Anda mengetuknya, tombol tersebut tidak berfungsi. Anda akan menerapkan perilaku tombol Up di codelab berikutnya.

89e0ea37d4146271.png

4. Membuat ViewModel bersama

Mari kita lanjutkan dengan mengisi data yang benar di setiap fragmen. Anda akan menggunakan ViewModel bersama untuk menyimpan data aplikasi dalam satu ViewModel. Beberapa fragmen di aplikasi akan mengakses ViewModel bersama menggunakan cakupan aktivitasnya.

Ini adalah kasus penggunaan yang umum untuk berbagi data antarfragmen di kebanyakan aplikasi produksi. Misalnya dalam versi akhir (dari codelab ini) aplikasi Cupcake (lihat screenshot di bawah ini), pengguna memilih jumlah cupcake di layar pertama, dan di layar kedua harga dihitung dan ditampilkan berdasarkan jumlah cupcake tersebut. Demikian pula dengan data aplikasi lain seperti rasa dan tanggal pengambilan juga digunakan di layar ringkasan.

3b6a68cab0b9ee2.png

Dengan melihat fitur aplikasi ini, Anda dapat menyimpulkan bahwa lebih baik menyimpan informasi pesanan ini dalam satu ViewModel, yang dapat dibagikan lintas fragmen di aktivitas ini. Ingat kembali bahwa ViewModel adalah bagian dari Komponen Arsitektur Android. Data aplikasi yang disimpan di dalam ViewModel akan dipertahankan selama perubahan konfigurasi. Untuk menambahkan ViewModel ke aplikasi, Anda perlu membuat class baru yang merupakan lanjutan dari class ViewModel.

Membuat OrderViewModel

Dalam tugas ini, Anda akan membuat ViewModel bersama untuk aplikasi Cupcake bernama OrderViewModel. Anda juga akan menambahkan data aplikasi sebagai properti di dalam ViewModel serta metode untuk memperbarui dan mengubah data. Berikut adalah properti class-nya:

  • Jumlah pesanan (Integer)
  • Rasa cupcake (String)
  • Tanggal pengambilan (String)
  • Harga (Double)

Mengikuti praktik terbaik ViewModel

Di ViewModel, praktik yang direkomendasikan adalah tidak menampilkan data model tampilan sebagai variabel public. Jika tidak, data aplikasi bisa diubah dengan cara yang tidak diharapkan oleh class eksternal dan membuat kasus ekstrem yang tidak dapat ditangani aplikasi Anda. Sebagai gantinya, jadikan properti yang dapat berubah ini private, terapkan properti pendukung, dan tampilkan versi public yang tidak dapat berubah untuk setiap properti, jika diperlukan. Menurut konvensi, tambahkan garis bawah (_) sebagai awalan pada nama properti private yang dapat berubah.

Berikut adalah metode untuk memperbarui properti di atas, tergantung pilihan pengguna:

  • setQuantity(numberCupcakes: Int)
  • setFlavor(desiredFlavor: String)
  • setDate(pickupDate: String)

Anda tidak memerlukan metode penyetel untuk harga karena Anda akan menghitungnya di dalam OrderViewModel menggunakan properti lain. Langkah-langkah di bawah menunjukkan cara menerapkan ViewModel bersama.

Anda akan membuat paket baru dalam project Anda yang bernama model dan menambahkan class OrderViewModel. Tindakan ini akan memisahkan kode model tampilan dari kode UI lainnya (fragmen dan aktivitas). Ini adalah praktik terbaik coding untuk memisahkan kode menjadi paket sesuai dengan fungsinya.

  1. Di jendela Project Android Studio, klik kanan pada com.example.cupcake > New > Package.
  2. Dialog New Package akan terbuka, beri paket nama sebagai com.example.cupcake.model.

d958ee5f3d2aef5a.png

  1. Buat class Kotlin OrderViewModel di bagian paket model. Di jendela Project, klik kanan pada paket model dan pilih New > Kotlin File/Class. Di dialog baru, berikan nama file OrderViewModel.

fc68c1d3861f1cca.png

  1. Di OrderViewModel.kt, ubah tanda tangan class agar melanjutkan dari ViewModel.
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. Di dalam class OrderViewModel, tambahkan properti yang telah dibahas di atas sebagai private val.
  2. Ubah jenis properti menjadi LiveData dan tambahkan kolom pendukung agar properti tersebut dapat diamati dan UI dapat diperbarui saat data sumber di model tampilan berubah.
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price

Anda harus mengimpor class ini:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
  1. Di class OrderViewModel, tambahkan metode yang telah dibahas di atas. Di dalam metode, tetapkan argumen yang diteruskan ke properti yang dapat berubah.
  2. Karena metode penyetel ini harus dipanggil dari luar model tampilan, biarkan metode tersebut sebagai metode public (artinya tidak ada private atau pengubah visibilitas lain yang diperlukan sebelum kata kunci fun). Pengubah visibilitas default di Kotlin adalah public.
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
}

fun setFlavor(desiredFlavor: String) {
    _flavor.value = desiredFlavor
}

fun setDate(pickupDate: String) {
    _date.value = pickupDate
}
  1. Build dan jalankan aplikasi Anda untuk memastikan tidak ada error kompilasi. Seharusnya belum ada perubahan yang terlihat di UI Anda.

Bagus! Sekarang Anda telah memulai model tampilan. Anda secara bertahap akan menambahkan lebih banyak hal ke class ini saat membuat lebih banyak fitur di aplikasi Anda, dan saat Anda tahu bahwa Anda memerlukan lebih banyak properti dan metode di class.

Jika Anda melihat nama class, nama properti, atau nama metode dalam font abu-abu di Android Studio, hal itu wajar. Itu artinya, class, properti, atau metode sedang tidak digunakan saat ini, tetapi akan digunakan nanti! Itu adalah topik selanjutnya.

5. Menggunakan ViewModel untuk mengupdate UI

Dalam tugas ini, Anda akan menggunakan model tampilan bersama yang Anda buat untuk mengupdate UI aplikasi. Perbedaan utama dalam penerapan model tampilan bersama adalah cara kita mengaksesnya dari pengontrol UI. Anda akan menggunakan instance aktivitas, bukan instance fragmen, dan Anda akan melihat cara melakukannya di bagian berikutnya.

Itu artinya model tampilan dapat dibagikan lintas fragmen. Setiap fragmen dapat mengakses model tampilan untuk memeriksa beberapa detail pesanan atau memperbarui beberapa data di model tampilan.

Memperbarui StartFragment untuk menggunakan model tampilan

Untuk menggunakan model tampilan bersama di StartFragment Anda akan melakukan inisialisasi OrderViewModel menggunakan activityViewModels(), bukan class delegasi viewModels().

  • viewModels() memberi Anda instance ViewModel yang dicakupkan ke fragmen saat ini. Tindakan ini akan berbeda untuk fragmen lain.
  • activityViewModels() memberi Anda instance ViewModel yang dicakupkan ke aktivitas saat ini. Oleh karena itu, instance ini akan tetap sama lintas fragmen di aktivitas yang sama.

Menggunakan delegasi properti Kotlin

Di Kotlin, setiap properti (var) yang dapat berubah memiliki pengambil default dan fungsi penyetel yang otomatis dibuat. Fungsi penyetel dan pengambil dipanggil saat Anda menetapkan nilai atau membaca nilai properti. (Untuk properti hanya baca (val), hanya fungsi pengambil yang dibuat secara default. Fungsi pengambil dipanggil saat Anda membaca nilai properti hanya baca.)

Delegasi properti di Kotlin membantu Anda menyerahkan tanggung jawab pengambil-penyetel ke class yang berbeda.

Class ini (disebut class delegasi) menyediakan fungsi pengambil dan penyetel dari properti dan menangani perubahannya.

Properti delegasi ditentukan menggunakan klausa by dan instance class delegasi:

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
  1. Di class StartFragment, dapatkan referensi ke model tampilan bersama sebagai variabel class. Gunakan delegasi properti Kotlin by activityViewModels() dari library fragment-ktx.
private val sharedViewModel: OrderViewModel by activityViewModels()

Anda mungkin memerlukan impor baru ini:

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. Ulangi langkah di atas untuk class FlavorFragment, PickupFragment, SummaryFragment, Anda akan menggunakan instance sharedViewModel ini di bagian codelab berikutnya.
  2. Dengan kembali ke class StartFragment, Anda sekarang dapat menggunakan model tampilan. Di awal metode orderCupcake(), panggil metode setQuantity() di model tampilan bersama untuk memperbarui jumlah, sebelum membuka fragmen rasa.
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Di dalam class OrderViewModel, tambahkan metode berikut untuk memeriksa apakah rasa untuk pesanan telah ditetapkan atau belum. Anda akan menggunakan metode ini di class StartFragment pada langkah berikutnya.
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. Di class StartFragment, di dalam metode orderCupcake(), setelah menetapkan jumlah, setel rasa default sebagai Vanilla jika tidak ada rasa yang disetel, sebelum membuka fragmen rasa. Metode lengkap Anda akan terlihat seperti ini:
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    if (sharedViewModel.hasNoFlavorSet()) {
        sharedViewModel.setFlavor(getString(R.string.vanilla))
    }
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Build aplikasi untuk memastikan tidak ada error kompilasi. Namun, seharusnya tidak ada perubahan yang terlihat di UI Anda.

6. Menggunakan ViewModel dengan data binding

Selanjutnya Anda akan menggunakan data binding untuk mengikat data tampilan model ke UI. Anda juga akan memperbarui model tampilan bersama berdasarkan pilihan yang dibuat pengguna di UI.

Pemuat ulang di Data binding

Ingat kembali bahwa Library Data Binding adalah bagian dari Android Jetpack. Data binding mengikat komponen UI di tata letak ke sumber data dalam aplikasi Anda menggunakan format deklaratif. Dalam istilah yang lebih sederhana, data binding adalah mengikat data (dari kode) ke tampilan, dan view binding adalah mengikat tampilan ke kode. Dengan menyiapkan pengikatan ini dan membuat pembaruan bersifat otomatis, hal ini membantu Anda mengurangi peluang error jika Anda lupa untuk secara manual mengupdate UI dari kode Anda.

Memperbarui rasa dengan pilihan pengguna

  1. Di layout/fragment_flavor.xml, tambahkan tag <data> di dalam tag root <layout>. Tambahkan variabel tata letak bernama viewModel dari jenis com.example.cupcake.model.OrderViewModel. Pastikan nama paket di atribut jenis cocok dengan nama paket dari class model tampilan bersama, OrderViewModel di aplikasi Anda.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Demikian pula, ulangi langkah di atas untuk fragment_pickup.xml, dan fragment_summary.xml untuk menambahkan variabel tata letak viewModel. Anda akan menggunakan variabel ini di bagian berikutnya. Anda tidak perlu menambahkan kode ini di fragment_start.xml, karena tata letak ini tidak menggunakan model tampilan bersama.
  2. Di class FlavorFragment, di dalam onViewCreated(), ikat instance model tampilan dengan instance model tampilan bersama di tata letak tersebut. Tambahkan kode berikut di dalam blok binding?.apply.
binding?.apply {
    viewModel = sharedViewModel
    ...
}

Menerapkan fungsi cakupan

Ini mungkin pertama kalinya Anda melihat fungsi apply di Kotlin. apply adalah fungsi cakupan di library standar Kotlin. Tindakan ini menjalankan blok kode di dalam konteks objek. Hal ini membentuk cakupan sementara, dan di cakupan tersebut, Anda dapat mengakses objek tanpa namanya. Kasus penggunaan umum untuk apply adalah mengonfigurasi objek. Panggilan seperti itu dapat dibaca sebagai "terapkan ketentuan berikut pada objek".

Contoh:

clark.apply {
    firstName = "Clark"
    lastName = "James"
    age = 18
}

// The equivalent code without apply scope function would look like the following.

clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
  1. Ulangi langkah yang sama untuk metode onViewCreated() di dalam class PickupFragment dan SummaryFragment.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Di fragment_flavor.xml, gunakan variabel tata letak baru, viewModel untuk menyetel atribut checked tombol pilihan berdasarkan nilai flavor di model tampilan. Jika rasa yang diwakili oleh tombol pilihan sama dengan rasa yang disimpan di model tampilan, maka tampilkan tombol pilihan seperti yang dipilih (checked = true). Ekspresi binding untuk status RadioButton Vanilla yang dicentang akan terlihat seperti berikut:

@{viewModel.flavor.equals(@string/vanilla)}

Pada dasarnya, Anda membandingkan properti viewModel.flavor dengan resource string yang sesuai menggunakan fungsi equals, untuk menentukan apakah status yang dicentang harus benar atau salah.

<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:checked="@{viewModel.flavor.equals(@string/coffee)}"
       .../>
</RadioGroup>

Binding pemroses

Binding pemroses adalah ekspresi lambda yang dijalankan saat suatu peristiwa terjadi, seperti peristiwa onClick. Binding ini mirip dengan referensi metode seperti textview.setOnClickListener(clickListener) tetapi binding pemroses memungkinkan Anda menjalankan ekspresi data binding sembarang.

  1. Di fragment_flavor.xml, tambahkan pemroses peristiwa ke tombol pilihan menggunakan binding pemroses. Gunakan ekspresi lambda tanpa parameter dan lakukan panggilan ke metode viewModel.setFlavor() dengan meneruskan resource string rasa yang sesuai.
<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
       .../>
</RadioGroup>
  1. Jalankan aplikasi dan perhatikan bagaimana opsi Vanilla dipilih secara default di fragmen rasa.

3095e824b4817b98.png

Bagus! Sekarang Anda dapat melanjutkan ke fragmen berikutnya.

7. Mengupdate fragmen pengambilan dan ringkasan untuk menggunakan model tampilan

Jelajahi aplikasi dan perhatikan bahwa di fragmen pengambilan, label opsi tombol pilihan kosong. Dalam tugas ini, Anda akan menghitung 4 tanggal pengambilan yang tersedia dan menampilkannya di fragmen pengambilan. Ada berbagai cara untuk menampilkan tanggal berformat, dan berikut beberapa alat berguna yang disediakan Android untuk melakukannya.

Membuat daftar opsi pengambilan

Pemformat tanggal

Framework Android menyediakan class bernama SimpleDateFormat, yang merupakan class untuk pemformatan dan penguraian tanggal dengan cara yang sensitif terhadap lokalitas. Hal ini memungkinkan pemformatan (tanggal → teks) dan penguraian (teks → tanggal) pada tanggal.

Anda dapat membuat instance SimpleDateFormat dengan meneruskan string pola dan lokalitas:

SimpleDateFormat("E MMM d", Locale.getDefault())

String pola seperti "E MMM d" adalah perwakilan format Tanggal dan Waktu. Huruf dari 'A' ke 'Z' dan dari 'a' ke 'z' diinterpretasikan sebagai huruf pola yang mewakili komponen tanggal atau string waktu. Misalnya, d mewakili tanggal dalam sebulan, y untuk tahun, dan M untuk bulan. Jika tanggalnya adalah 4 Juli 2018, string pola "EEE, MMM d" diuraikan menjadi "Wed, Jul 4". Untuk daftar lengkap huruf pola, lihat dokumentasi ini.

Objek Locale mewakili wilayah geografis, politis, atau kultural tertentu. Hal ini mewakili kombinasi bahasa/negara/varian. Lokalitas digunakan untuk mengubah tampilan informasi seperti angka atau tanggal agar sesuai dengan konvensi di wilayah tersebut. Tanggal dan waktu bersifat sensitif terhadap lokalitas, karena keduanya ditulis dengan cara yang berbeda di berbagai belahan dunia. Anda akan menggunakan metode Locale.getDefault() untuk mengambil kumpulan informasi lokal pada perangkat pengguna dan meneruskannya ke dalam konstruktor SimpleDateFormat.

Lokalitas di Android adalah kombinasi bahasa dan kode negara. Kode bahasa adalah kode bahasa ISO dengan dua huruf kecil, seperti "en" untuk bahasa Inggris. Kode negara adalah kode negara ISO dengan dua huruf besar, seperti "US" untuk Amerika Serikat.

Sekarang gunakan SimpleDateFormat dan Locale untuk menentukan tanggal pengambilan yang tersedia untuk aplikasi Cupcake.

  1. Di class OrderViewModel, tambahkan fungsi berikut bernama getPickupOptions() untuk membuat dan menampilkan daftar tanggal pengambilan. Di dalam metode ini, buat variabel val bernama options dan lakukan inisialisasi variabel tersebut ke mutableListOf<String>().
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. Buat string pemformat menggunakan SimpleDateFormat yang meneruskan string pola "E MMM d", dan lokalitasnya. Di dalam string pola, E adalah singkatan dari nama hari dalam seminggu dan diuraikan menjadi "Sel Des 10".
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

Impor java.text.SimpleDateFormat dan java.util.Locale, saat diminta oleh Android Studio.

  1. Dapatkan instance Calendar dan tetapkan ke variabel baru. Jadikan instance ini sebagai val. Variabel ini akan berisi tanggal dan waktu saat ini. Selain itu, impor java.util.Calendar.
val calendar = Calendar.getInstance()
  1. Buat daftar tanggal yang dimulai dengan tanggal saat ini dan tiga tanggal berikutnya. Karena Anda memerlukan 4 opsi tanggal, ulangi blok kode ini 4 kali. Blok repeat ini akan memformat tanggal, menambahkannya ke daftar opsi tanggal, lalu secara inkremental menambahkan 1 hari.
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  1. Tampilkan options yang telah diperbarui di akhir metode ini. Berikut adalah metode lengkap Anda:
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
   val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
   val calendar = Calendar.getInstance()
   // Create a list of dates starting with the current date and the following 3 dates
   repeat(4) {
       options.add(formatter.format(calendar.time))
       calendar.add(Calendar.DATE, 1)
   }
   return options
}
  1. Di class OrderViewModel, tambahkan properti class bernama dateOptions, yaitu val. Lakukan inisialisasi menggunakan metode getPickupOptions() yang baru saja Anda buat.
val dateOptions = getPickupOptions()

Memperbarui tata letak untuk menampilkan opsi pengambilan

Setelah Anda memiliki keempat tanggal pengambilan yang tersedia dalam model tampilan, perbarui tata letak fragment_pickup.xml untuk menampilkan tanggal tersebut. Anda juga akan menggunakan data binding untuk menampilkan status yang dicentang dari setiap tombol pilihan dan memperbarui tanggal di model tampilan saat tombol pilihan lain dipilih. Penerapan ini mirip dengan data binding di fragmen rasa.

Di fragment_pickup.xml:

Tombol pilihan option0 mewakili dateOptions[0] di viewModel (hari ini)

Tombol pilihan option1 mewakili dateOptions[1] di viewModel (besok)

Tombol pilihan option2 mewakili dateOptions[2] di viewModel (lusa)

Tombol pilihan option3 mewakili dateOptions[3] di viewModel (dua hari setelah besok)

  1. Di fragment_pickup.xml, untuk tombol pilihan option0, gunakan variabel tata letak baru, viewModel untuk menetapkan atribut checked berdasarkan nilai date di model tampilan. Bandingkan properti viewModel.date dengan string pertama dalam daftar dateOptions, yang merupakan tanggal saat ini. Gunakan fungsi equals untuk membandingkan dan ekspresi binding akhir terlihat seperti berikut:

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. Untuk tombol pilihan yang sama, tambahkan pemroses peristiwa menggunakan binding pemroses ke atribut onClick. Saat opsi tombol pilihan ini diklik, lakukan panggilan ke setDate() di viewModel, yang meneruskan dateOptions[0].
  2. Untuk tombol pilihan yang sama, setel nilai atribut text ke string pertama dalam daftar dateOptions.
<RadioButton
   android:id="@+id/option0"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
   android:text="@{viewModel.dateOptions[0]}"
   ...
   />
  1. Ulangi langkah-langkah di atas untuk tombol pilihan lainnya, ubah indeks dateOptions yang sesuai.
<RadioButton
   android:id="@+id/option1"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
   android:text="@{viewModel.dateOptions[1]}"
   ... />

<RadioButton
   android:id="@+id/option2"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
   android:text="@{viewModel.dateOptions[2]}"
   ... />

<RadioButton
   android:id="@+id/option3"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
   android:text="@{viewModel.dateOptions[3]}"
   ... />
  1. Jalankan aplikasi dan Anda akan melihat beberapa hari mendatang sebagai opsi pengambilan yang tersedia. Screenshot Anda akan berbeda, tergantung pada waktu Anda mencoba tugas ini. Perhatikan bahwa tidak ada opsi yang dipilih secara default. Anda akan menerapkan hal ini di langkah berikutnya.

b55b3a36e2aa7be6.png

  1. Di dalam class OrderViewModel, buat fungsi bernama resetOrder(), untuk mereset properti MutableLiveData di model tampilan. Tetapkan nilai tanggal saat ini dari daftar dateOptions ke _date.value.
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. Tambahkan blok init ke class, dan panggil metode resetOrder() baru dari blok tersebut.
init {
   resetOrder()
}
  1. Hapus nilai awal dari deklarasi properti di class. Sekarang Anda menggunakan blok init untuk melakukan inisialisasi properti saat instance OrderViewModel dibuat.
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
  1. Jalankan aplikasi Anda lagi, perhatikan bahwa tanggal hari ini dipilih secara default.

bfe4f1b82977b4bc.png

Memperbarui fragmen Ringkasan untuk menggunakan model tampilan

Sekarang mari kita beralih ke fragmen terakhir. Fragmen ringkasan pesanan dimaksudkan untuk menampilkan ringkasan detail pesanan. Dalam tugas ini, Anda memanfaatkan semua informasi pesanan dari model tampilan bersama dan memperbarui detail pesanan di layar menggunakan data binding.

78f510e10d848dd2.png

  1. Di fragment_summary.xml, pastikan variabel data model tampilan viewModel sudah dideklarasikan.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Di SummaryFragment, di onViewCreated(), pastikan binding.viewModel sudah diinisialisasi.
  2. Di fragment_summary.xml, baca dari model tampilan untuk memperbarui layar dengan detail ringkasan pesanan. Perbarui TextViews jumlah, rasa, dan tanggal dengan menambahkan atribut teks berikut. Jumlah dari jenis Int, sehingga Anda perlu mengonversinya menjadi string.
<TextView
   android:id="@+id/quantity"
   ...
   android:text="@{viewModel.quantity.toString()}"
   ... />
<TextView
   android:id="@+id/flavor"
   ...
   android:text="@{viewModel.flavor}"
   ... />
<TextView
   android:id="@+id/date"
   ...
   android:text="@{viewModel.date}"
   ... />
  1. Jalankan dan uji aplikasi untuk memastikan bahwa opsi pesanan yang Anda pilih muncul di ringkasan pesanan.

7091453fa817b55.png

8. Menghitung harga dari detail pesanan

Dengan melihat screenshot aplikasi akhir di codelab ini, Anda akan melihat bahwa harganya benar-benar ditampilkan pada setiap fragmen (kecuali StartFragment) sehingga pengguna tahu harganya sepanjang proses pemesanan.

3b6a68cab0b9ee2.png

Berikut adalah aturan dari toko cupcake kita tentang cara menghitung harga.

  • Setiap cupcake berharga $2,00
  • Pengambilan di hari yang sama menambahkan $3,00 ke pesanan

Oleh karena itu, untuk pesanan 6 cupcake, harganya adalah 6 cupcake x $2 = $12. Jika pengguna menginginkan pengambilan di hari yang sama, biaya tambahan sebesar $3 akan membuat total harga pesanan menjadi $15.

Memperbarui harga di model tampilan

Untuk menambahkan dukungan untuk fungsi ini di aplikasi Anda, selesaikan terlebih dahulu harga per cupcake dan abaikan biaya pengambilan di hari yang sama.

  1. Buka OrderViewModel.kt, dan simpan harga per cupcake di satu variabel. Deklarasikan sebagai konstanta pribadi tingkat teratas di bagian atas file, di luar definisi class (tetapi setelah pernyataan impor). Gunakan pengubah const dan untuk menjadikannya hanya baca gunakan val.
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

Ingat kembali bahwa nilai konstanta tersebut (yang ditandai dengan kata kunci const di Kotlin) tidak berubah dan nilainya diketahui pada waktu kompilasi. Untuk mempelajari konstanta selengkapnya, baca dokumentasi ini.

  1. Setelah Anda menentukan harga per cupcake, buat metode helper untuk menghitung harganya. Metode ini dapat berupa private karena hanya digunakan di dalam class ini. Anda akan mengubah logika harga untuk menyertakan biaya pengambilan di hari yang sama pada tugas berikutnya.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

Baris kode ini mengalikan harga per cupcake dengan jumlah cupcake yang dipesan. Untuk kode dalam tanda kurung, karena nilai quantity.value bisa null, gunakan operator elvis (?:). Operator elvis (?:) berarti bahwa jika ekspresi di sebelah kiri bukan null, maka gunakan operator tersebut. Jika ekspresi di sebelah kiri adalah null, gunakan ekspresi di sebelah kanan operator elvis (yang dalam kasus ini adalah 0).

  1. Pada class OrderViewModel yang sama, perbarui variabel harga saat jumlahnya ditetapkan. Lakukan panggilan ke fungsi baru di fungsi setQuantity().
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

Mengikat properti harga ke UI

  1. Di tata letak untuk fragment_flavor.xml, fragment_pickup.xml, dan fragment_summary.xml, pastikan variabel data viewModel dari jenis com.example.cupcake.model.OrderViewModel ditentukan.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Di metode onViewCreated() setiap class fragmen, pastikan Anda mengikat instance objek model tampilan di fragmen tersebut ke variabel data model tampilan di tata letak.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Di dalam setiap tata letak fragmen, gunakan variabel viewModel untuk menetapkan harga jika ditampilkan di tata letak. Mulai dengan memodifikasi file fragment_flavor.xml. Untuk tampilan teks subtotal, setel nilai atribut android:text menjadi "@{@string/subtotal_price(viewModel.price)}". Ekspresi tata letak data binding ini menggunakan resource string @string/subtotal_price dan diteruskan di parameter, yaitu harga dari model tampilan, sehingga output-nya akan menampilkan Subtotal 12.0, misalnya.
...

<TextView
    android:id="@+id/subtotal"
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

Anda menggunakan resource string ini yang telah dideklarasikan di file strings.xml:

<string name="subtotal_price">Subtotal %s</string>
  1. Jalankan aplikasi. Jika Anda memilih Satu cupcake di fragmen awal, fragmen rasa akan menampilkan Subtotal 2.0. Jika Anda memilih Enam cupcake, fragmen rasa akan menampilkan Subtotal 12.0, dan seterusnya. Anda akan memformat harga ke dalam format mata uang yang sesuai nanti, sehingga perilaku ini wajar untuk saat ini.

  1. Sekarang buat perubahan yang serupa untuk fragmen pengambilan dan ringkasan. Di tata letak fragment_pickup.xml dan fragment_summary.xml, ubah tampilan teks untuk menggunakan properti viewModel price juga.

fragment_pickup.xml

...

<TextView
    android:id="@+id/subtotal"
    ...
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

fragment_summary.xml

...

<TextView
   android:id="@+id/total"
   ...
   android:text="@{@string/total_price(viewModel.price)}"
   ... />

...

  1. Jalankan aplikasi. Pastikan harga yang ditampilkan dalam ringkasan pesanan dihitung dengan benar untuk jumlah pesanan 1, 6, dan 12 cupcake. Seperti yang telah disebutkan, pemformatan harga yang tidak benar saat ini merupakan hal yang wajar (formatnya akan ditampilkan sebagai 2.0 untuk $2 atau 12.0 untuk $12).

Mengenakan biaya ekstra untuk pengambilan di hari yang sama

Dalam tugas ini, Anda akan menerapkan aturan kedua, yaitu pengambilan di hari yang sama akan menambahkan $3,00 ke pesanan.

  1. Di class OrderViewModel, tentukan konstanta pribadi tingkat teratas yang baru untuk biaya pengambilan di hari yang sama.
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. Di updatePrice(), periksa apakah pengguna memilih pengambilan di hari yang sama. Periksa apakah tanggal di model tampilan (_date.value) sama dengan item pertama di daftar dateOptions yang selalu berisi hari ini.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. Untuk mempermudah penghitungan ini, perkenalkan variabel sementara, calculatedPrice. Hitung harga terbaru dan tetapkan kembali ke _price.value.
private fun updatePrice() {
    var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    // If the user selected the first option (today) for pickup, add the surcharge
    if (dateOptions[0] == _date.value) {
        calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
    }
    _price.value = calculatedPrice
}
  1. Panggil metode helper updatePrice() dari metode setDate() untuk menambahkan biaya pengambilan di hari yang sama.
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. Jalankan aplikasi Anda, jelajahi aplikasi. Anda akan melihat bahwa mengubah tanggal pengambilan tidak akan menghapus biaya pengambilan di hari yang sama dari harga total. Hal ini karena harga diubah di model tampilan tetapi tidak diberitahukan ke tata letak binding.

2ea8e000fb4e6ec8.png

Menetapkan pemilik Lifecycle untuk mengamati LiveData

LifecycleOwner adalah class yang memiliki siklus proses Android, seperti aktivitas atau fragmen. Pengamat LiveData mengamati perubahan pada data aplikasi hanya jika pemilik siklus proses berstatus aktif (STARTED atau RESUMED).

Di aplikasi Anda, objek LiveData atau data yang dapat diamati adalah properti price di model tampilan. Pemilik siklus proses adalah fragmen rasa, pengambilan, dan ringkasan. Pengamat LiveData adalah ekspresi binding di file tata letak dengan data yang dapat diamati seperti harga. Dengan Data Binding, saat nilai yang dapat diamati berubah, elemen UI yang terikat akan terupdate secara otomatis.

Contoh ekspresi binding: android:text="@{@string/subtotal_price(viewModel.price)}"

Agar elemen UI dapat diperbarui secara otomatis, Anda harus mengaitkan binding.lifecycleOwner

dengan pemilik siklus proses dalam aplikasi. Anda akan menerapkan ini nanti.

  1. Di class FlavorFragment, PickupFragment, SummaryFragment, di dalam metode onViewCreated(), tambahkan kode berikut di blok binding?.apply. Tindakan ini akan menetapkan pemilik siklus proses pada objek binding. Dengan menetapkan pemilik siklus proses, aplikasi akan dapat mengamati objek LiveData.
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. Jalankan aplikasi Anda lagi. Di layar pengambilan, ubah tanggal pengambilan dan perhatikan perbedaannya bagaimana harga berubah secara otomatis. Dan biaya pengambilan ditampilkan dengan benar di layar ringkasan.
  2. Perhatikan bahwa jika Anda memilih tanggal pengambilan hari ini, harga pesanan bertambah $3,00. Harga untuk memilih tanggal mendatang akan tetap berupa jumlah cupcake x $2,00.

  1. Uji berbagai kasus dengan jumlah, rasa, dan tanggal pengambilan yang lain. Sekarang Anda akan melihat harga diperbarui dari model tampilan di setiap fragmen. Sisi terbaiknya adalah Anda tidak perlu menulis kode Kotlin tambahan agar UI selalu diperbarui dengan harganya setiap saat.

f4c0a3c5ea916d03.png

Untuk menyelesaikan penerapan fitur harga, Anda perlu memformat harga ke mata uang lokal.

Memformat harga dengan transformasi LiveData

Metode transformasi LiveData menyediakan cara untuk melakukan manipulasi data pada LiveData sumber dan menampilkan objek LiveData yang dihasilkan. Secara sederhana, metode ini mengubah nilai LiveData menjadi nilai lain. Transformasi ini tidak dihitung kecuali jika pengamat mengamati objek LiveData.

Transformations.map() adalah salah satu fungsi transformasi, metode ini mengambil LiveData sumber dan suatu fungsi sebagai parameter. Fungsi ini memanipulasi LiveData sumber dan menampilkan nilai yang telah diperbarui, yang juga dapat diamati.

Beberapa contoh real-time tempat Anda dapat menggunakan transformasi LiveData:

  • Memformat tanggal dan string waktu untuk tampilan
  • Mengurutkan daftar item
  • Memfilter atau mengelompokkan item
  • Menghitung hasil dari daftar seperti total semua item, jumlah item, pengembalian item terakhir, dan sebagainya.

Dalam tugas ini, Anda akan menggunakan metode Transformations.map() untuk memformat harga menggunakan mata uang lokal. Anda akan mengubah harga asli sebagai nilai desimal (LiveData<Double>) menjadi nilai string (LiveData<String>).

  1. Di class OrderViewModel, ubah jenis properti pendukung ke LiveData<String>, bukan LiveData<Double>. Harga berformat akan berupa string dengan simbol mata uang seperti '$'. Anda akan memperbaiki kesalahan inisialisasi di langkah berikutnya.
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. Gunakan Transformations.map() untuk melakukan inisialisasi variabel baru, teruskan _price dan fungsi lambda. Gunakan metode getCurrencyInstance() di class NumberFormat untuk mengonversi harga ke format mata uang lokal. Kode transformasi akan terlihat seperti ini.
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

Anda harus mengimpor androidx.lifecycle.Transformations dan java.text.NumberFormat.

  1. Jalankan aplikasi. Anda akan melihat string harga berformat untuk subtotal dan total. Ini jauh lebih mudah digunakan!

1853bd13a07f1bc7.png

  1. Uji apakah implementasi tersebut berfungsi seperti yang diharapkan. Uji kasus seperti: Pesan satu cupcake, pesan enam cupcake, pesan 12 cupcake. Pastikan harganya diperbarui dengan benar pada setiap layar. Pada layar akan tertulis Subtotal $2.00 untuk fragmen Rasa dan Pengambilan, dan Total $2.00 untuk ringkasan pesanan. Selain itu, pastikan ringkasan pesanan menampilkan detail pesanan yang benar.

9. Menyiapkan pemroses klik menggunakan binding pemroses

Dalam tugas ini, Anda akan menggunakan binding pemroses untuk mengikat pemroses klik tombol di class fragmen ke tata letak.

  1. Pada file tata letak fragment_start.xml, tambahkan variabel data bernama startFragment dari jenis com.example.cupcake.StartFragment. Pastikan nama paket fragmen cocok dengan nama paket aplikasi Anda.
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. Di StartFragment.kt, di metode onViewCreated(), ikat variabel data baru ke instance fragmen. Anda dapat mengakses instance fragmen di dalam fragmen menggunakan kata kunci this. Hapus blok binding?.apply beserta kode di dalamnya. Metode lengkapnya akan terlihat seperti ini.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. Di fragment_start.xml, tambahkan pemroses peristiwa menggunakan binding pemroses ke atribut onClick untuk tombol, lakukan panggilan ke orderCupcake() pada startFragment, yang meneruskan jumlah cupcake.
<Button
    android:id="@+id/order_one_cupcake"
    android:onClick="@{() -> startFragment.orderCupcake(1)}"
    ... />

<Button
    android:id="@+id/order_six_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(6)}"
    ... />

<Button
    android:id="@+id/order_twelve_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(12)}"
    ... />
  1. Jalankan aplikasi. Perhatikan bahwa pemroses klik tombol di fragmen awal berfungsi seperti yang diharapkan.
  2. Demikian pula, tambahkan variabel data di atas di tata letak lain juga untuk mengikat instance fragmen, fragment_flavor.xml, fragment_pickup.xml, dan fragment_summary.xml.

Di fragment_flavor.xml

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="flavorFragment"
            type="com.example.cupcake.FlavorFragment" />
    </data>

    <ScrollView ...>

Di fragment_pickup.xml:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="pickupFragment"
            type="com.example.cupcake.PickupFragment" />
    </data>

    <ScrollView ...>

Di fragment_summary.xml:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="summaryFragment"
            type="com.example.cupcake.SummaryFragment" />
    </data>

    <ScrollView ...>
  1. Di class fragmen lainnya, di metode onViewCreated(), hapus kode yang secara manual menetapkan pemroses klik pada tombol.
  2. Di metode onViewCreated(), ikat variabel data fragmen dengan instance fragmen. Anda akan menggunakan kata kunci this dengan cara yang berbeda di sini, karena di dalam blok binding?.apply, kata kunci this merujuk pada instance binding, bukan instance fragmen. Gunakan @ dan secara eksplisit tentukan nama class fragmen, misalnya this@FlavorFragment. Metode onViewCreated() lengkapnya akan terlihat seperti berikut:

Metode onViewCreated() di class FlavorFragment akan terlihat seperti ini:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding?.apply {
        lifecycleOwner = viewLifecycleOwner
        viewModel = sharedViewModel
        flavorFragment = this@FlavorFragment
    }
}

Metode onViewCreated() di class PickupFragment akan terlihat seperti ini:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       pickupFragment = this@PickupFragment
   }
}

Metode onViewCreated() yang dihasilkan di metode class SummaryFragment akan terlihat seperti ini:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       summaryFragment = this@SummaryFragment
   }
}
  1. Demikian pula, di file tata letak lainnya, tambahkan ekspresi binding pemroses ke atribut onClick untuk tombol tersebut.

Di fragment_flavor.xml:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> flavorFragment.goToNextScreen()}"
    ... />

Di fragment_pickup.xml:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> pickupFragment.goToNextScreen()}"
    ... />

Di fragment_summary.xml:

<Button
    android:id="@+id/send_button"
    android:onClick="@{() -> summaryFragment.sendOrder()}"
    ...>
  1. Jalankan aplikasi untuk memastikan tombol masih berfungsi seperti yang diharapkan. Seharusnya tidak ada perubahan perilaku yang terlihat, tetapi kini Anda telah menggunakan binding pemroses untuk menyiapkan pemroses klik!

Selamat telah menyelesaikan codelab ini dan membuat aplikasi Cupcake! Namun, aplikasi ini belum selesai sepenuhnya. Di codelab berikutnya, Anda akan menambahkan tombol Cancel dan memodifikasi backstack. Anda juga akan mempelajari apa itu backstack dan topik baru lainnya. Sampai jumpa!

10. Kode solusi

Kode solusi untuk codelab ini ada dalam project yang ditampilkan di bawah. Gunakan cabang viewmodel untuk mengambil atau mendownload kodenya.

Untuk mendapatkan kode codelab ini dan membukanya di Android Studio, lakukan hal berikut.

Mendapatkan kode

  1. Klik URL yang diberikan. Tindakan ini akan membuka halaman GitHub project di browser.
  2. Di halaman GitHub project, klik tombol Code yang akan menampilkan dialog.

5b0a76c50478a73f.png

  1. Di dialog, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
  2. Temukan file di komputer Anda (mungkin di folder Downloads).
  3. Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.

Membuka project di Android Studio

  1. Mulai Android Studio.
  2. Di jendela Welcome to Android Studio, klik Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > New > Import Project.

21f3eec988dcfbe9.png

  1. Di dialog Import Project, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
  2. Klik dua kali pada folder project tersebut.
  3. Tunggu Android Studio membuka project.
  4. Klik tombol Run 11c34fc5e516fb1c.png untuk membangun dan menjalankan aplikasi. Pastikan aplikasi dibangun seperti yang diharapkan.
  5. Cari file project di jendela alat Project untuk melihat cara aplikasi disiapkan.

11. Ringkasan

  • ViewModel adalah bagian dari Komponen Arsitektur Android dan data aplikasi yang disimpan di dalam ViewModel tidak akan hilang selama perubahan konfigurasi. Untuk menambahkan ViewModel ke aplikasi, Anda dapat membuat class baru dan melanjutkannya dari class ViewModel.
  • ViewModel bersama digunakan untuk menyimpan data aplikasi dari beberapa fragmen dalam satu ViewModel. Beberapa fragmen di aplikasi akan mengakses ViewModel bersama menggunakan cakupan aktivitasnya.
  • LifecycleOwner adalah class yang memiliki siklus proses Android, seperti aktivitas atau fragmen.
  • Pengamat LiveData mengamati perubahan pada data aplikasi hanya jika pemilik siklus proses berstatus aktif (STARTED atau RESUMED).
  • Binding pemroses adalah ekspresi lambda yang dijalankan saat suatu peristiwa terjadi, seperti peristiwa onClick. Binding ini mirip dengan referensi metode seperti textview.setOnClickListener(clickListener) tetapi binding pemroses memungkinkan Anda menjalankan ekspresi data binding sembarang.
  • Metode transformasi LiveData menyediakan cara untuk melakukan manipulasi data pada LiveData sumber dan menampilkan objek LiveData yang dihasilkan.
  • Framework Android menyediakan class bernama SimpleDateFormat, yang merupakan class untuk pemformatan dan penguraian tanggal dengan cara yang sensitif terhadap lokalitas. Hal ini memungkinkan pemformatan (tanggal → teks) dan penguraian (teks → tanggal) pada tanggal.

12. Pelajari lebih lanjut