Cara Membuat Expense Tracker dengan Kotlin

Mengelola keuangan pribadi seringkali terasa rumit. Tanpa pencatatan yang rapi, pengeluaran bisa membengkak tanpa disadari. Di sinilah peran aplikasi expense tracker menjadi sangat vital. Bayangkan memiliki asisten pribadi di saku Anda yang siap mencatat setiap transaksi, memberikan gambaran jelas ke mana uang Anda pergi, dan membantu Anda mengambil keputusan finansial yang lebih baik.

Sebagai seorang developer, membangun aplikasi semacam ini dengan Kotlin di Android adalah proyek yang sangat rewarding. Selain melatih skill pengembangan aplikasi mobile Anda, aplikasi ini juga bisa sangat berguna untuk diri sendiri atau bahkan berpotensi menjadi produk yang bermanfaat bagi orang lain. Dalam panduan ini, kita akan menyelami langkah demi langkah cara membuat aplikasi expense tracker sederhana namun fungsional menggunakan Kotlin, mulai dari desain antarmuka hingga persistensi data dengan Room Database.

Kita akan fokus pada praktik terbaik pengembangan Android modern, menggunakan komponen seperti ViewModel dan LiveData untuk arsitektur yang bersih dan reaktif. Artikel ini dirancang agar mudah diikuti oleh developer Android pemula hingga menengah, namun tetap memberikan insight mendalam yang relevan untuk praktisi yang lebih berpengalaman.

Apa yang Akan Kita Bangun?

Aplikasi expense tracker kita akan memiliki fungsionalitas dasar namun esensial:

  • Mencatat Pengeluaran: Pengguna dapat menambahkan pengeluaran baru dengan detail seperti jumlah, kategori, dan deskripsi.
  • Menampilkan Daftar Pengeluaran: Semua pengeluaran yang tercatat akan ditampilkan dalam daftar yang mudah dibaca.
  • Ringkasan Total: Menampilkan total pengeluaran secara keseluruhan atau dalam periode tertentu.
  • Hapus Pengeluaran: Kemampuan untuk menghapus entri pengeluaran yang salah atau tidak relevan.

Fokus kita adalah pada inti fungsionalitas pencatatan keuangan yang solid. Fitur lanjutan seperti filter, laporan visual, atau sinkronisasi cloud bisa menjadi ide pengembangan di masa depan.

Persiapan Awal: Tools dan Pengetahuan

Sebelum memulai, pastikan Anda memiliki beberapa hal berikut:

  • Android Studio: Lingkungan pengembangan terintegrasi (IDE) resmi untuk pengembangan Android. Pastikan sudah terinstall versi terbaru.
  • Dasar Kotlin: Pemahaman dasar sintaksis Kotlin sangat membantu.
  • Dasar Pengembangan Android: Pengetahuan tentang Activity, Layout XML, dan cara kerja komponen Android dasar lainnya akan mempermudah.
  • Koneksi Internet: Untuk mengunduh dependencies.

Anda tidak perlu menjadi seorang ahli. Semangat untuk belajar dan mencoba adalah modal utama dalam project ini.

Struktur Proyek dan Dependencies yang Dibutuhkan

Untuk project ini, kita akan memanfaatkan beberapa library dan komponen Android Jetpack yang sangat populer dan direkomendasikan:

  • Room Database: Untuk persistensi data lokal. Ini adalah abstraksi di atas SQLite yang mempermudah interaksi database.
  • ViewModel: Untuk menyimpan dan mengelola data yang terkait dengan UI agar data tetap ada selama perubahan konfigurasi (misalnya rotasi layar).
  • LiveData: Komponen yang dapat diamati untuk menyimpan data. Ia adalah data holder yang dapat diamati dan sadar siklus hidup, artinya ia hanya memperbarui observer yang dalam kondisi aktif.
  • Material Design: Untuk UI yang modern dan konsisten.
  • Coroutines: Untuk operasi asinkron, terutama saat berinteraksi dengan database.

Tambahkan dependencies berikut ke file build.gradle (Module: app) Anda:

