Menyimpan data di ViewModel

1. Sebelum memulai

Anda telah mempelajari codelab sebelumnya tentang siklus proses aktivitas dan fragmen serta berbagai masalah terkait siklus proses ketika terjadi perubahan konfigurasi. Untuk menyimpan data aplikasi, menyimpan status instance adalah salah satu opsi, tetapi tindakan ini memiliki batasannya tersendiri. Dalam codelab ini, Anda akan mempelajari cara efektif untuk mendesain aplikasi dan mempertahankan data aplikasi selama perubahan konfigurasi, dengan memanfaatkan library Android Jetpack.

Library Android Jetpack adalah kumpulan library untuk mempermudah Anda mengembangkan aplikasi Android yang hebat. Library ini membantu Anda mengikuti praktik terbaik, menghindari keharusan menulis kode boilerplate, dan menyederhanakan tugas-tugas rumit, sehingga Anda dapat berfokus pada kode yang penting seperti logika aplikasi.

Komponen Arsitektur Android adalah bagian dari library Android Jetpack, untuk membantu Anda mendesain aplikasi dengan arsitektur yang baik. Komponen Arsitektur memberikan panduan tentang arsitektur aplikasi, dan merupakan praktik terbaik yang direkomendasikan.

Arsitektur aplikasi adalah seperangkat aturan desain. Mirip dengan cetak biru sebuah rumah, arsitektur memberikan struktur bagi aplikasi Anda. Arsitektur aplikasi yang baik dapat membuat kode Anda andal, fleksibel, skalabel, dan mudah dikelola selama bertahun-tahun.

Dalam codelab ini, Anda akan mempelajari cara menggunakan ViewModel, salah satu komponen Arsitektur untuk menyimpan data aplikasi Anda. Data yang disimpan tidak akan hilang jika framework menghancurkan dan membuat ulang aktivitas dan fragmen selama perubahan konfigurasi atau peristiwa lainnya.

Prasyarat

  • Cara mendownload kode sumber dari GitHub dan membukanya di Android Studio.
  • Cara membuat dan menjalankan aplikasi Android dasar di Kotlin, dengan menggunakan aktivitas dan fragmen.
  • Pengetahuan tentang kolom teks Material dan widget UI umum seperti TextView dan Button.
  • Cara menggunakan view binding di aplikasi.
  • Dasar-dasar siklus proses aktivitas dan fragmen.
  • Cara menambahkan informasi logging ke aplikasi dan membaca log menggunakan Logcat di Android Studio.

Yang akan Anda pelajari

Yang akan Anda build

  • Aplikasi game Unscramble tempat pengguna dapat menebak kata yang ejaannya diacak.

Yang Anda perlukan

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

2. Ringkasan aplikasi awal

Ringkasan game

Aplikasi Unscramble adalah game pengacak ejaan kata untuk satu pemain. Aplikasi ini menampilkan satu kata yang ejaannya diacak dan pemain harus menebak kata yang dimaksud menggunakan semua huruf yang ada. Pemain akan mendapatkan poin jika kata tersebut benar, jika tidak, pemain dapat mencoba berulang kali. Aplikasi ini juga memiliki opsi untuk melewati kata saat ini. Di pojok kiri atas, aplikasi menampilkan jumlah kata, yaitu jumlah kata yang dimainkan dalam game saat ini. Ada 10 kata per game.

8edd6191a40a57e1.png 992bf57f066caf49.png b82a9817b5ec4d11.png

Mendownload kode awal

Codelab ini menyediakan kode awal bagi Anda untuk diperluas dengan fitur yang dipelajari dalam codelab ini. Kode awal dapat berisi kode, baik yang Anda kenal maupun tidak, dari codelab sebelumnya. Anda akan mempelajari lebih lanjut kode yang tidak dikenal di codelab berikutnya.

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

  1. Buka halaman repositori GitHub yang disediakan untuk project.
  2. Pastikan nama cabang cocok dengan nama cabang yang ditentukan dalam codelab. Misalnya, dalam screenshot berikut, nama cabang adalah main (utama).

1e4c0d2c081a8fd2.png

  1. Di halaman GitHub project, klik tombol Code yang akan menampilkan pop-up.

1debcf330fd04c7b.png

  1. Pada pop-up, 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.

d8e9dbdeafe9038a.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > Open.

8d1fda7396afe8e5.png

  1. Di file browser, 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 8de56cba7583251f.png untuk mem-build dan menjalankan aplikasi. Pastikan aplikasi di-build seperti yang diharapkan.

Ringkasan kode awal

  1. Buka project dengan kode awal di Android Studio.
  2. Jalankan aplikasi di perangkat Android, atau di emulator.
  3. Mainkan beberapa kata di dalam game, dengan mengetuk tombol Submit dan Skip. Perhatikan bahwa mengetuk tombol tersebut akan menampilkan kata berikutnya dan menambah jumlah kata.
  4. Perhatikan bahwa skor meningkat hanya dengan mengetuk tombol Submit.

Masalah dengan kode awal

Saat memainkan game, Anda mungkin telah menemukan bug berikut:

  1. Saat mengklik tombol Submit, aplikasi tidak akan memeriksa kata pemain. Pemain selalu mendapatkan poin.
  2. Tidak ada cara untuk mengakhiri game. Aplikasi ini memungkinkan Anda memainkan lebih dari 10 kata.
  3. Layar game menampilkan kata yang ejaannya diacak, skor pemain, dan jumlah kata. Ubah orientasi layar dengan memutar perangkat atau emulator. Perhatikan bahwa kata, skor, dan jumlah kata saat ini hilang dan game dimulai ulang dari awal.

Masalah utama di aplikasi

Aplikasi awal tidak menyimpan dan memulihkan status dan data aplikasi selama perubahan konfigurasi, seperti saat orientasi perangkat berubah.

Anda dapat mengatasi masalah ini menggunakan callback onSaveInstanceState(). Namun, penggunaan metode onSaveInstanceState() mengharuskan Anda menulis kode tambahan untuk menyimpan status dalam paket, dan menerapkan logika untuk mengambil status tersebut. Selain itu, jumlah data yang dapat disimpan sangat sedikit.

Anda dapat mengatasi masalah ini menggunakan komponen Arsitektur Android yang Anda pelajari di jalur ini.

Panduan kode awal

Kode awal yang Anda download memiliki tata letak layar game yang telah dirancang sebelumnya untuk Anda. Di jalur ini, Anda akan berfokus untuk menerapkan logika game. Anda akan menggunakan komponen arsitektur untuk menerapkan arsitektur aplikasi yang disarankan dan menyelesaikan masalah yang disebutkan di atas. Berikut adalah panduan singkat beberapa file untuk membantu Anda memulai.

