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)
}
}
}
Related
I have created Kotlin Code for parsing APIs with retrofit in list view/grid view/recycler view, I wanted to know how can I do the same using jetpack compose? I have used retrofit to parse GET API responses using different ViewGroups. View Binding is used to interact with the views on this screen.
Code
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import okhttp3.ResponseBody
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import retrofit.Retrofit2
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import supports.*
import viewmodel.SIViewModel
import java.io.IOException
import java.net.SocketTimeoutException
import java.util.*
class TestIndex : AppCompatActivity() {
var adapter: Adapter1? = null
var dialog: AlertDialog? = null
var builder: AlertDialog.Builder? = null
private val viewModel: SIViewModel? by viewModels()
var test_arr = ArrayList<TestModel>()
var binding: TestGridBinding? = null
#SuppressLint("CommitPrefEdits", "ClickableViewAccessibility", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.test_grid)
setSupportActionBar(binding?.view?.toolbarr)
supportActionBar!!.elevation = 0f
viewModel
adapter = Adapter1(this#TestIndex, R.layout.row, test_arr)
//binding ViewModel retrofit API with activity, here ID1 and ID2 coming from the previous screen.
viewModel!!.getList(this#TestIndex , ID1!!, ID2!!)
binding?.gvTest?.adapter = adapter
binding?.swipeRefreshLayout?.setOnRefreshListener {
binding?.swipeRefreshLayout?.isRefreshing = true
if (ID1 != null && ID2 != null) {
// getting same server response on swipe refresh widget
getdata(ID1!!, ID2!!)
} else {
builder = AlertDialog.Builder(MyApplication.instance)
builder!!.setCancelable(false)
builder!!.setTitle("Alert")
builder!!.setNegativeButton("Cancel") { dialog: DialogInterface, which: Int ->
dialog.dismiss()
finish()
}
builder!!.setPositiveButton("OK") { dialog: DialogInterface, which: Int -> dialog.dismiss() }
dialog = builder!!.create()
dialog?.show()
}
}
subscribeObservers()
}
//this is checked on the dev portal but I don't know I could I use it //dynamically with adapters and ArrayList.
#Composable
fun LazyRowItemsDemo() {
LazyRow {
items((1..title_arr.size).toList()) {
Text(text = "Item $it")
}
}
}
private fun getdata(id1: String, id2: String) {
val mProgressDialog = ProgressDialog(this#TestIndex)
mProgressDialog.isIndeterminate = true
mProgressDialog.setMessage(Keys.KEY_pre_msg)
if (!this.isFinishing) {
mProgressDialog.show()
}
val retrofit = Retrofit.Builder()
.baseUrl(Keys.testURL)
.client(OkHttpClient().build())
.addConverterFactory(GsonConverterFactory.create())
.build()
val retrofitInterface = retrofit.create(
RetrofitInterface::class.java
)
val call = retrofitInterface.getTestdata(id1, id2)
call!!.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(call: Call<ResponseBody?>, response: Response<ResponseBody?>) {
var remoteResponse: String? = null
if (response.code() == 200) {
try {
assert(response.body() != null)
remoteResponse = response.body()!!.string()
} catch (e: Exception) {
e.printStackTrace()
}
} else {
try {
if (response.errorBody() != null) {
remoteResponse = response.errorBody()!!.string()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
if (remoteResponse != null) {
//getting response fields and parsing list view or grid view/recycler view in different screens
adapter =
Adapter1(this#TestIndex, R.layout.row, test_arr)
binding!!.gvTest.adapter = adapter
adapter!!.notifyDataSetChanged()
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Log.d(Keys.KEY_TAG, "onFailure: " + t.localizedMessage)
}
})
if (mProgressDialog.isShowing) mProgressDialog.dismiss()
}
//subscribed the Observers here from view model
private fun subscribeObservers() {
viewModel!!.lifting.observe(this, { TestModel: List<TestModel>? ->
adapter!!.updateTests(TestModel)
binding!!.swipeRefreshLayout.isRefreshing = false
}
}
Kindly let me know how can I do the same using jetpack compose for listview, grid view, recycler view. Thanks.
It's more a general example, without retrofit. You can implement your data fetch inside my getTestData method.
To begin with, in order to understand the basic principles of working with Compose, I suggest you study compose tutorials.
Compose uses view models to perform complex data manipulations. I will use the basic version, but you can also check out Hilt for more complex architecture.
In order for changing the state of an object to lead to a recomposition, you can use:
The mutableStateObject - this is a specially created container for compose that will update the view if the value has changed
you can also use LiveData and Flow, they can both be cast to mutableStateObject.
Note that mutableStateObject will not alert you to changes in the container object fields if you pass a complex class there. It will only notify you when the value itself changes, so it is recommended to use it only for simple types.
You can also use mutableStateListOf to store collections. In my example you will see both: with mutableStateListOf it is convenient to add/delete objects to the collection, while mutableStateObject with List lying inside is easier to completely replace with new objects.
Inside Composable functions you need to wrap your mutable state objects with remember to prevent reinitializing them on each composition, and inside your view model you don't need to do that, because it's not gonna be reinitialized in any case.
SwipeRefresh is not part of compose, it's a library made by compose maintainers too. To install it follow this instructions.
I'm using two columns here just to show difference between mutableStateOf and mutableStateListOf, you can remove Row and one of LazyColumn
class ScreenViewModel : ViewModel() {
var list by mutableStateOf(emptyList<String>())
var mutableList = mutableStateListOf<String>()
var isRefreshing by mutableStateOf(false)
init {
refresh()
}
fun refresh() {
isRefreshing = true
viewModelScope.launch {
list = getTestData()
mutableList.addAll(0, list)
isRefreshing = false
}
}
suspend fun getTestData(): List<String> {
// emulate network call
delay(1000)
return List(100) {
Random.nextInt(100).toString()
}
}
}
#Composable
fun TestView() {
val viewModel: ScreenViewModel = viewModel()
SwipeRefresh(
state = rememberSwipeRefreshState(viewModel.isRefreshing),
onRefresh = {
viewModel.refresh()
},
) {
Row {
LazyColumn(
modifier = Modifier.weight(1f) // this needed inly for two columns case
) {
itemsIndexed(viewModel.list) { i, item ->
Text("$i $item")
}
}
LazyColumn(
modifier = Modifier.weight(1f) // this needed inly for two columns case
) {
itemsIndexed(viewModel.mutableList) { i, item ->
Text("$i $item")
}
}
}
}
}
Result:
if I any change in firebase like delete, update. The data in recyclerView is duplicated if any of those CRUD occur, so I added temporary swipeRefresh to refresh the activity but this solution doesn't make sense.
This image below explain when I update data in firebase and what happend in RecyclerView
MainDashBoard.kt
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
class MainDashBoard : AppCompatActivity(), OnItemPatientClickListener{
data class PatientDataItem(val patientName: String, val patientMessage: String)
private lateinit var auth: FirebaseAuth
lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_dash_board)
var database = FirebaseDatabase.getInstance().reference
var patientDataItems = ArrayList<PatientDataItem>()
val patientRecycler = findViewById<RecyclerView>(R.id.patient_recycler)
val patienDashboardprogressBar = findViewById<ProgressBar>(R.id.patientDashboardprogressBar)
val noDataMain = findViewById<TextView>(R.id.no_data_main_dashboard)
swipeRefreshLayout = findViewById(R.id.swipe)
patientRecycler.layoutManager = LinearLayoutManager(this)
patientRecycler.adapter = MainDashboardAdapter(patientDataItems, this)
auth = FirebaseAuth.getInstance()
val user = auth.currentUser
val patientsListener = object : ValueEventListener {
override fun onDataChange(p0: DataSnapshot) {
val patients = p0.child("users").child(user!!.uid)
if (p0.value == null ){
noDataMain.visibility = View.VISIBLE
}else{
noDataMain.visibility = View.GONE
for (i in p0.children){
var patientName = i.key.toString()
var patientMessage = i.value.toString()
patientDataItems.add(PatientDataItem(patientName, patientMessage))
}
}
patientRecycler.scrollToPosition(patientDataItems.size-1)
patienDashboardprogressBar.visibility = View.GONE
}
override fun onCancelled(error: DatabaseError) {
println("error")
}
}
database.child("location").child("users").child(user!!.uid).addValueEventListener(patientsListener)
// database.child("location").addValueEventListener(postListener)
swipeRefreshLayout.setOnRefreshListener {
startActivity(intent);
Handler(Looper.getMainLooper()).postDelayed(Runnable {
swipeRefreshLayout.isRefreshing = false
}, 4000)
}
}
override fun onItemClick(patientDataItems: PatientDataItem) {
val patientMacAddressName = patientDataItems.patientName
val dashboardIntent = Intent(this, DashboardActivity::class.java)
dashboardIntent.putExtra("macAddressNamePatient", patientMacAddressName)
startActivity(dashboardIntent)
}
}
MainDashBoardAdapter.kt
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.example.ard_here.R
class MainDashboardAdapter(private val patientDataSet: ArrayList<MainDashBoard.PatientDataItem>,
private val onPatientClickListener: OnItemPatientClickListener): RecyclerView.Adapter<MainDashboardAdapter.PatientCustomHolder>(){
override fun getItemCount(): Int {
return patientDataSet.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatientCustomHolder {
var layoutInflater = LayoutInflater.from(parent?.context)
var cellForRow = layoutInflater.inflate(R.layout.main_patient_layout, parent, false)
return PatientCustomHolder(cellForRow)
}
override fun onBindViewHolder(holder: PatientCustomHolder, position: Int) {
holder.bindItems(patientDataSet[position])
holder.patientLayout.setOnClickListener {
onPatientClickListener.onItemClick(patientDataSet[position])
}
}
class PatientCustomHolder(v: View): RecyclerView.ViewHolder(v){
val patientLayout: ConstraintLayout = v.findViewById(R.id.patient_layout)
val patientName: TextView = v.findViewById(R.id.patient_name)
val patientMessage : TextView = v.findViewById(R.id.patient_message)
fun bindItems(data_item: MainDashBoard.PatientDataItem){
patientName.text = data_item.patientName
patientMessage.text = data_item.patientMessage
}
}
}
OnItemPatientClickListener.kt
interface OnItemPatientClickListener {
fun onItemClick(patientDataItems: MainDashBoard.PatientDataItem)
}
clear your data container then bind it again in recyclerview.
or you have mvvm pattern, you can use live data to observe data source and if there is any changes, your activity will easily notified and make some ui changes
Since you're reading the data with addValueEventListener, which means that:
The data from the path is read from the database right away, and passed to your onDataChange.
The client the continues monitor the path, and if anything changes it calls your onDataChange again with all data at the path.
In your onDataChange you're only ever adding data to patientDataItems. That works well the first time the data is loaded, so #1 above. But if you add or change a single child node (#2 above), you get called with all data at the path again. So that's when you end up duplicating the items in the view.
The simplest solution is to clear patientDataItems whenever onDataChange get called:
override fun onDataChange(p0: DataSnapshot) {
patientDataItems.clear()
...
This question already has answers here:
Possible to access AndroidViewModel of Activity via Fragment?
(2 answers)
Closed 2 years ago.
Cannot instantiate WordViewModel
Caused by: java.lang.InstantiationException: class com.example.roomwordsample.WordViewModel has no zero argument constructor
I am trying to display room data with recycler view but I cannot instantiate class via ViewModelProvider get method
WordViewModel.kt
package com.example.roomwordsample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
MainActivity.kt
package com.example.roomwordsample
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
class MainActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this#MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
I solved this problem by
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
changing the above code with this:
wordViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(WordViewModel::class.java)
try to add view model factory to your view model class.
package com.example.roomwordsample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
class Factory(
private val app: Application
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
return WordViewModel(app) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
Then in your MainActivity intialize your view model like this:
package com.example.roomwordsample
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
class MainActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private val wordViewModel by viewModels<WordViewModel> {
WordViewModel.Factory(application)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
//wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this#MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
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.
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.