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
...
}
}
Related
I'm fairly new to Jetpack Compose. Currently, I have a ViewModel making 1 network call.
class PlatformViewModel #Inject constructor(
private val getProductListUseCase: GetListUseCase
) : ViewModel()
I had 3 states.
sealed class PlatformState {
object Loading : PlatformState()
data class Success(val listOfProducts: List<Product>) : PlatformState()
object Error : PlatformState()
}
In the UI, it Was easy to handle observing 1 live data.
val state = viewModel.platformState.observeAsState(PlatformState.Loading)
when (state) {
is PlatformState.Success -> SuccessView(listOfProducts = state.listOfProducts)
is PlatformState.Loading -> LoadingView()
is PlatformState.Error -> ErrorView()
}
now, I need to add 1 more network call in viewModel for the same screen
class PlatformViewModel #Inject constructor(
private val getProductListUseCase: GetListUseCase,
private val getHeaderUseCase: GetHeaderUseCase,
) : ViewModel()
-Should I add 3 more states and 1 more live data to observe for the UI, what is the best way to handle this?
Note: both network calls are unrelated but their result populates the same composable.
fun bodyContent(listOfProducts:List<Products>,headerDetails:HeaderDetails){
LazyColumn{
item{ HeaderDetails(details=headerDetails)}
items(listOfProducts.size){
ProductItem()
}
Since the composable function, bodyContent requires both parameters listOfProducts:List<Products>, headerDetails:HeaderDetails.
I would create a data class that holds those values and that class should be sent to the composable function.
data class BodyContentUIData(listOfProducts:List<Products>, headerDetails:HeaderDetails)
The composable should be
fun BodyContent(bodyContentUIData: BodyContentUIData) {
LazyColumn {
item { HeaderDetails(details = bodyContentUIData.headerDetails) }
items(bodyContentUIData.listOfProducts.size) {
ProductItem()
}
}
}
//BTW, composable functions should start with a capital case.
At the view model, You should have a function called getBodyContentData that first calls getHeaderUseCase and if it's a success then call getProductListUseCase after the success you'll be having the listOfProducts and the headerDetails now you create the data class and send it to the composable function.
The view model could look like this:
class PlatformViewModel #Inject constructor(
private val getProductListUseCase: GetListUseCase,
private val getHeaderUseCase: GetHeaderUseCase,
) : ViewModel() {
fun getBodyContentData() {
getHeaderUseCase().onSuccess { headerDetails ->
getProductListUseCase().onSuccess { listOfProducts ->
_bodyContentLiveData.value = SuccessView(BodyContent(headerDetails, listOfProducts))
}
}
}
}
This way you have just 1 live data and 3 states for the composable.
I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}
I've one LiveData named sortOrder and then I've another variable named myData that observes any change to sortOrder and populates data accordingly.
class TestViewModel #ViewModelInject constructor() : ViewModel() {
private val sortOrder = MutableLiveData<String>()
val myData = sortOrder.map {
Timber.d("Sort order changed to $it")
"Sort order is $it"
}
init {
sortOrder.value = "year"
}
}
Observing in Activity
class TestActivity : AppCompatActivity() {
private val viewModel: TestViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
// Observing data
viewModel.myData.observe(this) {
Timber.d("Data is : $it")
}
}
}
Question
How can I replace the above scenario with Flow/StateFlow APIs without any change in output?
If you fail to convert the mapped cold Flow into a hot Flow, it will restart the flow every time you collect it (like when your Activity is recreated). That's how cold flows work.
I have a feeling they will flesh out the transform functions for StateFlow/SharedFlow, because it feels very awkward to map them to cold flows and have to turn them back into hot flows.
The public Flow has to be a SharedFlow if you don't want to manually map the first element distinctly because the stateIn function requires you to directly provide an initial state.
private val sortOrder = MutableStateFlow("year")
val myData = sortOrder.map {
Timber.d("Sort order changed to $it")
"Sort order is $it"
}.shareIn(viewModelScope, SharingStarted.Eagerly, 1)
Or you could create a separate function that is called within map and also in a stateIn function call.
private val sortOrder = MutableSharedFlow<String>()
private fun convertSortOrder(order: String): String {
Log.d("ViewModel", "Sort order changed to $order")
return "Sort order is $order"
}
val myData = sortOrder.map {
convertSortOrder(it)
}.stateIn(viewModelScope, SharingStarted.Eagerly, convertSortOrder("year"))
From the point of the fragment/activity, you have to create a job that collects the flow in onStart() and cancel it in onStop(). Using the lifecycleScope.launchWhenStarted will keep the flow active even in the background.
Use the bindin library to migrate to Flow with ease. I'm biased, tho.
See an article on medium about it.
To give some background to this question, I have a ViewModel that waits for some data, posts it to a MutableLiveData, and then exposes all the values through some properties. Here's a short gist of what that looks like:
class QuestionViewModel {
private val state = MutableLiveData<QuestionState>()
private val currentQuestion: Question?
get() = (state.value as? QuestionState.Loaded)?.question
val questionTitle: String
get() = currentQuestion?.title.orEmpty()
...
}
Then, in my test, I mock the data and just run an assertEquals check:
assertEquals("TestTitle", viewModel.questionTitle)
All of this works fine so far, but I actually want my fragment to observe for when the current question changes. So, I tried changing it around to use Transformations.map:
class QuestionViewModel {
private val state = MutableLiveData<QuestionState>()
private val currentQuestion: LiveData<Question> = Transformations.map(state) {
(it as? QuestionState.Loaded)?.question
}
val questionTitle: String
get() = currentQuestion.value?.title.orEmpty()
...
}
Suddenly, all of my assertions in the test class have failed. I made currentQuestion public and verified that it's value is null in my unit test. I've determined this is the issue because:
I can mock the data and still get the right value from my state LiveData
I can run my app and see the expected data on the screen, so this issue is specific to my unit test.
I have already added the InstantTaskExecutorRule to my unit test, but maybe that doesn't handle the Transformations methods?
I recently had the same problem, I've solved it by adding a mocked observer to the LiveData:
#Mock
private lateinit var observer: Observer<Question>
init {
initMocks(this)
}
fun `test using mocked observer`() {
viewModel.currentQuestion.observeForever(observer)
// ***************** Access currentQuestion.value here *****************
viewModel.questionTitle.removeObserver(observer)
}
fun `test using empty observer`() {
viewModel.currentQuestion.observeForever {}
// ***************** Access currentQuestion.value here *****************
}
Not sure how it works exactly or the consequences of not removing the empty observer the after test.
Also, make sure to import the right Observer class. If you're using AndroidX:
import androidx.lifecycle.Observer
Luciano is correct, it's because the LiveData is not being observed. Here is a Kotlin utility class to help with this.
class LiveDataObserver<T>(private val liveData: LiveData<T>): Closeable {
private val observer: Observer<T> = mock()
init {
liveData.observeForever(observer)
}
override fun close() {
liveData.removeObserver(observer)
}
}
// to use:
LiveDataObserver(unit.someLiveData).use {
assertFalse(unit.someLiveData.value!!)
}
Looks like you're missing the .value on the it variable.
private val currentQuestion: LiveData<Question> = Transformations.map(state) {
(it.value as? QuestionState.Loaded)?.question
}
I am currently experimenting with making a viewmodel for Fragment. My approach is to use exactly one viewmodel for one fragment. I have several use cases for different scenarios ex. to fetch books, to get info about a book. All these use cases happens in one Fragment. Now I made a ViewModel with 3 UseCases independent from each other and 3 corresponding LiveDatas.
I am wondering if it is a good practice. Any suggestions?
class GetBooksViewModel
#Inject constructor(private val getBooksUseCase: GetBooksUseCase,
private val getBooksListsUseCase: GetBooksListsUseCase,
private val getInfoByBookUseCase: GetInfoByBookUseCase) :
BaseViewModel() {
var books: MutableLiveData<java.util.LinkedHashMap<String, Book?>> = MutableLiveData()
var bookLists: MutableLiveData<List<BookList>> = MutableLiveData()
var infos: MutableLiveData<List<BookInfo>> = MutableLiveData()
//methods for fetching data will be below
fun getBooks() =
getChannelsUseCase() {
it.either(::handleFailure, ::handleGetBooksUseCase)
}
private fun handleGetBooksUseCase(response:
java.util.LinkedHashMap<String, Channel?>) {
this.books.value = response
}
inside Fragment
getBooksViewModel = viewModel(viewModelFactory) {
observe(books, ::getBooks)
observe(booksLists, ::getBooksLists)
observe(bookInfos, ::doSomethingWithInfos)
failure(failure, ::handleFailure)
}
A combined model can be used instead of three different models and liveData like this :-
class CombinedModel( var map : MutableLiveData<java.util.LinkedHashMap<String, Book?>, var books : MutableList<BookList>, var infos = MutableList<BookInfo> )
And the livedata can be like :-
var response: MutableLiveData<CombinedModel>
Hence only one Observer logic can take care of all three data in the activity