Caching an object to a file in android (kotlin) - android

I am currently retrieving data from firestore and saving the retrieved data to a list, the list is used to create a cache file in the internal storage. When the app is started I check for network, and if there is no network I use the data from the cache to populate a recyclerview. This works fine, however I would like to perform CRUD operations while offline and I struggle to find a way to accomplish this.
This is the code when retrieving data from firestore and calling to create a cache file.
override fun getFromFirestore(context: Context, callback: (MutableList<PostFirestore>) -> Unit) {
db.firestoreSettings = settings
val notesList = mutableListOf<PostFirestore>()
try {
db.collection("DiaryInputs")
.addSnapshotListener { snapshot, e ->
notesList.clear()
if (snapshot != null && !snapshot.isEmpty) {
for (doc in snapshot.documents) {
val note = doc.toObject(PostFirestore::class.java)
notesList.add(note!!)
}
cacheHelper.createCachedFile(context, notesList)
callback(notesList)
} else {
//Refreshing the RV and cache if firestore is empty.
cacheHelper.deleteCachedFile(context)
callback(notesList)
}
}
} catch (e: Exception){
Log.d(TAG, "Failure", e)
}
}
This is how the creation of the cache file is made:
#Throws(IOException::class)
override fun createCachedFile(context: Context, notesList: MutableList<PostFirestore>) {
val outputFile = File(context.cacheDir, "cache").toString() + ".tmp"
val out: ObjectOutput = ObjectOutputStream(
FileOutputStream(
File(outputFile)
)
)
out.writeObject(notesList)
out.close()
}
This is the dataclass PostFirestore
class PostFirestore: Serializable {
var id: String? = null
var diaryInput: String? = null
var temp: String? = null
var creationDate: String? = null
constructor() {}
constructor(id: String, diaryInput: String, temp: String, creationDate: String) {
this.id = id
this.diaryInput = diaryInput
this.temp = temp
this.creationDate = creationDate
}
}
What should I do to be able to add something to the cached file "cache.tmp" instead of overwriting it ? And possibly how to edit/delete something in the list that is stored in the cached file?

Firestore handles offline read and writes automatically. See its documentation on handling read and writes while offline.
If you don't want to depend on that, you'll have to replicate it in your own persistence layer. This includes (but may not be limited to):
Keeping a list of pending writes, that you send to the server-side database when the connection is restored.
Return the up-to-date data for local reads, meaning you merge the data from the local cache with any pending writes of that same data.
Resolve any conflicts that may happen when the connection is restored. So you'll either update your local cache at this point, or not, and remove the item from the list of pending writes.
There are probably many more steps, depending on your scenario. If you get stuck on a specific step, I recommend posting back with a new question with concrete details about that step.

Related

Firebase Storage. Is it possible to parallelize queries when upload an image?

I want to upload user avatar into Firebase Storage. When I do this, I remove all previous avatars.
Also I need to keep user updated with my local storage and Realtime Database.
Is it possible to do it in faster way?
override suspend fun uploadImage(uri: Uri) = withContext(dispatchers.io) {
val user = remoteRepository.getUser() // I do several parallel queries when save edited user info
clearPreviousImages(user.id) // so, I guess, I need remote user below
val fileName = "${user.id}/${uri.lastPathSegment}"
val uploadReference = referencesRepository.getImagesStorageReference().child(fileName)
uploadReference.putFile(uri).await()
val link = uploadReference.downloadUrl.await().toString()
val updatedUser = user.copy(avatar = link) // save image url in Realtime Database
remoteRepository.updateUser(updatedUser) // and return updated user
}
private suspend fun clearPreviousImages(userId: String) {
referencesRepository.getImagesStorageReference().child(userId).listAll().await()
.items
.forEach { it.delete().await() }
}
If I try to put clearPreviousImages in async block and remove await() inside forEach, I get the following error
Task is not yet complete
Can I speed up the upload?

Insert list of class's object to Room by parsing API data using MVVM in Kotlin

