Use Hilt for DI

This commit is contained in:
William Brawner 2021-08-23 15:31:02 -06:00
parent ed05d9bd97
commit 1e3788c670
30 changed files with 200 additions and 245 deletions

View file

@ -1,10 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -15,7 +18,7 @@ android {
defaultConfig {
applicationId "com.wbrawner.budget"
minSdkVersion 26
targetSdkVersion 30
targetSdkVersion 31
versionCode 1
versionName "1.0"
vectorDrawables {
@ -32,6 +35,12 @@ android {
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion rootProject.extensions.getExtraProperties().get("compose")
}
}
dependencies {
@ -39,7 +48,7 @@ dependencies {
implementation project(':budgetlib')
implementation project(':storage')
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.6.0'
@ -49,15 +58,26 @@ dependencies {
implementation 'androidx.emoji:emoji-bundled:1.1.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
// Dagger
implementation "com.google.dagger:dagger:$dagger"
kapt "com.google.dagger:dagger-compiler:$dagger"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
implementation "com.google.dagger:hilt-android:$dagger"
kapt "com.google.dagger:hilt-compiler:$dagger"
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
def navigation = '2.4.0-alpha07'
implementation "androidx.navigation:navigation-fragment-ktx:$navigation"
implementation "androidx.navigation:navigation-ui-ktx:$navigation"
implementation "androidx.navigation:navigation-compose:$navigation"
implementation "androidx.compose.ui:ui:$compose"
implementation "androidx.compose.ui:ui-tooling:$compose"
implementation "androidx.compose.foundation:foundation:$compose"
implementation "androidx.compose.material:material:$compose"
implementation "androidx.compose.material:material-icons-core:$compose"
implementation "androidx.compose.material:material-icons-extended:$compose"
implementation "androidx.compose.animation:animation:$compose"
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"

View file

@ -1,45 +0,0 @@
package com.wbrawner.budget
import android.content.Context
import com.wbrawner.budget.common.util.ErrorHandler
import com.wbrawner.budget.lib.network.NetworkModule
import com.wbrawner.budget.storage.StorageModule
import com.wbrawner.budget.ui.MainViewModel
import com.wbrawner.budget.ui.SplashViewModel
import com.wbrawner.budget.ui.budgets.BudgetFormViewModel
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
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [AppModule::class, StorageModule::class, NetworkModule::class])
interface AppComponent {
fun inject(app: TwigsApplication)
fun inject(viewModel: OverviewViewModel)
fun inject(viewModel: SplashViewModel)
fun inject(viewModel: MainViewModel)
fun inject(viewMode: BudgetListViewModel)
fun inject(viewModel: BudgetFormViewModel)
fun inject(viewModel: CategoryListViewModel)
fun inject(viewModel: CategoryDetailsViewModel)
fun inject(viewModel: CategoryFormViewModel)
fun inject(viewModel: TransactionListViewModel)
fun inject(viewModel: TransactionFormViewModel)
@Singleton
val errorHandler: ErrorHandler
@Component.Builder
interface Builder {
@BindsInstance
fun context(context: Context): Builder
fun build(): AppComponent
}
}

View file

@ -5,8 +5,11 @@ import android.util.Log
import com.wbrawner.budget.common.util.ErrorHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
fun provideErrorHandler(): ErrorHandler = object : ErrorHandler {

View file

@ -3,28 +3,25 @@ package com.wbrawner.budget
import android.app.Application
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.UserRepository
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@HiltAndroidApp
class TwigsApplication : Application(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
lateinit var appComponent: AppComponent
private set
@Inject
lateinit var budgetRepository: BudgetRepository
@Inject
lateinit var userRepository: UserRepository
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.builder()
.context(this)
.build()
appComponent.errorHandler.init(this)
appComponent.inject(this)
launch {
try {
userRepository.getProfile()

View file

@ -11,7 +11,7 @@ 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.TwigsApplication
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ -20,6 +20,7 @@ private const val MENU_GROUP_BUDGETS = 50
private const val MENU_ITEM_ADD_BUDGET = 100
private const val MENU_ITEM_SETTINGS = 101
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var toggle: ActionBarDrawerToggle
private val viewModel: MainViewModel by viewModels()
@ -28,7 +29,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
super.onCreate(savedInstanceState)
EmojiCompat.init(androidx.emoji.bundled.BundledEmojiCompatConfig(this))
setContentView(R.layout.activity_main)
(application as TwigsApplication).appComponent.inject(viewModel)
setSupportActionBar(action_bar)
toggle = ActionBarDrawerToggle(this, drawerLayout, R.string.action_open, R.string.action_close)
toggle.isDrawerIndicatorEnabled = true

View file

@ -5,29 +5,30 @@ import androidx.lifecycle.viewModelScope
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class MainViewModel : ViewModel() {
@Inject
lateinit var budgetRepository: BudgetRepository
@Inject
lateinit var userRepository: UserRepository
@HiltViewModel
class MainViewModel @Inject constructor(
val budgetRepository: BudgetRepository,
val userRepository: UserRepository
) : ViewModel() {
private val budgets = MutableSharedFlow<BudgetList>(replay = 1)
fun loadBudgets(): SharedFlow<BudgetList> {
viewModelScope.launch {
val list = budgetRepository.findAll().sortedBy { it.name }
budgets.emit(BudgetList(
budgets.emit(
BudgetList(
list,
budgetRepository.currentBudget.replayCache.firstOrNull()?.let {
list.indexOf(it)
}
))
))
}
return budgets
}

View file

@ -8,15 +8,15 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@AndroidEntryPoint
class SplashActivity : AppCompatActivity() {
private val viewModel: SplashViewModel by viewModels()
val viewModel: SplashViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
(application as TwigsApplication).appComponent.inject(viewModel)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
window.decorView.apply {

View file

@ -11,22 +11,19 @@ import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.lib.network.PREF_KEY_BASE_URL
import com.wbrawner.budget.load
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
class SplashViewModel : ViewModel(), AsyncViewModel<AuthenticationState> {
@HiltViewModel
class SplashViewModel @Inject constructor(
val budgetRepository: BudgetRepository,
val userRepository: UserRepository,
val sharedPreferences: SharedPreferences,
) : ViewModel(), AsyncViewModel<AuthenticationState> {
override val state: MutableStateFlow<AsyncState<AuthenticationState>> =
MutableStateFlow(AsyncState.Loading)
@Inject
lateinit var budgetRepository: BudgetRepository
@Inject
lateinit var sharedPreferences: SharedPreferences
@Inject
lateinit var userRepository: UserRepository
suspend fun checkForExistingCredentials() {
if (!sharedPreferences.contains(PREF_KEY_TOKEN)) {
state.emit(AsyncState.Success(AuthenticationState.Unauthenticated()))

View file

@ -10,12 +10,13 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_add_edit_budget.*
@AndroidEntryPoint
class AddEditBudgetFragment : Fragment() {
private val viewModel: BudgetFormViewModel by viewModels()
@ -28,17 +29,14 @@ class AddEditBudgetFragment : Fragment() {
}
override fun onAttach(context: Context) {
(requireActivity().application as TwigsApplication)
.appComponent
.inject(viewModel)
super.onAttach(context)
viewModel.getBudget(arguments?.getString(EXTRA_BUDGET_ID))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_add_edit_budget, container, false)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -51,9 +49,9 @@ class AddEditBudgetFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val suggestionsAdapter = ArrayAdapter<User>(
requireContext(),
android.R.layout.simple_spinner_item,
mutableListOf()
requireContext(),
android.R.layout.simple_spinner_item,
mutableListOf()
)
usersSearch.setAdapter(suggestionsAdapter)
viewModel.userSuggestions.observe(viewLifecycleOwner, Observer {
@ -87,10 +85,10 @@ class AddEditBudgetFragment : Fragment() {
android.R.id.home -> findNavController().navigateUp()
R.id.action_save -> {
viewModel.saveBudget(Budget(
id = id,
name = name.text.toString(),
description = description.text.toString(),
users = emptyList()
id = id,
name = name.text.toString(),
description = description.text.toString(),
users = emptyList()
))
}
R.id.action_delete -> {

View file

@ -10,21 +10,20 @@ import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.common.util.randomId
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class BudgetFormViewModel : ViewModel() {
@HiltViewModel
class BudgetFormViewModel @Inject constructor(
val budgetRepository: BudgetRepository,
val userRepository: UserRepository
) : ViewModel() {
val state = MutableLiveData<BudgetFormState>(BudgetFormState.Loading)
val users = MutableLiveData<List<User>>()
val userSuggestions = MutableLiveData<List<User>>()
@Inject
lateinit var budgetRepository: BudgetRepository
@Inject
lateinit var userRepository: UserRepository
fun getBudget(id: String? = null) {
viewModelScope.launch {
state.postValue(BudgetFormState.Loading)

View file

@ -1,7 +1,6 @@
package com.wbrawner.budget.ui.budgets
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.TextView
@ -9,21 +8,17 @@ import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.common.budget.Budget
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 dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class BudgetListFragment : ListWithAddButtonFragment<Budget, BudgetListViewModel>() {
override val noItemsStringRes: Int = R.string.overview_no_data
override fun onAttach(context: Context) {
(requireActivity().application as TwigsApplication).appComponent.inject(viewModel)
super.onAttach(context)
}
override val viewModel: BudgetListViewModel by viewModels()
override fun reloadItems() {

View file

@ -6,18 +6,20 @@ import com.wbrawner.budget.AsyncViewModel
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.load
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
class BudgetListViewModel : ViewModel(), AsyncViewModel<List<Budget>> {
override val state: MutableStateFlow<AsyncState<List<Budget>>> = MutableStateFlow(AsyncState.Loading)
@Inject
lateinit var budgetRepo: BudgetRepository
@HiltViewModel
class BudgetListViewModel @Inject constructor(
val budgetRepository: BudgetRepository
) : ViewModel(), AsyncViewModel<List<Budget>> {
override val state: MutableStateFlow<AsyncState<List<Budget>>> =
MutableStateFlow(AsyncState.Loading)
fun getBudgets() {
load {
budgetRepo.findAll().toList()
budgetRepository.findAll().toList()
}
}
}

View file

@ -1,7 +1,6 @@
package com.wbrawner.budget.ui.categories
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.*
@ -12,27 +11,20 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
import com.wbrawner.budget.ui.EXTRA_CATEGORY_NAME
import com.wbrawner.budget.ui.toAmountSpannable
import com.wbrawner.budget.ui.transactions.TransactionListFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_category_details.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
* A simple [Fragment] subclass.
*/
@AndroidEntryPoint
class CategoryDetailsFragment : Fragment() {
val viewModel: CategoryDetailsViewModel by viewModels()
override fun onAttach(context: Context) {
(requireActivity().application as TwigsApplication).appComponent.inject(viewModel)
super.onAttach(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View file

@ -7,15 +7,17 @@ import com.wbrawner.budget.AsyncViewModel
import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.load
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class CategoryDetailsViewModel : ViewModel(), AsyncViewModel<CategoryDetails> {
override val state: MutableStateFlow<AsyncState<CategoryDetails>> = MutableStateFlow(AsyncState.Loading)
@Inject
lateinit var categoryRepo: CategoryRepository
@HiltViewModel
class CategoryDetailsViewModel @Inject constructor(
val categoryRepo: CategoryRepository
) : ViewModel(), AsyncViewModel<CategoryDetails> {
override val state: MutableStateFlow<AsyncState<CategoryDetails>> =
MutableStateFlow(AsyncState.Loading)
fun getCategory(id: String? = null) {
viewModelScope.launch {

View file

@ -14,15 +14,16 @@ import androidx.core.app.TaskStackBuilder
import androidx.lifecycle.lifecycleScope
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
import com.wbrawner.budget.ui.transactions.toLong
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_add_edit_category.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@AndroidEntryPoint
class CategoryFormActivity : AppCompatActivity() {
val viewModel: CategoryFormViewModel by viewModels()
var id: String? = null
@ -33,7 +34,6 @@ class CategoryFormActivity : AppCompatActivity() {
setContentView(R.layout.activity_add_edit_category)
setSupportActionBar(action_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
(application as TwigsApplication).appComponent.inject(viewModel)
lifecycleScope.launch {
viewModel.state.collect { state ->
when (state) {

View file

@ -12,17 +12,18 @@ import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.common.util.randomId
import com.wbrawner.budget.load
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
override val state: MutableStateFlow<AsyncState<CategoryFormState>> = MutableStateFlow(AsyncState.Loading)
@Inject
lateinit var categoryRepository: CategoryRepository
@Inject
lateinit var budgetRepository: BudgetRepository
@HiltViewModel
class CategoryFormViewModel @Inject constructor(
val categoryRepository: CategoryRepository,
val budgetRepository: BudgetRepository
) : ViewModel(), AsyncViewModel<CategoryFormState> {
override val state: MutableStateFlow<AsyncState<CategoryFormState>> =
MutableStateFlow(AsyncState.Loading)
fun loadCategory(categoryId: String? = null) {
load {
@ -30,8 +31,8 @@ class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
categoryRepository.findById(it)
} ?: Category("", title = "", amount = 0)
CategoryFormState(
category,
budgetRepository.findAll().toList()
category,
budgetRepository.findAll().toList()
)
}
}
@ -65,15 +66,15 @@ class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
}
data class CategoryFormState(
val category: Category,
val budgets: List<Budget>,
@StringRes val titleRes: Int,
val showDeleteButton: Boolean
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
category,
budgets,
category.id?.let { R.string.title_edit_category } ?: R.string.title_add_category,
category.id != null
)
}

View file

@ -10,15 +10,16 @@ import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
import com.wbrawner.budget.ui.EXTRA_CATEGORY_NAME
import com.wbrawner.budget.ui.base.BindableAdapter
import com.wbrawner.budget.ui.base.BindableData
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class CategoryListFragment : ListWithAddButtonFragment<Category, CategoryListViewModel>() {
override val noItemsStringRes: Int = R.string.categories_no_data
override val viewModel: CategoryListViewModel by viewModels()
@ -30,11 +31,6 @@ class CategoryListFragment : ListWithAddButtonFragment<Category, CategoryListVie
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 TwigsApplication).appComponent.inject(viewModel)
super.onCreate(savedInstanceState)
}
override fun addItem() {
startActivity(Intent(activity, CategoryFormActivity::class.java))
}

View file

@ -7,21 +7,20 @@ 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.load
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class CategoryListViewModel : ViewModel(), AsyncViewModel<List<Category>> {
@HiltViewModel
class CategoryListViewModel @Inject constructor(
val budgetRepo: BudgetRepository,
val categoryRepo: CategoryRepository
) : ViewModel(), AsyncViewModel<List<Category>> {
override val state: MutableStateFlow<AsyncState<List<Category>>> =
MutableStateFlow(AsyncState.Loading)
@Inject
lateinit var budgetRepo: BudgetRepository
@Inject
lateinit var categoryRepo: CategoryRepository
fun getCategories() {
viewModelScope.launch {
budgetRepo.currentBudget.collect {

View file

@ -1,6 +1,5 @@
package com.wbrawner.budget.ui.overview
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -11,22 +10,18 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.ui.toAmountSpannable
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_overview.*
@AndroidEntryPoint
class OverviewFragment : Fragment() {
val viewModel: OverviewViewModel by viewModels()
override fun onAttach(context: Context) {
(requireActivity().application as TwigsApplication).appComponent.inject(viewModel)
super.onAttach(context)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_overview, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View file

@ -5,19 +5,18 @@ import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.transaction.TransactionRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class OverviewViewModel : ViewModel() {
@HiltViewModel
class OverviewViewModel @Inject constructor(
val budgetRepo: BudgetRepository,
val transactionRepo: TransactionRepository
) : ViewModel() {
val state = MutableLiveData<AsyncState<OverviewState>>(AsyncState.Loading)
@Inject
lateinit var budgetRepo: BudgetRepository
@Inject
lateinit var transactionRepo: TransactionRepository
fun loadOverview() {
viewModelScope.launch {
budgetRepo.currentBudget.collect { budget ->

View file

@ -16,12 +16,12 @@ import androidx.core.app.NavUtils
import androidx.core.app.TaskStackBuilder
import com.google.android.material.datepicker.MaterialDatePicker
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.common.budget.Budget
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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_add_edit_transaction.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -31,6 +31,7 @@ import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.max
@AndroidEntryPoint
class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
@ -47,7 +48,6 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setTitle(R.string.title_add_transaction)
edit_transaction_type_expense.isChecked = true
(application as TwigsApplication).appComponent.inject(viewModel)
launch {
val accounts = viewModel.getAccounts().toTypedArray()
setCategories()

View file

@ -7,22 +7,18 @@ import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.common.transaction.Transaction
import com.wbrawner.budget.common.transaction.TransactionRepository
import com.wbrawner.budget.common.user.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class TransactionFormViewModel : ViewModel() {
@Inject
lateinit var budgetRepository: BudgetRepository
@Inject
lateinit var categoryRepository: CategoryRepository
@Inject
lateinit var transactionRepository: TransactionRepository
@Inject
lateinit var userRepository: UserRepository
@HiltViewModel
class TransactionFormViewModel @Inject constructor(
val budgetRepository: BudgetRepository,
val categoryRepository: CategoryRepository,
val transactionRepository: TransactionRepository,
val userRepository: UserRepository,
) : ViewModel() {
var currentUserId: String? = null
private set

View file

@ -1,7 +1,6 @@
package com.wbrawner.budget.ui.transactions
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
@ -15,7 +14,6 @@ import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.wbrawner.budget.R
import com.wbrawner.budget.TwigsApplication
import com.wbrawner.budget.common.transaction.Transaction
import com.wbrawner.budget.ui.EXTRA_BUDGET_ID
import com.wbrawner.budget.ui.EXTRA_CATEGORY_ID
@ -23,8 +21,10 @@ import com.wbrawner.budget.ui.EXTRA_TRANSACTION_ID
import com.wbrawner.budget.ui.base.BindableAdapter
import com.wbrawner.budget.ui.base.BindableData
import com.wbrawner.budget.ui.base.ListWithAddButtonFragment
import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat
@AndroidEntryPoint
class TransactionListFragment : ListWithAddButtonFragment<Transaction, TransactionListViewModel>() {
override val noItemsStringRes: Int = R.string.transactions_no_data
override val viewModel: TransactionListViewModel by viewModels()
@ -37,11 +37,6 @@ class TransactionListFragment : ListWithAddButtonFragment<Transaction, Transacti
override fun bindData(data: Transaction): BindableData<Transaction> = BindableData(data, TRANSACTION_VIEW)
override fun onAttach(context: Context) {
(requireActivity().application as TwigsApplication).appComponent.inject(viewModel)
super.onAttach(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -57,15 +52,15 @@ class TransactionListFragment : ListWithAddButtonFragment<Transaction, Transacti
}
// TODO: Launch a Google Drive-style search/filter screen
AlertDialog.Builder(requireContext(), R.style.DialogTheme)
.setTitle("Filter Transactions")
.setPositiveButton(R.string.action_submit) { _, _ ->
reloadItems()
}
.setNegativeButton(R.string.action_cancel) { _, _ ->
// Do nothing
}
.create()
.show()
.setTitle("Filter Transactions")
.setPositiveButton(R.string.action_submit) { _, _ ->
reloadItems()
}
.setNegativeButton(R.string.action_cancel) { _, _ ->
// Do nothing
}
.create()
.show()
return true
}

View file

@ -8,21 +8,22 @@ import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.transaction.Transaction
import com.wbrawner.budget.common.transaction.TransactionRepository
import com.wbrawner.budget.load
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
class TransactionListViewModel : ViewModel(), AsyncViewModel<List<Transaction>> {
@Inject
lateinit var budgetRepository: BudgetRepository
@Inject
lateinit var transactionRepo: TransactionRepository
override val state: MutableStateFlow<AsyncState<List<Transaction>>> = MutableStateFlow(AsyncState.Loading)
@HiltViewModel
class TransactionListViewModel @Inject constructor(
val budgetRepository: BudgetRepository,
val transactionRepo: TransactionRepository,
) : ViewModel(), AsyncViewModel<List<Transaction>> {
override val state: MutableStateFlow<AsyncState<List<Transaction>>> =
MutableStateFlow(AsyncState.Loading)
fun getTransactions(
categoryId: String? = null,
categoryId: String? = null,
) {
viewModelScope.launch {
budgetRepository.currentBudget.collect { budget ->

View file

@ -2,14 +2,15 @@ plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
compileSdkVersion 31
defaultConfig {
minSdkVersion 26
targetSdkVersion 30
targetSdkVersion 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -35,8 +36,8 @@ dependencies {
implementation "io.ktor:ktor-client-logging:$ktor_version"
implementation "ch.qos.logback:logback-classic:1.2.5"
// Dagger
implementation "com.google.dagger:dagger:$rootProject.ext.dagger"
kapt "com.google.dagger:dagger-compiler:$rootProject.ext.dagger"
implementation "com.google.dagger:hilt-android:$rootProject.ext.dagger"
kapt "com.google.dagger:hilt-compiler:$rootProject.ext.dagger"
testImplementation 'junit:junit:4.13.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

View file

@ -12,11 +12,14 @@ import com.wbrawner.budget.lib.repository.NetworkTransactionRepository
import com.wbrawner.budget.lib.repository.NetworkUserRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
const val PREF_KEY_BASE_URL = "baseUrl"
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Singleton
@Provides

View file

@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.compose = '1.0.1'
ext.dagger = '2.38.1'
ext.hyperion = '0.9.33'
ext.kotlin_version = '1.5.21'
@ -15,6 +16,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$dagger"
}
}

View file

@ -6,11 +6,11 @@ plugins {
}
android {
compileSdkVersion 30
compileSdkVersion 31
defaultConfig {
minSdkVersion 26
targetSdkVersion 30
targetSdkVersion 31
versionCode 1
versionName "1.0"

View file

@ -1,14 +1,16 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
compileSdkVersion 31
defaultConfig {
minSdkVersion 23
targetSdkVersion 30
targetSdkVersion 31
versionCode 1
versionName "1.0"
@ -32,8 +34,8 @@ dependencies {
implementation "androidx.security:security-crypto:$security_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
// Dagger
implementation "com.google.dagger:dagger:$rootProject.ext.dagger"
kapt "com.google.dagger:dagger-compiler:$rootProject.ext.dagger"
implementation "com.google.dagger:hilt-android:$rootProject.ext.dagger"
kapt "com.google.dagger:hilt-compiler:$rootProject.ext.dagger"
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View file

@ -5,20 +5,24 @@ import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "shared-prefs.encrypted"
@Module
@InstallIn(SingletonComponent::class)
class StorageModule {
@Provides
@Singleton
fun provideSharedPreferences(context: Context): SharedPreferences =
EncryptedSharedPreferences.create(
ENCRYPTED_SHARED_PREFS_FILE_NAME,
"budget",
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
EncryptedSharedPreferences.create(
ENCRYPTED_SHARED_PREFS_FILE_NAME,
"budget",
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}