I'm relatively new to Kotlin and I'm working on a project for school. I've gotten stuck on something I can't figure out for a couple days now, either because I'm not just understanding how it works or I just don't know what to search for. I'm building an app for simple budget tracking, and using Room DB to allow the user to enter and store their expenses. I've gotten most of the app built and working, and have the DB, a DAO, a Repository and a ViewModel. I've successfully written a Query that returns the sum through a LiveData<Double>. I've managed to get this sum value to display through both a Toast message and in a TextView in the MainActivity (but the TV doesn't update on load, only after launching the activity for modifying the DB entries for the first time).
If it's possible, I want to be able to take this sum and store it inside a separate class I've written for calculation functions, and have it update whenever a user enters or deletes something from the DB. Or preferably, have the non-activity class call this sum whenever the class's relevant functions are called. I don't seem to understand how to get this value from anywhere but the MainActivity. Everything I've searched and read has sections of code which I think I understand, such as observeForever which require an application parameter, or they're over my head because it's just code snippets which I can't wrap my head around how they fit together.
Here is what I have so far:
My Entity:
#Entity(tableName = "expenses")
data class Expenses (
#PrimaryKey(autoGenerate = true)
val id: Int,
val expDesc: String,
val expAmount: Double
)
My DAO:
#Dao
interface Dao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addData(expenses: Expenses)
#Query("SELECT * FROM expenses ORDER BY id ASC")
fun readAllData(): LiveData<List<Expenses>>
#Query("SELECT SUM(expAmount) as expenseSum FROM expenses")
fun getExpenseSum(): LiveData<Double>
#Update
suspend fun updateExpense(expenses: Expenses)
#Delete
suspend fun deleteData(expenses: Expenses)
#Query("DELETE FROM expenses")
suspend fun deleteAllData()
}
My Database:
#Database(entities = [Expenses::class], version = 1, exportSchema = false)
abstract class Database:RoomDatabase() {
abstract fun dao(): Dao
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): Dao?
}
companion object{
#Volatile
private var INSTANCE: com.example.finalproject.roomDB.Database? = null
fun getDatabase(context: Context): com.example.finalproject.roomDB.Database {
val instance = INSTANCE
if(instance != null){
return instance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
com.example.finalproject.roomDB.Database::class.java,
"expenses").build()
INSTANCE = instance
return instance
}
}
}
}
My Repository:
class Repository(private val dao: Dao) {
val readAllData: LiveData<List<Expenses>> = dao.readAllData()
val getExpenseSum: LiveData<Double> = dao.getExpenseSum()
suspend fun addData(expenses: Expenses){
dao.addData(expenses)
}
suspend fun updateData(expenses: Expenses){
dao.updateExpense(expenses)
}
suspend fun deleteData(expenses: Expenses){
dao.deleteData(expenses)
}
suspend fun deleteAllData(){
dao.deleteAllData()
}
}
My ViewModel:
class ViewModel(application: Application): AndroidViewModel(application) {
val readAllData: LiveData<List<Expenses>>
val getExpenseSum: LiveData<Double>
private val repository: Repository
init{
val dao = Database.getDatabase(application).dao()
repository = Repository(dao)
readAllData = repository.readAllData
getExpenseSum = repository.getExpenseSum
}
fun addData(expenses: Expenses){
viewModelScope.launch(Dispatchers.IO) {
repository.addData(expenses)
}
}
fun updateData(expenses: Expenses){
viewModelScope.launch(Dispatchers.IO) { repository.updateData(expenses) }
}
fun deleteData(expenses: Expenses){
viewModelScope.launch(Dispatchers.IO) { repository.deleteData(expenses) }
}
fun deleteAllData(){
viewModelScope.launch(Dispatchers.IO) { repository.deleteAllData() }
}
}
My currently relevant part of the MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: ViewModel
var sumTotal: Double = 0.0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val expenseViewButton = findViewById<Button>(R.id.expViewButton)
val incomeViewButton = findViewById<Button>(R.id.incViewButton)
val goalsViewButton = findViewById<Button>(R.id.goalsViewButton)
val expectedExpAmtView = findViewById<TextView>(R.id.expectedExpAmt)
viewModel = ViewModelProvider.AndroidViewModelFactory(application).create(ViewModel::class.java)
//This observer successfully casts the LiveData<Double> to a Double and updates whenever changed
val sumObserver = Observer<Double> { expSumDbl -> sumTotal = expSumDbl }
viewModel.getExpenseSum.observe(this, sumObserver)
expectedExpAmtView.text = getString(R.string.monthly_expected_ExpAmt, sumTotal.toString())
expenseViewButton.setOnClickListener{
val myIntent: Intent = Intent(this, ExpenseActivity::class.java)
startActivity(myIntent)
}
incomeViewButton.setOnClickListener{
val myIntent: Intent = Intent(this, IncomeActivity::class.java)
startActivity(myIntent)
}
//This successfully displays the correct sum whenever the button is pressed
goalsViewButton.setOnClickListener{
Toast.makeText(this#MainActivity, sumTotal.toString(), Toast.LENGTH_SHORT).show()
}
}
}
So sumTotal is the value from MainActivity that I'd like to get in a different non-activity class* for calculations that won't affect the DB at all, only text views. I'd also like the TextView that is being updated to always be up-to-date, including when the app launches. If anyone can help me figure out what I am doing wrong and/or need to do differently I'd really appreciate it.
*Specifically at the moment, I have a budget class which handles getting things like income, entered into editText fields, and calculating how much that translates into on a monthly basis. I'd like to take the sum from the DB entries and subtract it from whatever that total becomes in the budget class and return the result. I might want to do other (undecided) things later with the sum, which is why I want to store it in a variable.
The value of sumTotal in MainActivity actually comes from Repository.getExpenseSum, so instead of sharing that variable to another class, it might be easier to call the repository method again from the other class. Actually, I wouldn't even recommend having that variable sumTotal in your Activity, it's better to rely on ViewModel only, but that's a different topic.
You interface Dao contains the method:
fun getExpenseSum(): LiveData<Double>
which returns a LiveData. If you want to observe a LiveData, you need to have a LifecycleOwner so the LiveData knows when it should start and stop emitting updates (or you can observeForever, but you need to know yourself when to stop observing it).
In your budget class, assuming it's not a Fragment/Activity, you won't have a LifecycleOwner, that's why one suggestion for you issue is to create another method in Dao:
fun getExpenseSum(): Double
Notice the lack of LiveData. That method will return a double whenever you call it, synchronously, but it needs to be executed on a background thread. You can call that method in your budget class and get that value there.
Lastly, I don't think you should be calling those DB methods in some "regular" classes, you should pass those variables when creating an instance of the budget class. It's much easier to deal with LiveData/background thread when you're on the standard Android classes, and just pass the values to other classes that need it, instead of making them query the repository themselves.
Related
dao
#Query("SELECT * FROM t_work WHERE id = :id")
fun id(id: Int): Work
How should I define a variable in ViewModel to save the data queried from the database?
The reason why Flow is not used is because the data to be queried is very simple, basically the function of the enumeration class. And this data does not need to be mutable, it only needs to be queried once.
// wrong code
class WorkViewModel #Inject constructor(private val workDao: IWorkDao): ViewModel(){
val work = viewModelScope.async {
workRepository.ids(listOf(1, 2, 3))
}.await()
}
I don't want to start the coroutine in Compose, which is the view layer, which will increase the complexity of Compose code. Is there an optimal solution?
You can basically start a coroutine in the ViewModel like the following:
class WorkViewModel #Inject constructor(private val workDao: IWorkDao): ViewModel(){
lateinit var work : Work
//when view Model is created
init {
viewModelScope.launch(Dispatchers.IO){
work= workDao.id(1)
}
}
// or with a function
fun updateWork(id : Int){
viewModelScope.launch(Dispatchers.IO){
work= workDao.id(id )
}
}
}
make sure also to update the DAO like the following:
#Query("SELECT * FROM t_work WHERE id = :id")
suspend fun id(id: Int): Work
That way you can offload the operation to a background dispatcher.
In my current solution to this I use coroutines and return a MutableLiveData in the ViewModel that the view .kt class observes.
DAO function:
#Query(
"SELECT workout_name FROM workout_table " +
"WHERE id = :workoutId"
)
suspend fun getWorkoutName(workoutId: Long): String
Repository function:
#WorkerThread
suspend fun getWorkoutName(workoutId: Long): String {
return database.workoutDao().getWorkoutName(workoutId)
}
ViewModel function:
fun getWorkoutName(workoutId: Long): MutableLiveData<String> {
val workoutName = MutableLiveData<String>()
CoroutineScope(Dispatchers.IO).launch {
workoutName.postValue(repository.getWorkoutName(workoutId))
}
return workoutName
}
View .kt function:
private fun setUpAppBar() {
binding?.apply {
viewModel.getWorkoutName(currentWorkoutId!!).observe(viewLifecycleOwner) {
editWorkoutTopAppbar.title = it
}
}
}
This works but I think there's a better way of doing this. My main problem is in the ViewModel because I'm making a MutableLiveData variable, returning it to the view and giving it a value when ready.
Two other ways I've seen is returning LiveData from the DAO and repository functions and having the view observe it, but this way I think the database query is not done in a background thread. Another way is having the view .kt file launch the coroutine but I believe the view is not supposed to launch coroutines.
The ViewModel using a coroutine when repository returns LiveData:
fun getWorkoutName(workoutId: Long): LiveData<String> {
CoroutineScope(Dispatchers.IO).launch {
return repository.getWorkoutName(workoutId)
}
}
The above code gives me an error because I'm returning the result from inside a coroutine.
Please let me know a solution, or maybe if I'm approaching this problem the wrong way.
First, it's better to use viewModelScope instead of creating new coroutines scope since viewModelScope cancels itself if the veiwModel gets destroyed. secondly, you need to expose a LiveData to fragment or activity instead of MutableLiveData because LiveData can not be manipulated from outside.
so your viewModel should look something like this:
private val _workoutName = MutableLiveData<String>()
val workoutName : LiveData<String>() get() = _workoutName
fun getWorkoutName(workoutId: Long) {
viewModelScope.launch {
_workoutName.postValue(repository.getWorkoutName(workoutId))
}
}
I am currently following the Android Room with a View codelab and trying to adopt it with Jetpack Compose. I am stuck in initializing the viewModel in a compose function.
The error I am getting:
None of the following functions can be called with the arguments supplied:
public inline fun <reified VM : ViewModel> viewModel(viewModelStoreOwner: ViewModelStoreOwner = ..., key: String? = ..., factory: ViewModelProvider.Factory? = ...): TypeVariable(VM) defined in androidx.lifecycle.viewmodel.compose
public fun <VM : ViewModel> viewModel(modelClass: Class<TypeVariable(VM)>, viewModelStoreOwner: ViewModelStoreOwner = ..., key: String? = ..., factory: ViewModelProvider.Factory? = ...): TypeVariable(VM) defined in androidx.lifecycle.viewmodel.compose
#Composable
fun WordBookApp() {
val context = LocalContext.current
val wordViewModel: WordViewModel by viewModel( // error here - viewModel
WordViewModelFactory((context.applicationContext as WordsApplication).repository)
)
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
...
The View Model and the View Model Factory:
class WordViewModel(private val repository: WordRepository) : ViewModel() {
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
The other parts of the code:
class WordRepository(private val wordDao: WordDao) {
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
class WordsApplication : Application() {
private val database by lazy { WordRoomDatabase.getDatabase(this) }
val repository by lazy { WordRepository(database.wordDao()) }
}
#Database(entities = [Word::class], version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
AndroidMenifest.xml
<application
android:name=".WordsApplication"
....
Can anyone please help? Thanks!
Since you did not mention the question, I already started typing this. This may not pertain to your question, but perhaps you still should read this.
If the problem is that values are not being updated inside the viewmodel, - Initialising a viewmodel inside a composable, very bad idea.
You see the composables often recompose, where every line of code inside it is re-executed. Hence, if you initialize the viewmodel inside this composable like that, it will be re-initialised at every recomposition. Recompositions can take place theoretically even at the frame rate (even do in many cases). Hence, this is not how to declare variables inside the composable.
Ok so there's the remember composable to help you out with that. If you wrap the initializing statement with remember, it will not be re-initialised upon recompositions. However, it has its limitations. For example, if the composable gets destroyed, for example, if you swipe it off the screen, the remembered value is lost. remember is destroyed with the destruction of the composable enclosing it.
Hence, for small stuff like animations and all, it is ok to store variables inside the composables, but for important things, you should not trust this framework.
Hence, the best way would be to initialise the viewmodel in your main activity, then pass its methods and varibales around to composables. You can even pass the viewmodel itself around, but its not required most of the time.
Code:
#Composable
fun WordBookApp() {
val context = LocalContext.current
val wordViewModel: WordViewModel by remember {
viewModel( // error here - viewModel
WordViewModelFactory((context.applicationContext as WordsApplication).repository)
)
}
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
...
Got a clue from the answer of #MARSK and fixed it. Moved the initialization of the view model to the onCreate() of the MainActivity, and passed it to the composable function. Working everything perfectly now!
Here is the code if anyone needs it in the future:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val wordViewModel by viewModels<WordViewModel> {
WordViewModelFactory((this.applicationContext as WordsApplication).repository)
}
setContent {
WordBookApp(wordViewModel)
}
}
}
#Composable
fun WordBookApp(wordViewModel: WordViewModel) {
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
...
I've met an issue with observing data in my app.
For the testing purposes, I have an activity with a single text view where I show the user's name. This is the code:
#Entity(tableName = "User")
data class User(
var name: String,
var surname: String,
#PrimaryKey(autoGenerate = true)
val internalID: Long = 0)
in the dao I've got just one method:
#Query("SELECT * FROM User WHERE surname LIKE :surname")
abstract suspend fun getUserForSurname(surname: String): User
in the activity onCreate's method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val model = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewmodel = model
model.user.observe(this, Observer {
binding.textTest.setText(it.name)
})
}
and finally, view model:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val surname = "Doe"
val user: MutableLiveData<User> = MutableLiveData()
private val userDao: UserDao =
MyRoomDatabase.getDatabase(application).clientDao()
init {
viewModelScope.launch {
user.value = userDao.getUserForSurname(surname)
}
}
}
That specific user's name is changed in the background thread. When I check for the value in db itself, the name is indeed different. After restarting activity, the text view is changed too. In other words: the db value is changed but the observer is never called. I know that I am asking for the value only once during viewmodel's init method and it may be a problem. Is it possible to see the actual change without restarting activity?
I suggest you have a look at this Codelab
Room exposes different wrappers around the returned Entities such as:
RxJava
Flow Coroutines
LiveData
So you can changed your Dao as such:
#Query("SELECT * FROM User WHERE surname LIKE :surname")
abstract fun getUserForSurname(surname: String): LiveData<User>
The above means that any changes to the user entry will emit an observation to the listeners of the LiveData.
ViewModel
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val surname = "Doe"
lateinit val client: LiveData<User>
private val userDao: UserDao =
MyRoomDatabase.getDatabase(application).clientDao()
init {
viewModelScope.launch {
user = userDao.getUserForSurname(surname)
}
}
}
Read more at:
- https://developer.android.com/training/data-storage/room/index.html
Disclaimer: Didn't test the above solution but it should give you an idea.
EDIT: Ideally LiveData should only be used in your view model as they were designed for such cases and not to observe DB transactions. I will suggest to replace the Dao's with Coroutine's Flow and use the extension to convert to LiveData.
I have a ViewModel and I am using LiveData, so I have a DAO that return LiveData> and I can get this to work, but really what I would want it to first show data from Room database if there are some, and then when the webservice has returned new data (if there are any) then write that to database and then update the ViewModel with the latest data from the database. I got as far as returning the data firstly from the database and also writing the new data to the database in the background, but then how do I get the ViewModel to read/update with the new data from the database again?
Thank you
Søren
You can make your DAO return LiveData<Any>. It means that you can get notified about every change on that entity.
Assume that you have a User entity:
#Entity
data class User(
#PrimaryKey(autoGenerate = true) var uid: Int = 0,
#ColumnInfo(name = "name")
val name: String
)
And its related DAO looks like:
#Dao
interface UserDao {
#Query("SELECT * FROM user")
fun all(): LiveData<List<User>>
#Insert
suspend fun insert(vararg users: User)
}
So, you just need to expose the result of all to your view layer:
class UserViewModel : ViewModel() {
val users: LiveData<List<User>> = userDao.all()
}
class UserActivity : AppCompatActivity() {
private val viewModel by viewModels<UserViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.users.observe(this, Observer { users ->
// show data
})
}
}
Now if you insert a new User, Your observer will be called.
This is the basic idea of how to get updated data from your DAO. But in your case what you actually must do is to create a Repository and inside that do your business logic, provide offline-first data and then try to get data from network and update database.