Flow working incorrectly. Called again when it shouldn't, but liveData is working correct - kotlin

I use Jetpack Compose and have 2 screens. When I open second screen and back to the fisrt, flow variable calling again and ui updated again. But, I don't understand why... When I use liveData was working perfect.
My code with LiveData:
class MainViewModel(private val roomRepository: Repository, private val sPref:SharedPreferences) : ViewModel() {
val words: LiveData<List<WordModel>> by lazy {
roomRepository.getAllWords()
}
...
}
MainScreen.kt:
#ExperimentalMaterialApi
#Composable
fun MainScreen(viewModel: MainViewModel) {
...
val words: List<WordModel> by viewModel
.words
.observeAsState(listOf())
...
WordList(
words = words,
onNoticeClick = { viewModel.onWordClick(it) },
state = textState,
lazyState = viewModel.listState!!
)
...
}
#Composable
private fun WordList(
words: List<WordModel>,
onNoticeClick: (WordModel) -> Unit,
state: MutableState<TextFieldValue>,
lazyState: LazyListState
) {
var filteredCountries: List<WordModel>
LazyColumn(state = lazyState) {
val searchedText = state.value.text
filteredCountries = if (searchedText.isEmpty()) {
words
} else {
words.filter {
it.word.lowercase().contains(searchedText) || it.translation.lowercase()
.contains(searchedText)
}
}
items(count = filteredCountries.size) { noteIndex ->
val note = filteredCountries[noteIndex]
Word(
word = note,
onWordClick = onNoticeClick
)
}
}
}
WordDao.kt:
#Dao
interface WordDao {
#Query("SELECT * FROM WordDbModel")
fun getAll(): LiveData<List<WordDbModel>>
}
RoomRepositoryImpl.kt:
class RoomRepositoryImpl(
private val wordDao: WordDao,
private val noticeDao: NoticeDao,
private val dbMapper: DbMapper
) : Repository {
override fun getAllWords(): LiveData<List<WordModel>> =
Transformations.map(wordDao.getAll()) {dbMapper.mapWords(it)}
...
}
DbMapperImpl.kt:
class DbMapperImpl: DbMapper {
...
override fun mapWords(words: List<WordDbModel>): List<WordModel> =
words.map { word -> mapWord(word, listOf<NoticeModel>()) }
}
My code with Flow, which calling every time when open the first screen:
class MainViewModel(private val roomRepository: Repository, private val sPref:SharedPreferences) : ViewModel() {
val words: Flow<List<WordModel>> = flow {
emitAll(repository.getAllWords())
}
}
MainScreen.kt:
#ExperimentalMaterialApi
#Composable
fun MainScreen(viewModel: MainViewModel) {
...
val words: List<WordModel> by viewModel
.words
.collectAsState(initial = listOf())
...
}
WordDao.kt:
#Dao
interface WordDao {
#Query("SELECT * FROM WordDbModel")
fun getAll(): Flow<List<WordDbModel>>
}
RoomRepositoryImpl.kt:
class RoomRepositoryImpl(
private val wordDao: WordDao,
private val noticeDao: NoticeDao,
private val dbMapper: DbMapper
) : Repository {
override fun getWords(): Flow<List<WordModel>> = wordDao.getAll().map { dbMapper.mapWords(it) }
}
And my router from MainRouting.kt:
sealed class Screen {
object Main : Screen()
object Notice : Screen()
object Your : Screen()
object Favorites : Screen()
}
object MainRouter {
var currentScreen: Screen by mutableStateOf(Screen.Main)
var beforeScreen: Screen? = null
fun navigateTo(destination: Screen) {
beforeScreen = currentScreen
currentScreen = destination
}
}
And MainActivity.kt:
class MainActivity : ComponentActivity() {
...
#Composable
#ExperimentalMaterialApi
private fun MainActivityScreen(viewModel: MainViewModel) {
Surface {
when (MainRouter.currentScreen) {
is Screen.Main -> MainScreen(viewModel)
is Screen.Your -> MainScreen(viewModel)
is Screen.Favorites -> MainScreen(viewModel)
is Screen.Notice -> NoticeScreen(viewModel = viewModel)
}
}
}
...
}
Perhaps someone knows why a new drawing does not occur with liveData (or, it is performed so quickly that it is not noticeable that it is), but with Flow the drawing of the list is visible.

