Auto Font Size dan Hide/Unhide Text Balance (Jetpack Compose, Data Store)
Bagian 1: Digital Bank Application Exploration
Hello Folks! Pada artikel kali ini, saya akan membagikan lanjutan dari eksplorasi Digital Bank App. Pada artikel sebelumnya, telah dibahas mengenai cara untuk membuat beberapa composable function untuk menampilkan komponen yang menyusun sebuah SavingCard. Jika teman — teman belum mempelajari pembahasan sebelumnya bisa cek bagian 0 dibawah ini, karena itu akan membantu memahami bagian ini.
Salah satu komponen dalam Saving Card adalah Text Balance. Text Balance memiliki beberapa kondisi yang perlu diperhatikan :
- Font size dibuat adaptive sehingga ketika balance memiliki text yang panjang, tidak terjadi overlapping.
- Visibility dapat diatur visible atau hide dengan button eye pada balance.
Membuat Custom Text Adaptive Font Size
Untuk membuat sebuah text dengan adaptive font size diperlukan sebuah composable function dengan nama AutoSizeText. Tambahkan composable function ini kedalam CustomText.kt yang telah dibuat sebelumnya pada artikel part 0. Untuk mendapatkan sebuah text dengan font size yang adaptive, tulislah kode seperti dibawah ini.
@Composable
fun AutoSizeText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = MaterialTheme.typography.bodyMedium,
maxLines: Int = Int.MAX_VALUE,
textAlign: TextAlign? = null
) {
var textStyleState by remember { mutableStateOf(style) }
var readyToDraw by remember { mutableStateOf(false) }
Text(
text = text,
modifier = modifier.drawWithContent {
if (readyToDraw) {
drawContent()
}
},
style = textStyleState,
maxLines = maxLines,
softWrap = false,
textAlign = textAlign,
onTextLayout = { textLayoutResult ->
if (textLayoutResult.didOverflowWidth) {
textStyleState = textStyleState.copy(fontSize = textStyleState.fontSize * 0.9)
} else {
readyToDraw = true
}
}
)
}
Kode diatas pada dasarnya membuat sebuah komponen Text yang mampu mengurangi font size jika panjang dari text semakin bertambah, sehingga seluruh angka akan tetap tertampung didalam card. Untuk penjelasan lebih detail perhatikan beberapa poin dibawah ini :
- textStyleState merupakan sebuah variable yang akan menyimpan style dari text. Selain itu penggunaan remember function membuat variable ini mampu mengingat value saat terjadi proses recomposition. Sehingga ketika style sudah diatur dan disimpan kedalam variable ini style tidak kembali ke setup awal ketika UI diupdate. Perlu dipahami bahwa variable ini bersifat mutable atau mampu mengupdate value yang disimpan jika terjadi perubahan serta mengupdate UI melalui proses recomposition.
- readyToDraw merupakan variable yang bersifat mutable yang digunakan untuk menandai apakah teks sudah siap untuk digambar setelah kalkulasi layout awal selesai, sehingga hala ini dapan mencegah teks digambar sebelum siap. Pada kode diatas implementasi dari variable ini adalah pengecekan value if (readyToDraw) { drawContent() } Teks hanya akan digambar (drawContent()) jika readyToDraw bernilai true. Ini mencegah teks digambar sebelum perhitungan tata letak awal selesai.
- didOverflowWidth merupakan fungsi yang digunakan untuk memerikasa teks over secara horizontal atau tidak yang diimplementasikan dengan logika condition if (textLayoutResult.didOverflowWidth).
- fontSize = textStyleState.fontSize * 0.9 merupakan perhitungan untuk mengurangi ukuran font 10%.
Selanjutnya pada BalanceText composable function replace Text() dengan AutoSizeText() seperti dibawah ini.
AutoSizeText(
text = if (isVisible) "RP $balance" else "RP ********",
style = style,
modifier = Modifier.weight(3f)
)
Mengatur Eye Button dan Mengimplementasikan Data Store
Untuk membuat balance dapat di hide dan unhide diperlukan sebuah button handle, seperti dibawah ini.
Terdapat 2 kondisi yang perlu diperhatikan untuk membuat komponen ini :
- Dalam kondisi Unhide maka balance akan menampilkan saldo dan button eye akan menampilkan hide icon.
- Dalam kondisi Hide maka balance akan menyembunyikan saldo dan button eye akan menampilkan unhide icon.
Perlu diperhatikan kondisi terakhir dari balance harus disimpan secara lokal. Hal ini bertujuan agar saat berpindah ke page lain atau menutup aplikasi, ketika page balance ini dibuka kembali, balance akan ditampilkan sesuai dengan kondisi terakhir balance. Sehingga diperlukan sebuah status visibility dari balance yang disimpan, diupdate serta dibaca untuk menampilkan balance sesuai last statusnya. Untuk menyimpan visibility status diperlukan sebuah local storage. Dalam hal ini kita akan menggunakan Jetpack Datastore.
Datastore sendiri merupakan sebuah Jetpack Library yang mampu menyimpan key-value atau objek yang ditulis dengan protocol buffers.
Mungkin teman — teman merasa bingung kenapa tidak menggunakan Shared Prefferences saja jika sama — sama mampu menyimpan key-value. Jawabannya adalah Datastore mampu menyimpan data secara asinkron, konsisten, dan transaksional dengan medukung penggunaan coroutines dan Flow. Selain Key-Value Datastore juga mampu menyimpan sebuah object dengan protocol buffers. Protocol buffers (protobuf) sendiri merupakan cara untuk mendefinisikan struktur data secara terstruktur. Sebagai contoh protobuf, kita tidak hanya bisa menyimpan Key-Value tetapi lebih kompleks dari itu misal Data User yang berisi nama, rekening, umur, alamat. Namun objek tersebut disimpan dalam format biner sehingga sangat efisien dan ringan. Untuk mengetahui lebih detail mengenai fitur yang dimiliki oleh Datastore perhatikan tabel di bawah ini.
Pada projek ini yang akan disimpan berupa Key-Value sehingga Preference Datastore yang tepat untuk digunakan dalam case ini. Langkah awal untuk mengimplementasikan preference datastore adalah menambahkan library Preference Datastore kedalam proyek.
- Tambahkan kode dibawah ini pada file libs.versions.toml, karena proyek ini menggunakan Catalog Version.
// add version
dataStoreVersion = "1.1.2"
// add library
datastore-preferences = {group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStoreVersion"}
- Tambahkan implementation library pada dependency section pada build.gradle.kts(Module :app)
dependencies {
...
implementation(libs.datastore.preferences)
...
}
- Klik Sync project button atau File → Sync Project with Gradle File
- Selanjutnya buatlah sebuah package baru dengan nama helper pada package utils (Right Click pada utils package → New → Package )
- Didalam package helper buat sebuah file baru berupa object dengan nama BankingDataStore.
- Tambahkan kode seperti dibawah ini pada object BankingDataStore
object BankingDataStore {
private const val PREFERENCES_DATA_STORE_NAME = "bank_pref"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = PREFERENCES_DATA_STORE_NAME)
private val BALANCE_VISIBLE_KEY = booleanPreferencesKey("BALANCE_VISIBLE_KEY")
suspend fun saveIsVisible(context: Context, isVisible: Boolean) {
context.dataStore.edit { preferences ->
preferences[BALANCE_VISIBLE_KEY] = isVisible
}
}
fun getIsVisible(context: Context): Flow<Boolean> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[BALANCE_VISIBLE_KEY] ?: true
}
}
Kode diatas merupakan abstraksi untuk menyimpan dan memperoleh data boolean dari balance visibility yang nantinya disimpan secara lokal dengan menggunakan DataStore Preferences. Jika kita perhatikan kode diatas menggunakan extension property untuk memudahkan dalam mengakses DataStore dari Context, property delegation untuk DataStore, dan suspend function agar dapat berjalan secara asyncronous. Untuk penjelasan lebih detail perhatikan beberapa poin berikut :
- PREFERENCES_DATA_STORE_NAME merupakan nama konstanta yang menjelaskan fungsinya sebagai nama file DataStore. Sedangkan bank_pref merupakan nilai dari konstanta tersebut. Nama file ini nantinya yang akan digunakan DataStore untuk menyimpan data. DataStore akan membuat file dengan nama ini di penyimpanan internal aplikasi.
- Context.dataStore merupakan extension property pada kelas Context. Sehingga ini berarti kita dapat mengakses dataStore langsung dari objek Context (seperti di Activity atau Application).
- DataStore<Preferences> merupakan data type dari dataStore, sehingga objek DataStore akan menyimpan data dalam bentuk Preferences (pair key-value).
- BALANCE_VISIBLE_KEY merupakan variable private yang mendeklarasikan nama properti berupa key untuk menyimpan balance visibility.
- booleanPreferencesKey() merupakan fungsi dari library DataStore yang akan membuat key untuk menyimpan nilai boolean. Sedangkan “BALANCE_VISIBLE_KEY” yang dideklarasikan didalam fungsi tersebut merupakan string yang menjadi nama key. Nantinya kita akan menggunakan key ini untuk menyimpan dan mendapatkan nilai boolean.
- context.dataStore merupakan kode yang digunakan untuk mengakses objek DataStore melalui Context. Sedangkan edit berperan sebagai fungsi untuk memodifikasi DataStore. Untuk preferences sendiri merupakan objek yang merepresentasikan data di DataStore. Blok kode ini akan dijalankan di luar main thread, karena berada pada suspend function.
- Flow<Boolean> merupakan tipe kembalian dari fungsi getVisible.
- context.dataStore.data merupakan blok code yang digunakan untuk mengakses data di DataStore sebagai Flow<Preferences>.
- .map { preferences -> } merupakan kode untuk merubah data berupa Flow<Preferences> menjadi Flow<Boolean>. Sedangkan didalam blok code terdapat preferences[BALANCE_VISIBLE_KEY] ?: true yang akan mengambil nilai dengan key BALANCE_VISIBLE_KEY. Jika key tersebut kosong, maka akan mengembalikan nilai true sebagai nilai default.
Flow merupakan stream data yang dapat meng-emit beberapa nilai dari waktu ke waktu. Sehingga cocok untuk membaca data dari DataStore karena data mungkin akan berubah.
Update Eye Button State dengan Datastore
- Buka MainActivity.kt lalu tambahkan variable isVisible dan isVisibleFlow, seperti dibawah ini:
val isVisibleFlow = BankingDataStore.getIsVisible(this)
val isVisible by isVisibleFlow.collectAsState(initial = true)
Kedua variable diatas memiliki kegunaan sebagai berikut :
- isVisibleFlow merupakan variable immutable yang memiliki value
BankingDataStore.getIsVisible(this). Value tersebut merupakan pemanggilan fungsi getIsVisible yang berada di dalam object BankingDataStore, sehingga nilai balance visibility yang disimpan di DataStore dapat dibaca. - isVisible merupakan variabel immutable dengan nilai isVisible yang tidak disimpan secara langsung, tetapi didelegasikan ke objek lain berupa hasil collectAsState dari isVisibleFlow. Sehingga
isVisible berperan sebagai variabel yang menyimpan nilai Boolean yang berasal dari Flow. Setiap kali Flow meng-emit nilai baru, isVisible akan secara otomatis diupdate, selain itu karena isVisible merupakan State maka nantinya komponen UI yang menggunakan isVisible akan otomatis di-recompose.
- Selanjutnya modify code pada koponen SavingCard pada MainActivity.kt seperti dibawah ini.
SavingCard(
// other code
isVisible = isVisible,
eyeClick = {
coroutineScope.launch {
BankingDataStore.saveIsVisible(this@MainActivity, !isVisible)
}
})
Kode diatas memiliki fungsi sebagai berikut :
- isVisible = isVisible merupakan parameter yang diberikan ke SavingCard dengan value isVisible yang merupakan variabel State<Boolean> sehingga jika isVisible bernilai true maka saldo akan ditampilkan dan sebaliknya jika false maka saldo akan disembunyikan. Karena isVisible adalah State, makan SavingCard akan otomatis di-recompose setiap kali nilai isVisible berubah.
- saveIsVisible() merupakan fungsi yang digunakan untuk menyimpan nilai visibilitas saldo ke DataStore. Karena kita akan berinteraksi dengan DataStore, yang merupakan operasi I/O dan harus dilakukan di luar main thread, maka blok code perlu wrap dengan sebuah coroutine. Sehingga coroutineScope.launch{} digunakan, karena fungsi tersebut akan memastikan BankingDataStore.saveIsVisible(this@MainActivity, !isVisible) dieksekusi didalam coroutine.
Sebagai langkah terakhir jalankan aplikasi, dan akan didapat hasil seperti dibawah ini.
Cukup mudah bukan? :) Jika teman — teman merasa artikel ini bermanfaat, jangan lupa follow akun ini dan clap untuk artikel berikutnya.
:) cheers!!