dependencies {

implementation 'androidx.core:core-ktx:1.X.X'

implementation 'androidx.appcompat:appcompat:1.X.X'

implementation 'com.google.android.material:material:1.X.X'

implementation 'androidx.constraintlayout:constraintlayout:2.X.X'

// Room components

implementation 'androidx.room:room-runtime:2.X.X'

kapt 'androidx.room:room-compiler:2.X.X'

implementation 'androidx.room:room-ktx:2.X.X' // Kotlin Extensions and Coroutines support for Room

// ViewModel and LiveData

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.X.X'

implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.X.X'

// Coroutines

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.X.X'

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.X.X'

}

Ganti X.X.X dengan versi terbaru yang tersedia. Jangan lupa tambahkan plugin kotlin-kapt di bagian atas file build.gradle (Module: app):

plugins {

id 'com.android.application'

id 'org.jetbrains.kotlin.android'

id 'kotlin-kapt' // Tambahkan ini

}

Setelah menambahkan dependencies, sinkronkan proyek Anda.

Langkah 1: Desain Database dengan Room

Room Database adalah inti dari penyimpanan data kita. Kita perlu membuat tiga komponen utama:

  1. Entity: Representasi tabel dalam database.
  2. DAO (Data Access Object): Interface untuk operasi database (insert, query, delete).
  3. Database: Kelas utama untuk mengakses DAO dan konfigurasi database.

Membuat Expense Entity

Buat file Kotlin baru bernama Expense.kt di folder data (buat jika belum ada):

package com.tubianto.expensetracker.data

import androidx.room.ColumnInfo

import androidx.room.Entity

import androidx.room.PrimaryKey

@Entity(tableName = "expenses")

data class Expense(

@PrimaryKey(autoGenerate = true) val id: Int = 0,

@ColumnInfo(name = "amount") val amount: Double,

@ColumnInfo(name = "category") val category: String,

@ColumnInfo(name = "description") val description: String,

@ColumnInfo(name = "timestamp") val timestamp: Long = System.currentTimeMillis()

)

Di sini, @Entity menandakan ini adalah tabel. @PrimaryKey(autoGenerate = true) untuk ID unik yang otomatis. @ColumnInfo untuk nama kolom.

Membuat Expense DAO

Buat file Kotlin baru bernama ExpenseDao.kt di folder data:

package com.tubianto.expensetracker.data

import androidx.room.Dao

import androidx.room.Insert

import androidx.room.OnConflictStrategy

import androidx.room.Query

import androidx.room.Delete

import kotlinx.coroutines.flow.Flow

@Dao

interface ExpenseDao {

@Query("SELECT * FROM expenses ORDER BY timestamp DESC")

fun getAllExpenses(): Flow>

@Insert(onConflict = OnConflictStrategy.IGNORE)

suspend fun insert(expense: Expense)

@Delete

suspend fun delete(expense: Expense)

@Query("SELECT SUM(amount) FROM expenses")

fun getTotalExpenses(): Flow

}

@Dao menandakan ini adalah Data Access Object. Kita memiliki method untuk mendapatkan semua pengeluaran (sebagai Flow untuk observasi real-time), menyisipkan, menghapus, dan mendapatkan total pengeluaran. Fungsi suspend menandakan bahwa operasi ini adalah coroutine dan akan berjalan di background.

Membuat AppDatabase

Buat file Kotlin baru bernama AppDatabase.kt di folder data:

package com.tubianto.expensetracker.data

import android.content.Context

import androidx.room.Database

import androidx.room.Room

import androidx.room.RoomDatabase

@Database(entities = [Expense::class], version = 1, exportSchema = false)

abstract class AppDatabase : RoomDatabase() {

abstract fun expenseDao(): ExpenseDao

companion object {

@Volatile

private var INSTANCE: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {

return INSTANCE ?: synchronized(this) {

val instance = Room.databaseBuilder(

context.applicationContext,

AppDatabase::class.java,

"expense_database"

).build()

INSTANCE = instance

instance

}

}

}

}

@Database menandakan ini adalah kelas database utama, dengan daftar entity yang digunakan dan versi database. exportSchema = false agar tidak mengekspor skema ke folder. getDatabase adalah metode singleton untuk memastikan hanya ada satu instance database.

Langkah 2: Model Data dan UI (Layout XML)