game_fragment.xml

  • Buka res/layout/game_fragment.xml dalam tampilan Design.
  • Ini berisi tata letak satu-satunya layar di aplikasi Anda yang merupakan layar game.
  • Tata letak ini berisi kolom teks untuk tempat menuliskan kata oleh pemain, bersama dengan TextViews untuk menampilkan skor dan jumlah kata. Kolom tersebut juga memiliki petunjuk dan tombol (Submit dan Skip) untuk memainkan game.

main_activity.xml

Menentukan tata letak aktivitas utama dengan satu fragmen game.

folder res/values

Anda sudah memahami file resource dalam folder ini.

  • colors.xml berisi warna tema yang digunakan dalam aplikasi
  • strings.xml berisi semua string yang dibutuhkan aplikasi Anda
  • Folder themes dan styles berisi penyesuaian UI yang dilakukan untuk aplikasi Anda

MainActivity.kt

Berisi kode yang dihasilkan template default untuk menetapkan tampilan konten aktivitas sebagai main_activity.xml.

ListOfWords.kt

File ini berisi daftar kata yang digunakan dalam game, serta konstanta untuk jumlah maksimum kata per game dan jumlah poin skor pemain untuk setiap kata yang benar.

GameFragment.kt

Ini adalah satu-satunya fragmen di aplikasi Anda, tempat sebagian besar tindakan game berlangsung:

  • Variabel ditetapkan untuk kata saat ini yang ejaannya diacak (currentScrambledWord), jumlah kata (currentWordCount), dan skor (score).
  • Instance objek binding dengan akses ke tampilan game_fragment yang disebut binding telah ditentukan.
  • Fungsi onCreateView() meng-inflate XML tata letak game_fragment menggunakan objek binding.
  • onViewCreated() berfungsi untuk menyiapkan pemroses klik tombol dan mengupdate UI.
  • onSubmitWord() adalah pemroses klik untuk tombol Submit, yang berfungsi menampilkan kata berikutnya yang ejaannya diacak, menghapus kolom teks, dan meningkatkan skor serta jumlah kata tanpa memvalidasi kata pemain.
  • onSkipWord() adalah pemroses klik untuk tombol Skip, yang berfungsi mengupdate UI yang mirip dengan onSubmitWord() kecuali skornya.
  • getNextScrambledWord() adalah fungsi bantuan yang memilih kata acak dari daftar kata dan mengacak huruf di dalamnya.
  • Fungsi restartGame() dan exitGame() digunakan masing-masing untuk memulai ulang dan mengakhiri game. Anda akan menggunakan fungsi ini nanti.
  • setErrorTextField() menghapus konten kolom teks dan mereset status error.
  • Fungsi updateNextWordOnScreen() menampilkan kata acak baru.

3. Mempelajari Arsitektur Aplikasi

Arsitektur menyediakan pedoman untuk membantu Anda mengalokasikan tanggung jawab dalam aplikasi, di antara banyak class. Arsitektur aplikasi yang dirancang dengan baik akan membantu Anda meningkatkan skala aplikasi dan menambahkan fitur tambahan di masa mendatang. Hal ini juga mempermudah kolaborasi tim.

Prinsip arsitektur yang paling umum adalah: memisahkan fokus dan menjalankan UI dari model.

Memisahkan fokus

Prinsip desain pemisahan fokus menyatakan bahwa aplikasi harus dibagi ke dalam beberapa class, masing-masing dengan tanggung jawab yang terpisah.

Menjalankan UI dari model

Prinsip penting lainnya adalah sebaiknya Anda menjalankan UI dari suatu model, terutama model yang persisten. Model adalah komponen yang bertanggung jawab menangani data untuk sebuah aplikasi. Model tidak terikat dengan Views dan komponen aplikasi dalam aplikasi Anda, sehingga tidak terpengaruh oleh siklus proses aplikasi dan masalah terkaitnya.

Class atau komponen utama dalam Arsitektur Android adalah Pengontrol UI (aktivitas/fragmen), ViewModel, LiveData, dan Room. Komponen ini menangani beberapa kerumitan siklus proses dan membantu Anda menghindari masalah terkait siklus proses. Anda akan mempelajari LiveData dan Room dalam codelab berikutnya.

Diagram ini menunjukkan bentuk dasar arsitektur:

597074ed0d08947b.png

Pengontrol UI (Aktivitas/Fragmen)

Aktivitas dan fragmen adalah pengontrol UI. Pengontrol UI mengontrol UI dengan menarik tampilan ke layar, merekam peristiwa pengguna, dan hal lain yang terkait dengan UI yang berinteraksi dengan pengguna. Data dalam aplikasi atau logika pengambilan keputusan tentang data tersebut tidak boleh berada dalam class pengontrol UI.

Sistem Android dapat menghancurkan pengontrol UI kapan saja berdasarkan interaksi pengguna tertentu atau karena kondisi sistem seperti memori yang rendah. Karena peristiwa ini tidak berada di bawah kendali Anda, Anda tidak boleh menyimpan data atau status aplikasi apa pun di pengontrol UI. Sebaliknya, logika pengambilan keputusan tentang data harus ditambahkan di ViewModel.

Misalnya, di aplikasi Unscramble, kata yang ejaannya diacak, skor, dan jumlah kata ditampilkan dalam fragmen (pengontrol UI). Kode pengambilan keputusan seperti mencari kata berikutnya dengan ejaan yang diacak, dan penghitungan skor serta jumlah kata harus berada di ViewModel.

ViewModel

ViewModel adalah model data aplikasi yang ditampilkan dalam tampilan. Model adalah komponen yang bertanggung jawab menangani data untuk aplikasi. Model ini memungkinkan aplikasi Anda mengikuti prinsip arsitektur, dengan menjalankan UI dari model.

ViewModel menyimpan data terkait aplikasi yang tidak dihancurkan saat aktivitas atau fragmen dihancurkan dan dibuat ulang oleh framework Android. Objek ViewModel secara otomatis dipertahankan (tidak dihancurkan seperti aktivitas atau instance fragmen) selama perubahan konfigurasi, sehingga data yang disimpannya akan segera tersedia untuk aktivitas atau instance fragmen berikutnya.

Untuk mengimplementasikan ViewModel di aplikasi Anda, perluas class ViewModel, yang berasal dari library komponen arsitektur, dan simpan data aplikasi dalam class tersebut.

