Issue
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
.
Solution
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)
}
Answered By - Tenfour04
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.