Kita akan mendesain tiga layout utama:

  1. activity_main.xml: Layout utama untuk menampilkan daftar pengeluaran dan total.
  2. item_expense.xml: Layout untuk setiap item pengeluaran dalam RecyclerView.
  3. add_expense_dialog.xml: Layout untuk dialog tambah pengeluaran.

activity_main.xml

Letakkan ini di res/layout/activity_main.xml:

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity">

android:id="@+id/tvTotalExpensesLabel"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Total Pengeluaran:"

android:textSize="20sp"

android:textStyle="bold"

android:layout_marginStart="16dp"

android:layout_marginTop="16dp"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/tvTotalExpenses"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Rp 0.00"

android:textSize="24sp"

android:textStyle="bold"

android:textColor="@color/design_default_color_primary"

android:layout_marginStart="8dp"

app:layout_constraintStart_toEndOf="@+id/tvTotalExpensesLabel"

app:layout_constraintTop_toTopOf="@+id/tvTotalExpensesLabel"

app:layout_constraintBottom_toBottomOf="@+id/tvTotalExpensesLabel" />

android:id="@+id/recyclerViewExpenses"

android:layout_width="0dp"

android:layout_height="0dp"

android:layout_marginTop="16dp"

app:layout_constraintTop_toBottomOf="@+id/tvTotalExpensesLabel"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintBottom_toBottomOf="parent"

tools:listitem="@layout/item_expense" />

android:id="@+id/fabAddExpense"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_margin="16dp"

android:clickable="true"

android:focusable="true"

app:srcCompat="@android:drawable/ic_input_add"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintBottom_toBottomOf="parent" />

Layout ini berisi TextView untuk total pengeluaran, RecyclerView untuk menampilkan daftar, dan FloatingActionButton untuk menambahkan pengeluaran baru.

item_expense.xml

Letakkan ini di res/layout/item_expense.xml:

xmlns:app="http://schemas.android.com/apk/res-auto"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_margin="8dp"

app:cardCornerRadius="8dp"

app:cardElevation="4dp">

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:padding="16dp">

android:id="@+id/tvExpenseAmount"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Rp 150.000"

android:textSize="18sp"

android:textStyle="bold"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/tvExpenseCategory"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Makanan"

android:textSize="14sp"

android:layout_marginTop="4dp"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toBottomOf="@+id/tvExpenseAmount" />

android:id="@+id/tvExpenseDescription"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:text="Makan siang di warung padang"

android:textSize="12sp"

android:layout_marginTop="4dp"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintTop_toBottomOf="@+id/tvExpenseCategory" />

android:id="@+id/btnDeleteExpense"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="?attr/selectableItemBackgroundBorderless"

android:contentDescription="Hapus Pengeluaran"

android:padding="8dp"

app:srcCompat="@android:drawable/ic_menu_delete"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintTop_toTopOf="parent"

app:layout_constraintBottom_toBottomOf="parent" />

Setiap item akan ditampilkan dalam CardView dengan detail pengeluaran dan tombol hapus.

add_expense_dialog.xml

Letakkan ini di res/layout/add_expense_dialog.xml:

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:orientation="vertical"

android:padding="24dp">

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Tambah Pengeluaran Baru"

android:textSize="20sp"

android:textStyle="bold"

android:layout_marginBottom="16dp" />

android:id="@+id/tilAmount"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:hint="Jumlah">

android:id="@+id/etAmount"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:inputType="numberDecimal" />

android:id="@+id/tilCategory"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_marginTop="8dp"

android:hint="Kategori">

android:id="@+id/etCategory"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:inputType="textCapWords" />

android:id="@+id/tilDescription"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_marginTop="8dp"

android:hint="Deskripsi (Opsional)">

android:id="@+id/etDescription"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:inputType="textMultiLine"

android:maxLines="3" />

android:id="@+id/btnAddExpenseDialog"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_marginTop="24dp"

android:text="Tambah" />

Dialog ini akan muncul ketika pengguna menekan FloatingActionButton, memungkinkan mereka memasukkan detail pengeluaran.

Langkah 3: Adapter untuk RecyclerView

RecyclerView membutuhkan Adapter untuk mengisi data ke dalam layout item. Buat file Kotlin baru bernama ExpenseAdapter.kt di folder adapter (buat jika belum ada):