Ringkasnya:

Tanggung jawab fragmen/aktivitas (pengontrol UI)

Tanggung jawab ViewModel

Aktivitas dan fragmen bertanggung jawab untuk menarik tampilan dan data ke layar dan merespons peristiwa pengguna.

ViewModel bertanggung jawab untuk menyimpan dan memproses semua data yang diperlukan untuk UI. Fungsi ini tidak boleh mengakses hierarki tampilan Anda (seperti objek view binding) atau menyimpan referensi ke aktivitas atau fragmen.

4. Menambahkan ViewModel

Dalam tugas ini, Anda menambahkan ViewModel ke aplikasi untuk menyimpan data aplikasi (kata yang ejaannya diacak, jumlah kata, dan skor).

Aplikasi Anda akan dirancang dengan cara berikut. MainActivity berisi GameFragment, dan GameFragment akan mengakses informasi tentang game dari GameViewModel.

2b29a13dde3481c3.png

  1. Di jendela Android Android Studio, pada folder Gradle Scripts, buka file build.gradle(Module:Unscramble.app).
  2. Untuk menggunakan ViewModel di aplikasi Anda, verifikasi bahwa Anda memiliki dependensi library ViewModel di dalam blok dependencies. Langkah ini sudah dilakukan untuk Anda. Bergantung pada versi terbaru library, nomor versi library dalam kode yang dihasilkan mungkin berbeda.
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

Sebaiknya selalu gunakan versi terbaru library meskipun versi yang disebutkan dalam codelab ini berbeda.

  1. Buat file class Kotlin baru bernama GameViewModel. Di jendela Android, klik kanan pada folder ui.game. Pilih New > Kotlin File/Class.

d48361a4f73d4acb.png

  1. Berikan nama GameViewModel, lalu pilih Class dari daftar.
  2. Ubah GameViewModel menjadi subclass dari ViewModel. ViewModel adalah class abstrak, jadi Anda perlu memperluasnya untuk menggunakannya di aplikasi. Lihat definisi class GameViewModel di bawah.
class GameViewModel : ViewModel() {
}

Melampirkan ViewModel ke Fragmen

Untuk mengaitkan ViewModel ke pengontrol UI (aktivitas/fragmen), buat referensi (objek) ke ViewModel di dalam pengontrol UI.

Pada langkah ini, Anda membuat instance objek GameViewModel di dalam pengontrol UI yang sesuai, yaitu GameFragment.

  1. Di bagian atas class GameFragment, tambahkan properti jenis GameViewModel.
  2. Lakukan inisialisasi GameViewModel menggunakan delegasi properti Kotlin by viewModels(). Anda akan mempelajarinya lebih lanjut di bagian berikutnya.
private val viewModel: GameViewModel by viewModels()
  1. Jika diminta oleh Android Studio, impor androidx.fragment.app.viewModels.

Delegasi properti Kotlin

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

Untuk properti hanya baca (val), properti ini sedikit berbeda dari properti yang dapat berubah. Hanya fungsi pengambil yang dihasilkan secara default. Fungsi pengambil ini 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>()

Dalam aplikasi, jika Anda menginisialisasi model tampilan menggunakan konstruktor GameViewModel default, seperti di bawah ini:

private val viewModel = GameViewModel()

Selanjutnya, aplikasi akan kehilangan status referensi viewModel saat perangkat mengalami perubahan konfigurasi. Misalnya, jika Anda merotasi perangkat, aktivitas akan dihancurkan dan dibuat lagi, dan Anda akan memiliki instance model tampilan baru dengan status awal lagi.

Sebagai gantinya, gunakan pendekatan delegasi properti dan delegasikan tanggung jawab objek viewModel ke class terpisah yang disebut viewModels. Itu berarti saat Anda mengakses objek viewModel, objek tersebut ditangani secara internal oleh class delegasi, viewModels. Class delegasi membuat objek viewModel untuk Anda pada akses pertama, dan mempertahankan nilainya melalui perubahan konfigurasi dan menampilkan nilai saat diminta.

5. Memindahkan data ke ViewModel

Memisahkan data UI aplikasi Anda dari pengontrol UI (class Activity/Fragment) memungkinkan Anda mengikuti prinsip tanggung jawab tunggal yang kita bahas di atas dengan lebih baik. Aktivitas dan fragmen Anda bertanggung jawab untuk menampilkan tampilan dan data ke layar, sedangkan ViewModel bertanggung jawab untuk menyimpan dan memproses semua data yang diperlukan untuk UI.

Dalam tugas ini, Anda memindahkan variabel data dari class GameFragment ke GameViewModel.

  1. Pindahkan variabel data score, currentWordCount, currentScrambledWord ke class GameViewModel.
class GameViewModel : ViewModel() {

    private var score = 0
    private var currentWordCount = 0
    private var currentScrambledWord = "test"
...
  1. Perhatikan error tentang referensi yang belum terselesaikan. Hal ini karena properti bersifat pribadi untuk ViewModel dan tidak dapat diakses oleh pengontrol UI. Anda akan memperbaiki error ini berikutnya.

Untuk mengatasi masalah ini, Anda tidak dapat membuat pengubah visibilitas properti public—data seharusnya tidak dapat diedit oleh class lain. Hal ini berisiko karena class luar dapat mengubah data dengan cara yang tidak diharapkan yang tidak mengikuti aturan game yang ditentukan dalam model tampilan. Misalnya, class luar dapat mengubah score menjadi nilai negatif.

Di dalam ViewModel, data harus dapat diedit, sehingga seharusnya berupa private dan var. Dari luar ViewModel, data harus dapat dibaca, tetapi tidak dapat diedit, sehingga data harus ditampilkan sebagai public dan val. Untuk mencapai perilaku ini, Kotlin memiliki fitur yang disebut properti pendukung.

Properti pendukung

Properti pendukung memungkinkan Anda menampilkan sesuatu dari pengambil selain dari objek yang tepat.

Anda telah mengetahui bahwa untuk setiap properti, framework Kotlin menghasilkan pengambil dan penyetel.

Untuk metode pengambil dan penyetel, Anda dapat mengganti salah satu atau kedua metode tersebut dan memberikan perilaku kustom Anda sendiri. Untuk mengimplementasikan properti pendukung, Anda akan mengganti metode pengambil untuk menampilkan versi hanya-baca data. Contoh properti pendukung:

// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0

// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
   get() = _count

Anggap saja sebuah contoh bahwa di aplikasi Anda, Anda ingin data aplikasi bersifat pribadi untuk ViewModel:

Di dalam class ViewModel:

