How to get response variable from viewModelScope Android - android

in my Android project i need to use one of my viewmodel's response in another fragment. But whenever i try to get that value it is always null. I tried to get it from its own fragment with livedata and it works!. But it is not same for another fragment. Here my viewmodel code that has the response ;
package com.tolgahantutar.bexworkfloww.ui.auth
import android.content.Intent
import android.view.View
import android.widget.Toast
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tolgahantutar.bexworkfloww.data.network.repositories.AuthorizeSessionRepository
import com.tolgahantutar.bexworkfloww.data.network.repositories.GetDomainRepository
import com.tolgahantutar.bexworkfloww.data.network.repositories.GetUserRepository
import com.tolgahantutar.bexworkfloww.data.network.responses.GetUserResponse
import com.tolgahantutar.bexworkfloww.ui.home.HomeActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AuthViewModel #ViewModelInject constructor (
private val authorizeSessionRepository: AuthorizeSessionRepository,
private val getDomainRepository: GetDomainRepository,
private val getUserRepository: GetUserRepository
):ViewModel() {
var userName :String?=null
var password: String ? = null
val isLoading = MutableLiveData<Boolean>()
private val location = "bexfatest.saasteknoloji.com"
val isSuccessfull = MutableLiveData<Boolean>()
var getUserResponseMutable = MutableLiveData<GetUserResponse>()
fun onClickUserLogin(view: View){
val sessionID = 0
val authorityID = 0
val loginType = "System"
viewModelScope.launch {
if(!(userName==null||password==null)){
isLoading.value = true
val authResponse = userLogin(sessionID,authorityID,userName!!,password!!,loginType)
if(authResponse.Result){
isLoading.value=false
val domainResponse=getDomain(location)
**`val getUserResponse`** = getUser(authResponse.authorizeSessionModel!!.ID,"Bearer "+domainResponse.getDomainModel.ApiKey)
if (getUserResponse.result){
isSuccessfull.value=true
getUserResponseMutable.value=getUserResponse
}
//Toast.makeText(view.context, "Login Successfull", Toast.LENGTH_LONG).show()
val intent = Intent(view.context,HomeActivity::class.java)
view.context.startActivity(intent)
}else{
isLoading.value=false
Toast.makeText(view.context, "Login Failed!!", Toast.LENGTH_LONG).show()
}
}
else{
Toast.makeText(view.context, "Kullanıcı adı ve şifre boş bırakılamaz!!", Toast.LENGTH_SHORT).show()
}
}
}
suspend fun userLogin(
SessionID : Int,
AuthorityID: Int,
UserName: String,
Password : String,
LoginType: String
)= withContext(Dispatchers.IO){authorizeSessionRepository.userLogin(SessionID, AuthorityID, UserName, Password, LoginType)}
suspend fun getUser(
id: Int,
authorization : String
)= withContext(Dispatchers.Main){getUserRepository.getUser(id,authorization)}
suspend fun getDomain(
Location: String
)= withContext(Dispatchers.IO){getDomainRepository.getDomain(Location)}
}
i need to get getUserResponse variable in my AddressBook Fragment like this ;
package com.tolgahantutar.bexworkfloww.ui.addressbook
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.tolgahantutar.bexworkfloww.R
import com.tolgahantutar.bexworkfloww.ui.auth.AuthViewModel
import dagger.hilt.android.AndroidEntryPoint
#AndroidEntryPoint
class AdressBookFragment : Fragment() {
private val addressBookViewModel : AdressBookViewModel by viewModels()
private val authViewModel : AuthViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.adress_book_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//addressBookViewModel.getContact(2,authViewModel.userResponseDelegate!!.getUserValue.apiKey)
authViewModel.getUserResponseMutable.observe(viewLifecycleOwner, Observer {
if (it.result){
Toast.makeText(requireContext(), "asdadasd", Toast.LENGTH_SHORT).show()
}
})
addressBookViewModel.isSuccessfull.observe(viewLifecycleOwner, Observer {
if (it){
Toast.makeText(requireContext(), "ContactList Get Successfully", Toast.LENGTH_SHORT).show()
}
})
}
}
but observe is always null how can i get that getUserResponse inside my AddressBookFragment ??

The AuthViewModel is most likely destroyed when you leave your AuthFragment (which I assume you have), so your AdressBookFragment is getting a new instance of the ViewModel, which doesn't retain any data from the previous screen.
I'd recommend that you store the result from AuthViewModel into a Repository or some other global state object and retrieve it from there.
ViewModels hold temporary data needed for a screen, but whether the user is authenticated matters to the whole application, not just a single screen. As such it should be stored someplace that lives for as long as the whole app, and is reachable from anywhere, like a Repository.

