WIP: Migrate transaction form to compose

This commit is contained in:
William Brawner 2021-08-26 07:08:50 -06:00
parent 16b1d56be2
commit 16b4823450
6 changed files with 367 additions and 95 deletions

View file

@ -80,15 +80,4 @@ dependencies {
implementation 'androidx.activity:activity-compose:1.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
debugImplementation "com.willowtreeapps.hyperion:hyperion-core:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-attr:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-build-config:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-crash:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-disk:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-geiger-counter:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-measurement:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-phoenix:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-recorder:$hyperion"
debugImplementation "com.willowtreeapps.hyperion:hyperion-shared-preferences:$hyperion"
}

View file

@ -16,10 +16,10 @@
tools:targetApi="n">
<activity
android:name=".ui.auth.AuthActivity"
android:theme="@style/Theme.App.Starting"
android:exported="true"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize"
android:exported="true">
android:theme="@style/Theme.App.Starting"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -30,25 +30,29 @@
android:resource="@xml/shortcuts" />
</activity>
<activity android:name=".ui.MainActivity"
<activity
android:name=".ui.MainActivity"
android:exported="false"
android:resizeableActivity="true"
android:theme="@style/AppTheme" />
<activity
android:name=".ui.transactions.TransactionFormActivity"
android:exported="true"
android:parentActivityName=".ui.MainActivity"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.MainActivity" />
</activity>
<activity
android:name=".ui.categories.CategoryFormActivity"
android:exported="false"
android:parentActivityName=".ui.MainActivity"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.MainActivity" />

View file

@ -2,15 +2,35 @@ package com.wbrawner.budget.ui
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.DrawableRes
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.emoji.text.EmojiCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationView
import com.wbrawner.budget.R
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.ui.base.TwigsApp
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.flow.collect
@ -27,7 +47,9 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EmojiCompat.init(androidx.emoji.bundled.BundledEmojiCompatConfig(this))
setContent {
}
setContentView(R.layout.activity_main)
setSupportActionBar(action_bar)
toggle = ActionBarDrawerToggle(this, drawerLayout, R.string.action_open, R.string.action_close)
@ -99,3 +121,74 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
return true
}
}
@Composable
fun MainScreen() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scaffoldState = rememberScaffoldState(drawerState)
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
}
) {
}
}
@Composable
fun TwigsDrawer(navController: NavController, budgets: List<Budget>) {
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.fillMaxWidth()
) {
val image = if (isSystemInDarkTheme()) R.drawable.ic_twigs_outline else R.drawable.ic_twigs_color
Image(painter = painterResource(id = image), null)
Text(
text = "twigs",
style = MaterialTheme.typography.h3
)
}
val currentBudget = navController.currentBackStackEntry?.arguments?.getString("id")
navController.currentDestination?.arguments?.get()
}
}
@Composable
fun DrawerItem(@DrawableRes image: Int, text: String, selected: Boolean) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = spacedBy(8.dp, Alignment.Start),
) {
val tint = if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface
Image(
painter = painterResource(id = image),
contentDescription = null,
colorFilter = ColorFilter.tint(tint)
)
Text(text = text, color = tint)
}
}
@Composable
@Preview
fun DrawerItem_Preview() {
TwigsApp {
DrawerItem(R.drawable.ic_folder_open)
}
}
@Composable
@Preview
fun TwigsDrawer_Preview() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(initialValue = DrawerValue.Open))
TwigsApp {
Scaffold(
scaffoldState = scaffoldState,
drawerContent = { TwigsDrawer() }
) {
}
}
}

View file

@ -7,11 +7,8 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
@ -83,7 +80,6 @@ fun LoginForm(
verticalArrangement = spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = if (isSystemInDarkTheme()) R.drawable.ic_twigs_outline else R.drawable.ic_twigs_color),
contentDescription = null

View file