  • Properti _count bersifat private dan dapat diubah. Oleh karena itu, ini hanya dapat diakses dan diedit dalam class ViewModel. Konvensinya adalah untuk memberi awalan pada properti private dengan garis bawah.

Di luar class ViewModel:

  • Pengubah visibilitas default di Kotlin adalah public, sehingga count bersifat publik dan dapat diakses dari class lain seperti pengontrol UI. Karena hanya metode get() yang sedang diganti, properti ini tidak dapat diubah dan hanya-baca. Saat class luar mengakses properti ini, properti akan menampilkan nilai _count dan nilainya tidak dapat diubah. Tindakan ini melindungi data aplikasi di dalam ViewModel dari perubahan yang tidak diinginkan dan tidak aman oleh class eksternal, tetapi memungkinkan pemanggil eksternal mengakses nilainya dengan aman.

Menambahkan properti pendukung ke currentScrambledWord

  1. Dalam GameViewModel, ubah deklarasi currentScrambledWord untuk menambahkan properti pendukung. Sekarang _currentScrambledWord hanya dapat diakses dan diedit dalam GameViewModel. Pengontrol UI, GameFragment, dapat membaca nilainya menggunakan properti hanya baca, currentScrambledWord.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
   get() = _currentScrambledWord
  1. Di GameFragment, perbarui metode updateNextWordOnScreen() untuk menggunakan properti viewModel hanya baca, currentScrambledWord.
private fun updateNextWordOnScreen() {
   binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
  1. Di GameFragment, hapus kode di dalam metode onSubmitWord() dan onSkipWord(). Anda akan mengimplementasikan metode ini nanti. Anda seharusnya dapat mengompilasi kode sekarang tanpa error.

6. Siklus proses ViewModel

Framework membuat ViewModel tetap aktif selama cakupan aktivitas atau fragmen aktif. ViewModel tidak akan dihancurkan jika pemiliknya dihancurkan karena perubahan konfigurasi, seperti rotasi layar. Instance baru dari pemilik ini terhubung kembali ke instance ViewModel yang ada, seperti yang ditunjukkan oleh diagram berikut:

91227008b74bf4bb.png

Memahami siklus proses ViewModel

Tambahkan logging di GameViewModel dan GameFragment untuk membantu Anda lebih memahami siklus proses ViewModel.

  1. Di GameViewModel.kt, tambahkan blok init dengan laporan log.
class GameViewModel : ViewModel() {
   init {
       Log.d("GameFragment", "GameViewModel created!")
   }

   ...
}

Kotlin menyediakan blok penginisialisasi (juga dikenal sebagai blok init) sebagai tempat untuk kode penyiapan awal yang diperlukan selama inisialisasi instance objek. Blok penginisialisasi diawali dengan kata kunci init yang diikuti dengan kurung kurawal {}. Blok kode ini dijalankan saat instance objek pertama kali dibuat dan diinisialisasi.

  1. Di class GameViewModel, ganti metode onCleared(). ViewModel akan dihancurkan saat fragmen yang terkait dilepas, atau saat aktivitas selesai. Tepat sebelum ViewModel dihancurkan, callback onCleared() dipanggil.
  2. Tambahkan laporan log di dalam onCleared() untuk melacak siklus proses GameViewModel.
override fun onCleared() {
    super.onCleared()
    Log.d("GameFragment", "GameViewModel destroyed!")
}
  1. Di GameFragment dalam onCreateView(), setelah Anda mendapatkan referensi ke objek binding, tambahkan laporan log untuk mencatat pembuatan fragmen. Callback onCreateView() akan dipicu saat fragmen dibuat untuk pertama kalinya dan juga setiap kali fragmen dibuat ulang untuk peristiwa seperti perubahan konfigurasi.
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View {
   binding = GameFragmentBinding.inflate(inflater, container, false)
   Log.d("GameFragment", "GameFragment created/re-created!")
   return binding.root
}
  1. Di GameFragment, ganti metode callback onDetach(), yang akan dipanggil saat aktivitas dan fragmen yang terkait dihancurkan.
override fun onDetach() {
    super.onDetach()
    Log.d("GameFragment", "GameFragment destroyed!")
}
  1. Di Android Studio, jalankan aplikasi, buka jendela Logcat, dan filter di GameFragment. Perhatikan bahwa GameFragment dan GameViewModel dibuat.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
  1. Aktifkan setelan putar otomatis di perangkat atau emulator dan ubah orientasi layar beberapa kali. GameFragment dihancurkan dan dibuat ulang setiap kali, tetapi GameViewModel dibuat hanya sekali, dan tidak dibuat ulang atau dihancurkan untuk setiap panggilan.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
  1. Keluar dari game atau keluar dari aplikasi menggunakan panah kembali. GameViewModel dihancurkan, dan callback onCleared() dipanggil. GameFragment telah dihancurkan.
com.example.android.unscramble D/GameFragment: GameViewModel destroyed!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!

7. Mengisi ViewModel

Dalam tugas ini, Anda mengisi GameViewModel lebih lanjut dengan metode bantuan untuk mendapatkan kata berikutnya, memvalidasi kata pemain untuk meningkatkan skor, dan memeriksa jumlah kata untuk mengakhiri game.

Inisialisasi terlambat

Biasanya saat Anda mendeklarasikan variabel, Anda memberikannya dengan nilai inisial di awal. Namun, jika Anda belum siap untuk menetapkan nilai, Anda dapat melakukan inisialisasi nanti. Untuk melakukan inisialisasi properti di Kotlin nantinya, gunakan kata kunci lateinit, yang berarti inisialisasi terlambat. Jika Anda menjamin bahwa Anda akan menginisialisasi properti sebelum menggunakannya, Anda dapat mendeklarasikan properti dengan lateinit. Memori tidak dialokasikan ke variabel sebelum diinisialisasi. Jika Anda mencoba mengakses variabel sebelum melakukan inisialisasi, aplikasi akan error.

Mendapatkan kata berikutnya

Buat metode getNextWord() di class GameViewModel, dengan fungsi berikut:

  • Dapatkan kata acak dari allWordsList dan tetapkan ke currentWord.
  • Buat kata yang ejaannya diacak dengan mengacak huruf di currentWord dan tetapkan ke currentScrambledWord.
  • Tangani kasus kata yang diacak sama dengan kata yang tidak diacak.
  • Pastikan Anda tidak menampilkan kata yang sama dua kali selama game.

Terapkan langkah-langkah berikut di class GameViewModel:

  1. Pada GameViewModel, tambahkan variabel class baru jenis MutableList<String> yang disebut wordsList, untuk menyimpan daftar kata yang Anda gunakan dalam game, untuk menghindari pengulangan.
  2. Tambahkan variabel class lain yang disebut currentWord untuk menyimpan kata yang ingin disusun oleh pemain. Gunakan kata kunci lateinit karena Anda akan menginisialisasi properti ini nanti.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
  1. Tambahkan metode private baru yang disebut getNextWord(), di atas blok init, tanpa parameter yang tidak menampilkan apa-apa.
  2. Dapatkan kata acak dari allWordsList dan tetapkan ke currentWord.
private fun getNextWord() {
   currentWord = allWordsList.random()
}
  1. Di getNextWord(), konversikan string currentWord ke array karakter dan tetapkan ke val baru yang disebut tempWord. Untuk mengacak kata, acak karakter dalam array ini menggunakan metode Kotlin, shuffle().
val tempWord = currentWord.toCharArray()
tempWord.shuffle()

Array mirip dengan MutableList, tetapi memiliki ukuran tetap saat diinisialisasi. Array tidak dapat meluaskan atau menyusutkan ukurannya (Anda perlu menyalin array untuk mengubah ukurannya) sementara MutableList memiliki fungsi add() dan remove(), sehingga dapat meningkatkan dan menurunkan ukuran.

  1. Terkadang urutan karakter yang diacak sama dengan kata aslinya. Tambahkan loop while berikut di sekitar panggilan untuk mengacak, untuk melanjutkan loop hingga kata yang diacak tidak sama dengan kata asli.
while (String(tempWord).equals(currentWord, false)) {
    tempWord.shuffle()
}
  1. Tambahkan blok if-else untuk memeriksa apakah suatu kata telah digunakan. Jika wordsList berisi currentWord, panggil getNextWord(). Jika tidak, perbarui nilai _currentScrambledWord dengan kata yang baru diacak, tingkatkan jumlah kata, dan tambahkan kata baru ke wordsList.
if (wordsList.contains(currentWord)) {
    getNextWord()
} else {
    _currentScrambledWord = String(tempWord)
    ++currentWordCount
    wordsList.add(currentWord)
}
  1. Berikut adalah metode getNextWord() yang lengkap untuk referensi Anda.
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
   currentWord = allWordsList.random()
   val tempWord = currentWord.toCharArray()
   tempWord.shuffle()

   while (String(tempWord).equals(currentWord, false)) {
       tempWord.shuffle()
   }
   if (wordsList.contains(currentWord)) {
       getNextWord()
   } else {
       _currentScrambledWord = String(tempWord)
       ++currentWordCount
       wordsList.add(currentWord)
   }
}

Menginisialisasi currentScrambledWord secara terlambat

Sekarang Anda telah membuat metode getNextWord(), untuk mendapatkan kata berikutnya yang ejaannya diacak. Anda akan membuat panggilan ke sana saat GameViewModel diinisialisasi untuk pertama kalinya. Gunakan blok init untuk menginisialisasi properti lateinit di class seperti kata saat ini. Hasilnya adalah kata pertama yang ditampilkan di layar akan berupa kata yang ejaannya diacak, bukan test.