I built an application in Kotlin using MVVM. I fetched the API response from the server successfully. Now I want to insert API's parsing data into RoomDB.
API response included both JSON Object & Array. I want to insert specific data from API to DB. Please help me with how I can make an entity class and set parsing data to the class data members with/without loops and insert it into RoomDB by making a single list of the whole class.
Please provide tutorial links or any kind of material links instead of the Android Developers Guide. Thanks a lot!
In API Response we have many data but actually, we don't need that all that so basically we to create one data class that is only constant the specific that actually, we need. and that all operation is performed in a repository and we manage it.
entity class that only contains essential data
#Entity(tableName = "wallpaper")
data class WallpaperDataClass (
#PrimaryKey(autoGenerate = true)
val note_id:Int=0,
val photoId: Int,
val photos_url: String,
val photographer_name: String,
val photographer_url: String,
val src:String
)
Fill the data in model
if (NetworkUtils.isOnline(applicationContext)) {
/**
* Online
* if Your net is online then call api
*/
try {
val result: Response<PhotoModel> =
wallpaperInterface.getWallpaper(authorization, page, per_page)
if (result.body() != null) {
val photos = mutableListOf<WallpaperDataClass>()
result.body()!!.photos.forEach {
// in blows line we set data in modal
val wallpaperDataClass = WallpaperDataClass(
photoId = it.id,
photos_url = it.url,
photographer_name = it.photographer,
photographer_url = it.photographerUrl,
src = it.src.portrait
)
photos.add(wallpaperDataClass)
if (!offlineDatabase.getDao().exists(it.id)){
offlineDatabase.getDao().insertWallpaper(wallpaperDataClass)
}
mutableLiveData.postValue(ErrorHandling.Success(photos))
}
} else {
Log.d("WallpaperResponse", "getWallpaper: ${result.message()}")
}
} catch (e: Exception) {
mutableLiveData.postValue(ErrorHandling.Faild(e.localizedMessage!!.toString()))
}
} else {
/**
* Offline
*if Your net is offline then fetch from db
*/
try {
val wallpaper = offlineDatabase.getDao().getOfflineWallpaper()
mutableLiveData.postValue(ErrorHandling.Success(wallpaper))
} catch (e: Exception) {
mutableLiveData.postValue(ErrorHandling.Faild(e.localizedMessage!!.toString()))
}
}
}
}
Video Tutorial

Android, MVI pattern + Kotlin Flow + Room + Retrofit, how fit them together?

I'm pretty new in the world of MVI pattern. So I'm trying to understand how fit together all the pieces.
I have an app that I structured using MVI pattern (or at least it was what I was meant to do). I have my fragment (I used navigation component but at the moment focus just on one fragment), which is supported by its own ViewModel. Then I have a repository class where all viewmodels retrieve data. Repository has 2 source of data, a web API and a local DB used as cache of data, I used Room for DB management.
I tried different approaches to the problem. At the moment I have done in this way:
In the DAO I used this instruction to retrieve data from the DB:
#Query("SELECT * FROM Users WHERE idTool=:idTool AND nickname LIKE '%' || :query || '%'")
fun users(idTool: Int, query: String) : Flow<List<User>>
Then in my repository I simple get this query to forward to ViewModels:
fun usersFlow(idTool: Int, query: String) = userDao.users(idTool, query)
In the ViewModel I created two MutableLiveData, coordinated by a MediatorLiveData:
val nicknameQuery = MutableStateFlow("")
private val nicknameQueryFlow = nicknameQuery.flatMapLatest {
repository.usersFlow(idToolQuery.value, it)
}
val idToolQuery = MutableStateFlow(DEFAULT_TOOL_ID)
private val idToolQueryFlow = idToolQuery.flatMapLatest {
repository.usersFlow(it, nicknameQuery.value)
}
val users = MediatorLiveData<List<User>>()
init {
users.addSource(nicknameQueryFlow.asLiveData()) {
users.value = it
}
users.addSource(idToolQueryFlow.asLiveData()) {
users.value = it
}
fetchUsers()
}
In this way, from my fragment, I can simply update nicknameQuery or idToolQuery to have an updated list in my RecyclerView. My first doubt is that in this way the fetch of data from my DB is done 2 times, one time for each mutable, but I'd like to retrieve data just one on the app opening (maybe the solution fro this is just check in the nicknameQuery that current query is different from the passed one, in this way since at the beginning current query is empty and it pass an empty query, it is bypassed).
In the Init method of ViewModel, I also call fetchUsers():
private fun fetchUsers() {
viewModelScope.launch {
repository.fetchUsers(DEFAULT_TOOL_ID).collect {
_dataState.value = it
}
}
}
This method checks into the database if there are already cached users with this specific idTool, if not it fetches them from the web and it stores retrieved data into the DB. This is the method inside my repository class:
suspend fun fetchUsers(
idTool: Int,
forceRefetch: Boolean = false
): Flow<DataState<List<User>>> = flow {
try {
var cachedUser = userDao.users(idTool, "").first()
val users: List<User>
if(cachedUser.isEmpty() || forceRefetch) {
Log.d(TAG, "Retrieve users: from web")
emit(DataState.Loading)
withContext(Dispatchers.IO) {
appJustOpen = false
val networkUsers =
api.getUsers(
idTool,
"Bearer ${sessionClient.tokens.accessToken.toString()}"
)
users = entityMapper.mapFromEntitiesList(networkUsers)
userDao.insertList(users)
}
} else {
users = cachedUser
}
emit(DataState.Success(users))
} catch (ex: Exception) {
emit(DataState.Error(ex))
}
}
This method checks if I have already users inside the DB with this specific idTool, if not it fetches them from API. It uses a DataState to update the UI, based on the result of the call. During the fetch of data, it emits a Loading state, this shows a progress bar in my fragment. If data is correctly fetched it emits a Success, and the fragment hides the progress bar to shows the recycler view. This is done in the following way. In my ViewModel I have this mutable state
private val _dataState = MutableLiveData<DataState<List<User>>>()
val dataState: LiveData<DataState<List<User>>> get() = _dataState
As you saw above, my fetch method is
private fun fetchUsers() {
viewModelScope.launch {
repository.fetchUsers(DEFAULT_TOOL_ID).collect {
_dataState.value = it
}
}
}
And finally in my fragment I have:
userListViewModel.dataState.observe(viewLifecycleOwner, { dataState ->
when (dataState) {
is DataState.Success -> {
showUserList()
}
is DataState.Error -> {
Log.e("TEST", dataState.exception.toString())
hideLoader()
Toast.makeText(activity, "Error retrieving data: ${dataState.exception}", Toast.LENGTH_LONG).show()
}
is DataState.Loading -> {
showLoader()
}
else -> {
// Do Nothing in any other case
}
}
})
At this moment Success state takes a list of users, but this list is there from a previous approach, at the moment it is useless since after data is fetched list is inserted into the DB, and I have a Flow to the DB which takes care to update the UI. In this way when I change idTool, when I change query, when I remove a user, the view is always notified
Is this approach correct?
Before this, I used another approach. I returned not a flow from my DB but just a List. Then my fetchUsers always returned a DataState<List>, it checked in the DB and if didn't found anything it fetched data from the web and returned that list. This approach caused me some problems, since every time I changed idTool or query, I always had to call fetchUsers method. Even if a user was removed from database, views didn't get notified since I didn't have a direct flow with the DB.

