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
danButton
. - 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
- Pengantar dasar-dasar arsitektur aplikasi Android.
- Cara menggunakan class
ViewModel
di aplikasi Anda. - Cara mempertahankan data UI melalui perubahan konfigurasi perangkat menggunakan
ViewModel
. - Properti pendukung di Kotlin.
- Cara menggunakan
MaterialAlertDialog
dari library Komponen Desain Material.
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.
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.
- Buka halaman repositori GitHub yang disediakan untuk project.
- Pastikan nama cabang cocok dengan nama cabang yang ditentukan dalam codelab. Misalnya, dalam screenshot berikut, nama cabang adalah main (utama).
- Di halaman GitHub project, klik tombol Code yang akan menampilkan pop-up.
- Pada pop-up, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
- Temukan file di komputer Anda (mungkin di folder Downloads).
- Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.
Membuka project di Android Studio
- Mulai Android Studio.
- Di jendela Welcome to Android Studio, klik Open.
Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > Open.
- Di file browser, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
- Klik dua kali pada folder project tersebut.
- Tunggu Android Studio membuka project.
- Klik tombol Run untuk mem-build dan menjalankan aplikasi. Pastikan aplikasi di-build seperti yang diharapkan.
Ringkasan kode awal
- Buka project dengan kode awal di Android Studio.
- Jalankan aplikasi di perangkat Android, atau di emulator.
- 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.
- Perhatikan bahwa skor meningkat hanya dengan mengetuk tombol Submit.
Masalah dengan kode awal
Saat memainkan game, Anda mungkin telah menemukan bug berikut:
- Saat mengklik tombol Submit, aplikasi tidak akan memeriksa kata pemain. Pemain selalu mendapatkan poin.
- Tidak ada cara untuk mengakhiri game. Aplikasi ini memungkinkan Anda memainkan lebih dari 10 kata.
- 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 aplikasistrings.xml
berisi semua string yang dibutuhkan aplikasi Anda- Folder
themes
danstyles
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 disebutbinding
telah ditentukan. - Fungsi
onCreateView()
meng-inflate XML tata letakgame_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 denganonSubmitWord()
kecuali skornya.getNextScrambledWord()
adalah fungsi bantuan yang memilih kata acak dari daftar kata dan mengacak huruf di dalamnya.- Fungsi
restartGame()
danexitGame()
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:
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 |
Aktivitas dan fragmen bertanggung jawab untuk menarik tampilan dan data ke layar dan merespons peristiwa pengguna. |
|
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
.
- Di jendela Android Android Studio, pada folder Gradle Scripts, buka file
build.gradle(Module:Unscramble.app)
. - Untuk menggunakan
ViewModel
di aplikasi Anda, verifikasi bahwa Anda memiliki dependensi library ViewModel di dalam blokdependencies
. 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.
- Buat file class Kotlin baru bernama
GameViewModel
. Di jendela Android, klik kanan pada folder ui.game. Pilih New > Kotlin File/Class.
- Berikan nama
GameViewModel
, lalu pilih Class dari daftar. - Ubah
GameViewModel
menjadi subclass dariViewModel
.ViewModel
adalah class abstrak, jadi Anda perlu memperluasnya untuk menggunakannya di aplikasi. Lihat definisi classGameViewModel
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
.
- Di bagian atas class
GameFragment
, tambahkan properti jenisGameViewModel
. - Lakukan inisialisasi
GameViewModel
menggunakan delegasi properti Kotlinby viewModels()
. Anda akan mempelajarinya lebih lanjut di bagian berikutnya.
private val viewModel: GameViewModel by viewModels()
- 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
.
- Pindahkan variabel data
score
,currentWordCount
,currentScrambledWord
ke classGameViewModel
.
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- 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
bersifatprivate
dan dapat diubah. Oleh karena itu, ini hanya dapat diakses dan diedit dalam classViewModel
. Konvensinya adalah untuk memberi awalan pada propertiprivate
dengan garis bawah.
Di luar class ViewModel
:
- Pengubah visibilitas default di Kotlin adalah
public
, sehinggacount
bersifat publik dan dapat diakses dari class lain seperti pengontrol UI. Karena hanya metodeget()
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 dalamViewModel
dari perubahan yang tidak diinginkan dan tidak aman oleh class eksternal, tetapi memungkinkan pemanggil eksternal mengakses nilainya dengan aman.
Menambahkan properti pendukung ke currentScrambledWord
- Dalam
GameViewModel
, ubah deklarasicurrentScrambledWord
untuk menambahkan properti pendukung. Sekarang_currentScrambledWord
hanya dapat diakses dan diedit dalamGameViewModel
. Pengontrol UI,GameFragment
, dapat membaca nilainya menggunakan properti hanya baca,currentScrambledWord
.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord
- Di
GameFragment
, perbarui metodeupdateNextWordOnScreen()
untuk menggunakan propertiviewModel
hanya baca,currentScrambledWord
.
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
- Di
GameFragment
, hapus kode di dalam metodeonSubmitWord()
danonSkipWord()
. 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:
Memahami siklus proses ViewModel
Tambahkan logging di GameViewModel
dan GameFragment
untuk membantu Anda lebih memahami siklus proses ViewModel
.
- Di
GameViewModel.kt
, tambahkan blokinit
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.
- Di class
GameViewModel
, ganti metodeonCleared()
.ViewModel
akan dihancurkan saat fragmen yang terkait dilepas, atau saat aktivitas selesai. Tepat sebelumViewModel
dihancurkan, callbackonCleared()
dipanggil. - Tambahkan laporan log di dalam
onCleared()
untuk melacak siklus prosesGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
- Di
GameFragment
dalamonCreateView()
, setelah Anda mendapatkan referensi ke objek binding, tambahkan laporan log untuk mencatat pembuatan fragmen. CallbackonCreateView()
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
}
- Di
GameFragment
, ganti metode callbackonDetach()
, yang akan dipanggil saat aktivitas dan fragmen yang terkait dihancurkan.
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
- Di Android Studio, jalankan aplikasi, buka jendela Logcat, dan filter di
GameFragment
. Perhatikan bahwaGameFragment
danGameViewModel
dibuat.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created!
- Aktifkan setelan putar otomatis di perangkat atau emulator dan ubah orientasi layar beberapa kali.
GameFragment
dihancurkan dan dibuat ulang setiap kali, tetapiGameViewModel
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!
- Keluar dari game atau keluar dari aplikasi menggunakan panah kembali.
GameViewModel
dihancurkan, dan callbackonCleared()
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 kecurrentWord.
- Buat kata yang ejaannya diacak dengan mengacak huruf di
currentWord
dan tetapkan kecurrentScrambledWord
. - 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
:
- Pada
GameViewModel,
tambahkan variabel class baru jenisMutableList<String>
yang disebutwordsList
, untuk menyimpan daftar kata yang Anda gunakan dalam game, untuk menghindari pengulangan. - Tambahkan variabel class lain yang disebut
currentWord
untuk menyimpan kata yang ingin disusun oleh pemain. Gunakan kata kuncilateinit
karena Anda akan menginisialisasi properti ini nanti.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
- Tambahkan metode
private
baru yang disebutgetNextWord()
, di atas blokinit
, tanpa parameter yang tidak menampilkan apa-apa. - Dapatkan kata acak dari
allWordsList
dan tetapkan kecurrentWord
.
private fun getNextWord() {
currentWord = allWordsList.random()
}
- Di
getNextWord()
, konversikan stringcurrentWord
ke array karakter dan tetapkan keval
baru yang disebuttempWord
. 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.
- 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()
}
- Tambahkan blok
if-else
untuk memeriksa apakah suatu kata telah digunakan. JikawordsList
berisicurrentWord
, panggilgetNextWord()
. Jika tidak, perbarui nilai_currentScrambledWord
dengan kata yang baru diacak, tingkatkan jumlah kata, dan tambahkan kata baru kewordsList
.
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
- 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.
- Jalankan aplikasi. Perhatikan bahwa kata pertama selalu "test".
- Untuk menampilkan kata yang ejaannya diacak di awal aplikasi, Anda perlu memanggil metode
getNextWord()
, yang selanjutnya memperbaruicurrentScrambledWord
. Lakukan panggilan ke metodegetNextWord()
di dalam blokinit
padaGameViewModel
.
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
- Tambahkan pengubah
lateinit
ke properti_currentScrambledWord
. Tambahkan penyebutan eksplisit jenis dataString
, karena tidak ada nilai awal yang diberikan.
private lateinit var _currentScrambledWord: String
- Jalankan aplikasi. Perhatikan kata baru yang ejaannya diacak akan ditampilkan saat peluncuran aplikasi. Keren!
Menambahkan metode bantuan
Selanjutnya, tambahkan metode bantuan untuk memproses dan memodifikasi data di dalam ViewModel
. Anda akan menggunakan metode ini di tugas selanjutnya.
- Di class
GameViewModel
, tambahkan metode lain yang disebutnextWord().
Dapatkan kata berikutnya dari daftar dan tampilkantrue
jika jumlah kata kurang dariMAX_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.
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
- Dialog Pemberitahuan
- Judul (opsional)
- Pesan
- 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.
- Pertama-tama, tambahkan properti pendukung ke variabel
score
. DiGameViewModel
, ubah deklarasi variabelscore
menjadi yang berikut.
private var _score = 0
val score: Int
get() = _score
- Di
GameFragment
, tambahkan fungsi pribadi bernamashowFinalScoreDialog()
. Untuk membuatMaterialAlertDialog
, gunakan classMaterialAlertDialogBuilder
untuk membuat bagian dialog langkah demi langkah. Panggil konstruktorMaterialAlertDialogBuilder
yang meneruskan konten menggunakan metoderequireContext()
fragmen. MetoderequireContext()
menampilkan nilai non-nullContext
.
/*
* 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
.
- Tambahkan kode untuk menetapkan judul pada dialog pemberitahuan, gunakan resource string dari
strings.xml
.
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
- 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))
- Jadikan dialog pemberitahuan tidak dapat dibatalkan saat tombol kembali ditekan, menggunakan metode
setCancelable()
dan meneruskanfalse
.
.setCancelable(false)
- Tambahkan dua tombol teks EXIT dan PLAY AGAIN menggunakan metode
setNegativeButton()
dansetPositiveButton()
. Panggil masing-masingexitGame()
danrestartGame()
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
.
- Di bagian akhir, tambahkan
show()
, yang akan membuat lalu menampilkan dialog pemberitahuan.
.show()
- 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
- Jika Anda belum melakukannya, di
GameFragment
, hapus kode di dalamonSubmitWord()
yang akan dipanggil saat tombol Submit diketuk. - Tambahkan centang pada nilai return metode
viewModel.nextWord()
. Jikatrue
, kata lain tersedia, jadi perbarui kata yang ejaannya diacak di layar menggunakanupdateNextWordOnScreen()
. Jika tidak diperbarui, game akan berakhir. Jadi, tampilkan dialog pemberitahuan dengan skor akhir.
private fun onSubmitWord() {
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- Jalankan aplikasi! Bermainlah dengan beberapa kata. Ingat, Anda belum menerapkan tombol Skip, sehingga Anda tidak dapat melewati kata tersebut.
- 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.
Menambahkan metode bantuan untuk memvalidasi kata pemain
- Di
GameViewModel
, tambahkan metode pribadi baru yang disebutincreaseScore()
tanpa parameter dan nilai yang ditampilkan. Tingkatkan variabelscore
sebesarSCORE_INCREASE
.
private fun increaseScore() {
_score += SCORE_INCREASE
}
- Dalam
GameViewModel
, tambahkan metode bantuan yang disebutisUserWordCorrect()
yang menampilkanBoolean
dan mengambilString
, kata dari pemain, sebagai parameter. - 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.
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.
- Pada
GameFragment,
di awalonSubmitWord()
, buatval
yang bernamaplayerWord
. Simpan kata pemain di dalamnya, dengan mengekstraknya dari kolom teks dalam variabelbinding
.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
...
}
- Pada
onSubmitWord()
, di bawah deklarasiplayerWord
, validasikan kata pemain. Tambahkan pernyataanif
untuk memeriksa kata pemain menggunakan metodeisUserWordCorrect()
, dengan meneruskanplayerWord
. - Di dalam blok
if
, reset kolom teks, panggilsetErrorTextField
dengan memasukkanfalse
. - 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()
}
}
}
- Jika kata pengguna salah, tampilkan pesan error di kolom teks. Tambahkan blok
else
ke blokif
di atas, lalu panggilsetErrorTextField()
yang meneruskantrue
. MetodeonSubmitWord()
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)
}
}
- 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.
10. Mengimplementasikan tombol Skip
Dalam tugas ini, Anda menambahkan implementasi untuk onSkipWord()
yang menangani saat tombol Skip diklik.
- Serupa dengan
onSubmitWord()
, tambahkan kondisi dalam metodeonSkipWord()
. Jikatrue
, tampilkan kata di layar dan reset kolom teks. Jikafalse
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()
}
}
- 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.
- Di
GameViewModel
, klik kanan pada variabelcurrentWordCount
, pilih Refactor > Rename... . Awali nama baru dengan garis bawah,_currentWordCount
. - Tambahkan kolom pendukung.
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
- Dalam
GameFragment
di dalamonCreateView()
, 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}")
- 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
- 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. - Untuk mereset data aplikasi, di
GameViewModel
, tambahkan metode yang disebutreinitializeData()
. Tetapkan skor dan jumlah kata menjadi0
. Hapus daftar kata dan panggil metodegetNextWord()
.
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
- Pada
GameFragment
di bagian atas metoderestartGame()
, lakukan panggilan ke metode yang baru dibuat,reinitializeData()
.
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
- 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.
Di akhir 10 kata, game berakhir dan dialog pemberitahuan muncul dengan skor akhir dan opsi untuk keluar dari game atau bermain lagi.
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
atauFragment
. 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 dalamViewModel
. - Class
ViewModel
menyimpan dan mengelola data terkait UI. ClassViewModel
memungkinkan data bertahan saat terjadi perubahan konfigurasi seperti pada saat rotasi layar. ViewModel
adalah salah satu Komponen Arsitektur Android yang direkomendasikan.