package com.tubianto.expensetracker.adapter

import android.view.LayoutInflater

import android.view.View

import android.view.ViewGroup

import android.widget.ImageButton

import android.widget.TextView

import androidx.recyclerview.widget.DiffUtil

import androidx.recyclerview.widget.ListAdapter

import androidx.recyclerview.widget.RecyclerView

import com.tubianto.expensetracker.R

import com.tubianto.expensetracker.data.Expense

import java.text.NumberFormat

import java.util.Locale

class ExpenseAdapter(private val onDeleteClick: (Expense) -> Unit) :

ListAdapter(ExpensesDiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpenseViewHolder {

val view = LayoutInflater.from(parent.context)

.inflate(R.layout.item_expense, parent, false)

return ExpenseViewHolder(view)

}

override fun onBindViewHolder(holder: ExpenseViewHolder, position: Int) {

val expense = getItem(position)

holder.bind(expense)

}

inner class ExpenseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

private val tvAmount: TextView = itemView.findViewById(R.id.tvExpenseAmount)

private val tvCategory: TextView = itemView.findViewById(R.id.tvExpenseCategory)

private val tvDescription: TextView = itemView.findViewById(R.id.tvExpenseDescription)

private val btnDelete: ImageButton = itemView.findViewById(R.id.btnDeleteExpense)

fun bind(expense: Expense) {

val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))

tvAmount.text = formatter.format(expense.amount)

tvCategory.text = expense.category

tvDescription.text = expense.description

btnDelete.setOnClickListener { onDeleteClick(expense) }

}

}

private class ExpensesDiffCallback : DiffUtil.ItemCallback() {

override fun areItemsTheSame(oldItem: Expense, newItem: Expense): Boolean {

return oldItem.id == newItem.id

}

override fun areContentsTheSame(oldItem: Expense, newItem: Expense): Boolean {

return oldItem == newItem

}

}

}

Kita menggunakan ListAdapter dengan DiffUtil untuk performa yang lebih baik saat daftar diperbarui. Callback onDeleteClick memungkinkan Activity untuk menangani event penghapusan.

Langkah 4: ViewModel dan Repository

Untuk memisahkan logika UI dari logika data, kita akan menggunakan pola Repository dan ViewModel.

Membuat ExpenseRepository

Repository bertugas sebagai satu-satunya sumber data. Buat file Kotlin baru bernama ExpenseRepository.kt di folder repository (buat jika belum ada):

package com.tubianto.expensetracker.repository

import com.tubianto.expensetracker.data.Expense

import com.tubianto.expensetracker.data.ExpenseDao

import kotlinx.coroutines.flow.Flow

class ExpenseRepository(private val expenseDao: ExpenseDao) {

val allExpenses: Flow> = expenseDao.getAllExpenses()

val totalExpenses: Flow = expenseDao.getTotalExpenses()

suspend fun insert(expense: Expense) {

expenseDao.insert(expense)

}

suspend fun delete(expense: Expense) {

expenseDao.delete(expense)

}

}

Repository menyediakan API yang bersih untuk mengakses data dari DAO. Ini adalah praktik terbaik untuk mengisolasi sumber data.

Membuat ExpenseViewModel

ViewModel bertanggung jawab untuk menyediakan data ke UI dan mempertahankan data selama perubahan konfigurasi. Buat file Kotlin baru bernama ExpenseViewModel.kt di folder viewmodel (buat jika belum ada):

package com.tubianto.expensetracker.viewmodel

import androidx.lifecycle.LiveData

import androidx.lifecycle.ViewModel

import androidx.lifecycle.ViewModelProvider

import androidx.lifecycle.asLiveData

import androidx.lifecycle.viewModelScope

import com.tubianto.expensetracker.data.Expense

import com.tubianto.expensetracker.repository.ExpenseRepository

import kotlinx.coroutines.launch

class ExpenseViewModel(private val repository: ExpenseRepository) : ViewModel() {

val allExpenses: LiveData> = repository.allExpenses.asLiveData()

val totalExpenses: LiveData = repository.totalExpenses.asLiveData()

fun insert(expense: Expense) = viewModelScope.launch {

repository.insert(expense)

}

fun delete(expense: Expense) = viewModelScope.launch {

repository.delete(expense)

}

}