  1. Jalankan aplikasi. Perhatikan bahwa kata pertama selalu "test".
  2. Untuk menampilkan kata yang ejaannya diacak di awal aplikasi, Anda perlu memanggil metode getNextWord(), yang selanjutnya memperbarui currentScrambledWord. Lakukan panggilan ke metode getNextWord() di dalam blok init pada GameViewModel.
init {
    Log.d("GameFragment", "GameViewModel created!")
    getNextWord()
}
  1. Tambahkan pengubah lateinit ke properti _currentScrambledWord. Tambahkan penyebutan eksplisit jenis data String, karena tidak ada nilai awal yang diberikan.
private lateinit var _currentScrambledWord: String
  1. Jalankan aplikasi. Perhatikan kata baru yang ejaannya diacak akan ditampilkan saat peluncuran aplikasi. Keren!

8edd6191a40a57e1.png

Menambahkan metode bantuan

Selanjutnya, tambahkan metode bantuan untuk memproses dan memodifikasi data di dalam ViewModel. Anda akan menggunakan metode ini di tugas selanjutnya.

  1. Di class GameViewModel, tambahkan metode lain yang disebut nextWord(). Dapatkan kata berikutnya dari daftar dan tampilkan true jika jumlah kata kurang dari MAX_NO_OF_WORDS.
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS.
* Updates the next word.
*/
fun nextWord(): Boolean {
    return if (currentWordCount < MAX_NO_OF_WORDS) {
        getNextWord()
        true
    } else false
}

8. Dialog

Dalam kode awal, game tidak pernah berakhir, bahkan setelah 10 kata dimainkan. Modifikasi aplikasi Anda sehingga setelah pengguna memainkan 10 kata, game selesai dan Anda akan menampilkan dialog dengan skor akhir. Anda juga akan memberikan opsi kepada pengguna untuk bermain lagi atau keluar dari game.

62aa368820ffbe31.png

Ini adalah pertama kalinya Anda akan menambahkan dialog ke aplikasi. Dialog adalah jendela (layar) kecil yang meminta pengguna untuk membuat keputusan atau memasukkan informasi tambahan. Biasanya dialog tidak mengisi seluruh layar, dan mengharuskan pengguna untuk melakukan suatu tindakan agar bisa melanjutkan. Android menyediakan berbagai jenis Dialog. Dalam codelab ini, Anda akan mempelajari Dialog Pemberitahuan.

Anatomi dialog pemberitahuan

f8650ca15e854fe4.png

  1. Dialog Pemberitahuan
  2. Judul (opsional)
  3. Pesan
  4. Tombol teks

Menerapkan dialog skor akhir

Gunakan MaterialAlertDialog dari library Komponen Desain Material untuk menambahkan dialog ke aplikasi Anda yang mengikuti panduan Material. Karena dialog terkait UI, GameFragment akan bertanggung jawab untuk membuat dan menampilkan dialog skor akhir.

  1. Pertama-tama, tambahkan properti pendukung ke variabel score. Di GameViewModel, ubah deklarasi variabel score menjadi yang berikut.
private var _score = 0
val score: Int
   get() = _score
  1. Di GameFragment, tambahkan fungsi pribadi bernama showFinalScoreDialog(). Untuk membuat MaterialAlertDialog, gunakan class MaterialAlertDialogBuilder untuk membuat bagian dialog langkah demi langkah. Panggil konstruktor MaterialAlertDialogBuilder yang meneruskan konten menggunakan metode requireContext() fragmen. Metode requireContext() menampilkan nilai non-null Context.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
}