private val authViewModel : AuthViewModel by viewModels()
is equivalent to
private val viewModel by lazy {
ViewModelProvider(this).get(AuthViewModel::class.java)
}
So as you can see this is passed as viewModelStoreOwner.
Since i guess that you do net use AdressBookFragment as viewModelStoreOwner in your other fragment you are creating a new viewmodel in this fragment.
You probably need a shared viewmodel which you get when you use
private val viewModel by lazy {
ViewModelProvider(requireActivity()).get(AuthViewModel::class.java)
}
in both fragments

Related

Why when update data in firebase the data in recyclerView duplicated

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()
...

How can you add multiple users in a Kotlin Login?

I'm trying to figure out how to add more users in a Login app made with Kotlin, there's no database or whatever, the accounts are hardcoded into the program, I heard about using arrays but I'm not too sure on how to implement it in this context.
Thank you to anyone who reads this.
package com.example.textandviewbinding
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import com.example.textandviewbinding.databinding.ActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import java.util.*
import kotlin.system.exitProcess
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.btnSubmit.setOnClickListener {
validateUser(it)
}
}
private fun addTextView(text:String){
val textView1 = TextView(this)
textView1.text = text
textView1.textSize = 16f
textView1.textAlignment = View.TEXT_ALIGNMENT_CENTER
binding.myLayout.addView(textView1)
}
private fun validateUser(it: View) {
val username = binding.editUsername.text
val password = binding.editPassword.text
if (username.toString().equals("joed", ignoreCase = true) && password.toString().equals("1234")) {
// Toast.makeText(this, "Logged In!", Toast.LENGTH_SHORT).show()
val message = getString(R.string.welcome_message,username)
Snackbar.make(it, message, Snackbar.LENGTH_LONG)
.setAction("Show details.. ", { addTextView("Login Successful: ${Calendar.getInstance().time}" ) })
.show()
} else {
Toast.makeText(this, "Invalid Details", Toast.LENGTH_SHORT).show()
exitProcess(-1)
}
}
private fun displayToast() {
Toast.makeText(this, "Login Successful ${Calendar.getInstance().time}", Toast.LENGTH_SHORT).show()
}
}
1- Create a class called User , ex:
data class User(
var id : Int
var name : String
)
2- Create an Array of users using the User model in your MainActivity :
private val users = ArrayList<User>()
3- Add users to the array :
users.add(User(1,"Alex"))
users.add(User(2,"Andrei"))

Caused by: java.lang.InstantiationException: class WordViewModel has no zero argument constructor [duplicate]

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()
}
}
}

UI is not updated when DB changes

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.

onClick not working in MVVM with DataBinding