@ -7,6 +7,10 @@ import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import com.wbrawner.budget.R
val lightColors = lightColors(
primary = Green500,
@ -24,6 +28,21 @@ val darkColors = darkColors(
fun TwigsTheme(darkMode: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
MaterialTheme(
colors = if (darkMode) darkColors else lightColors,
typography = MaterialTheme.typography.copy(
h1 = MaterialTheme.typography.h1.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
h2 = MaterialTheme.typography.h2.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
h3 = MaterialTheme.typography.h3.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
h4 = MaterialTheme.typography.h4.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
h5 = MaterialTheme.typography.h5.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
h6 = MaterialTheme.typography.h6.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
subtitle1 = MaterialTheme.typography.subtitle1.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
subtitle2 = MaterialTheme.typography.subtitle2.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
body1 = MaterialTheme.typography.body1.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
body2 = MaterialTheme.typography.body2.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
button = MaterialTheme.typography.button.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
caption = MaterialTheme.typography.caption.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
overline = MaterialTheme.typography.overline.copy(fontFamily = FontFamily(Font(R.font.ubuntu))),
),
content = content
)
}

View file

@ -12,6 +12,23 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.NavUtils
import androidx.core.app.TaskStackBuilder
import com.google.android.material.datepicker.MaterialDatePicker
@ -21,6 +38,7 @@ import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.common.transaction.Transaction
import com.wbrawner.budget.ui.EXTRA_TRANSACTION_ID
import com.wbrawner.budget.ui.MainActivity
import com.wbrawner.budget.ui.base.TwigsApp
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_add_edit_transaction.*
import kotlinx.coroutines.CoroutineScope
@ -52,18 +70,18 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
val accounts = viewModel.getAccounts().toTypedArray()
setCategories()
budgetSpinner.adapter = ArrayAdapter<Budget>(
this@TransactionFormActivity,
android.R.layout.simple_list_item_1,
accounts
this@TransactionFormActivity,
android.R.layout.simple_list_item_1,
accounts
)
container_edit_transaction_type.setOnCheckedChangeListener { _, _ ->
this@TransactionFormActivity.launch {
val budget = budgetSpinner.selectedItem as Budget
setCategories(
viewModel.getCategories(
budget.id!!,
edit_transaction_type_expense.isChecked
)
viewModel.getCategories(
budget.id!!,
edit_transaction_type_expense.isChecked
)
)
}
}
@ -72,18 +90,18 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
}
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
this@TransactionFormActivity.launch {
val budget = budgetSpinner.selectedItem as Budget
setCategories(
viewModel.getCategories(
budget.id!!,
edit_transaction_type_expense.isChecked
)
viewModel.getCategories(
budget.id!!,
edit_transaction_type_expense.isChecked
)
)
}
}
@ -97,37 +115,37 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
loadTransaction()
transactionDate.setOnClickListener {
val currentDate = DateFormat.getDateFormat(this@TransactionFormActivity)
.parse(transactionDate.text.toString()) ?: Date()
.parse(transactionDate.text.toString()) ?: Date()
MaterialDatePicker.Builder.datePicker()
.setSelection(currentDate.time)
.setTheme(R.style.DateTimePickerDialogTheme)
.build()
.also { picker ->
picker.addOnPositiveButtonClickListener {
transactionDate.text =
DateFormat.getDateFormat(this@TransactionFormActivity)
.format(Date(it))
.setSelection(currentDate.time)
.setTheme(R.style.DateTimePickerDialogTheme)
.build()
.also { picker ->
picker.addOnPositiveButtonClickListener {
transactionDate.text =
DateFormat.getDateFormat(this@TransactionFormActivity)
.format(Date(it))
}
}
}
.show(supportFragmentManager, null)
.show(supportFragmentManager, null)
}
transactionTime.setOnClickListener {
val currentDate = DateFormat.getTimeFormat(this@TransactionFormActivity)
.parse(transactionTime.text.toString()) ?: Date()
.parse(transactionTime.text.toString()) ?: Date()
TimePickerDialog(
this@TransactionFormActivity,
{ _, hourOfDay, minute ->
val newTime = Date().apply {
hours = hourOfDay
minutes = minute
}
transactionTime.text =
DateFormat.getTimeFormat(this@TransactionFormActivity)
.format(newTime)
},
currentDate.hours,
currentDate.minutes,
DateFormat.is24HourFormat(this@TransactionFormActivity)
this@TransactionFormActivity,
{ _, hourOfDay, minute ->
val newTime = Date().apply {
hours = hourOfDay
minutes = minute
}
transactionTime.text =
DateFormat.getTimeFormat(this@TransactionFormActivity)
.format(newTime)
},
currentDate.hours,
currentDate.minutes,
DateFormat.is24HourFormat(this@TransactionFormActivity)
).show()
}
}
@ -168,14 +186,14 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
private fun setCategories(categories: List<Category> = emptyList()) {
val adapter = ArrayAdapter<Category>(
this@TransactionFormActivity,
android.R.layout.simple_list_item_1
this@TransactionFormActivity,
android.R.layout.simple_list_item_1
)
adapter.add(
Category(
title = getString(R.string.uncategorized),
amount = 0, budgetId = ""
)
Category(
title = getString(R.string.uncategorized),
amount = 0, budgetId = ""
)
)
adapter.addAll(categories)
edit_transaction_category.adapter = adapter
@ -204,32 +222,32 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
R.id.action_save -> {
val date = GregorianCalendar.getInstance().apply {
DateFormat.getDateFormat(this@TransactionFormActivity)
.parse(transactionDate.text.toString())
?.let {
time = it
}
.parse(transactionDate.text.toString())
?.let {
time = it
}
DateFormat.getTimeFormat(this@TransactionFormActivity)
.parse(transactionTime.text.toString())
?.let { GregorianCalendar.getInstance().apply { time = it } }
?.let {
set(Calendar.HOUR_OF_DAY, it.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, it.get(Calendar.MINUTE))
}
.parse(transactionTime.text.toString())
?.let { GregorianCalendar.getInstance().apply { time = it } }
?.let {
set(Calendar.HOUR_OF_DAY, it.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, it.get(Calendar.MINUTE))
}
}
val categoryId = (edit_transaction_category.selectedItem as? Category)?.id
launch {
viewModel.saveTransaction(
Transaction(
id = id,
budgetId = (budgetSpinner.selectedItem as Budget).id!!,
title = edit_transaction_title.text.toString(),
date = date.time,
description = edit_transaction_description.text.toString(),
amount = (BigDecimal(edit_transaction_amount.text.toString()) * 100.toBigDecimal()).toLong(),
expense = edit_transaction_type_expense.isChecked,
categoryId = categoryId,
createdBy = viewModel.currentUserId!!
)
Transaction(
id = id,
budgetId = (budgetSpinner.selectedItem as Budget).id!!,
title = edit_transaction_title.text.toString(),
date = date.time,
description = edit_transaction_description.text.toString(),
amount = (BigDecimal(edit_transaction_amount.text.toString()) * 100.toBigDecimal()).toLong(),
expense = edit_transaction_type_expense.isChecked,
categoryId = categoryId,
createdBy = viewModel.currentUserId!!
)
)
onNavigateUp()
}
@ -246,14 +264,14 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
override fun onNavigateUp(): Boolean {
val upIntent: Intent = NavUtils.getParentActivityIntent(this)
?: throw IllegalStateException("No Parent Activity Intent")
?: throw IllegalStateException("No Parent Activity Intent")
upIntent.putExtra(MainActivity.EXTRA_OPEN_FRAGMENT, TransactionListFragment.TAG_FRAGMENT)
when {
NavUtils.shouldUpRecreateTask(this, upIntent) || isTaskRoot -> {
TaskStackBuilder.create(this)
.addNextIntentWithParentStack(upIntent)
.startActivities()
.addNextIntentWithParentStack(upIntent)
.startActivities()
}
else -> {
finish()
@ -265,3 +283,156 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
}
fun Editable?.toLong(): Long = toString().toDouble().toLong() * 100
@Composable
fun TransactionForm(
title: String,
setTitle: (String) -> Unit,
description: String,
setDescription: (String) -> Unit,
amount: String,
setAmount: (String) -> Unit,
expense: Boolean,
setExpense: (Boolean) -> Unit,
date: Long,
setDate: (Long) -> Unit,
budget: Budget,
setBudget: (Budget) -> Unit,
budgets: List<Budget>,
category: Category,
setCategory: (Category) -> Unit,
categories: List<Category>,
) {
val scrollState = rememberScrollState()
val (budgetsExpanded, setBudgetsExpanded) = remember { mutableStateOf(false) }
val (categoriesExpanded, setCategoriesExpanded) = remember { mutableStateOf(false) }
val context = LocalContext.current
val formattedDate = remember(date) { DateFormat.getDateFormat(context).format(Date(date)) }
val formattedTime = remember(date) { DateFormat.getTimeFormat(context).format(Date(date)) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(scrollState),
verticalArrangement = spacedBy(8.dp, Alignment.CenterVertically)
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = title,
onValueChange = setTitle,
placeholder = { Text("Title") },
maxLines = 1
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = description,
onValueChange = setDescription,
placeholder = { Text("Description") }
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = amount,
onValueChange = setAmount,
placeholder = { Text("Amount") },
maxLines = 1,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number)
)
Row(modifier = Modifier.fillMaxWidth()) {
RadioButton(selected = expense, onClick = { setExpense(true) })
Text("Expense")
Spacer(modifier = Modifier.width(8.dp))
RadioButton(selected = !expense, onClick = { setExpense(false) })
Text("Income")
}
Text(text = "Date", style = MaterialTheme.typography.caption)
Row(modifier = Modifier.fillMaxWidth()) {
TextButton(
onClick = { /*TODO: Show date picker dialog */ },
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.onSurface)
) {
Text(formattedDate)
}
TextButton(
onClick = { /*TODO: Show time picker dialog */ },
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.onSurface)
) {
Text(formattedTime)
}
}
Spinner("Budget", budget.name, { it.name }, budgets, setBudget, budgetsExpanded, setBudgetsExpanded)
Spinner("Categories", category.title, { it.title }, categories, setCategory, categoriesExpanded, setCategoriesExpanded)
}
}
@Composable
fun <T> Spinner(
title: String,
selectedItemTitle: String,
itemTitle: (T) -> String,
items: List<T>,
selectItem: (T) -> Unit,
expanded: Boolean,
setExpanded: (Boolean) -> Unit
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(text = title, style = MaterialTheme.typography.caption)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = selectedItemTitle,
onValueChange = {},
readOnly = true
)
DropdownMenu(
modifier = Modifier.fillMaxWidth(),
expanded = expanded,
onDismissRequest = { setExpanded(false) }
) {
items.forEach {
DropdownMenuItem(onClick = { selectItem(it) }) {
Text(itemTitle(it))
}
}
}
}
}
@Composable
@Preview
fun TransactionForm_Preview() {
TwigsApp {
TransactionForm(
title = "",
setTitle = { },
description = "",
setDescription = { },
amount = "",
setAmount = { },
expense = false,
setExpense = { },
date = System.currentTimeMillis(),
setDate = {},
budget = Budget(name = "Uncategorized"),
setBudget = {},
budgets = emptyList(),
category = Category("budget", title = "Uncategorized", amount = 0L),
setCategory = {},
categories = emptyList()
)
}
}
@Composable
@Preview(heightDp = 400)
fun Spinner_ExpandedPreview() {
TwigsApp {
Spinner(
"Items",
"Uncategorized",
{ it },
listOf("one", "two", "three"),
{},
true,
{}
)
}
}