In my KMM project I have configured Realm in my shared module. When I try to write in the realm it works fine as shown in picture below, that data is saved,
But when I try to retrieve this data back, orderId field gets overwritten by it's default value which is 0.
this is how I am storing and retrieving data from realm.
In realm service class
class RealmServiceImpl(private val realm: Realm) {
fun insertPackedOrder(order: OrderDomainModel) {
CoroutineScope(Dispatchers.Default).async {
writeOrderPacked(order)
}
}
private suspend fun writeOrderPacked(order: OrderDomainModel) =
realm.write {
copyToRealm(
order.asOrderEntity()
)
}
fun removePackedOrder(order: OrderDomainModel) {
CoroutineScope(Dispatchers.Default).launch {
removeOrder(order)
}
}
private suspend fun removeOrder(order: OrderDomainModel) {
realm.write {
val deletable = this.query<OrderEntity>("orderId == ${order.orderItemId}").find().first()
delete(deletable)
}
}
fun getPackedOrders() = realm.query<OrderEntity>().find()
}
And this is how I am fetching data in my repository,
suspend fun getShippingBookedOrders(): DataState<List<OrdersDTO>> {
return withContext(Dispatchers.Default + SupervisorJob()) {
val ktor = async {
ktorService.getShippingBookedOrders(supplierId = supplier!!.sid)
}
val orders = async {
realmServiceImpl.getPackedOrders()
}
val ktorServiceList = ktor.await()
val ordersList = orders.await()
ktorServiceList.data?.asDomainModel()?.map { item ->
item.isPacked = ordersList.any {
item.orderItemId == it.orderId
}
}
ktorServiceList
}
}
I am getting data from remote server as well using Ktor and then mapping it with data being retrieved from Realm.
Related
I'm using coroutines and Set the Internet Permissions in manifest Everything But cant display my data on app when INTERNET is OFF, I'm caching my API response and successfully stored it in the database but cant retrive it when the internet is off
My Code In Repository
class HomeActivityRepository(
private val photoDatabase: PhotoDatabase,
private val applicationContext: Context
) {
private var photoLiveData = MutableLiveData<List<Photos>>()
val errorMessage = MutableLiveData<String>()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getServicesAPICall(): MutableLiveData<List<Photos>> {
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = RetrofitClient.getInstance().create(ApiInterface::class.java)
val res = response.getServicesAPICall()
withContext(Dispatchers.Main) {
if (NetworkUtils.isInternetAvailable(applicationContext)) {
if (res.isSuccessful) {
photoDatabase.photoDao().insertMemes(res.body()!!)
photoLiveData.postValue(res.body())
} else {
onError("Error : ${res.message()}")
}
} else {
val photos = photoDatabase.photoDao().getPhotos()
photoLiveData.postValue(photos)
}
}
}
return photoLiveData
}
private fun onError(message: String) {
errorMessage.postValue(message)
}
}
I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it?
Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings.
I have created a State to control the values in the view model:
data class StringItemsState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String = ""
)
and the flow:
private val stringItemsState = StringtemsState()
private val _stateFlow = MutableStateFlow(stringItemsState)
val stateFlow = _stateFlow.asStateFlow()
and finally the method that performs all the logic in the viewModel:
fun fetchStringItems() {
try {
_stateFlow.value = stringItemsState.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) {
val result = getStringItemsUseCase.execute()
if (result.isEmpty()) {
_stateFlow.value = stringItemsState
} else {
_stateFlow.value = stringItemsState.copy(items = result)
}
}
} catch (e: Exception) {
e.localizedMessage?.let {
_stateFlow.value = stringItemsState.copy(error = it)
}
}
}
I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails:
private val stringItems = listOf<String>("A", "B", "C")
#Test
fun `get string items - not empty`() = runBlocking {
// What
coEvery {
useCase.execute()
} returns stringItems
// Where
viewModel.fetchStringItems()
// Then
assert(viewModel.stateFlow.value.items == stringItems)
coVerify(exactly = 1) { viewModel.fetchStringItems() }
}
Can someone help me and tell me if I am doing it correctly? Thanks.
#HiltViewModel
class HistoryViewModel #Inject constructor(private val firebaseRepository: FirebaseRepository) :
ViewModel() {
private val translateList: MutableList<Translate> = mutableListOf()
private val _translateListState: MutableStateFlow<List<Translate>> =
MutableStateFlow(translateList)
val translateListState = _translateListState.asStateFlow()
init {
listenToSnapshotData()
}
private suspend fun addItemToList(translate: Translate) {
Log.d("customTag", "item added adapter $translate")
translateList.add(translate)
_translateListState.emit(translateList)
}
private suspend fun removeItemFromList(translate: Translate) {
Log.d("customTag", "item removed adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList.removeAt(indexOfItem)
_translateListState.emit(translateList)
}
}
private suspend fun updateItemFromList(translate: Translate) {
Log.d("customTag", "item modified adapter $translate")
val indexOfItem = translateList.indexOfFirst {
it.id == translate.id
}
if (indexOfItem != -1) {
translateList[indexOfItem] = translate
_translateListState.emit(translateList)
}
}
private fun listenToSnapshotData() {
viewModelScope.launch {
firebaseRepository.translateListSnapshotListener().collect { querySnapshot ->
querySnapshot?.let {
for (document in it.documentChanges) {
val translateData = document.document.toObject(Translate::class.java)
when (document.type) {
DocumentChange.Type.ADDED -> {
addItemToList(translate = translateData)
}
DocumentChange.Type.MODIFIED
-> {
updateItemFromList(translate = translateData)
}
DocumentChange.Type.REMOVED
-> {
removeItemFromList(translate = translateData)
}
}
}
}
}
}
}
}
Here data comes properly in querySnapshot in listenToSnapshotData function. And post that it properly calls corresponding function to update the list.
But after this line _translateListState.emit(translateList) flow doesn't go to corresponding collectLatest
private fun observeSnapShotResponse() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
historyViewModel.translateListState.collectLatest {
Log.d("customTag", "calling submitList from fragment")
translateListAdapter.submitList(it)
}
}
}
}
calling submitList from fragment is called once at the start, but as & when data is modified in list viewmodel, callback doesn't come to collectLatest
This is from StateFlow documentation:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
You are trying to emit the same instance of List all the time, which has no effect because of what is written in the docs. You will have to create new instance of the list every time.
I want to implement system with live updates (similar to onSnapshotListener). I heard that this can be done with Kotlin Flow.
Thats my function from repository.
suspend fun getList(groupId: String): Flow<List<Product>> = flow {
val myList = mutableListOf<Product>()
db.collection("group")
.document(groupId)
.collection("Objects")
.addSnapshotListener { querySnapshot: QuerySnapshot?,
e: FirebaseFirestoreException? ->
if (e != null) {}
querySnapshot?.forEach {
val singleProduct = it.toObject(Product::class.java)
singleProduct.productId = it.id
myList.add(singleProduct)
}
}
emit(myList)
}
And my ViewModel
class ListViewModel: ViewModel() {
private val repository = FirebaseRepository()
private var _products = MutableLiveData<List<Product>>()
val products: LiveData<List<Product>> get() = _produkty
init {
viewModelScope.launch(Dispatchers.Main){
repository.getList("xGRWy21hwQ7yuBGIJtnA")
.collect { items ->
_products.value = items
}
}
}
What do I need to change to make it work? I know data is loaded asynchronously and it doesn't currently work (the list I emit is empty).
You can use this extension function that I use in my projects:
fun Query.snapshotFlow(): Flow<QuerySnapshot> = callbackFlow {
val listenerRegistration = addSnapshotListener { value, error ->
if (error != null) {
close()
return#addSnapshotListener
}
if (value != null)
trySend(value)
}
awaitClose {
listenerRegistration.remove()
}
}
It uses the callbackFlow builder to create a new flow instance.
Usage:
fun getList(groupId: String): Flow<List<Product>> {
return db.collection("group")
.document(groupId)
.collection("Objects")
.snapshotFlow()
.map { querySnapshot ->
querySnapshot.documents.map { it.toObject<Product>() }
}
}
Note that you don't need to mark getList as suspend.
Starting in firestore-ktx:24.3.0, you can use the Query.snapshots() Kotlin flow to get realtime updates:
suspend fun getList(groupId: String): Flow<List<Product>> {
return db.collection("group")
.document(groupId)
.collection("Objects")
.snapshots().map { querySnapshot -> querySnapshot.toObjects()}
}
As of 2 days ago, firestore has this functionality provided out of the box: https://github.com/firebase/firebase-android-sdk/pull/1252/
I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}