For some reason, onClick isn't being registered with my adapter. I'm using the MVVM pattern and I've made sure that all the pieces are tied together but for the life of me I can't figure out why this won't work.
StoreFragment
package com.example.brandroidtest.main
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.fragment.findNavController
import com.example.brandroidtest.databinding.FragmentStoreBinding
class StoreFragment : Fragment() {
//Will Create a ViewModelProivders object of class DetailViewModel the first time viewModel is used
//Allows us to move this code from on create to the declaration
private val viewModel: StoreViewModel by lazy {
val factory = StoreViewModelFactory(requireNotNull(activity).application)
ViewModelProviders.of(this, factory).get(StoreViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.i("onCreateView", "StoreFragment created")
val binding = FragmentStoreBinding.inflate(inflater)
binding.setLifecycleOwner(this)
binding.viewModel = viewModel
binding.storeList.adapter = StoreAdapter(StoreAdapter.OnClickListener {
viewModel.displayStoreDetails(it)
Log.i("inside OnClickListener", "after displayDetails")
})
Log.i("between adapter.onclick", "and viewModel observe")
viewModel.selectedStore.observe(this, Observer {
Log.i("observe", "inside the selectedStore observe method")
if (null != it) {
this.findNavController().navigate(
StoreFragmentDirections.actionMainListFragmentToDetailFragment(
it
)
)
viewModel.displayStoreDetailsComplete()
}
})
return binding.root
}
}
StoreViewModel
package com.example.brandroidtest.main
import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.brandroidtest.model.Store
import com.example.brandroidtest.network.StoreAPI
import kotlinx.coroutines.*
enum class StoreAPIStatus {LOADING, DONE, NO_CONNECTION}
class StoreViewModel(application: Application) : AndroidViewModel(application) {
// Response from server: Either Store Data or Failure Message
private val _status = MutableLiveData<StoreAPIStatus>()
// for status of get request
//displayed when there is no internet connection or if the connection is unstable and the data is being loaded
val status: LiveData<StoreAPIStatus>
get() = _status
//internal variable accessed within this file
private val listOfStores = MutableLiveData<List<Store>>()
//external variable for anywhere else
val stores: LiveData<List<Store>>
get() = listOfStores
private val _selectedStore = MutableLiveData<Store>()
val selectedStore: LiveData<Store>
get() = _selectedStore
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
/**
* Call getStoreData() in init so we can display the result immediately.
*/
init {
Log.i("viewModel init", "inside StoreViewModel init block")
if (isNetworkConnected(application.applicationContext))
getStoreData()
else
// Log.i("Bypassed network call", "")
listOfStores.value = emptyList()
_status.value = StoreAPIStatus.NO_CONNECTION
}
/**
* Sets the value of the status LiveData to the Store API data.
*/
private fun getStoreData() {
Log.i("getStoreData()", " inside getStoreData")
coroutineScope.launch {
try {
Log.i("getStoreData()", "Inside the coroutine before getData")
_status.value = StoreAPIStatus.LOADING
var storeData = async { StoreAPI.retrofitService.getData().stores }.await()
Log.i("getStoreData()", "Inside the coroutine after getData")
_status.value = StoreAPIStatus.DONE
listOfStores.value = storeData
} catch (e: Exception) {
_status.value = StoreAPIStatus.NO_CONNECTION
listOfStores.value = ArrayList()
e.printStackTrace()
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
private fun isNetworkConnected(context: Context): Boolean {
val cm =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
return cm!!.activeNetworkInfo != null && cm.activeNetworkInfo.isConnected
}
//will be called to set the store as the one that was clicked
fun displayStoreDetails(store : Store){
Log.i("displayStoreDetails", "inside this method")
_selectedStore.value = store
}
//sets the selected store's value to null so that live data can be updated when we select a new store and not show us the detail apge of the same store
fun displayStoreDetailsComplete() {
Log.i("displayStoreDetailsComplete", "inside this method")
_selectedStore.value = null
}
}
StoreAdapter
package com.example.brandroidtest.main
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.brandroidtest.model.Store
import com.example.brandroidtest.databinding.ListItemBinding
class StoreAdapter(val onClickListener: OnClickListener) :
ListAdapter<Store, StoreAdapter.StoreViewHolder>(DiffCallback) {
class StoreViewHolder(private var binding: ListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(store: Store) {
binding.store = store
Log.i("Adapter bind", store.storeLogoURL)
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Store>() {
override fun areItemsTheSame(oldItem: Store, newItem: Store): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Store, newItem: Store): Boolean {
return oldItem.storeID == newItem.storeID
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): StoreViewHolder {
return StoreViewHolder(ListItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: StoreViewHolder, position: Int) {
val store = getItem(position)
Log.i("inside onBindViewHolder", "")
holder.itemView.setOnClickListener {
Log.i("inside onBindViewHolder", "setOnClickListener")
onClickListener.onClick(store)
}
holder.bind(store)
}
class OnClickListener(val clickListener: (store: Store) -> Unit) {
fun onClick(store: Store) {
Log.i("inside onClick", "click is being registered ${store.city}")
return clickListener(store)
}
}
}
StoreDetailFragment
package com.example.brandroidtest.detailed
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.example.brandroidtest.R
import com.example.brandroidtest.databinding.FragmentStoreDetailBinding
/**
* A simple [Fragment] subclass.
*/
class StoreDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val application = requireNotNull(activity).application
val binding = FragmentStoreDetailBinding.inflate(inflater)
binding.setLifecycleOwner(this)
val store = StoreDetailFragmentArgs.fromBundle(arguments!!).selectedStore
val viewModelFactory = StoreDetailViewModelFactory(store, application)
binding.viewModel = ViewModelProviders.of(this, viewModelFactory).get(StoreDetailViewModel::class.java)
return binding.root
}
}
StoreDetailViewModel
package com.example.brandroidtest.detailed
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.brandroidtest.model.Store
class StoreDetailViewModel(store: Store, application: Application) : AndroidViewModel(application) {
private val _selectedStore = MutableLiveData<Store>()
val selectedStore : LiveData<Store>
get() = _selectedStore
init {
_selectedStore.value = store
}
}
I have no idea why onClick won't work and the Detail Fragment won't show because of it
Here is the project link: https://drive.google.com/open?id=1m8R8HvCt4m0KIp_IwdeO1YdB5yY8A8LK
The issue come from your adapter item layout.
The height of every item show be wrap_content. But you are using a ScrollView as the root view of your item view.
Remove the useless ScrollView and also the next LinearLayout. You layout should become like this:
<LinearLayout
...
android:padding="16dp"/>
<ImageView
android:id="#+id/store_logo"
.../>
<LinearLayout
android:id="#+id/store_detail"
...>
</LinearLayout>

Categories

Resources