You're passing the viewModel around, which is a terrible practice in a framework like Compose. The Model is like a waiter. It hangs around you, serves you water, does its job while you make the order. As you get distracted talking, it leaves. When it comes back, it is not the same waiter you had earlier. It wears the same uniform, with the same characteristics, but is still essentially a different object. When you pass the model around, it gets destroyed in the process of navigation. In case of flow, you are getting biased. Notice how you manually do a lazy initialization for the LiveData, but a standard proc. for Flow? Seems like the only logical reason for your observed inconsistency. If you want to use Flow in your calls instead of LiveData, just convert it at the site of initialization in the ViewModel. Your symptoms should go away.

Related

why my data is coming as null or default in ui from view model in kotlin?

I set up mvvm structure and call two functions in viewmodel. Functions return values, but these values ​​always come as values ​​that I define as null or default.
Why can't I access my data in the view model in the UI?
I can see the data properly in the view model
hear is my code
my ui
if (viewModel.isDialogShown) {
AlertDialog(
onDismiss = {
viewModel.onDismissClick()
},
onConfirm = {
println("First"+viewModel.getFirstConversionRateByCurrency(viewModel.dropDownMenuItem1))
println("SECOND:"+viewModel.getSecondConversionRateByCurrency(viewModel.dropDownMenuItem2))
}
)
}
If the user says confirm in the alert dialog in my UI, these functions are called and I print the returned values ​​for testing purposes, but because I define null as default in the viewmodel, it always comes null.
my view model
#HiltViewModel
class ExchangeMainViewModel #Inject constructor(
private val getConversionRateByCurrencyUseCase: GetConversionRateByCurrencyUseCase
) : ViewModel() {
var second : Double?=null
var first : Double? = null
fun getFirstConversionRateByCurrency(currency:String) : String {
viewModelScope.launch {
first = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
}
return first.toString()
}
fun getSecondConversionRateByCurrency(currency:String) :String {
viewModelScope.launch {
second = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
}
return second.toString()
}
}
Also, I defined the viewmodel in ui as you can see the below like this, could it be because of this?
#Composable
fun DropDownMenu(
viewModel: ExchangeMainViewModel = hiltViewModel()
) {
The result in the console is as follows
All those functions your are calling are executing a coroutine from viewModelScope.launch{…} and at the time you launched them you immediately return a value from the method return.xxx.toString() where the xxx (first and second) doesn't have a value yet since what you are expecting is coming from a concurrent execution not a sequential one.
viewModelScope.launch { // launch me separately and wait for any value that will set the `first`
first = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
}
// return from this function now with a null value, since it was initialized as null
return first.toString()
This is a rough implementation, though I'm not sure if this would work, but this should give you a headstart
Your new ViewModel
#HiltViewModel
class ExchangeMainViewModel #Inject constructor(
private val getConversionRateByCurrencyUseCase: GetConversionRateByCurrencyUseCase
) : ViewModel() {
var conversionValue by mutableStateOf<ConversionValues?>(null)
fun getFirstConversionRateByCurrency(currency:String) {
viewModelScope.launch {
val first = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
val second = getConversionRateByCurrencyUseCase.getConversionRateByCurrency(currency)
conversionValue = ConversionValues(first, second)
}
}
}
the Data Class for first and second
data class ConversionValues(
val first : Double,
val second: Double
)
and your composable
#Composable
fun DropDownMenu(
viewModel: ExchangeMainViewModel = hiltViewModel()
) {
val conversionValue = vieModel.conversionValue
if (viewModel.isDialogShown) {
AlertDialog(
onDismiss = {
viewModel.onDismissClick()
},
onConfirm {
println("First" + conversionValue.first)
println("SECOND:"+conversionValue.second)
}
)
}
}

viewModelScope blocks UI in Jetpack Compose

