Evauasi Akhir Semester

 

    Nama : Muhamad Faiz Fernanda

    Kelas  : PBB-A

    NRP   : 5025211186

My Cash Flow untuk keuangan anda

1. Model Data

Semua transaksi direpresentasikan oleh data class Transaction dengan tipe (pemasukan/pengeluaran), kategori, nominal, dan tanggal.

data class Transaction(
    val id: Int = 0,
    val type: TransactionType,
    val category: String,
    val amount: Double,
    val date: Long = System.currentTimeMillis()
)

enum class TransactionType { INCOME, EXPENSE }

Penjelasan

  • id: identifier unik untuk transaksi.
  • type: membedakan pemasukan (INCOME) dan pengeluaran (EXPENSE).
  • category & amount: keterangan dan nilai nominal.
  • date: waktu transaksi disimpan sebagai Long (timestamp). Nilai bawaan adalah tanggal saat transaksi dibuat.
2. TransactionRepository
Repositori ini mengurus penyimpanan permanen menggunakan SharedPreferences. Fungsi load() membaca daftar transaksi yang tersimpan dalam bentuk JSON, sedangkan save() menulis ulang seluruh daftar ke penyimpanan.

class TransactionRepository(context: Context) {
    private val prefs: SharedPreferences =
        context.getSharedPreferences("transactions", Context.MODE_PRIVATE)

    fun load(): MutableList<Transaction> {
        val json = prefs.getString(KEY_TRANSACTIONS, "[]") ?: "[]"
        val array = JSONArray(json)
        val list = mutableListOf<Transaction>()
        for (i in 0 until array.length()) {
            val obj = array.getJSONObject(i)
            list.add(
                Transaction(
                    id = obj.getInt("id"),
                    type = TransactionType.valueOf(obj.getString("type")),
                    category = obj.getString("category"),
                    amount = obj.getDouble("amount"),
                    date = obj.getLong("date")
                )
            )
        }
        return list
    }

    fun save(list: List<Transaction>) {
        val array = JSONArray()
        list.forEach { t ->
            val obj = JSONObject()
            obj.put("id", t.id)
            obj.put("type", t.type.name)
            obj.put("category", t.category)
            obj.put("amount", t.amount)
            obj.put("date", t.date)
            array.put(obj)
        }
        prefs.edit().putString(KEY_TRANSACTIONS, array.toString()).apply()
    }

    companion object {
        private const val KEY_TRANSACTIONS = "transactions_json"
    }
}


Penjelasan
  • load(): mengubah JSON tersimpan menjadi objek Transaction lalu mengembalikan MutableList<Transaction>.
  • save(): membuat JSONArray, menambahkan setiap transaksi ke array, lalu menyimpan string JSON ke SharedPreferences.
  • Data disimpan di bawah kunci transactions_json.
3. TransactionViewModel
ViewModel menyimpan daftar transaksi dalam mutableStateListOf dan memanggil repositori untuk memuat atau menyimpan data.

class TransactionViewModel(application: Application) : AndroidViewModel(application) {
    private var nextId = 0
    private val repo = TransactionRepository(application.applicationContext)
    val transactions = mutableStateListOf<Transaction>()

    init {
        transactions.addAll(repo.load())
        nextId = (transactions.maxOfOrNull { it.id } ?: -1) + 1
    }

    private fun persist() {
        repo.save(transactions)
    }

    fun addTransaction(type: TransactionType, category: String, amount: Double, date: Long) {
        transactions.add(Transaction(nextId++, type, category, amount, date))
        persist()
    }

    fun updateTransaction(updated: Transaction) {
        val index = transactions.indexOfFirst { it.id == updated.id }
        if (index >= 0) {
            transactions[index] = updated
            persist()
        }
    }

    fun deleteTransaction(transaction: Transaction) {
        transactions.removeAll { it.id == transaction.id }
        persist()
    }
}


