class MainAcitvity
fun roomSetup(){
setFavouriteDao = FavouriteDatabase.getDatabase(applicationContext).setFavouriteDao()
repositoryRoom = LorRepository(setFavouriteDao)
viewModelRoom = ViewModelProvider(this,LorViewModelFactory(repositoryRoom!!)).get(LorViewModel::class.java)
}
override fun onMovieClick(position: Int) {
roomSetup()
Toast.makeText(this#MainActivity, "clicked!"+position, Toast.LENGTH_SHORT).show()
var setFavourite = SetFavourite(movieResponse!!.docs[position].Id.toString(),movieResponse!!.docs[position].name.toString())
viewModelRoom.addToFavourites(setFavourite)
}
class ViewModel
fun addToFavourites(setFavourite: SetFavourite){
viewModelScope.launch(Dispatchers.IO){
lorRepository.addToFavourites(setFavourite)
}
}
class LorRepository( favouriteDao: SetFavouriteDao?) {
var favouriteDao : SetFavouriteDao
init {
this.favouriteDao = favouriteDao!!
}
private var lorApi: LORApi.LorCalls? = null
constructor(lorApi2 : LORApi.LorCalls?, favouriteDao: SetFavouriteDao?) : this(favouriteDao){
this.lorApi = lorApi2
}
I have 2 constructors
one to initialize room other for initializing retrofit
I am Also doubtful about the constructor in Repository. Thoose are made for 2 different purposes, one for initializing room database and other for repository. but everytime I create one object of room/retrofit the second constructor , when called, fills it with null values
My questions for you are:
Why do you to initialize retrofit and room's dao in a separate constructor?
What is it that you try to achieve?
In your code you only call to initialize dao constructor, therefore lorApi is null.
For your case you wouldn't want to initialize them separately.
Change your code to this:
class LorRepository(private val lorApi : LORApi.LorCalls, private val favouriteDao: SetFavouriteDao)
Related
In a Jetpack Compose component I'm subscribing to Room LiveData object using observeAsState.
The initial composition goes fine, data is received from ViewModel/LiveData/Room.
val settings by viewModel.settings.observeAsState(initial = AppSettings()) // Works fine the first time
A second composition is initiated, where settings - A non nullable variable is set to null, and the app crashed with an NPE.
DAO:
#Query("select * from settings order by id desc limit 1")
fun getSettings(): LiveData<AppSettings>
Repository:
fun getSettings(): LiveData<AppSettings> {
return dao.getSettings()
}
ViewModel:
#HiltViewModel
class SomeViewModel #Inject constructor(
private val repository: AppRepository
) : ViewModel() {
val settings = repository.getSettings()
}
Compose:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ItemsListScreen(viewModel: AppViewModel = hiltViewModel()) {
val settings by viewModel.settings.observeAsState(initial = AppSettings())
Edit:
Just to clearify, the DB data does not change. the first time settings is fetched within the composable, a valid instance is returned.
Then the component goes into recomposition, when ItemsListScreen is invoked for the second time, then settings is null (the variable in ItemsListScreen).
Once the LiveData<Appsettings> is subscribed to will have a default value of null. So you get the default value required by a State<T> object, when you call LiveData<T>::observeAsState, followed by the default LiveData<T> value, this being null
LiveData<T> is a Java class that allows nullable objects. If your room database doesn't have AppSettings it will set it a null object on the LiveData<AppSettings> instance. As Room is also a Java library and not aware of kotlin language semantics.
Simply put this is an interop issue.
You should use LiveData<AppSettings?> in kotlin code and handle null objects, or use some sort of MediatorLiveData<T> that can filter null values for example some extensions functions like :
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any, default : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> value = t ?: default }
}.observeAsState(initial = initial)
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> t?.run { value = this } }
}.observeAsState(initial = initial)
If you only need to fetch settings when viewModel is initialised, you can try putting it in an init block inside your ViewModel.
I have problem working with MutableStateFlow, I cannot understand how it is working or I am mistaken somewhere. For example purpose I created simpler classes to get the idea what I am doing.
First I have data class which holds the values and controller which update values in the data class
data class ExampleUiState(
val dataFlag: Boolean = false
)
class ExampleController {
private val _exampleUiState = MutableStateFlow(ExampleUiState())
val exampleUiState = _exampleUiState.asStateFlow()
fun onChangeFlag(flag: Boolean) {
_exampleUiState.update { it.copy(dataFlag = flag) }
}
}
I am using koin, and I created Example controller singleton.
Second I am injection it in my ViewModel where I have two functions there
class ExampleViewModel(
private val exampleController: ExampleController
) : ViewModel() {
val exampleUiState = exampleController.exampleUiState.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
ExampleUiState()
)
//called second
private fun useFlagInViewModelFun() {
//here the value is not updated
exampleUiState.value.dataFlag
}
//called first from UI
fun changeValueFromUi(flag: Boolean) {
//change it from default false to true
exampleController.onChangeFlag(flag)
useFlagInViewModelFun()
}
}
The idea is when I call changeValueFromUi from some compose function, I update the value with my controller function, and after it I call other function where I want to use already updated state of data class, but I don't get the correct value.
Where I am mistaken?
Is there any time needed for onChangeFlag() to react and update the value?
Am I mistaken the way that I am trying to get the value after exampleUiState.value.dataFlag ?
How to initialize a field in view model if I need to call the suspend function to get the value?
I a have suspend function that returns value from a database.
suspend fun fetchProduct(): Product
When I create the view model I have to get product in this field
private val selectedProduct: Product
I tried doing it this way but it doesn't work because I'm calling this method outside of the coroutines
private val selectedProduct: Product = repository.fetchProduct()
You can't initialize a field in the way you described. suspend function must be called from a coroutine or another suspend function. To launch a coroutine there are a couple of builders for that: CoroutineScope.launch, CoroutineScope.async, runBlocking. The latter is not recommended to use in production code. There are also a couple of builders - liveData, flow - which can be used to initialize the field. For your case I would recommend to use a LiveData or Flow to observe the field initialization. The sample code, which uses the liveData builder function to call a suspend function:
val selectedProduct: LiveData<Product> = liveData {
val product = repository.fetchProduct()
emit(product)
}
And if you want to do something in UI after this field is initialized you need to observe it. In Activity or Fragment it will look something like the following:
// Create the observer which updates the UI.
val productObserver = Observer<Product> { product ->
// Update the UI, in this case, a TextView.
productNameTextView.text = product.name
}
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
viewModel.selectedProduct.observe(this, productObserver)
For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 or higher.
Since fetchProduct() is a suspend function, you have to invoke it inside a coroutine scope.
For you case I would suggest the following options:
Define selectedProduct as nullable and initialize it inside your ViewModel as null:
class AnyViewModel : ViewModel {
private val selectedProduct: Product? = null
init {
viewModelScope.launch {
selectedProduct = repository.fetchProduct()
}
}
}
Define selectedProduct as a lateinit var and do the same as above;
Personally I prefer the first cause I feel I have more control over the fact that the variable is defined or not.
You need to run the function inside a coroutine scope to get the value.
if you're in a ViewModel() class you can safely use the viewModelScope
private lateinit var selectedProduct:Product
fun initialize(){
viewModelScope.launch {
selectedProduct = repository.fetchProduct()
}
}
I'm trying to combine three different flows in my ViewModel to make a list of items that will then be displayed on a RecyclerView in a fragment. I found out that when navigating to the screen, when there is no data in the table yet, the flow for testData1 doesn't emit the data in the table. Happens probably 1/5 of the time. I assume it's a timing issue because it only happens so often, but I don't quite understand why it happens. Also, this only happens when I'm combining flows so maybe I can only have so many flows in one ViewModel?
I added some code to check to see if the data was in the table during setListData() and it's definitely there. I can also see the emit happening but, there is no data coming from room. Any guidance would be greatly appreciated!
Versions I'm using:
Kotlin: 1.4.20-RC
Room: 2.3.0-alpha03
Here is my ViewModel
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.flowOn(Dispatchers.Default)
.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> {
// package the three data elements up to one list of rows
...
}
}
And here is my Repository/DAO layer (repeats for each type of data)
#Query("SELECT * FROM demo_data_1_table")
abstract fun getData1AsFlow() : Flow<List<DemoData1>>
I was able to get around this issue by removing flowOn in the combine function. After removing that call, I no longer had the issue.
I still wanted to run the setListData function on the default dispatcher, so I just changed the context in the setListData instead.
class DemoViewModel #Inject constructor(
demoService: DemoService,
private val demoRepository: DemoRepository
) : ViewModel() {
private val _testData1 = demoRepository.getData1AsFlow()
private val _testData2 = demoRepository.getData2AsFlow()
private val _testData3 = demoRepository.getData3AsFlow()
override val mainList = combine(_testData1, _testData2, _testData3) { testData1, testData2, testData3 ->
setListData(testData1, testData2, testData3)
}.asLiveData()
init {
viewModelScope.launch(Dispatchers.IO) {
demoService.getData()
}
}
private suspend fun setListData(testData1: List<DemoData1>, testData2: List<DemoData2>, testData3: List<DemoData3>): List<CombinedData> = withContext(Dispatchers.Default) {
// package the three data elements up to one list of rows
...
}
}
I am using Room for handling db entities and I am adapting the code from the WordRoom example from adroid developers.
I understand that in order to eprform operations that can take a long time I have to use coroutines, and this seems to work fine for inserting and deleting objects into the database. In the main activity I have a recyclerview that onCreate gets binded to its layoutmanager and to the ViewModelProvider.
In the adapter I set an onClick listener to get the current ID of the object in the recycled view>
holder.mealItemView.setOnClickListener {
(callerContext as MainActivity).getID(current)
}
the getID from the main activity starts a new activity that should retrieve the element from the database and display its properties:
fun getID(meal:Meal){
val intent = Intent(applicationContext, ActivtyViewMeal::class.java)
intent.putExtra("mealId", meal.id.toString())
startActivity(intent)
}
Then in the ActivityViewMeal in the oncreate I get the intent and add an observer to the variable that should store the Entity from the database:
mealViewModel = ViewModelProvider(this).get(MealViewModel::class.java)
mealViewModel.aMeal.observe(this, Observer {meal ->
meal?.let {Log.d(...)
dataIn.text = it.mealAddDate.toString()})
mealViewModel.getSingleContentById(mealID.toInt())
and after binding the variable with the observer I try to retrieve the data.
My issue is that the log never gets executed.
aMeal is declared inside the ViewModel, a separate kotlin class:
class MealViewModel(application: Application) : AndroidViewModel(application) {
private val repository: MealRepository
val allMeals: LiveData<List<Meal>>
var aMeal: LiveData<Meal>
init {
val mealsDao = MealRoomDatabase.getDatabase(application, viewModelScope).mealDao()
repository = MealRepository(mealsDao)
allMeals = repository.allMeals
aMeal = repository.aMeal
}
...
fun getSingleContentById(id: Int)=viewModelScope.launch(Dispatchers.IO) {
repository.getSingleContentById(id)
}
}
I understand that the coroutine cannot return a value and shall not block.
The meal repository class is defined as follow:
/**
* Abstracted Repository as promoted by the Architecture Guide.
* https://developer.android.com/topic/libraries/architecture/guide.html
*/
class MealRepository(private val mealDao: MealDao) {
val allMeals: LiveData<List<Meal>> = mealDao.getAllContentById()
var aMeal: LiveData<Meal> = mealDao.getSingleContentById(0) //init to avoid null pointer errors
...
#Suppress("RedundantSuspendModifier")
#WorkerThread
fun getSingleContentById(id:Int){
aMeal = mealDao.getSingleContentById(id)
// no return from here
}
and to conclude, getSingleContentById is defined inside the DAO class, that is in another kotlin file.
#Query("SELECT * FROM meals_table WHERE id=:id")
fun getSingleContentById(id:Int):LiveData<Meal>
I have no hint on why I don't get the aMeal updated, while the variable allmeals gets updated correctly.
Any hint would be gladly appreciated.
You can change you DB operation to be suspend and return Meal object:
#Query("SELECT * FROM meals_table WHERE id=:id")
suspend fun getSingleContentById(id:Int): Meal
In your repository make getSingleContentById function suspend as well:
class MealRepository(private val mealDao: MealDao) {
suspend fun getSingleContentById(id: Int): Meal {
aMeal = mealDao.getSingleContentById(id)
return aMeal
}
}
In your MealViewModel make aMeal of type MutableLiveData and update it in getSingleContentById function:
class MealViewModel(application: Application) : AndroidViewModel(application) {
private val repository: MealRepository
val aMeal: MutableLiveData<Meal> = MutableLiveData()
init {
val mealsDao = MealRoomDatabase.getDatabase(application, viewModelScope).mealDao()
repository = MealRepository(mealsDao)
}
fun getSingleContentById(id: Int) = viewModelScope.launch(Dispatchers.Main) {
val meal = repository.getSingleContentById(id)
aMeal.postValue(meal)
}
}