Seperti namanya, Context merujuk pada konteks atau status saat ini untuk aplikasi, aktivitas, atau fragmen. Ini berisi informasi mengenai aktivitas, fragmen, atau aplikasi. Biasanya ini digunakan untuk mendapatkan akses ke resource, database, dan layanan sistem lainnya. Pada langkah ini, Anda meneruskan konteks fragmen untuk membuat dialog pemberitahuan.

Jika diminta oleh Android Studio, import com.google.android.material.dialog.MaterialAlertDialogBuilder.

  1. Tambahkan kode untuk menetapkan judul pada dialog pemberitahuan, gunakan resource string dari strings.xml.
MaterialAlertDialogBuilder(requireContext())
   .setTitle(getString(R.string.congratulations))
  1. Setel pesan agar menampilkan skor akhir, gunakan versi hanya baca dari variabel skor (viewModel.score), yang telah Anda tambahkan sebelumnya.
   .setMessage(getString(R.string.you_scored, viewModel.score))
  1. Jadikan dialog pemberitahuan tidak dapat dibatalkan saat tombol kembali ditekan, menggunakan metode setCancelable() dan meneruskan false.
    .setCancelable(false)
  1. Tambahkan dua tombol teks EXIT dan PLAY AGAIN menggunakan metode setNegativeButton() dan setPositiveButton(). Panggil masing-masing exitGame() dan restartGame() dari lambda.
    .setNegativeButton(getString(R.string.exit)) { _, _ ->
        exitGame()
    }
    .setPositiveButton(getString(R.string.play_again)) { _, _ ->
        restartGame()
    }

Sintaksis ini mungkin baru bagi Anda, tetapi ini adalah penyingkatan untuk setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()}) saat metode setNegativeButton() menggunakan dua parameter: String dan fungsi, DialogInterface.OnClickListener() yang dapat dinyatakan sebagai lambda. Jika argumen terakhir yang diteruskan adalah fungsi, Anda bisa menempatkan ekspresi lambda di luar tanda kurung. Ini dikenal sebagai sintaksis lambda akhir. Kedua cara penulisan kode (dengan lambda di dalam atau di luar tanda kurung) dapat diterima. Hal yang sama berlaku untuk fungsi setPositiveButton.

  1. Di bagian akhir, tambahkan show(), yang akan membuat lalu menampilkan dialog pemberitahuan.
      .show()
  1. Berikut adalah metode showFinalScoreDialog() lengkap untuk referensi.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
       .setTitle(getString(R.string.congratulations))
       .setMessage(getString(R.string.you_scored, viewModel.score))
       .setCancelable(false)
       .setNegativeButton(getString(R.string.exit)) { _, _ ->
           exitGame()
       }
       .setPositiveButton(getString(R.string.play_again)) { _, _ ->
           restartGame()
       }
       .show()
}

9. Mengimplementasikan OnClickListener untuk tombol Submit

Dalam tugas ini, Anda menggunakan ViewModel dan dialog pemberitahuan yang ditambahkan untuk mengimplementasikan logika game bagi pemroses klik tombol Submit.

Menampilkan kata-kata yang diacak

  1. Jika Anda belum melakukannya, di GameFragment, hapus kode di dalam onSubmitWord() yang akan dipanggil saat tombol Submit diketuk.
  2. Tambahkan centang pada nilai return metode viewModel.nextWord(). Jika true, kata lain tersedia, jadi perbarui kata yang ejaannya diacak di layar menggunakan updateNextWordOnScreen(). Jika tidak diperbarui, game akan berakhir. Jadi, tampilkan dialog pemberitahuan dengan skor akhir.
private fun onSubmitWord() {
    if (viewModel.nextWord()) {
        updateNextWordOnScreen()
    } else {
        showFinalScoreDialog()
    }
}
  1. Jalankan aplikasi! Bermainlah dengan beberapa kata. Ingat, Anda belum menerapkan tombol Skip, sehingga Anda tidak dapat melewati kata tersebut.
  2. Perhatikan bahwa kolom teks tidak diperbarui, sehingga pemain harus menghapus kata sebelumnya secara manual. Skor akhir dalam dialog pemberitahuan selalu nol. Anda akan memperbaiki bug ini pada langkah berikutnya.

a4c660e212ce2c31.png 12a42987a0edd2c4.png

Menambahkan metode bantuan untuk memvalidasi kata pemain

  1. Di GameViewModel, tambahkan metode pribadi baru yang disebut increaseScore() tanpa parameter dan nilai yang ditampilkan. Tingkatkan variabel score sebesar SCORE_INCREASE.
private fun increaseScore() {
   _score += SCORE_INCREASE
}
  1. Dalam GameViewModel, tambahkan metode bantuan yang disebut isUserWordCorrect() yang menampilkan Boolean dan mengambil String, kata dari pemain, sebagai parameter.
  2. Di isUserWordCorrect() validasikan kata dari pemain dan tingkatkan skor jika tebakannya benar. Tindakan ini akan memperbarui skor akhir dalam dialog pemberitahuan.
fun isUserWordCorrect(playerWord: String): Boolean {
   if (playerWord.equals(currentWord, true)) {
       increaseScore()
       return true
   }
   return false
}

Memperbarui kolom teks

Menampilkan error di kolom teks

Untuk kolom teks Material, TextInputLayout dilengkapi dengan fungsi bawaan untuk menampilkan pesan error. Misalnya dalam kolom teks berikut, warna label diubah, ikon error ditampilkan, pesan error ditampilkan, dan seterusnya.

520cc685ae1317ac.png

Untuk menampilkan error di kolom teks, Anda dapat menyetel pesan error secara dinamis dalam kode atau secara statis di file tata letak. Contoh untuk menyetel dan mereset error dalam kode ditampilkan di bawah:

// Set error text
passwordLayout.error = getString(R.string.error)

// Clear error text
passwordLayout.error = null

Pada kode awal, Anda akan mendapati bahwa metode bantuan setErrorTextField(error: Boolean) sudah ditentukan untuk membantu Anda menyetel dan mereset error di kolom teks. Panggil metode ini dengan true atau false sebagai parameter input berdasarkan apakah Anda ingin error muncul di kolom teks atau tidak.

Cuplikan kode di kode awal

private fun setErrorTextField(error: Boolean) {
   if (error) {
       binding.textField.isErrorEnabled = true
       binding.textField.error = getString(R.string.try_again)
   } else {
       binding.textField.isErrorEnabled = false
       binding.textInputEditText.text = null
   }
}