viewModelScope blocks UI in Jetpack Compose
I know viewModelScope.launch(Dispatchers.IO) {} can avoid this problem, but how to use viewModelScope.launch(Dispatchers.IO) {}?
This is my UI level code
#Composable
fun CountryContent(viewModel: CountryViewModel) {
SingleRun {
viewModel.getCountryList()
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
// ...
}
Here is my ViewModel, Pager is my pagination
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
This is the small package
#Composable
fun SingleRun(onClick: () -> Unit) {
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
onClick()
execute.value = false
}
}
I don't use Compose much yet, so I could be wrong, but this stood out to me.
I don't think your thread is being blocked. I think you subscribed to an empty flow before replacing it, so there is no data to show.
You shouldn't use a var property for your flow, because the empty original flow could be collected before the new one replaces it. Also, it defeats the purpose of using cachedIn because the flow could be replaced multiple times.
You should eliminate the getCountryList() function and just directly assign the flow. Since it is a cachedIn flow, it doesn't do work until it is first collected anyway. See the documentation:
It won't execute any unnecessary code unless it is being collected.
So your view model should look like:
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
val countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
...and you can remove the SingleRun block from your Composable.
You are not doing anything that would require you to specify dispatchers. The default of Dispatchers.Main is fine here because you are not calling any blocking functions directly anywhere in your code.

Kotlin on Android: How to use LiveData from a database in a fragment?

I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}

Why does the author design localCacheBillingClient as lateinit?

The Code A is from the project play-billing-samples, you can see it.
I don't know why the author design localCacheBillingClient as lateinit, it cause the code is little complex, if (::localCacheBillingClient.isInitialized == false) {...} are invoked many times.
I think Code B can works well, right?
Code A
class BillingRepository private constructor(private val application: Application) :
PurchasesUpdatedListener, BillingClientStateListener {
lateinit private var localCacheBillingClient: LocalBillingDb
val subsSkuDetailsListLiveData: LiveData<List<AugmentedSkuDetails>> by lazy {
if (::localCacheBillingClient.isInitialized == false) {
localCacheBillingClient = LocalBillingDb.getInstance(application)
}
localCacheBillingClient.skuDetailsDao().getSubscriptionSkuDetails()
}
val inappSkuDetailsListLiveData: LiveData<List<AugmentedSkuDetails>> by lazy {
if (::localCacheBillingClient.isInitialized == false) {
localCacheBillingClient = LocalBillingDb.getInstance(application)
}
localCacheBillingClient.skuDetailsDao().getInappSkuDetails()
}
fun startDataSourceConnections() {
Log.d(LOG_TAG, "startDataSourceConnections")
instantiateAndConnectToPlayBillingService()
localCacheBillingClient = LocalBillingDb.getInstance(application)
}
...
}
Code B
class BillingRepository private constructor(private val application: Application) :
PurchasesUpdatedListener, BillingClientStateListener {
private val localCacheBillingClient: LocalBillingDb by lazy {
LocalBillingDb.getInstance(application)
}
val subsSkuDetailsListLiveData: LiveData<List<AugmentedSkuDetails>> by lazy {
localCacheBillingClient.skuDetailsDao().getSubscriptionSkuDetails()
}
val inappSkuDetailsListLiveData: LiveData<List<AugmentedSkuDetails>> by lazy {
localCacheBillingClient.skuDetailsDao().getInappSkuDetails()
}
fun startDataSourceConnections() {
Log.d(LOG_TAG, "startDataSourceConnections")
instantiateAndConnectToPlayBillingService()
}
...
}
Kotlin - Property initialization using "by lazy" vs. "lateinit"
I believe the answer lies in this thread and precisely below point.
lateinit var can be initialized from anywhere the object is seen from, e.g. from inside a framework code, and multiple initialization scenarios are possible for different objects of a single class. by lazy { ... }, in turn, defines the only initializer for the property, which can be altered only by overriding the property in a subclass. If you want your property to be initialized from outside in a way probably unknown beforehand, use lateinit.

Use a class from a list of generic interface

