Another massive rewrite, this time geared towards making the code more reactive
This commit is contained in:
parent
2210222b22
commit
7109764e09
40 changed files with 531 additions and 445 deletions
|
@ -2,7 +2,6 @@ package com.wbrawner.budget
|
|||
|
||||
import android.app.Application
|
||||
import com.wbrawner.budget.common.user.User
|
||||
import com.wbrawner.budget.di.DaggerAppComponent
|
||||
|
||||
class AllowanceApplication : Application() {
|
||||
var currentUser: User? = null
|
||||
|
|
|
@ -5,8 +5,10 @@ import com.wbrawner.budget.lib.network.NetworkModule
|
|||
import com.wbrawner.budget.storage.StorageModule
|
||||
import com.wbrawner.budget.ui.SplashViewModel
|
||||
import com.wbrawner.budget.ui.budgets.BudgetFormViewModel
|
||||
import com.wbrawner.budget.ui.budgets.BudgetViewModel
|
||||
import com.wbrawner.budget.ui.categories.CategoryViewModel
|
||||
import com.wbrawner.budget.ui.budgets.BudgetListViewModel
|
||||
import com.wbrawner.budget.ui.categories.CategoryDetailsViewModel
|
||||
import com.wbrawner.budget.ui.categories.CategoryFormViewModel
|
||||
import com.wbrawner.budget.ui.categories.CategoryListViewModel
|
||||
import com.wbrawner.budget.ui.overview.OverviewViewModel
|
||||
import com.wbrawner.budget.ui.transactions.TransactionFormViewModel
|
||||
import com.wbrawner.budget.ui.transactions.TransactionListViewModel
|
||||
|
@ -20,9 +22,11 @@ import javax.inject.Singleton
|
|||
interface AppComponent {
|
||||
fun inject(viewModel: OverviewViewModel)
|
||||
fun inject(viewModel: SplashViewModel)
|
||||
fun inject(viewMode: BudgetViewModel)
|
||||
fun inject(viewMode: BudgetListViewModel)
|
||||
fun inject(viewModel: BudgetFormViewModel)
|
||||
fun inject(viewModel: CategoryViewModel)
|
||||
fun inject(viewModel: CategoryListViewModel)
|
||||
fun inject(viewModel: CategoryDetailsViewModel)
|
||||
fun inject(viewModel: CategoryFormViewModel)
|
||||
fun inject(viewModel: TransactionListViewModel)
|
||||
fun inject(viewModel: TransactionFormViewModel)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.wbrawner.budget
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -9,7 +10,11 @@ import kotlinx.coroutines.launch
|
|||
sealed class AsyncState<out T> {
|
||||
object Loading : AsyncState<Nothing>()
|
||||
class Success<T>(val data: T) : AsyncState<T>()
|
||||
class Error(val exception: Exception) : AsyncState<Nothing>()
|
||||
class Error(val exception: Exception) : AsyncState<Nothing>() {
|
||||
constructor(message: String) : this(RuntimeException(message))
|
||||
}
|
||||
|
||||
object Exit : AsyncState<Nothing>()
|
||||
}
|
||||
|
||||
interface AsyncViewModel<T> {
|
||||
|
@ -22,6 +27,7 @@ fun <VM, T> VM.launch(block: suspend () -> T): Job where VM : ViewModel, VM : As
|
|||
state.postValue(AsyncState.Success(block()))
|
||||
} catch (e: Exception) {
|
||||
state.postValue(AsyncState.Error(e))
|
||||
Log.e("AsyncViewModel", "Failed to load data", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import androidx.emoji.text.EmojiCompat
|
|||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.ui.categories.CategoryListFragment
|
||||
import kotlinx.android.synthetic.main.activity_transaction_list.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
@ -21,6 +20,7 @@ class MainActivity : AppCompatActivity() {
|
|||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
title = destination.label
|
||||
val showHomeAsUp = when (destination.label) {
|
||||
getString(R.string.title_overview) -> false
|
||||
getString(R.string.title_transactions) -> false
|
||||
getString(R.string.title_profile) -> false
|
||||
getString(R.string.title_budgets) -> false
|
||||
|
@ -28,15 +28,10 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp)
|
||||
}
|
||||
val openFragmentId = when (intent?.getStringExtra(EXTRA_OPEN_FRAGMENT)) {
|
||||
CategoryListFragment.TAG_FRAGMENT -> R.id.categoryListFragment
|
||||
else -> R.id.transactionListFragment
|
||||
}
|
||||
menu_main.selectedItemId = openFragmentId
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
if (item?.itemId == android.R.id.home) {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
findNavController(R.id.content_container).navigateUp()
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -2,25 +2,22 @@ package com.wbrawner.budget.ui
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class SplashActivity : AppCompatActivity(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private lateinit var viewModel: SplashViewModel
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
private val viewModel: SplashViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_splash)
|
||||
window.decorView.apply {
|
||||
|
@ -30,8 +27,6 @@ class SplashActivity : AppCompatActivity(), CoroutineScope {
|
|||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
)
|
||||
}
|
||||
(application as AllowanceApplication).appComponent.inject(this)
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory).get(SplashViewModel::class.java)
|
||||
val navController = findNavController(R.id.auth_content)
|
||||
launch {
|
||||
val navId = try {
|
||||
|
|
|
@ -6,12 +6,11 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.SplashViewModel
|
||||
import com.wbrawner.budget.ui.ensureNotEmpty
|
||||
import com.wbrawner.budget.ui.show
|
||||
|
@ -19,7 +18,6 @@ import kotlinx.android.synthetic.main.fragment_login.*
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
|
@ -27,24 +25,13 @@ import kotlin.coroutines.CoroutineContext
|
|||
*/
|
||||
class LoginFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private lateinit var viewModel: SplashViewModel
|
||||
@Inject
|
||||
lateinit var viewModelFactory: BudgetViewModelFactory
|
||||
private val viewModel: SplashViewModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as AllowanceApplication)
|
||||
.appComponent
|
||||
.inject(this)
|
||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
|
||||
.get(SplashViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_login, container, false)
|
||||
}
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_login, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
|
|
@ -29,7 +29,8 @@ abstract class ListWithAddButtonFragment<Data : Identifiable, ViewModel : AsyncV
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
recyclerView.hideFabOnScroll(addFab)
|
||||
val adapter = BindableAdapter<Data>(constructors, diffUtilItemCallback)
|
||||
val adapter = BindableAdapter(constructors, diffUtilItemCallback)
|
||||
recyclerView.adapter = adapter
|
||||
addFab.setOnClickListener {
|
||||
addItem()
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ class BindableAdapter<Data>(
|
|||
holder.onUnbind()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = getItem(position).viewType
|
||||
|
||||
abstract class BindableViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView), Bindable<BindableData<T>>
|
||||
|
||||
abstract class CoroutineViewHolder<T>(itemView: View) : BindableViewHolder<T>(itemView), CoroutineScope {
|
||||
|
@ -46,8 +48,8 @@ interface Bindable<T> {
|
|||
fun onUnbind() {}
|
||||
}
|
||||
|
||||
interface BindableData<T> {
|
||||
val data: T
|
||||
@get:LayoutRes
|
||||
val viewType: Int
|
||||
}
|
||||
data class BindableData<T>(
|
||||
val data: T,
|
||||
@get:LayoutRes
|
||||
val viewType: Int
|
||||
)
|
|
@ -1,9 +1,12 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
|
@ -12,33 +15,26 @@ import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
|
|||
import com.wbrawner.budget.ui.base.BindableAdapter
|
||||
import com.wbrawner.budget.ui.base.BindableData
|
||||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class BudgetListFragment : ListWithAddButtonFragment<BudgetViewModel, BudgetData>(), CoroutineScope {
|
||||
class BudgetListFragment : ListWithAddButtonFragment<Budget, BudgetListViewModel>() {
|
||||
override val noItemsStringRes: Int = R.string.overview_no_data
|
||||
override val viewModelClass: Class<BudgetViewModel> = BudgetViewModel::class.java
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
override fun onAttach(context: Context) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
override suspend fun loadItems(): Pair<List<BudgetData>, Map<Int, (view: View) -> BudgetViewHolder>> {
|
||||
val budgetItems = viewModel.getBudgets().map { BudgetData(it) }
|
||||
override val viewModel: BudgetListViewModel by viewModels()
|
||||
|
||||
return Pair(
|
||||
budgetItems,
|
||||
mapOf(BUDGET_VIEW to { v ->
|
||||
BudgetViewHolder(v) { _, budget ->
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_BUDGET_ID, budget.id!!)
|
||||
}
|
||||
findNavController().navigate(R.id.categoryListFragment, bundle)
|
||||
}
|
||||
})
|
||||
)
|
||||
override fun reloadItems() {
|
||||
viewModel.getBudgets()
|
||||
}
|
||||
|
||||
override fun bindData(data: Budget): BindableData<Budget> = BindableData(data, BUDGET_VIEW)
|
||||
|
||||
override val constructors: Map<Int, (View) -> BindableAdapter.BindableViewHolder<Budget>>
|
||||
get() = mapOf(BUDGET_VIEW to { v -> BudgetViewHolder(v, findNavController()) })
|
||||
|
||||
override fun addItem() {
|
||||
findNavController().navigate(R.id.addEditBudget)
|
||||
}
|
||||
|
@ -46,30 +42,25 @@ class BudgetListFragment : ListWithAddButtonFragment<BudgetViewModel, BudgetData
|
|||
|
||||
const val BUDGET_VIEW = R.layout.list_item_budget
|
||||
|
||||
class BudgetData(val budget: Budget) : BindableData {
|
||||
override val viewType: Int = BUDGET_VIEW
|
||||
}
|
||||
|
||||
class BudgetViewHolder(
|
||||
itemView: View,
|
||||
private val budgetClickListener: (View, Budget) -> Unit
|
||||
) : BindableAdapter.BindableViewHolder<BudgetData>(itemView) {
|
||||
class BudgetViewHolder(itemView: View, val navController: NavController) : BindableAdapter.BindableViewHolder<Budget>(itemView) {
|
||||
private val name: TextView = itemView.findViewById(R.id.budgetName)
|
||||
private val description: TextView = itemView.findViewById(R.id.budgetDescription)
|
||||
// private val balance: TextView = itemView.findViewById(R.id.budgetBalance)
|
||||
|
||||
override fun onBind(item: BudgetData) {
|
||||
with(item) {
|
||||
name.text = budget.name
|
||||
if (budget.description.isNullOrBlank()) {
|
||||
description.visibility = View.GONE
|
||||
} else {
|
||||
description.visibility = View.VISIBLE
|
||||
description.text = budget.description
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
budgetClickListener(it, budget)
|
||||
override fun onBind(item: BindableData<Budget>) {
|
||||
val budget = item.data
|
||||
name.text = budget.name
|
||||
if (budget.description.isNullOrBlank()) {
|
||||
description.visibility = View.GONE
|
||||
} else {
|
||||
description.visibility = View.VISIBLE
|
||||
description.text = budget.description
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_BUDGET_ID, budget.id!!)
|
||||
}
|
||||
navController.navigate(R.id.categoryListFragment, bundle)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.AsyncViewModel
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BudgetListViewModel : ViewModel(), AsyncViewModel<List<Budget>> {
|
||||
override val state: MutableLiveData<AsyncState<List<Budget>>> = MutableLiveData(AsyncState.Loading)
|
||||
|
||||
@Inject
|
||||
lateinit var budgetRepo: BudgetRepository
|
||||
|
||||
fun getBudgets() {
|
||||
launch {
|
||||
budgetRepo.findAll().toList()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package com.wbrawner.budget.ui.budgets
|
||||
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.ui.base.LoadingViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class BudgetViewModel : LoadingViewModel() {
|
||||
@Inject lateinit var budgetRepo: BudgetRepository
|
||||
|
||||
suspend fun getBudget(id: Long): Budget = showLoader {
|
||||
budgetRepo.findById(id)
|
||||
}
|
||||
|
||||
suspend fun getBudgets(): Collection<Budget> = showLoader {
|
||||
budgetRepo.findAll()
|
||||
}
|
||||
|
||||
suspend fun saveBudget(budget: Budget) = showLoader {
|
||||
if (budget.id == null)
|
||||
budgetRepo.create(budget)
|
||||
else
|
||||
budgetRepo.update(budget)
|
||||
}
|
||||
|
||||
suspend fun deleteBudgetById(id: Long) = showLoader {
|
||||
budgetRepo.delete(id)
|
||||
}
|
||||
|
||||
suspend fun getBalance(id: Long) = showLoader {
|
||||
budgetRepo.getBalance(id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.transactions.TransactionListFragment
|
||||
import kotlinx.android.synthetic.main.fragment_category_details.*
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class CategoryDetailsFragment : Fragment() {
|
||||
val viewModel: CategoryDetailsViewModel by viewModels()
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
||||
inflater.inflate(R.layout.fragment_category_details, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel.state.observe(viewLifecycleOwner, Observer { state ->
|
||||
when (state) {
|
||||
is AsyncState.Loading -> {
|
||||
categoryDetails.visibility = View.GONE
|
||||
progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is AsyncState.Success -> {
|
||||
categoryDetails.visibility = View.VISIBLE
|
||||
progressBar.visibility = View.GONE
|
||||
val category = state.data.category
|
||||
activity?.title = category.title
|
||||
categoryDescription.text = category.description
|
||||
childFragmentManager.fragments.firstOrNull()?.let {
|
||||
if (it !is TransactionListFragment) return@let
|
||||
it.reloadItems()
|
||||
} ?: run {
|
||||
val transactionsFragment = TransactionListFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putLong(EXTRA_BUDGET_ID, category.budgetId)
|
||||
putLong(EXTRA_CATEGORY_ID, category.id!!)
|
||||
}
|
||||
}
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.transactionsFragmentContainer, transactionsFragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
is AsyncState.Error -> {
|
||||
categoryDetails.visibility = View.VISIBLE
|
||||
progressBar.visibility = View.GONE
|
||||
Toast.makeText(view.context, "Failed to load context", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
is AsyncState.Exit -> {
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
})
|
||||
viewModel.getCategory(arguments?.getLong(EXTRA_CATEGORY_ID))
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_edit) {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_CATEGORY_ID, arguments?.getLong(EXTRA_CATEGORY_ID) ?: -1)
|
||||
}
|
||||
findNavController().navigate(R.id.addEditCategoryActivity, bundle)
|
||||
} else if (item.itemId == android.R.id.home) {
|
||||
return findNavController().navigateUp()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_editable, menu)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.AsyncViewModel
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryDetailsViewModel : ViewModel(), AsyncViewModel<CategoryDetails> {
|
||||
override val state: MutableLiveData<AsyncState<CategoryDetails>> = MutableLiveData(AsyncState.Loading)
|
||||
|
||||
@Inject
|
||||
lateinit var categoryRepo: CategoryRepository
|
||||
|
||||
fun getCategory(id: Long? = null) {
|
||||
if (id == null) {
|
||||
state.postValue(AsyncState.Error("Invalid category ID"))
|
||||
return
|
||||
}
|
||||
launch {
|
||||
val category = categoryRepo.findById(id)
|
||||
val multiplier = if (category.expense) -1 else 1
|
||||
val balance = categoryRepo.getBalance(category.id!!).toInt() * multiplier
|
||||
CategoryDetails(
|
||||
category,
|
||||
balance
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CategoryDetails(
|
||||
val category: Category,
|
||||
val balance: Int
|
||||
)
|
|
@ -4,26 +4,24 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.lifecycle.Observer
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.di.BudgetViewModelFactory
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.transactions.toLong
|
||||
import kotlinx.android.synthetic.main.activity_add_edit_category.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class CategoryFormActivity : AppCompatActivity(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
lateinit var viewModel: CategoryViewModel
|
||||
class CategoryFormActivity : AppCompatActivity() {
|
||||
lateinit var viewModel: CategoryFormViewModel
|
||||
var id: Long? = null
|
||||
var menu: Menu? = null
|
||||
|
||||
|
@ -32,40 +30,41 @@ class CategoryFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
setContentView(R.layout.activity_add_edit_category)
|
||||
setSupportActionBar(action_bar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
(application as AllowanceApplication).appComponent.inject(this)
|
||||
launch {
|
||||
val budgets = viewModel.getBudgets().toTypedArray()
|
||||
budgetSpinner.adapter = ArrayAdapter<Budget>(
|
||||
this@CategoryFormActivity,
|
||||
android.R.layout.simple_list_item_1,
|
||||
budgets
|
||||
)
|
||||
loadCategory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCategory() {
|
||||
val categoryId = intent?.extras?.getLong(EXTRA_CATEGORY_ID)
|
||||
if (categoryId == null) {
|
||||
setTitle(R.string.title_add_category)
|
||||
return
|
||||
}
|
||||
launch {
|
||||
val category = try {
|
||||
viewModel.getCategory(categoryId)
|
||||
} catch (e: Exception) {
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = false
|
||||
null
|
||||
} ?: return@launch
|
||||
id = category.id
|
||||
setTitle(R.string.title_edit_category)
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = true
|
||||
edit_category_name.setText(category.title)
|
||||
edit_category_amount.setText(String.format("%.02f", category.amount / 100.0f))
|
||||
expense.isChecked = category.expense
|
||||
income.isChecked = !category.expense
|
||||
archived.isChecked = category.archived
|
||||
}
|
||||
(application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
viewModel.state.observe(this, Observer { state ->
|
||||
when (state) {
|
||||
is AsyncState.Loading -> {
|
||||
categoryForm.visibility = View.GONE
|
||||
progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is AsyncState.Success -> {
|
||||
categoryForm.visibility = View.VISIBLE
|
||||
progressBar.visibility = View.GONE
|
||||
val category = state.data.category
|
||||
id = category.id
|
||||
setTitle(state.data.titleRes)
|
||||
menu?.findItem(R.id.action_delete)?.isVisible = state.data.showDeleteButton
|
||||
edit_category_name.setText(category.title)
|
||||
edit_category_amount.setText(String.format("%.02f", (category.amount.toBigDecimal() / 100.toBigDecimal()).toFloat()))
|
||||
expense.isChecked = category.expense
|
||||
income.isChecked = !category.expense
|
||||
archived.isChecked = category.archived
|
||||
budgetSpinner.adapter = ArrayAdapter<Budget>(
|
||||
this@CategoryFormActivity,
|
||||
android.R.layout.simple_list_item_1,
|
||||
state.data.budgets
|
||||
)
|
||||
}
|
||||
is AsyncState.Error -> {
|
||||
// TODO: Show error message
|
||||
categoryForm.visibility = View.VISIBLE
|
||||
progressBar.visibility = View.GONE
|
||||
Toast.makeText(this, "Failed to save Category", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
is AsyncState.Exit -> finish()
|
||||
}
|
||||
})
|
||||
viewModel.loadCategory(intent?.extras?.getLong(EXTRA_CATEGORY_ID))
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
|
@ -96,23 +95,17 @@ class CategoryFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
}
|
||||
R.id.action_save -> {
|
||||
if (!validateFields()) return true
|
||||
launch {
|
||||
viewModel.saveCategory(Category(
|
||||
id = id,
|
||||
title = edit_category_name.text.toString(),
|
||||
amount = edit_category_amount.text.toLong(),
|
||||
budgetId = (budgetSpinner.selectedItem as Budget).id!!,
|
||||
expense = expense.isChecked,
|
||||
archived = archived.isChecked
|
||||
))
|
||||
finish()
|
||||
}
|
||||
viewModel.saveCategory(Category(
|
||||
id = id,
|
||||
title = edit_category_name.text.toString(),
|
||||
amount = edit_category_amount.text.toLong(),
|
||||
budgetId = (budgetSpinner.selectedItem as Budget).id!!,
|
||||
expense = expense.isChecked,
|
||||
archived = archived.isChecked
|
||||
))
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
launch {
|
||||
viewModel.deleteCategoryById(this@CategoryFormActivity.id!!)
|
||||
finish()
|
||||
}
|
||||
viewModel.deleteCategoryById(this@CategoryFormActivity.id!!)
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.AsyncViewModel
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.launch
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
|
||||
override val state: MutableLiveData<AsyncState<CategoryFormState>> = MutableLiveData(AsyncState.Loading)
|
||||
|
||||
@Inject
|
||||
lateinit var categoryRepository: CategoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var budgetRepository: BudgetRepository
|
||||
|
||||
fun loadCategory(categoryId: Long? = null) {
|
||||
launch {
|
||||
val category = categoryId?.let {
|
||||
categoryRepository.findById(it)
|
||||
} ?: Category(-1, title = "", amount = 0)
|
||||
CategoryFormState(
|
||||
category,
|
||||
budgetRepository.findAll().toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCategory(category: Category) {
|
||||
viewModelScope.launch {
|
||||
state.postValue(AsyncState.Loading)
|
||||
try {
|
||||
if (category.id == null)
|
||||
categoryRepository.create(category)
|
||||
else
|
||||
categoryRepository.update(category)
|
||||
state.postValue(AsyncState.Exit)
|
||||
} catch (e: Exception) {
|
||||
state.postValue(AsyncState.Error(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCategoryById(id: Long) {
|
||||
viewModelScope.launch {
|
||||
state.postValue(AsyncState.Loading)
|
||||
try {
|
||||
categoryRepository.delete(id)
|
||||
state.postValue(AsyncState.Exit)
|
||||
} catch (e: Exception) {
|
||||
state.postValue(AsyncState.Error(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CategoryFormState(
|
||||
val category: Category,
|
||||
val budgets: List<Budget>,
|
||||
@StringRes val titleRes: Int,
|
||||
val showDeleteButton: Boolean
|
||||
) {
|
||||
constructor(category: Category, budgets: List<Budget>) : this(
|
||||
category,
|
||||
budgets,
|
||||
category.id?.let { R.string.title_edit_category } ?: R.string.title_add_category,
|
||||
category.id != null
|
||||
)
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.R
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
|
||||
import com.wbrawner.budget.ui.base.BindableAdapter
|
||||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import com.wbrawner.budget.ui.transactions.TRANSACTION_VIEW
|
||||
import com.wbrawner.budget.ui.transactions.TransactionData
|
||||
import com.wbrawner.budget.ui.transactions.TransactionViewHolder
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class CategoryFragment : ListWithAddButtonFragment<Category, CategoryViewModel>() {
|
||||
override val viewModel: CategoryViewModel by viewModels()
|
||||
override val noItemsStringRes: Int = R.string.transactions_no_data
|
||||
|
||||
override fun reloadItems() {
|
||||
|
||||
}
|
||||
|
||||
override val constructors: Map<Int, (View) -> BindableAdapter.BindableViewHolder<Category>>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val diffUtilItemCallback: DiffUtil.ItemCallback<Category>
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override suspend fun loadItems(): Pair<List<TransactionData>, Map<Int, (view: View) -> BindableAdapter.BindableViewHolder<TransactionData>>> {
|
||||
val categoryId = arguments?.getLong(EXTRA_CATEGORY_ID)
|
||||
if (categoryId == null) {
|
||||
findNavController().navigateUp()
|
||||
return Pair(emptyList(), emptyMap())
|
||||
}
|
||||
val category = viewModel.getCategory(categoryId)
|
||||
activity?.title = category.title
|
||||
// TODO: Add category details here as well
|
||||
val items = ArrayList<TransactionData>()
|
||||
items.addAll(viewModel.getTransactions(categoryId).map { TransactionData(it) })
|
||||
return Pair(
|
||||
items,
|
||||
mapOf(TRANSACTION_VIEW to { v ->
|
||||
TransactionViewHolder(v, findNavController())
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_edit) {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_CATEGORY_ID, arguments?.getLong(EXTRA_CATEGORY_ID) ?: -1)
|
||||
}
|
||||
findNavController().navigate(R.id.addEditCategoryActivity, bundle)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_editable, menu)
|
||||
}
|
||||
|
||||
override fun addItem() {
|
||||
// TODO: Open new transaction flow with budget and category pre-filled
|
||||
findNavController().navigate(R.id.addEditTransactionActivity)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
|||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
|
@ -18,29 +19,20 @@ import com.wbrawner.budget.ui.base.BindableData
|
|||
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CategoryListFragment : ListWithAddButtonFragment<Category, CategoryViewModel>() {
|
||||
class CategoryListFragment : ListWithAddButtonFragment<Category, CategoryListViewModel>() {
|
||||
override val noItemsStringRes: Int = R.string.categories_no_data
|
||||
override val viewModelClass: Class<CategoryViewModel> = CategoryViewModel::class.java
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
override val viewModel: CategoryListViewModel by viewModels()
|
||||
override fun reloadItems() {
|
||||
viewModel.getCategories(arguments?.getLong(EXTRA_BUDGET_ID))
|
||||
}
|
||||
|
||||
override suspend fun loadItems(): Pair<List<CategoryData>, Map<Int, (view: View) -> CategoryViewHolder>> {
|
||||
val budgetId = arguments?.getLong(EXTRA_BUDGET_ID)
|
||||
if (budgetId == null) {
|
||||
findNavController().navigateUp()
|
||||
return Pair(emptyList(), emptyMap())
|
||||
}
|
||||
val budget = viewModel.getBudget(budgetId)
|
||||
activity?.title = budget.name
|
||||
return Pair(
|
||||
viewModel.getCategories(budgetId).map { CategoryData(it) },
|
||||
mapOf(CATEGORY_VIEW to { v ->
|
||||
CategoryViewHolder(v, viewModel, findNavController())
|
||||
})
|
||||
)
|
||||
override fun bindData(data: Category): BindableData<Category> = BindableData(data, CATEGORY_VIEW)
|
||||
|
||||
override val constructors: Map<Int, (View) -> BindableAdapter.BindableViewHolder<Category>> = mapOf(CATEGORY_VIEW to { v -> CategoryViewHolder(v, viewModel, findNavController()) })
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun addItem() {
|
||||
|
@ -54,50 +46,45 @@ class CategoryListFragment : ListWithAddButtonFragment<Category, CategoryViewMod
|
|||
|
||||
const val CATEGORY_VIEW = R.layout.list_item_category
|
||||
|
||||
class CategoryData(val category: Category) : BindableData {
|
||||
override val viewType: Int = CATEGORY_VIEW
|
||||
}
|
||||
|
||||
class CategoryViewHolder(
|
||||
itemView: View,
|
||||
private val viewModel: CategoryViewModel,
|
||||
private val viewModel: CategoryListViewModel,
|
||||
private val navController: NavController
|
||||
) : BindableAdapter.CoroutineViewHolder<CategoryData>(itemView) {
|
||||
) : BindableAdapter.CoroutineViewHolder<Category>(itemView) {
|
||||
private val name: TextView = itemView.findViewById(R.id.category_title)
|
||||
private val amount: TextView = itemView.findViewById(R.id.category_amount)
|
||||
private val progressBar: ProgressBar = itemView.findViewById(R.id.category_progress)
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onBind(item: CategoryData) {
|
||||
with(item) {
|
||||
name.text = category.title
|
||||
// TODO: Format according to budget's currency
|
||||
amount.text = String.format("${'$'}%.02f", category.amount / 100.0f)
|
||||
val tintColor = if (category.expense) R.color.colorTextRed else R.color.colorTextGreen
|
||||
val colorStateList = with(itemView.context) {
|
||||
android.content.res.ColorStateList.valueOf(getColor(tintColor))
|
||||
}
|
||||
progressBar.progressTintList = colorStateList
|
||||
progressBar.indeterminateTintList = colorStateList
|
||||
progressBar.max = category.amount.toInt()
|
||||
launch {
|
||||
val balance = viewModel.getBalance(category)
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.setProgress(
|
||||
balance,
|
||||
true
|
||||
)
|
||||
amount.text = itemView.context.getString(
|
||||
R.string.balance_remaning,
|
||||
(category.amount - balance) / 100.0f
|
||||
)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_CATEGORY_ID, category.id ?: -1)
|
||||
}
|
||||
navController.navigate(R.id.categoryFragment, bundle)
|
||||
override fun onBind(item: BindableData<Category>) {
|
||||
val category = item.data
|
||||
name.text = category.title
|
||||
// TODO: Format according to budget's currency
|
||||
amount.text = String.format("${'$'}%.02f", category.amount / 100.0f)
|
||||
val tintColor = if (category.expense) R.color.colorTextRed else R.color.colorTextGreen
|
||||
val colorStateList = with(itemView.context) {
|
||||
android.content.res.ColorStateList.valueOf(getColor(tintColor))
|
||||
}
|
||||
progressBar.progressTintList = colorStateList
|
||||
progressBar.indeterminateTintList = colorStateList
|
||||
progressBar.max = category.amount.toInt()
|
||||
launch {
|
||||
val balance = viewModel.getBalance(category).toInt()
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.setProgress(
|
||||
balance,
|
||||
true
|
||||
)
|
||||
amount.text = itemView.context.getString(
|
||||
R.string.balance_remaning,
|
||||
(category.amount - balance) / 100.0f
|
||||
)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
val bundle = Bundle().apply {
|
||||
putLong(EXTRA_CATEGORY_ID, category.id ?: -1)
|
||||
}
|
||||
navController.navigate(R.id.categoryFragment, bundle)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,36 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.AsyncViewModel
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryListViewModel : ViewModel() {
|
||||
@Inject lateinit var budgetRepo: BudgetRepository
|
||||
@Inject lateinit var categoryRepo: CategoryRepository
|
||||
class CategoryListViewModel : ViewModel(), AsyncViewModel<List<Category>> {
|
||||
override val state: MutableLiveData<AsyncState<List<Category>>> = MutableLiveData(AsyncState.Loading)
|
||||
|
||||
suspend fun getCategory(id: Long): Category = categoryRepo.findById(id)
|
||||
@Inject
|
||||
lateinit var budgetRepo: BudgetRepository
|
||||
|
||||
suspend fun getCategories(budgetId: Long? = null): Collection<Category> =
|
||||
categoryRepo.findAll(budgetId?.let { arrayOf(it) })
|
||||
@Inject
|
||||
lateinit var categoryRepo: CategoryRepository
|
||||
|
||||
suspend fun saveCategory(category: Category) = if (category.id == null) categoryRepo.create(category)
|
||||
else categoryRepo.update(category)
|
||||
fun getCategories(budgetId: Long? = null) {
|
||||
if (budgetId == null) {
|
||||
state.postValue(AsyncState.Error("Invalid budget ID"))
|
||||
return
|
||||
}
|
||||
launch {
|
||||
categoryRepo.findAll(arrayOf(budgetId)).toList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteCategoryById(id: Long) = categoryRepo.delete(id)
|
||||
|
||||
suspend fun getBalance(id: Long) = categoryRepo.getBalance(id)
|
||||
suspend fun getBalance(category: Category): Long {
|
||||
val multiplier = if (category.expense) -1 else 1
|
||||
return categoryRepo.getBalance(category.id!!) * multiplier
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
package com.wbrawner.budget.ui.categories
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.AsyncViewModel
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.Category
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class CategoryViewModel : ViewModel(), AsyncViewModel<List<Category>> {
|
||||
override val state: MutableLiveData<AsyncState<List<Category>>> = MutableLiveData(AsyncState.Loading)
|
||||
|
||||
@Inject lateinit var transactionRepo: TransactionRepository
|
||||
@Inject lateinit var categoryRepo: CategoryRepository
|
||||
@Inject lateinit var budgetRepo: BudgetRepository
|
||||
|
||||
fun getBudget(budgetId: Long): Budget {
|
||||
budgetRepo.findById(budgetId)
|
||||
}
|
||||
|
||||
suspend fun getBudgets() = showLoader {
|
||||
budgetRepo.findAll()
|
||||
}
|
||||
|
||||
suspend fun getTransactions(categoryId: Long) = showLoader {
|
||||
transactionRepo.findAll(categoryIds = listOf(categoryId))
|
||||
}
|
||||
|
||||
suspend fun getCategory(id: Long): Category = showLoader {
|
||||
categoryRepo.findById(id)
|
||||
}
|
||||
|
||||
suspend fun getCategories(budgetId: Long? = null): Collection<Category> = showLoader {
|
||||
categoryRepo.findAll(budgetId?.let { arrayOf(it) })
|
||||
}
|
||||
|
||||
suspend fun saveCategory(category: Category) = showLoader {
|
||||
if (category.id == null)
|
||||
categoryRepo.create(category)
|
||||
else
|
||||
categoryRepo.update(category)
|
||||
}
|
||||
|
||||
suspend fun deleteCategoryById(id: Long) = showLoader {
|
||||
categoryRepo.delete(id)
|
||||
}
|
||||
|
||||
suspend fun getBalance(category: Category) = showLoader {
|
||||
val balance = categoryRepo.getBalance(category.id!!)
|
||||
val multiplier = if (category.expense) -1 else 1
|
||||
return@showLoader (balance * multiplier).toInt()
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ class OverviewFragment : Fragment() {
|
|||
override fun onAttach(context: Context) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onAttach(context)
|
||||
viewModel.loadOverview(arguments?.getLong(EXTRA_BUDGET_ID))
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -54,6 +53,11 @@ class OverviewFragment : Fragment() {
|
|||
})
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
viewModel.loadOverview(arguments?.getLong(EXTRA_BUDGET_ID))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_BUDGET_ID = "budgetId"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,10 @@ class OverviewViewModel : ViewModel() {
|
|||
lateinit var budgetRepo: BudgetRepository
|
||||
|
||||
fun loadOverview(id: Long? = null) {
|
||||
if (id == null) return // TODO: Handle this or make it non-null
|
||||
if (id == null) {
|
||||
state.postValue(AsyncState.Error("Invalid Budget ID"))
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
state.postValue(AsyncState.Loading)
|
||||
try {
|
||||
|
|
|
@ -38,6 +38,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
var menu: Menu? = null
|
||||
var transaction: Transaction? = null
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_add_edit_transaction)
|
||||
|
@ -45,7 +46,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
setTitle(R.string.title_add_transaction)
|
||||
edit_transaction_type_expense.isChecked = true
|
||||
(application as AllowanceApplication).appComponent.inject(this)
|
||||
(application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
launch {
|
||||
val accounts = viewModel.getAccounts().toTypedArray()
|
||||
setCategories()
|
||||
|
@ -69,7 +70,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
loadTransaction()
|
||||
transactionDate.setOnClickListener {
|
||||
val currentDate = DateFormat.getDateFormat(this@TransactionFormActivity)
|
||||
.parse(transactionDate.text.toString())
|
||||
.parse(transactionDate.text.toString()) ?: Date()
|
||||
DatePickerDialog(
|
||||
this@TransactionFormActivity,
|
||||
{ _, year, month, dayOfMonth ->
|
||||
|
@ -83,7 +84,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
}
|
||||
transactionTime.setOnClickListener {
|
||||
val currentDate = DateFormat.getTimeFormat(this@TransactionFormActivity)
|
||||
.parse(transactionTime.text.toString())
|
||||
.parse(transactionTime.text.toString()) ?: Date()
|
||||
TimePickerDialog(
|
||||
this@TransactionFormActivity,
|
||||
TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute ->
|
||||
|
@ -135,7 +136,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setCategories(categories: Collection<Category> = emptyList()) {
|
||||
private fun setCategories(categories: List<Category> = emptyList()) {
|
||||
val adapter = ArrayAdapter<Category>(
|
||||
this@TransactionFormActivity,
|
||||
android.R.layout.simple_list_item_1
|
||||
|
@ -191,7 +192,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
|
|||
id = id,
|
||||
budgetId = (budgetSpinner.selectedItem as Budget).id!!,
|
||||
title = edit_transaction_title.text.toString(),
|
||||
date = date,
|
||||
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,
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.view.Menu
|
|||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -36,12 +35,11 @@ class TransactionListFragment : ListWithAddButtonFragment<Transaction, Transacti
|
|||
viewModel.getTransactions(arguments?.getLong(EXTRA_BUDGET_ID), arguments?.getLong(EXTRA_CATEGORY_ID))
|
||||
}
|
||||
|
||||
override fun bindData(data: Transaction): BindableData<Transaction> = TransactionData(data)
|
||||
override fun bindData(data: Transaction): BindableData<Transaction> = BindableData(data, TRANSACTION_VIEW)
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
(requireActivity().application as AllowanceApplication).appComponent.inject(viewModel)
|
||||
super.onAttach(context)
|
||||
viewModel.getTransactions()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -57,9 +55,6 @@ class TransactionListFragment : ListWithAddButtonFragment<Transaction, Transacti
|
|||
if (item.itemId != R.id.filter) {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
val dialogView = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
// TODO: Launch a Google Drive-style search/filter screen
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Filter Transactions")
|
||||
|
@ -85,14 +80,7 @@ class TransactionListFragment : ListWithAddButtonFragment<Transaction, Transacti
|
|||
|
||||
const val TRANSACTION_VIEW = R.layout.list_item_transaction
|
||||
|
||||
class TransactionData(override val data: Transaction) : BindableData<Transaction> {
|
||||
override val viewType: Int = TRANSACTION_VIEW
|
||||
}
|
||||
|
||||
class TransactionViewHolder(
|
||||
itemView: View,
|
||||
private val navController: NavController
|
||||
) : BindableAdapter.CoroutineViewHolder<Transaction>(itemView) {
|
||||
class TransactionViewHolder(itemView: View, val navController: NavController) : BindableAdapter.CoroutineViewHolder<Transaction>(itemView) {
|
||||
private val name: TextView = itemView.findViewById(R.id.transaction_title)
|
||||
private val date: TextView = itemView.findViewById(R.id.transaction_date)
|
||||
private val amount: TextView = itemView.findViewById(R.id.transaction_amount)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView2"
|
||||
android:id="@+id/categoryForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -58,16 +58,16 @@
|
|||
|
||||
<RadioButton
|
||||
android:id="@+id/expense"
|
||||
android:text="@string/type_expense"
|
||||
android:checked="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/type_expense" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/income"
|
||||
android:text="@string/type_income"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/type_income" />
|
||||
</RadioGroup>
|
||||
|
||||
<CheckBox
|
||||
|
@ -89,4 +89,13 @@
|
|||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
52
android/src/main/res/layout/fragment_category_details.xml
Normal file
52
android/src/main/res/layout/fragment_category_details.xml
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/categoryDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:padding="16dp"
|
||||
android:id="@+id/categoryDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balanceLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/label_current_balance"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="36sp" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/transactionsFragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
|
@ -1,12 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/overviewContent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -15,25 +16,16 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/label_current_balance"
|
||||
app:layout_constraintBottom_toTopOf="@+id/balance"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
android:text="@string/label_current_balance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:textSize="36sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/balanceLabel" />
|
||||
android:textSize="36sp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/noData"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/categoryListFragment">
|
||||
app:startDestination="@id/overviewFragment">
|
||||
<fragment
|
||||
android:id="@+id/categoryListFragment"
|
||||
android:name="com.wbrawner.budget.ui.categories.CategoryListFragment"
|
||||
|
@ -47,7 +47,7 @@
|
|||
tools:layout="@layout/fragment_profile" />
|
||||
<fragment
|
||||
android:id="@+id/categoryFragment"
|
||||
android:name="com.wbrawner.budget.ui.categories.CategoryFragment"
|
||||
android:name="com.wbrawner.budget.ui.categories.CategoryDetailsFragment"
|
||||
android:label="fragment_category"
|
||||
tools:layout="@layout/fragment_list_with_add_button" />
|
||||
<activity
|
||||
|
|
|
@ -14,7 +14,7 @@ interface BudgetApiService {
|
|||
suspend fun getBudgets(
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Collection<Budget>
|
||||
): List<Budget>
|
||||
|
||||
@GET("budgets/{id}")
|
||||
suspend fun getBudget(@Path("id") id: Long): Budget
|
||||
|
@ -40,7 +40,7 @@ interface BudgetApiService {
|
|||
@Query("budgetIds") budgetIds: Array<Long>? = null,
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Collection<Category>
|
||||
): List<Category>
|
||||
|
||||
@GET("categories/{id}")
|
||||
suspend fun getCategory(@Path("id") id: Long): Category
|
||||
|
@ -69,7 +69,7 @@ interface BudgetApiService {
|
|||
@Query("to") to: String? = null,
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Collection<Transaction>
|
||||
): List<Transaction>
|
||||
|
||||
@GET("transactions/{id}")
|
||||
suspend fun getTransaction(@Path("id") id: Long): Transaction
|
||||
|
@ -92,7 +92,7 @@ interface BudgetApiService {
|
|||
@Query("budgetId") budgetId: Long? = null,
|
||||
@Query("count") count: Int? = null,
|
||||
@Query("page") page: Int? = null
|
||||
): Collection<User>
|
||||
): List<User>
|
||||
|
||||
@POST("users/login")
|
||||
suspend fun login(@Body request: LoginRequest): User
|
||||
|
|
|
@ -105,4 +105,4 @@ class NetworkModule {
|
|||
@Provides
|
||||
fun provideUserRepository(apiService: BudgetApiService): UserRepository =
|
||||
NetworkUserRepository(apiService)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ class NetworkBudgetRepository @Inject constructor(private val apiService: Budget
|
|||
override suspend fun create(newItem: Budget): Budget =
|
||||
apiService.newBudget(NewBudgetRequest(newItem))
|
||||
|
||||
override suspend fun findAll(): Collection<Budget> = apiService.getBudgets().sortedBy { it.name }
|
||||
override suspend fun findAll(): List<Budget> = apiService.getBudgets().sortedBy { it.name }
|
||||
|
||||
override suspend fun findById(id: Long): Budget = apiService.getBudget(id)
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ import javax.inject.Inject
|
|||
class NetworkCategoryRepository @Inject constructor(private val apiService: BudgetApiService) : CategoryRepository {
|
||||
override suspend fun create(newItem: Category): Category = apiService.newCategory(newItem)
|
||||
|
||||
override suspend fun findAll(budgetIds: Array<Long>?): Collection<Category> = apiService.getCategories(budgetIds).sortedBy { it.title }
|
||||
override suspend fun findAll(budgetIds: Array<Long>?): List<Category> = apiService.getCategories(budgetIds).sortedBy { it.title }
|
||||
|
||||
override suspend fun findAll(): Collection<Category> = findAll(null)
|
||||
override suspend fun findAll(): List<Category> = findAll(null)
|
||||
|
||||
override suspend fun findById(id: Long): Category = apiService.getCategory(id)
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class NetworkTransactionRepository @Inject constructor(private val apiService: B
|
|||
categoryIds: List<Long>?,
|
||||
start: Calendar?,
|
||||
end: Calendar?
|
||||
): Collection<Transaction> = apiService.getTransactions(
|
||||
): List<Transaction> = apiService.getTransactions(
|
||||
budgetIds,
|
||||
categoryIds,
|
||||
start?.let {
|
||||
|
@ -30,7 +30,7 @@ class NetworkTransactionRepository @Inject constructor(private val apiService: B
|
|||
}
|
||||
)
|
||||
|
||||
override suspend fun findAll(): Collection<Transaction> = findAll(null)
|
||||
override suspend fun findAll(): List<Transaction> = findAll(null)
|
||||
|
||||
override suspend fun findById(id: Long): Transaction = apiService.getTransaction(id)
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@ class NetworkUserRepository @Inject constructor(private val apiService: BudgetAp
|
|||
|
||||
override suspend fun create(newItem: User): User = apiService.newUser(newItem)
|
||||
|
||||
override suspend fun findAll(accountId: Long?): Collection<User> = apiService.getUsers(accountId)
|
||||
override suspend fun findAll(accountId: Long?): List<User> = apiService.getUsers(accountId)
|
||||
|
||||
override suspend fun findAll(): Collection<User> = findAll(null)
|
||||
override suspend fun findAll(): List<User> = findAll(null)
|
||||
|
||||
override suspend fun findById(id: Long): User = apiService.getUser(id)
|
||||
|
||||
override suspend fun findAllByNameLike(query: String): Collection<User> =
|
||||
override suspend fun findAllByNameLike(query: String): List<User> =
|
||||
apiService.searchUsers(query)
|
||||
|
||||
override suspend fun update(updatedItem: User): User =
|
||||
|
|
|
@ -8,7 +8,7 @@ package com.wbrawner.budget.common
|
|||
*/
|
||||
interface Repository<T, K> {
|
||||
suspend fun create(newItem: T): T
|
||||
suspend fun findAll(): Collection<T>
|
||||
suspend fun findAll(): List<T>
|
||||
suspend fun findById(id: K): T
|
||||
suspend fun update(updatedItem: T): T
|
||||
suspend fun delete(id: K)
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package com.wbrawner.budget.common.budget
|
||||
|
||||
import com.wbrawner.budget.common.Identifiable
|
||||
import com.wbrawner.budget.common.user.User
|
||||
|
||||
data class Budget(
|
||||
val id: Long? = null,
|
||||
override val id: Long? = null,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val users: List<User> = emptyList()
|
||||
) {
|
||||
) : Identifiable {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.wbrawner.budget.common.Identifiable
|
|||
|
||||
data class Category(
|
||||
val budgetId: Long,
|
||||
val id: Long? = null,
|
||||
override val id: Long? = null,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
val amount: Long,
|
||||
|
|
|
@ -3,6 +3,6 @@ package com.wbrawner.budget.common.category
|
|||
import com.wbrawner.budget.common.Repository
|
||||
|
||||
interface CategoryRepository : Repository<Category, Long> {
|
||||
suspend fun findAll(budgetIds: Array<Long>? = null): Collection<Category>
|
||||
suspend fun findAll(budgetIds: Array<Long>? = null): List<Category>
|
||||
suspend fun getBalance(id: Long): Long
|
||||
}
|
|
@ -6,7 +6,7 @@ import java.util.*
|
|||
data class Transaction(
|
||||
override val id: Long? = null,
|
||||
val title: String,
|
||||
val date: Calendar,
|
||||
val date: Date,
|
||||
val description: String,
|
||||
val amount: Long,
|
||||
val categoryId: Long? = null,
|
||||
|
|
|
@ -9,5 +9,5 @@ interface TransactionRepository : Repository<Transaction, Long> {
|
|||
categoryIds: List<Long>? = null,
|
||||
start: Calendar? = null,
|
||||
end: Calendar? = null
|
||||
): Collection<Transaction>
|
||||
): List<Transaction>
|
||||
}
|
|
@ -5,6 +5,6 @@ import com.wbrawner.budget.common.Repository
|
|||
interface UserRepository : Repository<User, Long> {
|
||||
suspend fun login(username: String, password: String): User
|
||||
suspend fun getProfile(): User
|
||||
suspend fun findAll(accountId: Long? = null): Collection<User>
|
||||
suspend fun findAllByNameLike(query: String): Collection<User>
|
||||
suspend fun findAll(accountId: Long? = null): List<User>
|
||||
suspend fun findAllByNameLike(query: String): List<User>
|
||||
}
|
Loading…
Reference in a new issue