How to pass value from activity to ViewModel? - android

How to pass value from Activity to View Model? I try to find anything on web but I failed. What I want is this: I have two recyclerviews in one activity. If user click on item A in recyclerview 1 I want to send ID of this item to View Model and return something by this ID. There is an error with dokladId parameter in testToShow variable.
What is the easy way to handle it?
This is my ViewModel:
#HiltViewModel
class SkladViewModel #Inject constructor(
repository: SybaseRepository
): ViewModel(){
val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
val testToShow = repository.getSelectedDokladyBySklad(dokladId).asLiveData()
}
This is the activity
#AndroidEntryPoint
class DokladActivity : AppCompatActivity(), SkladAdapter.OnItemClickListener, DokladAdapter.OnItemClickListener {
private val skladViewModel: SkladViewModel by viewModels()
//private val dokladViewModel: DokladViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDokladBinding.inflate(layoutInflater)
//setContentView(R.layout.activity_doklad)
setContentView(binding.root)
binding.btVybratDoklad.setOnClickListener{
openActivity(binding.root)
}
val skladAdapter = SkladAdapter (this)
val dokladAdapter = DokladAdapter(this)
binding.apply {
recyclerViewSklady.apply {
adapter = skladAdapter
layoutManager = LinearLayoutManager(this#DokladActivity)
}
skladViewModel.skladyPolozky.observe(this#DokladActivity) {
skladAdapter.submitList(it)
Log.d("Doklad", skladAdapter.currentList.toString())
}
recyclerViewDoklady.apply {
adapter = dokladAdapter
layoutManager = LinearLayoutManager(this#DokladActivity)
}
skladViewModel.dokladyPolozky.observe(this#DokladActivity){
dokladAdapter.submitList(it)
Log.d("Doklad", dokladAdapter.currentList.toString())
}
}
}
fun openActivity(view: View){
val intent = Intent(this,PolozkaActivity::class.java )
startActivity(intent)
}
override fun onItemClick(polozkaSklad: SkladTuple) {
val action = polozkaSklad.reg
}
override fun onItemClick(polozkaHlavicka: DokladTuple) {
val intent = Intent(this, PolozkaActivity::class.java)
intent.putExtra("doklad", polozkaHlavicka.doklad)
//intent.putExtra("polozkaHlavicka", polozkaHlavicka as Serializable)
startActivity(intent)
}
}
Repository with some function:
fun getSelectedDokladyBySklad(sklad: Int) : Flow<List<SkladDokladTuple>>{
return sybaseDao.getAllDokladFromPolozkaBySklad(sklad)
}
and DAO:
#Query("SELECT distinct doklad FROM cis06zebrap where sklad=:skladId")
fun getAllDokladFromPolozkaBySklad(skladId:Int?=null): Flow<List<SkladDokladTuple>>

#HiltViewModel
class SkladViewModel #Inject constructor(
repository: SybaseRepository
): ViewModel(){
val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
val testToShow = repository.getSelectedDokladyBySklad(dokladId).asLiveData()
fun someNameYouFindUseful(id: String) {
// do something with the id
...
// notify the UI
someLiveDataYouShoudlBeObservingFromTheUI.value = SomeSealedClassWrappingTheStates.SomeStateYouFindDescriptive
}
}
Then in your Activity/Fragment you'd do:
viewModel.someNameYouFindUseful("the Id")
Since you will likely have a viewModel reference there.
To complete the missing pieces, please take a look at the Google official documentation about ViewModels including how to expose a "state" and react to it.

Related

Why not auto update list when used LiveData in Android