Penjelasan
  • init: saat ViewModel dibuat, data dimuat dari TransactionRepository dan nextId disesuaikan.
  • addTransaction/updateTransaction/deleteTransaction: operasi CRUD yang memodifikasi daftar transaksi lalu memanggil persist() untuk menyimpan ke penyimpanan permanen.
  • Dengan Compose, perubahan pada mutableStateListOf otomatis membuat tampilan memperbarui diri.
4. AddEditTransactionScreen
Composable untuk menambah atau mengedit transaksi. Menerapkan pemilihan tanggal dengan DatePickerDialog dan opsi simpan atau hapus.

@Composable
fun AddEditTransactionScreen(
    transaction: Transaction? = null,
    onSave: (TransactionType, String, Double, Long, Int?) -> Unit,
    onDelete: ((Transaction) -> Unit)? = null,
    onCancel: () -> Unit
) {
    var type by remember { mutableStateOf(transaction?.type ?: TransactionType.INCOME) }
    var category by remember { mutableStateOf(transaction?.category ?: "") }
    var amount by remember { mutableStateOf(transaction?.amount?.toString() ?: "") }
    var dateMillis by remember { mutableStateOf(transaction?.date ?: System.currentTimeMillis()) }
    var showDatePicker by remember { mutableStateOf(false) }
    val dateFormatted = remember(dateMillis) {
        DateTimeFormatter.ISO_DATE.format(
            Instant.ofEpochMilli(dateMillis).atZone(ZoneId.systemDefault()).toLocalDate()
        )
    }
    val datePickerState = rememberDatePickerState(initialSelectedDateMillis = dateMillis)

    Scaffold(topBar = {
        TopAppBar(title = { Text(if (transaction == null) "Tambah Transaksi" else "Edit Transaksi") })
    }) { padding ->
        Column(
            Modifier
                .padding(padding)
                .padding(16.dp)
                .fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Row {
                RadioButton(
                    selected = type == TransactionType.INCOME,
                    onClick = { type = TransactionType.INCOME }
                )
                Text("Pemasukan")
                Spacer(modifier = Modifier.width(16.dp))
                RadioButton(
                    selected = type == TransactionType.EXPENSE,
                    onClick = { type = TransactionType.EXPENSE }
                )
                Text("Pengeluaran")
            }

            OutlinedTextField(
                value = category,
                onValueChange = { category = it },
                label = { Text("Kategori") },
                modifier = Modifier.fillMaxWidth()
            )

            OutlinedTextField(
                value = amount,
                onValueChange = { amount = it },
                label = { Text("Nominal") },
                modifier = Modifier.fillMaxWidth(),
                singleLine = true
            )

            if (showDatePicker) {
                DatePickerDialog(
                    onDismissRequest = { showDatePicker = false },
                    confirmButton = {
                        TextButton(onClick = {
                            dateMillis = datePickerState.selectedDateMillis ?: dateMillis
                            showDatePicker = false
                        }) { Text("OK") }
                    },
                    dismissButton = {
                        TextButton(onClick = { showDatePicker = false }) { Text("Batal") }
                    }
                ) { DatePicker(state = datePickerState) }
            }

            OutlinedTextField(
                value = dateFormatted,
                onValueChange = {},
                label = { Text("Tanggal") },
                modifier = Modifier.fillMaxWidth(),
                readOnly = true,
                trailingIcon = {
                    IconButton(onClick = { showDatePicker = true }) {
                        Icon(Icons.Default.DateRange, contentDescription = null)
                    }
                }
            )

            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Button(onClick = onCancel) { Text("Batal") }
                if (transaction != null && onDelete != null) {
                    Button(onClick = { onDelete(transaction) }) { Text("Hapus") }
                }
                Button(onClick = {
                    dateMillis = datePickerState.selectedDateMillis ?: dateMillis
                    onSave(
                        type,
                        category,
                        amount.toDoubleOrNull() ?: 0.0,
                        dateMillis,
                        transaction?.id
                    )
                }) {
                    Text("Simpan")
                }
            }
        }
    }
}