Firestore and Unicode

The documentation describes that Firestore supports Unicode. You just need to insert already formatted text into Firestore. But when unloading, the following are not taken into account:
Line break;
Unicode characters inserted directly into the text (eg \u000a).
The code is below.
Repository
suspend fun getData(): Response<List<Model>> =
suspendCoroutine { cont ->
val collection =
firestore
.collection(COLLECTION_NAME)
.whereEqualTo(DEFAULT_CONDITION_FIELD, DEFAULT_CONDITION_VALUE)
.orderBy(SORT_FIELD, SORT_DIRECTION)
.get()
collection
.addOnSuccessListener { query ->
val data = arrayListOf<Model>()
query.toObjects(ModelDomain::class.java).forEach { data.add(it.toModel()) }
cont.resume(Response.Success(data))
}
.addOnFailureListener { cont.resume(Response.Error(it)) }
}
ViewModel
private val _data: LiveData<Response<List<Model>>> = loadData()
val data get() = _data
private fun loadData(): LiveData<Response<List<Model>>> =
liveData(Dispatchers.IO) {
emit(Response.Loading)
try {
emit(repository.getData())
} catch (e: Exception) {
emit(Response.Error(e))
}
}
Model
data class ModelDomain(
var description: String = ""
) : KoinComponent {
fun toModel() =
Model(
description = description
)
}
data class Model(
val description: String
)
Part of the code has been omitted.
UPDATE
Just wrote in Notepad ++:
Copied this to Firestore:
Result:
Firestore does not, in any way, modify data that you write to it. If you write something to a document, then read the document, you will get exactly the same data that you put into it.
If you're looking at the document in the Firebase console, you will not see all carriage returns and whitespace. Those are collapsed to save space on screen when rendering large amounts of data. But if you read the data programmatically, it will definitely be exactly as you wrote it.

Firebase Cloud Firestore does not return data or errors

I am learning to use Firebase Firestore and I have created a list of items that I want to display in my app. When trying to load the list, I don't receive the data but I also don't get any error. I cannot display the data in the Log. What could be happening?
fun getDriverData(): LiveData<MutableList<Driver>> {
val mutableData = MutableLiveData<MutableList<Driver>>()
FirebaseFirestore.getInstance().collection("drivers").get().addOnSuccessListener { result ->
val listData = mutableListOf<Driver>()
Log.i("repo","called")
for (document in result) {
val photoUrl = document.getString("photoUrl")!!
val name = document.getString("name")!!
val team = document.getString("team")!!
Log.i("repo", "${document.id}} => ${document.data}")
val driver = Driver(name,team,photoUrl)
listData.add(driver)
}
mutableData.value = listData
}.addOnFailureListener {
Log.i("repo", "getDriverData: ${it.message}")
}
return mutableData
}
Your collection is actually called "drivers" - WITH the quotation marks. Whatever is generating the documents here is using extra quota when building the name of the collection.
You could read them back by adding those quotes:
FirebaseFirestore.getInstance().collection("\"drivers\"").get()
But you probably want to fix the code that generates the document to not add those quotes.

Categories

Resources