In my application I want use Koin , Room and LiveData.
I write below codes, but after add new item into room not auto update recyclerview list!
Should close app and open again for show updated list !
Dao codes :
#Query("SELECT * FROM $NOTE_TABLE")
fun getAllNote(): MutableList<NoteModel>
Repository codes :
class RoomRepository(private val dao: NoteDao) {
suspend fun saveNote(note: NoteModel) = dao.saveNote(note)
fun getAllNotes() = dao.getAllNote()
}
ViewModel codes:
class RoomViewModel(private val repository: RoomRepository) : ViewModel() {
val notesList = MutableLiveData<MutableList<NoteModel>>()
fun saveNote(note:NoteModel) = viewModelScope.launch {
repository.saveNote(note)
}
fun loadAllNotes() = viewModelScope.launch {
val list = repository.getAllNotes()
if (list.isNotEmpty()) {
notesList.postValue(list)
}
}
}
Activity codes :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityKoinRoomBinding.inflate(layoutInflater)
setContentView(binding.root)
//InitViews
binding.apply {
//Save
btnSave.setOnClickListener {
val title = titleEdt.text.toString()
note.id = 0
note.title = title
viewModel.saveNote(note)
}
//Load notes
viewModel.loadAllNotes()
viewModel.notesList.observe(this#KoinRoomActivity) {
noteAdapter.differ.submitList(it)
notesList.apply {
layoutManager = LinearLayoutManager(this#KoinRoomActivity)
adapter = noteAdapter
}
}
}
How can I fix this issue?
In order to update data immediately, you need to return LiveData<T> from the database itself. In your case, do something like that:
#Query("SELECT * FROM $NOTE_TABLE")
fun getAllNote(): LiveData<List<NoteModel>>
I also changed MutableList to List, because there is no need for mutability here.
You ViewModel can then be like this:
class RoomViewModel(private val repository: RoomRepository) : ViewModel() {
val notesList = repository.getAllNotes()
...
}
and you can also remove function loadAllNotes() from it.

How to pass data from an AppCompatDialog to an AppCompatActivity

I don't know how to pass data from the dialog fragment to the Activity. I have an Activity, which creates the Dialog. From this Dialog I want to pass Data to another Activity. Anyone know I can do this?
this is my 1st Activity:
class EinkaufslisteActivity : AppCompatActivity() {
//override val kodein by kodein()
//private val factory : EinkaufsViewModelFactory by instance()
#SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_einkaufsliste)
val database = EinkaufDatenbank(this)
val repository = EinkaufsRepository(database)
val factory = EinkaufsViewModelFactory(repository)
val viewModel = ViewModelProviders.of(this, factory).get(EinkaufsViewModel::class.java)
val adapter = ProduktAdapter(listOf(), viewModel)
rvVorratsliste.layoutManager = LinearLayoutManager(this)
rvVorratsliste.adapter = adapter
viewModel.getAllProdukte().observe(this, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
adapter.setOnItemClickListener {
val produkt = it
Intent(this, VorratslisteActivity::class.java).also {
it.putExtra("EXTRA_PRODUKT", produkt)
}
EinkaufslisteProduktGekauftDialog(this, produkt, object : AddDialogListener{
override fun onAddButtonClicked(produkt: Produkt) {
}
override fun onAddButtonClickedVorrat(produktVorrat: ProduktVorrat) {
viewModel.delete(produkt)
}
}).show()
}
This is my Dialog:
ass EinkaufslisteProduktGekauftDialog (context: Context, var produkt : Produkt?, var addDialogListener: AddDialogListener?) : AppCompatDialog(context){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_einkaufsliste_produkt_gekauft)
tvProduktgekauftName.text = produkt?.name.toString()
etProduktGekauftAnzahl.hint = produkt?.anzahl.toString()
btnProduktGekauftOk.setOnClickListener {
val name = tvProduktgekauftName.text.toString()
val anzahl = etProduktGekauftPreis.text.toString()
val datum = etProduktGekauftDatum.text.toString()
val preis = etProduktGekauftPreis.text.toString()
if(name.isEmpty() || anzahl.isEmpty()){
Toast.makeText(context, "Bitte fülle alle Felder aus", Toast.LENGTH_SHORT).show()
return#setOnClickListener
}
val produktVorrat = ProduktVorrat(name, anzahl.toInt(), datum)
addDialogListener?.onAddButtonClickedVorrat(produktVorrat)
dismiss()
}
This is my 2nd Activity:
class VorratslisteActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_vorratsliste)
val database = EinkaufDatenbank(this)
val repository = VorratsRepository(database)
val factory = VorratViewModelFactory(repository)
val viewModel = ViewModelProviders.of(this, factory).get(VorratViewModel::class.java)
val adapter = ProduktVorratAdapter(listOf(), viewModel)
rvVorratsliste.layoutManager = LinearLayoutManager(this)
rvVorratsliste.adapter = adapter
viewModel.getAllProdukteVorratsliste().observe(this, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
val produkt = intent.getSerializableExtra("EXTRA_PRODUKT") as? ProduktVorrat
if(produkt != null) {
viewModel.upsertVorrat(produkt)
}
btnVorratNeuesProdukt.setOnClickListener {
VorratProduktHinzufuegenDialog(this,
object : AddDialogListener {
override fun onAddButtonClicked(produkt: Produkt) {
TODO("Not yet implemented")
}
override fun onAddButtonClickedVorrat(produktVorrat: ProduktVorrat) {
viewModel.upsertVorrat(produktVorrat)
}
}).show()
}
The "produkt" in activity 2 is null and i don't know why
Since you are already using a ViewModel in your code, add a LiveData variable in your view model and set that live data on the Dialog.
To get the value of the live data from another activity, ensure that you are using the same view model instance (using the activity view model factory). Then, you can access that view model (and live data) from that activity.
In this way, you have a single source of data that is shared between multiple ui components (activity, fragment, dialogs)
Check the official docs for Live Data here: https://developer.android.com/topic/libraries/architecture/livedata
ActivityA launches Dialog
Dialog passes result back to ActivityA
ActivityA launches ActivityB passing result from Dialog

How to pass argument to detail ViewModel via navArgs

I have a screen with a RecyclerView of podcasts, where when you click one, it takes you to a detail screen for that particular podcast. Using Kodein for ViewModel injection, how can I pass the id of the podcast that was clicked from the list fragment to the detail fragment's ViewModel so that it can be fetched from an API?
The detail ViewModel is structured like this:
class PodcastDetailViewModel internal constructor(
private val podcastRepository: PodcastRepository,
podcastId: String = ""
): ViewModel() {
// viewmodel stuff
}
The detail fragment looks like this:
class PodcastDetailFragment : ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val args: PodcastDetailFragmentArgs by navArgs()
private val viewModelFactory: PodcastDetailViewModelFactory by kodein.newInstance { PodcastDetailViewModelFactory(args.podcastId, instance()) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = viewModelFactory.create(PodcastDetailViewModel::class.java)
}
// other stuff
}
And this is how I'm navigating to the detail screen from the list:
private fun navigateToPodcastDetailFragment(podcastId: String) {
val args = Bundle()
args.putString("podcast_id", podcastId)
val directions =
TopPodcastsFragmentDirections.viewPodcastDetails(
podcastId
)
val extras = FragmentNavigatorExtras(
podcast_image to "podcastImage_$podcastId"
)
Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
.navigate(directions, extras)
}
This is how I'm binding it:
bind() from provider { PodcastDetailViewModelFactory(instance(), instance()) }
I'm not sure how to bind that string parameter in the ViewModelFactory constructor, or how else to pass the data there, so any help would be much appreciated.
You could use a factory for your bindings:
bind() from factory { podcastId: String ->
PodcastDetailViewModelFactory(podcastId, instance())
}
calling it on-site with:
private val viewModelFactory: PodcastDetailViewModelFactory by instance(arg = args.podcastId)
hope this helps.
I fixed this by switching to a factory binding, like romainbsl suggested, but the call in the fragment was a little different.
The binding became:
bind() from factory { podcastId: String ->
PodcastDetailViewModelFactory(podcastId, instance())
}
and the on-site call became:
private val viewModelFactory: PodcastDetailViewModelFactory by instance(arg = args.podcastId)
Try this
private val viewModel by viewModels<PodcastDetailViewModel> {
val viewModelFactory: PodcastDetailViewModelFactory by instance(arg = args.podcastId)
viewModelFactory
}
OR
private val viewModel by activityViewModels<PodcastDetailViewModel> {
val viewModelFactory: PodcastDetailViewModelFactory by instance(arg = args.podcastId)
viewModelFactory
}
Try this :
private val viewModelFactory: PodcastDetailViewModelFactory by instance {arg = args.podcastId}
to instantiate by Lazy because navArgs() is an implementation of Lazy used by android.app.Activity.navArgs and androidx.fragment.app.Fragment.navArgs.

Why do it still need launch notifyDataSetChanged() when I have used LiveData?

I'm learning Room with the sample project RoomWordsSample at https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin.
The following code are from the project.
In my mind, the LiveDate will update UI automatically when the data changed if it was observed.
But in the file WordListAdapter.kt, I find notifyDataSetChanged() is added to the function setWords(words: List<Word>), it's seems that it must notify UI manually when data changed.
Why do it still need launch notifyDataSetChanged() when I have used LiveData ?
MainActivity.kt
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 ->
words?.let { adapter.setWords(it) }
})
}
}
WordViewModel.kt
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
WordListAdapter.kt
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Actually, livedata will give you updated data in your activity. But now, it is your activity's job to update the ui. So, whenever live data gives you updated data, you will have to tell the ui to update the data. Hence, notifyDataSetChanged().
notifyDataSetChanged has nothing to do with LiveData, it's part of RecyclerView api.
LiveData - is way of receiving data in lifecycle-aware way, RecyclerView simply displays views.