Dalam tugas ini, Anda akan mengimplementasikan metode onSubmitWord(). Saat kata dikirimkan, validasikan tebakan pengguna dengan membandingkannya dengan kata asli. Jika kata tersebut benar, buka kata berikutnya (atau tampilkan dialog jika game telah berakhir). Jika kata tersebut salah, tampilkan kesalahan pada kolom teks dan tetap tampilkan kata saat ini.

  1. Pada GameFragment, di awal onSubmitWord(), buat val yang bernama playerWord. Simpan kata pemain di dalamnya, dengan mengekstraknya dari kolom teks dalam variabel binding.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()
    ...
}
  1. Pada onSubmitWord(), di bawah deklarasi playerWord, validasikan kata pemain. Tambahkan pernyataan if untuk memeriksa kata pemain menggunakan metode isUserWordCorrect(), dengan meneruskan playerWord.
  2. Di dalam blok if, reset kolom teks, panggil setErrorTextField dengan memasukkan false.
  3. Pindahkan kode yang ada ke dalam blok if.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (viewModel.nextWord()) {
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    }
}
  1. Jika kata pengguna salah, tampilkan pesan error di kolom teks. Tambahkan blok else ke blok if di atas, lalu panggil setErrorTextField() yang meneruskan true. Metode onSubmitWord() yang sudah selesai akan terlihat seperti ini:
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (viewModel.nextWord()) {
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    } else {
        setErrorTextField(true)
    }
}
  1. Jalankan aplikasi Anda. Mainkan beberapa kata. Jika kata pemain benar, kata tersebut akan dihapus saat tombol Submit diklik; jika tidak, pesan yang menyatakan "Try again!" akan ditampilkan. Perhatikan bahwa tombol Skip masih tidak berfungsi. Anda akan menambahkan implementasi ini di tugas berikutnya.

a10c7d77aa26b9db.png

10. Mengimplementasikan tombol Skip

Dalam tugas ini, Anda menambahkan implementasi untuk onSkipWord() yang menangani saat tombol Skip diklik.

  1. Serupa dengan onSubmitWord(), tambahkan kondisi dalam metode onSkipWord(). Jika true, tampilkan kata di layar dan reset kolom teks. Jika false dan tidak ada lagi kata yang tersisa di babak ini, tampilkan dialog pemberitahuan dengan skor akhir.
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
    if (viewModel.nextWord()) {
        setErrorTextField(false)
        updateNextWordOnScreen()
    } else {
        showFinalScoreDialog()
    }
}
  1. Jalankan aplikasi Anda. Mainkan game-nya. Perhatikan bahwa tombol Skip dan Submit berfungsi sebagaimana mestinya. Sempurna!

11. Memverifikasi bahwa ViewModel mempertahankan data

Untuk tugas ini, tambahkan logging di GameFragment untuk mengamati bahwa data aplikasi Anda disimpan di ViewModel, selama perubahan konfigurasi. Untuk mengakses currentWordCount di GameFragment, Anda perlu menampilkan versi hanya-baca menggunakan properti pendukung.

  1. Di GameViewModel, klik kanan pada variabel currentWordCount, pilih Refactor > Rename... . Awali nama baru dengan garis bawah, _currentWordCount.
  2. Tambahkan kolom pendukung.
private var _currentWordCount = 0
val currentWordCount: Int
   get() = _currentWordCount
  1. Dalam GameFragment di dalam onCreateView(), di atas pernyataan return, tambahkan log lain untuk mencetak data aplikasi, kata, skor, dan jumlah kata.
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
       "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
  1. Di Android Studio, buka Logcat, filter di GameFragment. Jalankan aplikasi dan mainkan beberapa kata. Ubah orientasi perangkat. Fragmen (pengontrol UI) dihancurkan dan dibuat ulang. Amati lognya. Kini Anda dapat melihat skor dan jumlah kata meningkat!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
com.example.android.unscramble D/GameFragment: Word: oimfnru Score: 0 WordCount: 1
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9

Perhatikan bahwa data aplikasi disimpan di ViewModel selama perubahan orientasi. Anda akan memperbarui nilai skor dan jumlah kata di UI menggunakan LiveData dan Data Binding dalam codelab berikutnya.

12. Memperbarui logika restart game

  1. Jalankan aplikasi lagi, mainkan semua kata. Pada dialog pemberitahuan Congratulations!, klik PLAY AGAIN. Aplikasi tidak akan mengizinkan Anda bermain lagi karena jumlah kata telah mencapai nilai MAX_NO_OF_WORDS. Anda perlu mereset jumlah kata ke 0 untuk memainkan game lagi dari awal.
  2. Untuk mereset data aplikasi, di GameViewModel, tambahkan metode yang disebut reinitializeData(). Tetapkan skor dan jumlah kata menjadi 0. Hapus daftar kata dan panggil metode getNextWord().
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
   _score = 0
   _currentWordCount = 0
   wordsList.clear()
   getNextWord()
}
  1. Pada GameFragment di bagian atas metode restartGame(), lakukan panggilan ke metode yang baru dibuat, reinitializeData().
private fun restartGame() {
   viewModel.reinitializeData()
   setErrorTextField(false)
   updateNextWordOnScreen()
}
  1. Jalankan aplikasi Anda lagi. Mainkan game-nya. Saat mencapai dialog ucapan selamat, klik Play Again. Sekarang Anda sudah dapat bermain game lagi!

Seperti inilah tampilan aplikasi akhir Anda. Game ini menampilkan sepuluh kata yang ejaannya diacak untuk disusun oleh pemain. Anda dapat Skip kata atau menebak kata, lalu mengetuk Submit. Jika Anda menebak dengan benar, skor akan meningkat. Tebakan yang salah akan menampilkan status error di kolom teks. Dengan setiap kata baru, jumlah kata juga meningkat.

Perhatikan bahwa skor dan jumlah kata yang ditampilkan di layar belum diperbarui. Namun, informasi tersebut tetap disimpan dalam model tampilan dan dipertahankan selama perubahan konfigurasi seperti rotasi perangkat. Anda akan memperbarui skor dan jumlah kata di layar dalam codelab berikutnya.

f332979d6f63d0e5.png 2803d4855f5d401f.png

Di akhir 10 kata, game berakhir dan dialog pemberitahuan muncul dengan skor akhir dan opsi untuk keluar dari game atau bermain lagi.

d8e0111f5f160ead.png

Selamat! Anda telah membuat ViewModel pertama dan menghemat data!