Penjelasan
  • type, category, amount, dan dateMillis disimpan dalam remember.
  • DatePickerDialog muncul saat pengguna menekan ikon kalender, mengubah dateMillis.
  • onSave memanggil callback dengan data yang sudah diubah, mendukung pembuatan baru maupun edit.
  • Jika transaksi sudah ada dan onDelete tersedia, tombol Hapus juga muncul.
5. TransactionListScreen
Menampilkan daftar transaksi beserta filter kategori, filter tipe (pemasukan/pengeluaran), opsi pengurutan, serta pembatas tampilan 50 item pertama.

@Composable
fun TransactionListScreen(
    transactions: List<Transaction>,
    onAddClicked: () -> Unit,
    onItemClick: (Transaction) -> Unit,
    onReport: () -> Unit
) {
    val categories = listOf("Semua") + transactions.map { it.category }.distinct()
    var selectedCategory by remember { mutableStateOf("Semua") }
    var filterType by remember { mutableStateOf<Int>(0) } //0=all,1=income,2=expense
    val shown = remember(transactions, selectedCategory, filterType) {
        transactions.filter { t ->
            (filterType == 0 || (filterType == 1 && t.type == TransactionType.INCOME) || (filterType == 2 && t.type == TransactionType.EXPENSE)) &&
                    (selectedCategory == "Semua" || t.category == selectedCategory)
        }
    }

    var sort by remember { mutableStateOf(1) } //1=newest,0=oldest,2=amount asc,3=amount desc
    var showAll by remember { mutableStateOf(false) }

    val filtered = remember(shown, sort) {
        val base = when (sort) {
            0 -> shown.sortedBy { it.date }
            1 -> shown.sortedByDescending { it.date }
            2 -> shown.sortedBy { it.amount }
            3 -> shown.sortedByDescending { it.amount }
            else -> shown
        }
        base
    }

    val displayList = if (showAll || filtered.size <= 50) filtered else filtered.take(50)

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("MyMoney Notes") },
                actions = {
                    IconButton(onClick = onReport) {
                        Icon(Icons.Default.Assessment, contentDescription = "Laporan")
                    }
                }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = onAddClicked) { Text("+") }
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .fillMaxSize()
        ) {
            TotalBalance(transactions)
            TransactionChart(transactions)

            Row(
                Modifier
                    .padding(horizontal = 16.dp)
                    .fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text("Kategori:")
                Spacer(Modifier.width(8.dp))
                var expanded by remember { mutableStateOf(false) }
                Box {
                    Button(onClick = { expanded = true }) { Text(selectedCategory) }
                    DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
                        categories.forEach { cat ->
                            DropdownMenuItem(text = { Text(cat) }, onClick = {
                                selectedCategory = cat
                                expanded = false
                            })
                        }
                    }
                }
            }

            Row(
                Modifier
                    .padding(horizontal = 16.dp)
                    .fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                FilterChip(selected = filterType == 0, onClick = { filterType = 0 }, label = { Text("Semua") })
                FilterChip(selected = filterType == 1, onClick = { filterType = 1 }, label = { Text("Pemasukan") })
                FilterChip(selected = filterType == 2, onClick = { filterType = 2 }, label = { Text("Pengeluaran") })
            }

            Row(
                Modifier
                    .padding(horizontal = 16.dp, vertical = 8.dp)
                    .fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text("Sort:")
                Spacer(Modifier.width(8.dp))
                var exp by remember { mutableStateOf(false) }
                val label = when (sort) {
                    0 -> "Tanggal Terlama"
                    1 -> "Tanggal Terbaru"
                    2 -> "Nominal Kecil"
                    3 -> "Nominal Besar"
                    else -> "Tanggal Terbaru"
                }
                Box {
                    Button(onClick = { exp = true }) { Text(label) }
                    DropdownMenu(expanded = exp, onDismissRequest = { exp = false }) {
                        DropdownMenuItem(text = { Text("Tanggal Terlama") }, onClick = { sort = 0; exp = false })
                        DropdownMenuItem(text = { Text("Tanggal Terbaru") }, onClick = { sort = 1; exp = false })
                        DropdownMenuItem(text = { Text("Nominal Kecil") }, onClick = { sort = 2; exp = false })
                        DropdownMenuItem(text = { Text("Nominal Besar") }, onClick = { sort = 3; exp = false })
                    }
                }
            }

            LazyColumn {
                items(displayList) { transaction ->
                    val isIncome = transaction.type == TransactionType.INCOME
                    val labelColor = if (isIncome) Color(0xFF4CAF50) else Color(0xFFF44336)
                    Card(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(8.dp)
                            .clickable { onItemClick(transaction) },
                        elevation = CardDefaults.cardElevation(4.dp),
                        colors = CardDefaults.cardColors(containerColor = Color.White)
                    ) {
                        Box(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(16.dp)
                        ) {
                            Text(
                                text = if (isIncome) "Pemasukan" else "Pengeluaran",
                                color = Color.White,
                                style = MaterialTheme.typography.labelSmall,
                                modifier = Modifier
                                    .align(Alignment.TopEnd)
                                    .background(labelColor, shape = RoundedCornerShape(4.dp))
                                    .padding(horizontal = 8.dp, vertical = 4.dp)
                            )

                            Column(modifier = Modifier.align(Alignment.TopStart)) {
                                Text(text = transaction.category)
                                Text(
                                    DateTimeFormatter.ISO_DATE.format(
                                        Instant.ofEpochMilli(transaction.date)
                                            .atZone(ZoneId.systemDefault())
                                            .toLocalDate()
                                    ),
                                    style = MaterialTheme.typography.labelSmall
                                )
                                Text(text = "Rp ${"%,.0f".format(transaction.amount)}")
                            }
                        }
                    }
                }
                if (!showAll && filtered.size > 50) {
                    item {
                        TextButton(
                            onClick = { showAll = true },
                            modifier = Modifier.fillMaxWidth()
                        ) { Text("Tampilkan Semua") }
                    }
                }
            }
        }
    }
}