class ExpenseViewModelFactory(private val repository: ExpenseRepository) : ViewModelProvider.Factory {

override fun create(modelClass: Class): T {

if (modelClass.isAssignableFrom(ExpenseViewModel::class.java)) {

@Suppress("UNCHECKED_CAST")

return ExpenseViewModel(repository) as T

}

throw IllegalArgumentException("Unknown ViewModel class")

}

}

ViewModel menerima instance Repository dan menyediakan LiveData untuk diamati oleh UI. Semua operasi database dijalankan dalam viewModelScope.launch untuk memastikan operasi dilakukan di background dan terkait dengan siklus hidup ViewModel. Kita juga membutuhkan ExpenseViewModelFactory untuk membuat instance ViewModel dengan dependency Repository.

Langkah 5: Mengimplementasikan Logika di MainActivity

Sekarang saatnya menghubungkan semua komponen di MainActivity.kt.

package com.tubianto.expensetracker

import android.app.AlertDialog

import android.os.Bundle

import android.view.LayoutInflater

import android.widget.Button

import android.widget.EditText

import android.widget.Toast

import androidx.activity.viewModels

import androidx.appcompat.app.AppCompatActivity

import androidx.recyclerview.widget.LinearLayoutManager

import com.tubianto.expensetracker.adapter.ExpenseAdapter

import com.tubianto.expensetracker.data.AppDatabase

import com.tubianto.expensetracker.data.Expense

import com.tubianto.expensetracker.databinding.ActivityMainBinding

import com.tubianto.expensetracker.repository.ExpenseRepository

import com.tubianto.expensetracker.viewmodel.ExpenseViewModel

import com.tubianto.expensetracker.viewmodel.ExpenseViewModelFactory

import java.text.NumberFormat

import java.util.Locale

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private val expenseViewModel: ExpenseViewModel by viewModels {

ExpenseViewModelFactory(ExpenseRepository(AppDatabase.getDatabase(applicationContext).expenseDao()))

}

private lateinit var expenseAdapter: ExpenseAdapter

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)

setContentView(binding.root)

setupRecyclerView()

setupObservers()

binding.fabAddExpense.setOnClickListener {

showAddExpenseDialog()

}

}

private fun setupRecyclerView() {

expenseAdapter = ExpenseAdapter { expense ->

// Handle delete action

expenseViewModel.delete(expense)

Toast.makeText(this, "Pengeluaran dihapus!", Toast.LENGTH_SHORT).show()

}

binding.recyclerViewExpenses.apply {

adapter = expenseAdapter

layoutManager = LinearLayoutManager(this@MainActivity)

}

}

private fun setupObservers() {

expenseViewModel.allExpenses.observe(this) { expenses ->

expenses?.let {

expenseAdapter.submitList(it)

}

}

expenseViewModel.totalExpenses.observe(this) { total ->

val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))

binding.tvTotalExpenses.text = formatter.format(total ?: 0.0)

}

}

private fun showAddExpenseDialog() {

val dialogView = LayoutInflater.from(this).inflate(R.layout.add_expense_dialog, null)

val etAmount = dialogView.findViewById(R.id.etAmount)

val etCategory = dialogView.findViewById(R.id.etCategory)

val etDescription = dialogView.findViewById(R.id.etDescription)

val btnAdd = dialogView.findViewById

val dialog = AlertDialog.Builder(this)

.setView(dialogView)

.create()

btnAdd.setOnClickListener {

val amountStr = etAmount.text.toString()

val category = etCategory.text.toString().trim()

val description = etDescription.text.toString().trim()

if (amountStr.isNotBlank() && category.isNotBlank()) {

try {

val amount = amountStr.toDouble()

if (amount > 0) {

val expense = Expense(amount = amount, category = category, description = description)

expenseViewModel.insert(expense)

Toast.makeText(this, "Pengeluaran ditambahkan!", Toast.LENGTH_SHORT).show()

dialog.dismiss()

} else {

Toast.makeText(this, "Jumlah harus lebih dari 0", Toast.LENGTH_SHORT).show()

}

} catch (e: NumberFormatException) {

Toast.makeText(this, "Jumlah harus berupa angka yang valid", Toast.LENGTH_SHORT).show()

}

} else {

Toast.makeText(this, "Jumlah dan Kategori tidak boleh kosong", Toast.LENGTH_SHORT).show()

}

}

dialog.show()

}

}