MVVM LiveData slow on first time in bottomSheetFragment

I don't understand why my MVVM with LiveData takes a lot of time to respond within a BottomSheetFragment, but just the first time I open the fragment.
Here is my code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("HERE", "onViewCreated")
abilityViewModel = ViewModelProvider(this)[AbilityViewModel::class.java]
setupViewModel()
Log.d("HERE", "viewModelCreated")
abilityViewModel.getAbilityByName(abilityName)
private fun setupViewModel() {
abilityViewModel.abilityName.observe(viewLifecycleOwner, Observer { ability ->
Log.d("HERE", ability.name)
fillInfo(ability)
})
}
Here is the ViewModel:
class AbilityViewModel(application: Application) : AndroidViewModel(application) {
private val repository: AbilityRepository
private val _abilityName: MutableLiveData<String> = MutableLiveData()
val abilityName: LiveData<Ability>
fun getAbilityByName(name: String) {
_abilityName.value = name
}
init {
val abilityDao: AbilityDao = MainDatabase(application, viewModelScope).abilityDao()
repository = AbilityRepository(abilityDao)
abilityName = Transformations.switchMap(_abilityName){abilityName ->
repository.getAbilityByName(abilityName)
}
}
}
This is the log for the first time I open the fragment:
After the first time, both the same fragment and different fragments of the same type take zero delay in loading:
Help would be greatly appreciated, thank you
EDIT
In order to get the time in places suggested by #commonsware I did this:
class AbilityViewModel(application: Application) : AndroidViewModel(application) {
private val repository: AbilityRepository
private val _abilityName: MutableLiveData<String> = MutableLiveData()
val abilityName: LiveData<Ability>
fun getAbilityByName(name: String) {
Log.d("HERE", "getAbilityByName called")
_abilityName.value = name
Log.d("HERE", "getAbilityByName value assigned")
}
init {
val abilityDao: AbilityDao = MainDatabase(application, viewModelScope).abilityDao()
repository = AbilityRepository(abilityDao)
abilityName = Transformations.switchMap(_abilityName){abilityName ->
Log.d("HERE", "switchMap called")
repository.getAbilityByName(abilityName)
}
}
}
However the problem does not seem to be in any of those two methods as you can see here:
In addition the database is initialised when the app starts, which is before this point
EDIT 2
Here is the repository modified to accomodate the log:
class AbilityRepository(private val abilityDao: AbilityDao) {
//fun getAbilityByName(name: String) = abilityDao.getAbilityByName(name)
fun getAbilityByName(name: String): LiveData<Ability> {
Log.d("HERE", "getAbilityByName called from repo")
return abilityDao.getAbilityByName(name)
}
}
However the time seems to be fine here as well

Categories

Resources