Penjelasan
  • Filter kategori dan filter tipe (pemasukan/pengeluaran/semua) memanfaatkan DropdownMenu dan FilterChip.
  • Sort memberi pilihan tanggal terlama/terbaru atau nominal kecil/besar.
  • Hanya 50 transaksi pertama yang ditampilkan, sisanya muncul saat pengguna menekan tombol “Tampilkan Semua”.
  • Setiap kartu transaksi menampilkan jenis (label warna hijau/merah), kategori, tanggal, dan nominal.
6. ReportScreen & BalanceChart
ReportScreen memungkinkan pengguna memilih rentang tanggal, menampilkan grafik saldo, serta mengekspor hasil filter ke PDF.

@Composable
fun ReportScreen(onBack: () -> Unit, transactions: List<Transaction>) {
    var startMillis by remember { mutableStateOf<Long?>(null) }
    var endMillis by remember { mutableStateOf<Long?>(null) }
    var showStartPicker by remember { mutableStateOf(false) }
    var showEndPicker by remember { mutableStateOf(false) }
    var filtered by remember { mutableStateOf(transactions) }

    val context = LocalContext.current

    val startText = remember(startMillis) {
        startMillis?.let {
            DateTimeFormatter.ISO_DATE.format(
                Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate()
            )
        } ?: ""
    }
    val endText = remember(endMillis) {
        endMillis?.let {
            DateTimeFormatter.ISO_DATE.format(
                Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate()
            )
        } ?: ""
    }

    val startState = rememberDatePickerState(initialSelectedDateMillis = startMillis)
    val endState = rememberDatePickerState(initialSelectedDateMillis = endMillis)

    Scaffold(topBar = { TopAppBar(title = { Text("Laporan") }) }) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .padding(16.dp)
        ) {
            if (showStartPicker) {
                DatePickerDialog(
                    onDismissRequest = { showStartPicker = false },
                    confirmButton = {
                        TextButton(onClick = {
                            startMillis = startState.selectedDateMillis
                            showStartPicker = false
                        }) { Text("OK") }
                    },
                    dismissButton = {
                        TextButton(onClick = { showStartPicker = false }) { Text("Batal") }
                    }
                ) { DatePicker(state = startState) }
            }

            if (showEndPicker) {
                DatePickerDialog(
                    onDismissRequest = { showEndPicker = false },
                    confirmButton = {
                        TextButton(onClick = {
                            endMillis = endState.selectedDateMillis
                            showEndPicker = false
                        }) { Text("OK") }
                    },
                    dismissButton = {
                        TextButton(onClick = { showEndPicker = false }) { Text("Batal") }
                    }
                ) { DatePicker(state = endState) }
            }

            OutlinedTextField(
                value = startText,
                onValueChange = {},
                label = { Text("Tanggal Awal") },
                modifier = Modifier.fillMaxWidth(),
                readOnly = true,
                trailingIcon = {
                    IconButton(onClick = { showStartPicker = true }) {
                        Icon(Icons.Default.DateRange, contentDescription = null)
                    }
                }
            )
            OutlinedTextField(
                value = endText,
                onValueChange = {},
                label = { Text("Tanggal Akhir") },
                modifier = Modifier.fillMaxWidth(),
                readOnly = true,
                trailingIcon = {
                    IconButton(onClick = { showEndPicker = true }) {
                        Icon(Icons.Default.DateRange, contentDescription = null)
                    }
                }
            )
            Row(
                Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Button(onClick = onBack) { Text("Kembali") }
                Button(onClick = {
                    filtered = transactions.filter { t ->
                        (startMillis == null || t.date >= startMillis!!) &&
                                (endMillis == null || t.date <= endMillis!!)
                    }
                }) { Text("Terapkan") }
                IconButton(onClick = {
                    val file = ExportUtils.export(context, filtered)
                    if (file != null) {
                        Toast.makeText(context, "PDF disimpan di ${file.absolutePath}", Toast.LENGTH_LONG).show()
                    } else {
                        Toast.makeText(context, "Gagal membuat PDF", Toast.LENGTH_LONG).show()
                    }
                }) {
                    Icon(Icons.Default.PictureAsPdf, contentDescription = "PDF")
                }
            }

            Spacer(Modifier.height(16.dp))

            BalanceChart(filtered)

            Spacer(Modifier.height(16.dp))

            val incomeTotal = filtered.filter { it.type == TransactionType.INCOME }.sumOf { it.amount }
            val expenseTotal = filtered.filter { it.type == TransactionType.EXPENSE }.sumOf { it.amount }
            val balance = incomeTotal - expenseTotal

            Text("Total Pemasukan: Rp ${"%,.0f".format(incomeTotal)}")
            Text("Total Pengeluaran: Rp ${"%,.0f".format(expenseTotal)}")
            Text("Total Balance: Rp ${"%,.0f".format(balance)}")

            LazyColumn {
                items(filtered) { t ->
                    Card(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(vertical = 4.dp),
                        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
                    ) {
                        Row(
                            Modifier
                                .fillMaxWidth()
                                .padding(16.dp),
                            horizontalArrangement = Arrangement.SpaceBetween,
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Column {
                                Text(
                                    DateTimeFormatter.ISO_DATE.format(
                                        Instant.ofEpochMilli(t.date).atZone(ZoneId.systemDefault()).toLocalDate()
                                    ),
                                    style = MaterialTheme.typography.labelLarge
                                )
                                Text(t.category)
                            }
                            Text(
                                "Rp ${"%,.0f".format(t.amount)}",
                                color = if (t.type == TransactionType.INCOME) Color(0xFF4CAF50) else Color(0xFFF44336)
                            )
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun BalanceChart(transactions: List<Transaction>) {
    if (transactions.isEmpty()) return

    val sorted = transactions.sortedBy { it.date }
    var balance = 0.0
    val points = sorted.map {
        balance += if (it.type == TransactionType.INCOME) it.amount else -it.amount
        balance
    }
    val max = points.maxOrNull() ?: 0.0
    val min = points.minOrNull() ?: 0.0
    val range = (max - min).takeIf { it > 0 } ?: 1.0

    Canvas(modifier = Modifier
        .fillMaxWidth()
        .height(180.dp)
        .padding(vertical = 8.dp)) {
        val stepX = size.width / (points.lastIndex.coerceAtLeast(1).toFloat())
        val textPaint = android.graphics.Paint().apply { textSize = 24f }
        var previous = Offset(0f, size.height * (1f - ((points.first() - min) / range).toFloat()))
        points.forEachIndexed { index, value ->
            val x = stepX * index
            val y = size.height * (1f - ((value - min) / range).toFloat())
            if (index > 0) {
                drawLine(Color.Blue, previous, Offset(x, y), strokeWidth = 4f)
            }
            drawContext.canvas.nativeCanvas.drawText(
                "${DateTimeFormatter.ISO_DATE.format(Instant.ofEpochMilli(sorted[index].date).atZone(ZoneId.systemDefault()).toLocalDate())}",
                x,
                size.height + 20,
                textPaint
            )
            drawContext.canvas.nativeCanvas.drawText(
                "${"%,.0f".format(value)}",
                x,
                y - 4,
                textPaint
            )
            previous = Offset(x, y)
        }
    }
}


Penjelasan
  • Dua DatePickerDialog memungkinkan pemilihan tanggal awal dan akhir. Filter diterapkan pada daftar transaksi setelah menekan tombol Terapkan.
  • Tombol bergambar ikon PDF memanggil ExportUtils.export() untuk menyimpan hasil filter ke file PDF dan menampilkan lokasi file lewat Toast.
  • BalanceChart menggambar grafik garis saldo kumulatif berdasarkan tanggal transaksi, lengkap dengan label tanggal dan nilai saldo di setiap titik.
7. ExportUtils.export
Fungsi utilitas untuk membuat file PDF dari daftar transaksi yang difilter.

object ExportUtils {
    fun export(context: Context, transactions: List<Transaction>): File? {
        return try {
            val doc = PdfDocument()
            val page = doc.startPage(PdfDocument.PageInfo.Builder(595, 842, 1).create())
            val canvas = page.canvas
            val paint = android.graphics.Paint().apply { textSize = 12f }
            var y = 40

            val income = transactions.filter { it.type == TransactionType.INCOME }
            val expense = transactions.filter { it.type == TransactionType.EXPENSE }

            fun drawSection(title: String, list: List<Transaction>) {
                canvas.drawText(title, 20f, y.toFloat(), paint)
                y += 20
                canvas.drawText("Tanggal  Kategori  Nominal", 20f, y.toFloat(), paint)
                y += 20
                list.forEach {
                    val date = DateTimeFormatter.ISO_DATE.format(
                        java.time.Instant.ofEpochMilli(it.date).atZone(ZoneId.systemDefault()).toLocalDate()
                    )
                    canvas.drawText("$date  ${it.category}  ${"%,.0f".format(it.amount)}", 20f, y.toFloat(), paint)
                    y += 20
                }
                y += 20
            }

            drawSection("Pemasukan", income)
            drawSection("Pengeluaran", expense)

            val totalIncome = income.sumOf { it.amount }
            val totalExpense = expense.sumOf { it.amount }
            val balance = totalIncome - totalExpense

            canvas.drawText("Total Pemasukan : ${"%,.0f".format(totalIncome)}", 20f, y.toFloat(), paint)
            y += 20
            canvas.drawText("Total Pengeluaran : ${"%,.0f".format(totalExpense)}", 20f, y.toFloat(), paint)
            y += 20
            canvas.drawText("Total Balance : ${"%,.0f".format(balance)}", 20f, y.toFloat(), paint)

            doc.finishPage(page)
            val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
            if (dir != null && !dir.exists()) dir.mkdirs()
            val file = File(dir, "transactions.pdf")
            FileOutputStream(file).use { out -> doc.writeTo(out) }
            doc.close()
            file
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }
}


Penjelasan
  • Membuat PdfDocument ukuran A4.
  • Memisahkan transaksi menjadi dua bagian: Pemasukan dan Pengeluaran, masing-masing digambar dengan judul dan kolom Tanggal, Kategori, dan Nominal.
  • Setelah semua item dicetak, menuliskan total pemasukan, pengeluaran, dan saldo akhir.
  • File disimpan pada direktori getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) dengan nama transactions.pdf.
8. MainActivity
Mengatur navigasi antar layar: daftar transaksi, formulir tambah/edit, dan laporan.

class MainActivity : ComponentActivity() {

    private val viewModel: TransactionViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyMoneyNotesTheme {
                var current by remember { mutableStateOf<Transaction?>(null) }
                var showForm by remember { mutableStateOf(false) }
                var showReport by remember { mutableStateOf(false) }

                when {
                    showForm -> {
                        AddEditTransactionScreen(
                            transaction = current,
                            onSave = { type, category, amount, date, id ->
                                if (id == null) {
                                    viewModel.addTransaction(type, category, amount, date)
                                } else {
                                    viewModel.updateTransaction(Transaction(id, type, category, amount, date))
                                }
                                showForm = false
                            },
                            onDelete = {
                                if (it.id != 0) viewModel.deleteTransaction(it)
                                showForm = false
                            },
                            onCancel = { showForm = false }
                        )
                    }
                    showReport -> {
                        ReportScreen(onBack = { showReport = false }, transactions = viewModel.transactions)
                    }
                    else -> {
                        TransactionListScreen(
                            transactions = viewModel.transactions,
                            onAddClicked = {
                                current = null
                                showForm = true
                            },
                            onItemClick = {
                                current = it
                                showForm = true
                            },
                            onReport = { showReport = true }
                        )
                    }
                }
            }
        }
    }
}


Penjelasan
  • viewModel disediakan dengan delegasi by viewModels().
  • Kondisi when menentukan tampilan mana yang aktif:
  • Form tambah/edit (AddEditTransactionScreen).
  • Laporan (ReportScreen).
  • Daftar transaksi (TransactionListScreen).
  • Setelah menambah, mengedit, atau menghapus transaksi, status showForm diubah agar kembali ke daftar.

Kesimpulan
Proyek ini memanfaatkan Jetpack Compose dengan arsitektur sederhana:
  • Data dikelola dalam TransactionViewModel dan disimpan permanen oleh TransactionRepository.
  • Formulir transaksi, daftar dengan filter dan sort, hingga laporan PDF digabungkan dalam MainActivity.
  • Fungsi ExportUtils.export memberikan kemampuan ekspor PDF, sedangkan ReportScreen menampilkan grafik saldo kumulatif dengan Canvas.
Dengan memahami fungsi-fungsi di atas, Anda dapat memperluas aplikasi menjadi sistem pencatatan keuangan bisnis yang lebih kompleks, misalnya dengan sinkronisasi cloud atau analitik yang lebih kaya.



Vidio Demo : 




Source Code : Link Github MyCashFlow

Komentar

Postingan populer dari blog ini

Tugas 9

Tugas 1 PPB A

Tugas 5