Penjelasan kunci:

  • View Binding: Digunakan untuk mengakses view secara aman dan efisien (pastikan Anda mengaktifkannya di build.gradle: buildFeatures { viewBinding true }).
  • by viewModels: Delegasi properti ini digunakan untuk inisialisasi ViewModel dengan Factory.
  • setupRecyclerView(): Menginisialisasi adapter dan LinearLayoutManager untuk RecyclerView.
  • setupObservers(): Mengamati allExpenses dan totalExpenses dari ViewModel. Setiap kali data berubah, UI akan otomatis diperbarui.
  • showAddExpenseDialog(): Menampilkan dialog kustom untuk memasukkan pengeluaran baru. Melakukan validasi input sebelum menyisipkan ke database melalui ViewModel.

Masalah yang Sering Terjadi dan Solusinya

Membangun aplikasi Android, terutama yang melibatkan database, seringkali diwarnai oleh beberapa kendala umum. Berikut adalah beberapa masalah yang mungkin Anda hadapi dan cara mengatasinya:

  1. Database Schema Changed (Migration Error)

    Gejala: Aplikasi crash saat diluncurkan atau saat mencoba mengakses database dengan pesan error seperti "Schema changed, but no migration was provided". Ini sering terjadi ketika Anda mengubah struktur Expense Entity (menambah/menghapus kolom, mengubah tipe data) tanpa memperbarui versi database atau menyediakan strategi migrasi.

    Penyebab: Versi database di @Database(..., version = 1, ...) tidak sesuai dengan skema database yang disimpan di perangkat pengguna.

    Solusi: Untuk tujuan pengembangan dan aplikasi sederhana seperti ini, cara tercepat adalah dengan menaikkan versi database di @Database annotation (misal dari 1 menjadi 2) dan menambahkan .fallbackToDestructiveMigration() ke builder database di AppDatabase.kt. Ini akan menghapus dan membuat ulang database setiap kali skema berubah. Namun, hati-hati, ini akan menghapus semua data pengguna yang ada. Untuk aplikasi produksi, Anda harus membuat strategi migrasi yang tepat.

    Room.databaseBuilder(...).fallbackToDestructiveMigration().build()

  2. UI Tidak Diperbarui Secara Otomatis

    Gejala: Anda menambahkan atau menghapus pengeluaran, tetapi RecyclerView atau total pengeluaran tidak berubah sampai Anda merestart aplikasi.

    Penyebab: Observer LiveData mungkin tidak terpasang dengan benar atau ada masalah dalam pembaruan data dari Room. Pastikan Flow dari DAO dikonversi ke LiveData menggunakan .asLiveData() dan di-observe di Activity/Fragment.

    Solusi: Periksa kembali metode setupObservers() di MainActivity Anda. Pastikan expenseViewModel.allExpenses.observe(this) { ... } dan expenseViewModel.totalExpenses.observe(this) { ... } dipanggil dan menerima data. Pastikan juga submitList(it) dipanggil pada ExpenseAdapter Anda ketika allExpenses berubah.

  3. NumberFormatException Saat Menambahkan Pengeluaran

    Gejala: Aplikasi crash atau menampilkan toast error ketika pengguna memasukkan teks non-numerik ke kolom jumlah, atau jika menggunakan koma (,) sebagai pemisah desimal alih-alih titik (.).

    Penyebab: Konversi string ke double menggunakan .toDouble() gagal karena input tidak valid.

    Solusi: Implementasikan penanganan error yang lebih robust. Seperti yang kita lakukan, gunakan blok try-catch untuk NumberFormatException saat mencoba mengonversi string jumlah ke double. Juga, pertimbangkan untuk menggunakan DecimalFormat atau mengatur android:inputType="numberDecimal" pada EditText untuk membatasi input pengguna, meskipun pengguna bisa saja memasukkan koma. Edukasi pengguna atau ubah lokal input untuk memproses koma.

  4. Leak Memori (Context Leak) dengan Dialog

    Gejala: Peringatan leak memori di Logcat, terutama ketika Anda rotasi layar saat dialog masih terbuka.

    Penyebab: Dialog dapat mempertahankan referensi ke Activity yang lama jika tidak di-dismiss dengan benar atau jika referensi ke View dari dialog masih ada setelah Activity dihancurkan.

    Solusi: Pastikan dialog selalu di-dismiss saat Activity dihancurkan atau saat sudah tidak dibutuhkan. Dalam contoh kita, dialog di-dismiss setelah data berhasil ditambahkan. Untuk kasus rotasi, pertimbangkan menggunakan DialogFragment yang dirancang khusus untuk menangani siklus hidup.

  5. Coroutines Tidak Berjalan di Background

    Gejala: Aplikasi terasa lag atau ANR (Application Not Responding) saat melakukan operasi database, terutama pada perangkat lama atau operasi yang kompleks.

    Penyebab: Operasi database (insert, delete) dijalankan di Main Thread.

    Solusi: Pastikan semua operasi database (DAO method yang suspend) dipanggil dalam coroutine scope yang tepat, seperti viewModelScope.launch { ... } di ViewModel. Room secara otomatis menjalankan operasi suspend di background thread pool, tetapi pemanggilan method suspend itu sendiri harus dari dalam coroutine.

