I am trying to get the SUM of all transaction amounts from my TransactionDatabase but it's always returning null.
Thanks for any help!!
This is my fragment
val transactionViewModelSum = ViewModelProvider(
requireActivity(),
TransactionViewModelFactory(requireActivity().application))
.get(TransactionViewModel::class.java)
transactionViewModelSum.getTransactionByDate().observe(viewLifecycleOwner, Observer {
totalAmount = it.div(10)
})
if(totalAmount == null ) binding.cpbMainExpenses.progress = 15f else binding.cpbMainExpenses.progress = totalAmount!!.toFloat()
My DAO
#Query("SELECT total(amount) FROM `Transaction`")
fun getTransactionByDate(): LiveData<Double>
My Repository
fun getTransactionByDate(): LiveData<Double> {
return transactionDao.getTransactionByDate()
}
My View Model
private val liveTransactionDate = repository.getTransactionByDate()
...
fun getTransactionByDate(): LiveData<Double> = liveTransactionDate
Your Query looks like fine. I think problem in async working of this transactionViewModelSum.getTransactionByDate().observe() code.
Try to put if(totalAmount == null ) binding.cpbMainExpenses.progress = 15f else binding.cpbMainExpenses.progress = totalAmount!!.toFloat() in observer lambda like bellow:
transactionViewModelSum.getTransactionByDate().observe(viewLifecycleOwner, Observer {
totalAmount = it.div(10)
if(totalAmount == null ) binding.cpbMainExpenses.progress = 15f else binding.cpbMainExpenses.progress = totalAmount!!.toFloat()
})
I think that you try to read value of totalAmount before getTransactionByDate emits a value. If if I'am not right please write me about it in comment.
Related
I have a problem with Room that return LiveData.
I create Dao with function to returns list of data. I suppose to return as LiveData. But, it doesn't work as expected.
Dao function
#Transaction
#Query("SELECT * FROM AllocationPercentage WHERE id IN (:ids)")
fun getByIds(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>>
Here is how I observe it inside the ViewModel:
class AllocationViewModel(
private val getAllocationByIdUseCase: GetAllocationByIdUseCase,
private val getDetailByIdUseCase: GetAllocationPercentageByIdUseCase
) : ViewModel() {
var allocationUiState: LiveData<AllocationUiState> = MutableLiveData()
private set
var allocationPercentageUiState: LiveData<List<AllocationPercentageUiState>> = MutableLiveData()
private set
val mediatorLiveData = MediatorLiveData<List<AllocationPercentageUiState>>()
fun getAllocationById(allocationId: Long) = viewModelScope.launch(Dispatchers.IO) {
val result = getAllocationByIdUseCase(allocationId) // LiveData
allocationUiState = Transformations.map(result) {
AllocationUiState(allocation = it.allocation)
}
mediatorLiveData.addSource(result) { allocation ->
Log.d(TAG, "> getAllocationById")
val ids = allocation.percentages.map { percentage -> percentage.id }
val detailResult: LiveData<List<AllocationPercentageWithDetails>> =
getDetailByIdUseCase(ids) // LiveData
allocationPercentageUiState = Transformations.map(detailResult) { details ->
Log.d(TAG, ">> Transform : $details")
details.map {
AllocationPercentageUiState(
id = it.allocationPercentage.id,
percentage = it.allocationPercentage.percentage,
description = it.allocationPercentage.description,
currentProgress = it.allocationPercentage.currentProgress
)
}
}
}
}
}
The allocationPercentageUiState is observed by Fragment.
Log.d(TAG, "observeViewModel: ${it?.size}")
val percentages = it ?: return#observe
setAllocationPercentages(percentages) // update UI
}
allocationViewModel.mediatorLiveData.observe(viewLifecycleOwner) {}
And getDetailByIdUseCase just a function which directly return result from Dao.
class GetAllocationPercentageByIdUseCase(private val repository: AllocationPercentageRepository) {
operator fun invoke(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>> {
return repository.getAllocationPercentageByIds(ids)
}
}
Any idea why? Thank you.
Combining var with LiveData or MutableLiveData doesn't make sense. It defeats the purpose of using LiveData. If something comes along and observes the original LiveData that you have in that property, it will never receive anything. It will have no way of knowing there's a new LiveData instance it should be observing instead.
I can't exactly tell you how to fix it because your code above is incomplete, so I can't tell what you're trying to do in your mapping function, or whether it is called in some function vs. during ViewModel initialization.
I have an issue with getting only information with passed coinList values. To begin with, I want to make coinList to List<Coin> instead of List<Unit> and I am out of ideas on how to change it. I want to compare it.id to the coinList if it has the same values then pass it to the function coin. Thank you in advance!
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
){
private val coinList = listOf("btc-bitcoin", "usdt-tether", "eth-ethereum")
operator fun invoke(): Flow<Resource<List<Coin>>> = flow{
try{
emit(Resource.Loading<List<Coin>>())
val coins = repository.getCoins().map { if (it.id.equals(coinList)){ it.toCoin() }}
emit(Resource.Success<List<Coin>>(coins))
}catch (e: HttpException){
emit(Resource.Error<List<Coin>>(e.localizedMessage ?: "Error occurred"))
}
}
}
maybe something like this would work, first doing a filter to only retrieve relevant items, then mapping
val coinsList = arrayListOf<String>("foo", "bar")
val coins = repository.getCoins().filter { it.id in coinsList }.map {
it.toCoin()
}
I just want to know if it is possible for me to return activePodcastViewData. I get return not allow here anytime I tried to call it on the activePodcastViewData.Without the GlobalScope I do get everything working fine.However I updated my repository by adding suspend method to it.Hence I was getting Suspend function should only be called from a coroutine or another suspend function.
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
GlobalScope.launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
activePodcastViewData
}
}
}
return null
}
class PodcastRepo {
val rssFeedService =RssFeedService.instance
suspend fun getPodcast(url:String):Podcast?{
rssFeedService.getFeed(url)
return Podcast(url,"No name","No Desc","No image")
}
I'm not sure that I understand you correctly but if you want to get activePodcastViewData from coroutine scope you should use some observable data holder. I will show you a simple example with LiveData.
At first, add implementation:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
Now, in your ViewModel we need to create mutableLiveData to hold and emit our future data.
val podcastsLiveData by lazy { MutableLiveData<Podcast>() }
Here your method: (I wouldn't recommend GlobalScope, let's replace it)
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
CoroutineScope(Dispatchers.IO).launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
}
}
}
podcastsLiveData.postValue(activePodcastViewData)
}
As you can see your return null is turned to postValue(). Now you finally can observe this from your Activity:
viewModel.podcastsLiveData.observe(this) {
val podcast = it
//Use your data
}
viewModel.getPodcast()
Now every time you call viewModel.getPodcast() method, code in observe will be invoked.
I hope that I helped some :D
My question is actually quite generic. I want to know how to unit test a Room Dao query that returns a PagingSource From Paging 3.
I have a Room Dao query:
#Query("SELECT * FROM database")
fun getChocolateListData(): PagingSource<Int, Chocolate>
I'm wondering how this query can be unit tested.
What I've tried so far (using in-memory Room database for testing):
#FlowPreview
#Test
fun saveChocolateToDbSavesData() = runBlocking {
val dao: Dao by inject()
val chocolate = Chocolate(
name = "Dove"
)
dao.saveChocolate(chocolate)
val pagingSourceFactory = { dao.getChocolateListData() }
val pagingDataFlow: Flow<PagingData<Chocolate>> = Pager(
config = PagingConfig(
pageSize = 50,
maxSize = 200,
enablePlaceholders = false
),
pagingSourceFactory = pagingSourceFactory
).flow
val chocolateListFlow = pagingDataFlow.testIn(coroutinesTestRule)
Assert.assertEquals(PagingData.from(listOf(chocolate)), chocolateListFlow.emissions[0])
}
This doesn't pass, however:
junit.framework.AssertionFailedError: Expected
:androidx.paging.PagingData#7d6c23a1 Actual
:androidx.paging.PagingData#321123d2
Not sure how to get it right. Any help would be greatly appreciated!
PagingData is wrapper around an internal event stream, you cannot compare it directly and the error you are getting is throwing referential inequality as expected.
Instead you should either query the PagingSource directly to compare the data in LoadResult.Page or you'll need to hook it up to a presenter API such as AsyncPagingDataDiffer or PagingDataAdapter and use .snapshot()
val flow = Pager(..).flow
val adapter = MyPagingDataAdapter()
val job = launch {
flow.collectLatest { adapter.submitData(it) }
}
// Do your asserts here
job.cancel()
if you need a test scope, I recommend runBlockingTest from the kotlinx.coroutines.test library
To query PagingSource directly, it has a single suspending .load() method, so you can simply wrap it in runBlockingTest and assert the result:
#Test
fun test() = runBlockingTest {
val pagingSource = MyPagingSource()
val actual = pagingSource.load(LoadParams.Refresh(...))
assertEquals(actual as? LoadResult.Page)?.data, listOf(...))
}
Based on the answer marked as correct I did my own, is not pretty but at least get the job done if any feedback I would be glad, thanks in advance.
fun <PaginationKey: Any, Model: Any>PagingSource<PaginationKey, Model>.getData(): List<Model> {
val data = mutableListOf<Model>()
val latch = CountDownLatch(1)
val job = CoroutineScope(Dispatchers.Main).launch {
val loadResult: PagingSource.LoadResult<PaginationKey, Model> = this#getData.load(
PagingSource.LoadParams.Refresh(
key = null, loadSize = Int.MAX_VALUE, placeholdersEnabled = false
)
)
when (loadResult) {
is PagingSource.LoadResult.Error -> throw loadResult.throwable
is PagingSource.LoadResult.Page -> data.addAll(loadResult.data)
}
latch.countDown()
}
latch.await()
job.cancel()
return data
}
So in your testing, you can use it like this
val obtainedData = myDao.getSomePagingSource().getData()
assertEquals(expectedData, obtainedData)
WARNING: You are gonna see a rather extended log
WARNING: pageSize on the LegacyPagingSource is not set.
When using legacy DataSource / DataSourceFactory with Paging3, page size...
Just in case you if need to mock PagingSource:
create helper class PagingSourceUtils.kt
Example :
class PagingSourceUtils<T : Any>(
private val data: List<T>
) : PagingSource<Int, T>() {
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return 0
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
return LoadResult.Page(
data = data,
prevKey = null,
nextKey = null
)
}
}
YourTest.kt
#Test
fun `should success get Chocolate `() {
val chocolates = listOf(Chocolate(
name = "Dove"
))
runBlocking {
val tData = PagingSourceUtils(chocolates)
`when`(dao.getChocolateListData()).thenReturn(tData)
val data = ...
val actual = ..
assertEquals(actual, data)
}
}
I'm trying to load data from Firebase into a RecyclerView, however nothing shows up until I reload my fragment.
This is my onCreate method in SubjectsFragment:
viewModel.subjectsListLiveData.observe(
this,
Observer { list ->
subjectsAdapter.swapSubjectsList(list)
if (subject_list != null && list.size != 0) Animations.runLayoutAnimation(
subject_list
)
})
viewModel.lessonsListLiveData.observe(
this,
Observer { list ->
subjectsAdapter.swapLessonsList(list)
if (subject_list != null && list.size != 0) Animations.runLayoutAnimation(
subject_list
)
})
This is SubjectsFragmentViewModel:
private val subjectsList = MutableLiveData<ArrayList<Subject>>()
val subjectsListLiveData: LiveData<ArrayList<Subject>>
get() = subjectsList
private val lessonsList = MutableLiveData<ArrayList<Lesson>>()
val lessonsListLiveData: LiveData<ArrayList<Lesson>>
get() = lessonsList
init {
loadSubjects()
loadLessonsForSubjects()
}
fun loadSubjects() {
GlobalScope.launch {
val subjects = FirebaseUtils.loadAllSubjects()
subjectsList.postValue(subjects)
}
}
fun loadLessonsForSubjects() {
GlobalScope.launch {
val lessons = FirebaseUtils.loadAllLessons()
lessonsList.postValue(lessons)
}
}
I don't have any problems once I reload the fragment. Could someone please explain to me what I'm doing wrong?
Try using setValue directly.
But you may be right, using postValue from a background thread is the way it should be done.
Also, attach your observers in onActivityCreated()