I am trying to implement a QueryBus. Basically, I want to register a list of QueryHandlers. Each QueryHandler implements a handle method defined by an interface. Each QueryHandler is associated to a Query. I want to be able to retrieve a QueryHandler using the Query and call handle on it.
The thing is the handle has to be generic because each QueryHandler handles a Query differently. They all take a dedicated Query and may return whatever they want.
interface Query<R>
interface QueryHandler<R, Q : Query<R>> {
fun handle(query: Q): R
fun listenTo(): String
}
// DTOs
data class BookDto(val name: String)
// List books query
data class ListBooksQuery(val page: Int = 1): Query<List<BookDto>>
class ListBooksQueryHandler: QueryHandler<List<BookDto>, ListBooksQuery> {
override fun handle(query: ListBooksQuery): List<BookDto> {
return listOf(BookDto("Dune"), BookDto("Dune II"))
}
override fun listenTo(): String = ListBooksQuery::class.toString()
}
// Get book query
data class GetBookQuery(val name: String): Query<BookDto?>
class GetBookQueryHandler: QueryHandler<BookDto?, GetBookQuery> {
override fun handle(query: GetBookQuery): BookDto {
return BookDto("Dune")
}
override fun listenTo(): String = GetBookQuery::class.toString()
}
// Run it!
fun main(args: Array<String>) {
// Initializing query bus
val queryHandlers = mapOf(
with(ListBooksQueryHandler()) {this.listenTo() to this},
with(GetBookQueryHandler()) {this.listenTo() to this}
)
val command = ListBooksQuery()
val result = queryHandlers[command::class.toString()].handle(command)
// Should print the list of BookDto
print(result)
}
I don't even know if its possible, to be honest.
UPDATE 1:
I changed the usage example in the main to show what I am really trying to do. The List was for (bad?) demonstration purpose. I want to store the QueryHandlers and retrieve them from a map.
Additional resources:
Here is what I really want to do:
https://gist.github.com/ValentinTrinque/76b7a32221884a46e657090b9ee60193
UPDATE I've read your gist and tried to come up with a solution that will provide a clean interface to the user of the QueryBusMiddleware.
Note that I used objects instead of classes for the QueryHandler implementations, which felt more natural to me (since there is only one possible entry in the map for each Query implementation).
interface Query<R>
interface QueryHandler<R, Q: Query<R>> {
fun handle(query: Q): R
fun listenTo(): String
}
// DTOs
data class BookDto(val name: String)
// List books query
data class ListBooksQuery(val page: Int = 1): Query<List<BookDto>>
object ListBooksQueryHandler: QueryHandler<List<BookDto>, ListBooksQuery> {
override fun handle(query: ListBooksQuery): List<BookDto> {
return listOf(BookDto("Dune"), BookDto("Dune II"))
}
override fun listenTo(): String = ListBooksQuery::class.toString()
}
// Get book query
data class GetBookQuery(val name: String): Query<BookDto?>
object GetBookQueryHandler: QueryHandler<BookDto?, GetBookQuery> {
override fun handle(query: GetBookQuery): BookDto {
return BookDto("Dune")
}
override fun listenTo(): String = GetBookQuery::class.toString()
}
// Run it!
fun main(args: Array<String>) {
// Initializing query bus
val queryHandlers = listOf(
ListBooksQueryHandler,
GetBookQueryHandler
)
val dispatcher: QueryBusMiddleware = QueryDispatcherMiddleware(queryHandlers)
// Calling query bus
val query = ListBooksQuery()
// Result should be List<BookDto>
val result = dispatcher.dispatch(query)
print(result)
}
interface QueryBusMiddleware {
fun <R, Q : Query<R>> dispatch(query: Q): R
}
class QueryDispatcherMiddleware constructor(handlers: List<QueryHandler<*, *>>) : QueryBusMiddleware {
private val handlers = HashMap<String, QueryHandler<*, *>>()
init {
handlers.forEach { handler -> this.handlers[handler.listenTo()] = handler }
}
override fun <R, Q : Query<R>> dispatch(query: Q): R {
val queryClass = query::class.toString()
val handler = handlers[queryClass] ?: throw Exception("No handler listen to the query: $queryClass")
return handler::class.members.find { it.name == "handle" }!!.call(handler, query) as R
}
}