Pengalaman dan Pertimbangan Praktis

Setelah membangun fondasi aplikasi expense tracker ini, ada beberapa pertimbangan praktis dan langkah selanjutnya yang bisa Anda explore:

  • Skalabilitas dan Fitur Lanjutan: Aplikasi ini baru permulaan. Dalam praktiknya, pengguna akan menginginkan fitur seperti filter berdasarkan kategori atau tanggal, laporan visual (grafik batang/lingkaran), dukungan multi-akun, budget planning, hingga sinkronisasi cloud. Setiap fitur tambahan membutuhkan desain database dan UI yang lebih kompleks. Pertimbangkan untuk menggunakan Paging Library jika daftar pengeluaran sangat panjang.
  • User Experience (UX) yang Lebih Baik:
    • Validasi Input: Selain validasi dasar, berikan feedback instan pada EditText (misalnya, error state) jika input tidak valid.
    • Kustomisasi Kategori: Izinkan pengguna menambah, mengedit, atau menghapus kategori mereka sendiri.
    • Tanggal dan Waktu: Tambahkan Date/Time Picker pada dialog penambahan pengeluaran agar pengguna bisa memilih tanggal transaksi secara spesifik, bukan hanya timestamp otomatis.
    • Konfirmasi Hapus: Selalu tanyakan konfirmasi sebelum menghapus pengeluaran agar tidak ada data yang hilang secara tidak sengaja.
  • Pengujian Otomatis (Unit & UI Tests): Untuk aplikasi yang lebih serius, pengujian adalah krusial. Tulis unit test untuk ViewModel dan Repository Anda (menggunakan mock DAO), dan UI test untuk memastikan antarmuka pengguna berfungsi sesuai harapan.
  • Keamanan Data: Untuk data sensitif (meskipun pengeluaran pribadi mungkin tidak dianggap sangat sensitif, tetapi jika ada fitur akun bank atau kartu kredit di masa depan), pertimbangkan untuk mengenkripsi database. Library seperti SQLCipher dapat membantu dalam hal ini.
  • Performa: Optimalkan RecyclerView dengan DiffUtil (seperti yang sudah kita lakukan). Pastikan operasi database berjalan di background. Hindari melakukan pekerjaan berat di Main Thread.
  • Internasionalisasi: Jika Anda menargetkan audiens yang lebih luas, pastikan string teks Anda ada di file strings.xml dan mendukung berbagai bahasa dan format mata uang (seperti yang sudah kita lakukan dengan NumberFormat).

Dalam pengujian saya pribadi, aplikasi semacam ini seringkali menjadi titik awal yang bagus untuk memahami siklus hidup komponen Android, persistensi data, dan arsitektur MVVM (Model-View-ViewModel) yang modern. Masalah utama yang sering muncul adalah terkait manajemen state input dan sinkronisasi data real-time, yang diatasi cukup baik dengan LiveData dan Room Flow. Menggunakan pendekatan modulasi juga bisa menjadi langkah selanjutnya untuk project yang lebih besar, memisahkan fitur menjadi modul-modul yang lebih kecil.

