Running into an Issue early on in importing a declared extension function in another Kotlin File (Extensions.kt), call the extension function From another class (ForecastsRepository.kt) it doesn't compile but when i remove it there is no problem with the build. Obviously I need it and wonder why importing it would become an issue .
Here is the class:
import com.benmohammad.climatemvvm.base.Success
import com.benmohammad.climatemvvm.custom.errors.ErrorHandler
import com.benmohammad.climatemvvm.custom.errors.NoDataException
import com.benmohammad.climatemvvm.custom.errors.NoResponseException
import com.benmohammad.climatemvvm.entitymappers.forecasts.ForecastMapper
import com.benmohammad.climatemvvm.features.home.di.HomeScope
import com.benmohammad.climatemvvm.network.api.OpenWeatherApi
import com.benmohammad.climatemvvm.network.response.ErrorResponse
import com.benmohammad.climatemvvm.room.dao.forecasts.ForecastDao
import com.benmohammad.climatemvvm.room.dao.utils.StringKeyValueDao
import com.benmohammad.climatemvvm.room.models.forecasts.DbForecast
import com.benmohammad.climatemvvm.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
import com.benmohammad.climatemvvm.extensions.applyCommonSideEffects//import
#HomeScope
class ForecastsRepository #Inject constructor(
private val openWeatherApi: OpenWeatherApi,
private val forecastDao: ForecastDao,
private val stringKeyValueDao: StringKeyValueDao
) {
private val forecastCacheThresholdMillis = 3 * 3600000L //3 hours//
fun getForecasts(cityId: Int) = flow {
stringKeyValueDao.get(Utils.LAST_FORECASTS_API_CALL_TIMESTAMP)
?.takeIf { !Utils.shouldCallApi(it.value, forecastCacheThresholdMillis) }
?.let { emit(getDataOrError(NoDataException())) }
?: emit((getForecastFromAPI(cityId)))
}
//.applyCommonSideEffects()
.catch {
emit(getDataOrError(it))
}
private suspend fun getForecastFromAPI(cityId: Int) = openWeatherApi.getWeatherForecast(cityId)
.run {
if (isSuccessful && body() != null) {
stringKeyValueDao.insert(
Utils.getCurrentTimeKeyValuePair(Utils.LAST_FORECASTS_API_CALL_TIMESTAMP)
)
forecastDao.deleteAllAndInsert(ForecastMapper(body()!!).map())
getDataOrError(NoDataException())
} else {
Error(
NoResponseException(
ErrorHandler.parseError<ErrorResponse>(errorBody())?.message
)
)
}
}
private suspend fun getDataOrError(throwable: Throwable) =
forecastDao.get()
?.let { dbValue -> Success(getForecastList(dbValue)) }
?: Error(throwable)
private suspend fun getForecastList(dbForecast: DbForecast) = withContext(Dispatchers.Default) {
dbForecast.list.map { it.forecast }
}
}
and here is the file for the Extension functions:
package com.benmohammad.climatemvvm.extensions
import com.benmohammad.climatemvvm.base.Progress
import com.benmohammad.climatemvvm.base.Result
import com.benmohammad.climatemvvm.utils.Utils
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.retryWhen
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import java.io.IOException
fun String.capitalizeWords(): String = this.split(' ').joinToString(" "){it.capitalize()}
#PublishedApi
internal inline fun Retrofit.Builder.callFactory(crossinline body: (Request) -> Call) =
callFactory(object: Call.Factory {
override fun newCall(request: Request): Call = body(request)
})
#Suppress("NOTHING_TO_INLINE")
inline fun Retrofit.Builder.delegatingCallFactory(delegate: dagger.Lazy<OkHttpClient>): Retrofit.Builder =
callFactory {
delegate.get().newCall(it) }
fun < T: Any> Flow<Result<T>>.applyCommonSideEffects() = //<<-----------T H I S Function!!!!HERE
retryWhen { cause, attempt ->
when {
(cause is IOException && attempt < Utils.MAX_RETRIES) -> {
delay(Utils.getBackOffDelay(attempt))
true
}
else -> {
false
}
}
}
.onStart { emit(Progress(isLoading = true)) }
.onCompletion { emit(Progress(isLoading = false)) }
fun Job?.cancelIfActive() {
if(this?.isActive == true) {
cancel()
}
}
as it doesnt compile it leads me to think the bug is deeper.
the IDE also hunderlines the function call stating it is "Unresolved reference"
GitHub Repo
Thanks any advice appreciated.
Related
Need a bit of help on why data from viewmodel is not shown in the composable function MainContent. I tried to use MVVM style with coroutine but without DI which I think will be easier but somehow, I could not get it to work.
The viewmodel is working as the log.d is showing the correct data from server but somehow, I could not get it to display in
Text(text = viewModel.posts[it].phrase)
Any help will be greatly appreciated. The github link for this program is in https://github.com/somaria/LearnChnCompose
package com.gamecrawl.learnchncompose
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import com.gamecrawl.learnchncompose.ui.theme.LearnChnComposeTheme
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: MainViewModel by viewModels()
setContent {
LearnChnComposeTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainContent(viewModel)
}
}
}
}
}
class MainViewModel : ViewModel() {
private var _posts = mutableListOf(Post("12", "test phrase", true))
var posts get() = _posts; set(value) {
_posts = value
}
init {
CoroutineScope(Dispatchers.IO).launch {
_posts = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
header("Content-Type", "application/json")
}
Log.d("HomeViewModel", "init: ${_posts[1].phrase}")
Log.d("HomeViewModel", "init: ${_posts[1].id}")
}
}
fun addPost(post: Post) {
CoroutineScope(Dispatchers.IO).launch {
val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
header("Content-Type", "application/json")
body = post
}
}
}
}
#Composable
fun MainContent(viewModel: MainViewModel) {
Column {
LazyColumn {
items(viewModel.posts.size) {
Text(text = viewModel.posts[it].phrase)
}
}
Button(onClick = {
viewModel.addPost(Post("test", "adding post 222", true))
}) {
Text(text = "Add Post")
}
}
}
#Serializable
data class Post(
val id: String,
val phrase: String,
val published: Boolean
)
object KtorClient {
val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
}
val httpClient = HttpClient(Android) {
install(HttpTimeout) {
socketTimeoutMillis = 200000
requestTimeoutMillis = 200000
connectTimeoutMillis = 200000
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.d("TAG", "log: $message")
}
}
}
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
}
}
The data type of the posts is a MutableList<Post>. This means that changes to this variable will not cause the function to recompose. When the UI is loaded, then the variable does not have any data, since you fetch the data in an asynchronous coroutine. However, when the variable is updated, the UI is not recomposed.
To fix this issue, you must declare _posts to be a MutableState<List<Post>> from the compose library instead. Reconfigure your ViewModel in the following way:
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
private val _posts = mutableStateOf(listOf<Post>()) // <- requires init value
val posts: State<List<Post>> = _posts // <- keep both variables immutable 'val'
/* always expose the immutable form of State */
init {
CoroutineScope(Dispatchers.IO).launch {
/* _posts.value is used now due to the datatype change */
_posts.value = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
header("Content-Type", "application/json")
}
Log.d("HomeViewModel", "init: ${_posts.value[1].phrase}")
Log.d("HomeViewModel", "init: ${_posts.value[1].id}")
}
}
fun addPost(post: Post) {
CoroutineScope(Dispatchers.IO).launch {
val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
header("Content-Type", "application/json")
body = post
}
}
}
}
Now since your public posts variable is of type State<T>, you need to make changes to your composable function:
#Composable
fun MainContent(viewModel: MainViewModel) {
val posts = viewModel.posts.value // <- grab the value of the state variable.
/* The function will recompose whenever there's a change in posts */
Column {
LazyColumn {
items(posts.size) {
Text(text = posts[it].phrase)
}
}
Button(onClick = {
viewModel.addPost(Post("test", "adding post 222", true))
}) {
Text(text = "Add Post")
}
}
}
This should help your issue.
i'm practising a bit with kotlin and was testing Room and livedata, my app gets data from a json and the stores it in room, i want to move this network call to its own file and class, but if i do so the observer i set to get the changes don't trigger anymore, any help would be appreciated
here is a snipped of my mainactivity, if more is needed to know what happens please let me know
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.room.Room
import com.optiva.videoplayer.data.*
import com.optiva.videoplayer.network.GetData
import com.optiva.videoplayer.network.Networking
import com.optiva.videoplayer.network.RetrofitConnect
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbcategories = Room.databaseBuilder(applicationContext, CategoriesDatabase::class.java,"categories.db").build()
val dbvideo = Room.databaseBuilder(applicationContext, VideosDatabase::class.java,"videos.db").build()
val retrofitData = RetrofitConnect.retrofitInst?.create(GetData::class.java)
val categoriesList = retrofitData?.getAll()
categoriesList?.enqueue(object: Callback<DataList> {
override fun onResponse(
call: Call<DataList>,
response: Response<DataList>
) {
val test = response?.body()
val cat = test?.categories
val vid = test?.videos
lifecycleScope.launch(Dispatchers.IO) {
if (cat != null) {
for(c in cat){
dbcategories.categoriesDAO().insertAll(CategoriesEntity(c.id,c.title,c.type))
}
}
if (vid != null) {
for(v in vid){
dbvideo.VideosDAO().insertAll(VideosEntity(v.id,v.thumb,v.videoUrl,v.categoryId,v.name))
}
}
}
}
override fun onFailure(call: Call<DataList>, t: Throwable) {
Toast.makeText(applicationContext,"error", Toast.LENGTH_LONG).show()
}
})
val textView: TextView = findViewById(R.id.test) as TextView
dbcategories.categoriesDAO().getALL().observeForever({categories ->
if(categories.size>0){
textView.text= categories[0].title
}
})
dbcategories.categoriesDAO().getALL().observe(this, {categories ->
if(categories.size>0){
textView.text= categories[0].title
}
}
} ```
I am creating an app that right now is just supposed to be able to use Firebase authentication with mvvm as a standard to get some kind of seperation of concerns. So i have a database class that uses Firebase and gets an injection from Dagger hilt to be able to use Authentication from firebase. Now i am trying to check if the user is logged in. So i type the auth.getCurrentUser(). This does only give me the possibility to check if a user exist and with firebase when you check this and you have deleted the user while you are testing it does not update the value so it's still logged in for another hour when you have deleted the user. If you check around the internet about this you get the answer to use the authstatelistener. My question is though how to use this together with mvvm? is there a way to do this when i have my clases seperated by a viewmodel a repository and a database.
My classes look like this right now.
Database: //This has a comment written in it that has some use for the problem
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Exception
import javax.inject.Inject
import javax.security.auth.callback.Callback
class FirDatabase #Inject constructor(var auth : FirebaseAuth) {
suspend fun register(user: User) : Task<AuthResult>{
return auth.createUserWithEmailAndPassword(user.email, user.password)
}
suspend fun checkIfUserExist() : Boolean? {
//i would like to be able to check it right here somehow
println("Currentuser " + auth.currentUser?.uid)
return auth.currentUser != null
}
}
Repository:
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks.await
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AuthRepository #Inject constructor (private val database: FirDatabase) : IAuthRepository {
override suspend fun register(user: User) : Resource<AuthResult> {
return try{
val response = database.register(user)
val result = response.result
if(response.isSuccessful && result != null){
Resource.Success(result)
}else{
Resource.Error(response.exception?.message.toString())
}
}catch (e: Exception){
Resource.Error(e.message ?: "An Error occurred")
}
}
override suspend fun CheckIfloggedIn() : Resource<Boolean>{
return try{
val user = database.checkIfUserExist()
if(user != false){
Resource.IsLoggedIn("User is already logged in" )
}else{
Resource.IsNotLoggedIn("User is not logged in")
}
}catch(e: Exception){
Resource.Error(e.message ?: "An Error occurred")
}
}
}
ViewModel:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class AuthViewModel #Inject constructor(private val repository: AuthRepository, private val dispatchers: DispatcherProvider) : ViewModel(){
sealed class AuthEvent{
class Success(val result: String): AuthEvent()
class Failure(val errorText: String): AuthEvent()
object Loading : AuthEvent()
object LoggedIn : AuthEvent()
object NotRegistered : AuthEvent()
object NotLoggedIn : AuthEvent()
object Empty : AuthEvent()
}
private val _registering = MutableStateFlow<AuthEvent>(AuthEvent.Empty)
val registering : StateFlow<AuthEvent> = _registering
private val _checkUserIsLoggedIn = MutableStateFlow<AuthEvent>(AuthEvent.Empty)
val checkUserIsLoggedIn : StateFlow<AuthEvent> = _checkUserIsLoggedIn
fun register(user: User){
viewModelScope.launch(dispatchers.io) {
_registering.value = AuthEvent.Loading
when(val authResponse = repository.register(user)){
is Resource.Error -> _registering.value = AuthEvent.Failure(authResponse.message!!)
is Resource.Success -> {
val response = authResponse.data!!.user
_registering.value = AuthEvent.Success("Success")
}
}
}
}
fun CheckIfUserIsLoggedIn()
{
viewModelScope.launch(dispatchers.io) {
when(val isUserLoggedIn = repository.CheckIfloggedIn()){
is Resource.IsLoggedIn -> _checkUserIsLoggedIn.value = AuthEvent.LoggedIn
is Resource.IsNotLoggedIn -> _checkUserIsLoggedIn.value = AuthEvent.NotLoggedIn
is Resource.Error -> _checkUserIsLoggedIn.value = AuthEvent.Failure(isUserLoggedIn.message!!)
}
}
}
}
I have followed tutorials from this dude https://www.youtube.com/watch?v=ct5etYgB5pQ
and i have already seen alot of the documentation on this page like this for example Firebase: how to check if user is logged in?
and here How does the firebase AuthStateListener work?. So with further investigations into the answer you gave me i have come up with this solution... but it is not really working? why is this?
Repository function:
override fun CheckIfloggedIn() = callbackFlow<Resource<Boolean>>{
val isUserLoggedIn = flow<Resource<Boolean>>{
database.checkIfUserExist().collect { isLoggedIn ->
if(isLoggedIn){
Resource.Success(isLoggedIn)
}else{
Resource.Error("User is not logged in")
}
}
}
}
Database function:
fun checkIfUserExist() = callbackFlow {
val authStatelistener = FirebaseAuth.AuthStateListener {auth ->
trySend(auth.currentUser == null)
}
auth.addAuthStateListener(authStatelistener)
awaitClose {
auth.removeAuthStateListener(authStatelistener)
}
}
I am using extension function to bind list data to recyclerview , with pagedlist it doesn't need any coroutine
#BindingAdapter("pagedListAdapterData")
fun <T : Any> submitPagedList(recyclerView: RecyclerView, list: PagedList<T>?) {
if (list != null && recyclerView.adapter is PagedListAdapter<*, *>) {
(recyclerView.adapter as? PagedListAdapter<T, RecyclerView.ViewHolder>)?.submitList((list))
}
}
pagedListAdapterData="#{viewModel.list}"
but as im upgrading to Paging 3.0 it requires the use of coroutine
#BindingAdapter("pagingDataAdapter")
fun <T : Any> submitPagingDataList(recyclerView: RecyclerView, list: PagingData<T>?) {
if (list != null && recyclerView.adapter is PagingDataAdapter<*, *>) {
GlobalScope.launch {
(recyclerView.adapter as? PagingDataAdapter<T, RecyclerView.ViewHolder>)?.submitData((list))
}
}
}
this works just fine, but my worry is on the use of Globalscope, seems like there is a better way to do it since globalScope is not recomended
androidx.lifecycle has an extension function View.findViewTreeLifecycleOwner, so you don't have to create your own:
view.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
...
}
Indeed, it isn't a good idea to use the GlobalScope, according to the documentation submitData() should be run inside 'lifecycleScope'. You can get 'lifecycleScope' from the view, but it requires a little bit of boilerplate:
package com.test.pagingadapter
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import androidx.activity.ComponentActivity
import androidx.databinding.BindingAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.findFragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
/**
* https://stackoverflow.com/a/58249983/4858777
*/
tailrec fun Context?.getActivity(): Activity? = when (this) {
is Activity -> this
else -> {
val contextWrapper = this as? ContextWrapper
contextWrapper?.baseContext?.getActivity()
}
}
val View.lifecycleOwner: LifecycleOwner? get() = try {
val fragment = findFragment<Fragment>()
fragment.viewLifecycleOwner
} catch (e: IllegalStateException) {
when (val activity = context.getActivity()) {
is ComponentActivity -> activity
else -> null
}
}
#BindingAdapter("pagingDataAdapter")
fun <T : Any> submitPagingDataList(recyclerView: RecyclerView, data: PagingData<T>?) {
val adapter = recyclerView.adapter
if (data != null && adapter is PagingDataAdapter<*, *>) {
// but it isn't a perfect solution because the cast is required
#Suppress("UNCHECKED_CAST")
val castedAdapter = adapter as PagingDataAdapter<T, RecyclerView.ViewHolder>
recyclerView.lifecycleOwner?.lifecycleScope?.launch {
castedAdapter.submitData(data)
}
}
}
I'm writing an application using the Android Architecture Components, originally based on the famous article, however that is now outdated and not accurate, so based on other documentation, articles and videos, I build something using the latest components, which turned out in a very simple architecture with very little code.
The idea is the app starts with its tables empty, and goes to read from a Firestore db to get its data, stores the data in a local SqlLite DB (using Room) and displays the updated data. Whenever the data is updated on Firestore, it should be updated in SqlLite and update the UI.
However, my UI (just a text box for now) is only updated when the application starts, and never ever after the DB is modified.
PorteroDao
package com.sarcobjects.portero.db
import androidx.room.*
import com.sarcobjects.portero.entities.Portero
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
#Dao
abstract class PorteroDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(portero: Portero): Long
#Transaction
#Query("SELECT * FROM Portero WHERE porteroId == :porteroId")
abstract suspend fun getPortero(porteroId: Long): PorteroWithLevelsAndUnits
}
PorteroRepository
package com.sarcobjects.portero.repository
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.sarcobjects.portero.db.PorteroDao
import com.sarcobjects.portero.entities.Portero
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import timber.log.Timber.d
import timber.log.Timber.w
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
#Singleton
class PorteroRepository #Inject constructor(
private val porteroDao: PorteroDao,
private val firestore: FirebaseFirestore
) {
#ExperimentalCoroutinesApi
suspend fun getPortero(porteroId: Long): PorteroWithLevelsAndUnits {
GlobalScope.launch {refreshPortero(porteroId)}
val portero = porteroDao.getPortero(porteroId)
d("Retrieved portero: $portero")
return portero
}
#ExperimentalCoroutinesApi
private suspend fun refreshPortero(porteroId: Long) {
d("Refreshing")
//retrieve from firestore
retrieveFromFirestore(porteroId)
.collect { portero ->
d("Retrieved and collected: $portero")
porteroDao.insert(portero)
}
}
#ExperimentalCoroutinesApi
private fun retrieveFromFirestore(porteroId: Long): Flow<Portero> = callbackFlow {
val callback = EventListener<DocumentSnapshot> { document, e ->
if (e != null) {
w(e, "Listen from Firestore failed.")
close(e)
}
d("Read successfully from Firestore")
if (document != null && document.exists()) {
//Convert to objects
val portero = document.toObject(Portero::class.java)
d("New Portero: ${portero.toString()}")
offer(portero!!)
} else {
d("Portero not found for porteroId: $porteroId")
}
}
val addSnapshotListener = firestore.collection("portero").document(porteroId.toString())
.addSnapshotListener(callback)
awaitClose { addSnapshotListener.remove()}
}
}
ButtonsViewModel
package com.sarcobjects.portero.ui.buttons
import androidx.hilt.Assisted
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import com.sarcobjects.portero.repository.PorteroRepository
import timber.log.Timber.d
class ButtonsViewModel #ViewModelInject
constructor(#Assisted savedStateHandle: SavedStateHandle, porteroRepository: PorteroRepository) : ViewModel() {
private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
val portero: LiveData<PorteroWithLevelsAndUnits> = liveData {
val data = porteroRepository.getPortero(porteroId)
d("Creating LiveData with: $data")
emit(data)
}
}
ButtonsFragment
package com.sarcobjects.portero.ui.buttons
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.sarcobjects.portero.R
import com.sarcobjects.portero.entities.PorteroWithLevelsAndUnits
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.buttons_fragment.*
import timber.log.Timber.d
#AndroidEntryPoint
class ButtonsFragment : Fragment() {
companion object {
fun newInstance() = ButtonsFragment()
}
private val viewModel: ButtonsViewModel by viewModels (
)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.buttons_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.portero.observe(viewLifecycleOwner, Observer<PorteroWithLevelsAndUnits> {porteroWLAU ->
d("Observing portero: $porteroWLAU")
message.text = porteroWLAU?.portero?.name ?: "Portero not found."
})
}
}
All the dependency injection seems to be OK (no NPEs), I even checked that the ViewModel instance is the same on the Fragment and in the ViewModel itself, and the persistence via Room is correct; the new data is actually being saved into SqlLite when I update Firestore. Also, no exceptions or errors in logcat.
But the UI is not updated.
So, I managed to find a way to make this work, although in a different way. My idea was to make Room trigger a liveData reload whenever I wrote to SqlLite, but I never managed to make it work, and still I don't know why.
What I did in the end was:
Return a Flow from the repository, triggered by the updates in Firestore:
#ExperimentalCoroutinesApi
fun getPorteroFlow(porteroId: Long): Flow<Portero> = retrieveFromFirestore(porteroId)
#ExperimentalCoroutinesApi
private fun retrieveFromFirestore(porteroId: Long): Flow<Portero> = callbackFlow {
val callback = EventListener<DocumentSnapshot> { document, e ->
if (e != null) {
w(e, "Listen from Firestore failed.")
return#EventListener
}
d("Read successfully from Firestore")
if (document != null && document.exists()) {
//Convert to objects
val portero = document.toObject(Portero::class.java)
d("New Portero: ${portero.toString()}")
GlobalScope.launch {
d("Saved new portero: $portero")
porteroDao.insert(portero!!)
}
offer(portero!!)
} else {
d("Portero not found for porteroId: $porteroId")
}
}
val addSnapshotListener = firestore.collection("portero").document(porteroId.toString()) //.get()
.addSnapshotListener(callback)
awaitClose { addSnapshotListener.remove()}
}
Convert the Flow to liveData in the ViewModel:
private val porteroId: Long = savedStateHandle["porteroId"] ?: 0
#ExperimentalCoroutinesApi
val portero = porteroRepository.getPorteroFlow(porteroId)
.onStart { porteroRepository.getPortero(porteroId) }
.asLiveData()
}
(onStart is used to read data from SqlLite when the app starts, in case there's no internet and Firestore is unreachable).
This works flawlessly and is very fast, as soon as I update data in Firestore console, I can see the UI update in the device.