13. Kode solusi

GameFragment.kt

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.unscramble.R
import com.example.android.unscramble.databinding.GameFragmentBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder

/**
 * Fragment where the game is played, contains the game logic.
 */
class GameFragment : Fragment() {

    private val viewModel: GameViewModel by viewModels()

    // Binding object instance with access to the views in the game_fragment.xml layout
    private lateinit var binding: GameFragmentBinding

    // Create a ViewModel the first time the fragment is created.
    // If the fragment is re-created, it receives the same GameViewModel instance created by the
    // first fragment

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout XML file and return a binding object instance
        binding = GameFragmentBinding.inflate(inflater, container, false)
        Log.d("GameFragment", "GameFragment created/re-created!")
        Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
                "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
        return binding.root
    }

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

        // Setup a click listener for the Submit and Skip buttons.
        binding.submit.setOnClickListener { onSubmitWord() }
        binding.skip.setOnClickListener { onSkipWord() }
        // Update the UI
        updateNextWordOnScreen()
        binding.score.text = getString(R.string.score, 0)
        binding.wordCount.text = getString(
            R.string.word_count, 0, MAX_NO_OF_WORDS)
    }

    /*
    * Checks the user's word, and updates the score accordingly.
    * Displays the next scrambled word.
    * After the last word, the user is shown a Dialog with the final score.
    */
    private fun onSubmitWord() {
        val playerWord = binding.textInputEditText.text.toString()

        if (viewModel.isUserWordCorrect(playerWord)) {
            setErrorTextField(false)
            if (viewModel.nextWord()) {
                updateNextWordOnScreen()
            } else {
                showFinalScoreDialog()
            }
        } else {
            setErrorTextField(true)
        }
    }

    /*
    * Skips the current word without changing the score.
    */
    private fun onSkipWord() {
        if (viewModel.nextWord()) {
            setErrorTextField(false)
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    }

    /*
     * Gets a random word for the list of words and shuffles the letters in it.
     */
    private fun getNextScrambledWord(): String {
        val tempWord = allWordsList.random().toCharArray()
        tempWord.shuffle()
        return String(tempWord)
    }

    /*
    * Creates and shows an AlertDialog with the final score.
    */
    private fun showFinalScoreDialog() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(getString(R.string.congratulations))
            .setMessage(getString(R.string.you_scored, viewModel.score))
            .setCancelable(false)
            .setNegativeButton(getString(R.string.exit)) { _, _ ->
                exitGame()
            }
            .setPositiveButton(getString(R.string.play_again)) { _, _ ->
                restartGame()
            }
            .show()
    }

    /*
     * Re-initializes the data in the ViewModel and updates the views with the new data, to
     * restart the game.
     */
    private fun restartGame() {
        viewModel.reinitializeData()
        setErrorTextField(false)
        updateNextWordOnScreen()
    }

    /*
     * Exits the game.
     */
    private fun exitGame() {
        activity?.finish()
    }

    override fun onDetach() {
        super.onDetach()
        Log.d("GameFragment", "GameFragment destroyed!")
    }

    /*
    * Sets and resets the text field error status.
    */
    private fun setErrorTextField(error: Boolean) {
        if (error) {
            binding.textField.isErrorEnabled = true
            binding.textField.error = getString(R.string.try_again)
        } else {
            binding.textField.isErrorEnabled = false
            binding.textInputEditText.text = null
        }
    }

    /*
     * Displays the next scrambled word on screen.
     */
    private fun updateNextWordOnScreen() {
        binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
    }
}

GameViewModel.kt

import android.util.Log
import androidx.lifecycle.ViewModel

/**
 * ViewModel containing the app data and methods to process the data
 */
class GameViewModel : ViewModel(){
    private var _score = 0
    val score: Int
        get() = _score

    private var _currentWordCount = 0
    val currentWordCount: Int
        get() = _currentWordCount

    private lateinit var _currentScrambledWord: String
    val currentScrambledWord: String
        get() = _currentScrambledWord

    // List of words used in the game
    private var wordsList: MutableList<String> = mutableListOf()
    private lateinit var currentWord: String

    init {
        Log.d("GameFragment", "GameViewModel created!")
        getNextWord()
    }

    override fun onCleared() {
        super.onCleared()
        Log.d("GameFragment", "GameViewModel destroyed!")
    }

    /*
    * Updates currentWord and currentScrambledWord with the next word.
    */
    private fun getNextWord() {
        currentWord = allWordsList.random()
        val tempWord = currentWord.toCharArray()
        tempWord.shuffle()

        while (String(tempWord).equals(currentWord, false)) {
            tempWord.shuffle()
        }
        if (wordsList.contains(currentWord)) {
            getNextWord()
        } else {
            _currentScrambledWord = String(tempWord)
            ++_currentWordCount
            wordsList.add(currentWord)
        }
    }

    /*
    * Re-initializes the game data to restart the game.
    */
    fun reinitializeData() {
       _score = 0
       _currentWordCount = 0
       wordsList.clear()
       getNextWord()
    }

    /*
    * Increases the game score if the player's word is correct.
    */
    private fun increaseScore() {
        _score += SCORE_INCREASE
    }

    /*
    * Returns true if the player word is correct.
    * Increases the score accordingly.
    */
    fun isUserWordCorrect(playerWord: String): Boolean {
        if (playerWord.equals(currentWord, true)) {
            increaseScore()
            return true
        }
        return false
    }

    /*
    * Returns true if the current word count is less than MAX_NO_OF_WORDS
    */
    fun nextWord(): Boolean {
        return if (_currentWordCount < MAX_NO_OF_WORDS) {
            getNextWord()
            true
        } else false
    }
}

14. Ringkasan

  • Panduan arsitektur aplikasi Android merekomendasikan untuk memisahkan class yang memiliki tanggung jawab berbeda dan menjalankan UI dari model.
  • Pengontrol UI adalah class berbasis UI seperti Activity atau Fragment. Pengontrol UI hanya boleh berisi logika yang menangani UI dan interaksi sistem operasi; pengontrol UI tersebut tidak boleh menjadi sumber data yang akan ditampilkan di UI. Masukkan data tersebut dan logika apa pun yang terkait dalam ViewModel.
  • Class ViewModel menyimpan dan mengelola data terkait UI. Class ViewModel memungkinkan data bertahan saat terjadi perubahan konfigurasi seperti pada saat rotasi layar.
  • ViewModel adalah salah satu Komponen Arsitektur Android yang direkomendasikan.

15. Mempelajari lebih lanjut