FAQ

Q: Apa itu Room Database dan mengapa lebih baik dari SQLiteOpenHelper?

A: Room Database adalah abstraction layer di atas SQLite yang merupakan bagian dari Android Jetpack. Ia menyediakan cara yang lebih mudah dan aman untuk bekerja dengan database SQLite di Android. Kelebihannya antara lain: validasi query SQL saat compile time (mengurangi bug saat runtime), bekerja dengan objek data alih-alih kursor dan kolom mentah, serta dukungan untuk LiveData dan Coroutines untuk operasi asinkron. SQLiteOpenHelper membutuhkan lebih banyak boilerplate code dan rentan terhadap kesalahan SQL saat runtime.

Q: Bisakah saya menambahkan kategori kustom?

A: Tentu saja! Untuk menambahkan kategori kustom, Anda perlu memodifikasi dialog penambahan pengeluaran agar tidak hanya berupa EditText biasa. Anda bisa menggunakan Spinner (dropdown) yang diisi dari daftar kategori yang sudah ada, ditambah opsi "Lainnya" yang kemudian memunculkan EditText untuk kategori baru. Kategori-kategori ini sendiri bisa disimpan dalam tabel terpisah di Room Database.

Q: Bagaimana cara membuat laporan pengeluaran bulanan atau tahunan?

A: Untuk laporan, Anda akan membutuhkan query SQL yang lebih kompleks di ExpenseDao. Misalnya, untuk pengeluaran bulanan, Anda bisa menambahkan query yang memfilter berdasarkan rentang waktu timestamp (misal, dari awal bulan hingga akhir bulan) dan kemudian menggunakan fungsi agregasi SQL seperti SUM() atau GROUP BY untuk mengelompokkan pengeluaran berdasarkan kategori dalam periode tersebut. Data ini kemudian bisa ditampilkan dalam grafik menggunakan library charting pihak ketiga seperti MPAndroidChart.

Q: Apakah aplikasi ini aman untuk menyimpan data keuangan?

A: Aplikasi ini menyimpan data secara lokal di perangkat pengguna. Untuk aplikasi sederhana seperti ini, keamanan standarnya sudah cukup baik (data disimpan dalam sandbox aplikasi). Namun, jika Anda berencana menambahkan fitur yang lebih sensitif (misalnya integrasi dengan bank), Anda perlu mempertimbangkan enkripsi database (misalnya dengan SQLCipher), otentikasi biometrik, dan praktik keamanan aplikasi mobile lainnya yang lebih ketat.

Q: Bagaimana cara menambahkan dukungan untuk mata uang lain?

A: Saat ini kita menggunakan Locale("in", "ID") untuk format Rupiah Indonesia. Anda bisa membuat pengguna memilih mata uang di pengaturan aplikasi. Kemudian, berdasarkan pilihan pengguna, Anda bisa mengubah Locale yang digunakan oleh NumberFormat.getCurrencyInstance() saat menampilkan jumlah pengeluaran.

Kesimpulan

Membangun aplikasi expense tracker dengan Kotlin di Android adalah cara yang fantastis untuk mengasah skill Anda sekaligus menciptakan sesuatu yang praktis dan berguna. Kita telah berhasil membangun fondasi yang kokoh, mulai dari merancang database dengan Room, membangun antarmuka pengguna yang intuitif, hingga mengimplementasikan arsitektur MVVM yang modern dengan ViewModel dan LiveData.

Project ini bukan hanya tentang menulis kode, tetapi juga memahami bagaimana komponen Android bekerja sama untuk memberikan pengalaman pengguna yang mulus. Dengan dasar yang kuat ini, Anda memiliki pijakan yang sangat baik untuk mengembangkan aplikasi Anda lebih jauh, menambahkan fitur-fitur yang lebih kompleks, dan bahkan mengoptimalkannya untuk performa dan keamanan. Teruslah bereksperimen, karena dunia pengembangan aplikasi Android dengan Kotlin adalah lautan inovasi tanpa batas!

TAGS: Kotlin, Android Development, Expense Tracker, Room Database, ViewModel, LiveData, Tutorial, Mobile App, Coding, Developer Tools


Baca Juga

You May Also Like

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *