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.
Related
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.
I'm trying to show a Snackbar in MainFragment when I click a button inside DialogFragment.
When I call the send event function (in ViewModel) from the dialog class nothing happens. I don't know what I'm doing wrong but other functions which do similar things in the MainFragment work just fine.
In ViewModel, the clearInput() function which clears the editText in MainFragment works but onEditNoteClicked() doesn't work and nothing happens when I call it.
NoteOptionsDialog class:
#AndroidEntryPoint
class NoteOptionsDialog : BottomSheetDialogFragment() {
private val viewModel: NotesViewModel by viewModels()
private fun setupClickListeners(view: View) {
view.bottom_options_edit.setOnClickListener {
viewModel.onEditNoteClicked()
dismiss()
}
}
NotesViewModel class:
#HiltViewModel
class NotesViewModel #Inject constructor(
private val noteDao: NoteDao,
private val preferencesManager: PreferencesManager,
private val state: SavedStateHandle
) : ViewModel() {
private val notesEventChannel = Channel<NotesEvent>()
val notesEvent = notesEventChannel.receiveAsFlow()
fun onSaveNoteClick() {
val newNote = Note(noteText = noteText, noteLabelId = labelId.value)
createNote(newNote)
}
private fun createNote(newNote: Note) = viewModelScope.launch {
noteDao.insertNote(newNote)
clearInput()
}
private fun clearInput() = viewModelScope.launch {
notesEventChannel.send(NotesEvent.ClearEditText)
}
fun onEditNoteClicked() = viewModelScope.launch {
notesEventChannel.send(NotesEvent.ShowToast)
}
fun onNoteSelected(note: Note, view: View) = viewModelScope.launch {
notesEventChannel.send(NotesEvent.ShowBottomSheetDialog(note, view))
}
sealed class NotesEvent {
object ClearEditText : NotesEvent()
object ShowToast : NotesEvent()
data class ShowBottomSheetDialog(val note: Note, val view: View) : NotesEvent()
}
NotesFragment class:
#AndroidEntryPoint
class NotesFragment : Fragment(R.layout.fragment_home), NotesAdapter.OnNoteItemClickListener,
LabelsAdapter.OnLabelItemClickListener {
private val viewModel: NotesViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?){
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.notesEvent.collect { event ->
when (event) {
is NotesViewModel.NotesEvent.ShowBottomSheetDialog -> {
NoteOptionsDialog().show(childFragmentManager, null)
}
is NotesViewModel.NotesEvent.ClearEditText -> {
et_home_note.text.clear()
}
is NotesViewModel.NotesEvent.ShowToast -> {
Snackbar.make(requireView(), "Snack!", Snackbar.LENGTH_LONG).show()
}
}
Found the solution. For fragments, I should have used by activityViewModels() instead of by viewModels()
private val viewModel: NotesViewModel by activityViewModels()
initialize viewmodel like below in fragments for sharing same instance
private val viewModel: NotesViewModel by activityViewModels()
I use Navigation Component and load data in a recyclerview (MVVM architecture so this fragment has a viewmodel) if I move to another fragment and then I navigate back to the fragment which has a viewmodel then the viewmodel livedata is empty but I don't know why.
If I know correctly I don't need to manually call savedStateHandle.set to persist the data because savedStateHandle does it anyway.
I store Place objects in the livedata and this class is extends Parcelable.
ViewModel:
class OverviewViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
companion object {
private const val SAVED_All_PLACES_LIVEDATA_KEY = "savedAllPlacesLiveDataKey"
}
private val repository: PlacesRepository = PlacesRepository(this)
private var allPlacesList: MutableList<Place> = mutableListOf()
var allPlacesLiveData: MutableLiveData<MutableList<Place>> = savedStateHandle.getLiveData<MutableList<Place>>(SAVED_All_PLACES_LIVEDATA_KEY)
fun getAllPlacesFromRepository(...)
repository.getAllPlaces(...) //when successfully gets the data then call the viewmodel's addAllPlacesLiveData function
}
fun addAllPlacesLiveData(sites: List<Site>) {
allPlacesList.clear()
val places = mutableListOf<Place>()
for(i in 0..9) {
val site = sites[i]
val distance = site.distance / 1000
val place = Place(site.name, site.formatAddress, distance)
places.add(place)
}
places.sortBy {
it.distance
}
allPlacesList.addAll(places)
allPlacesLiveData.value = allPlacesList
}
}
Place:
#Parcelize
data class Place(
var name: String = "",
var address: String = "",
var distance: Double = 0.0
) : Parcelable
I subscribe for the changes of the livedata and then call getAll:
overviewViewModel.allPlacesLiveData.observe(viewLifecycleOwner) { places ->
recyclerViewAdapter.submitList(places.toMutableList()) }
overviewViewModel.getAllPlacesFromRepository(...)
Could you show how you're initializing your VM inside your Fragment?
Make sure you send over the savedState like this:
private val viewModel: YourViewModel by viewModels {
SavedStateViewModelFactory(requireActivity().application, this)
}
TLDR: How could I properly implement an MVVM architecture with LiveData?
I have a fragment class that observe a viewModel exposed livedata:
viewModel.loginResultLiveData.observe
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private val viewModel by fragmentScopedViewModel { injector.loginViewModel }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLoginBinding.inflate(inflater, container, false)
val username = binding.loginInputField.toString()
val password = binding.passwordInputField.toString()
binding.loginSignInButton.setOnClickListener { viewModel.login(
username,
password
) }
viewModel.loginResultLiveData.observe(viewLifecycleOwner){
when(it){
is LoginResult.Success -> doSmth()
}
}
return binding.root
}
}
View model class simply ask for a mapped livedata object.
class LoginViewModel #Inject internal constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
lateinit var loginResultLiveData: MutableLiveData<LoginResult>
fun login(username: String, password: String) {
loginResultLiveData = loginUseCase.login(username, password)
}
}
Model uses use case, to map a result from the original format and also would map errors:
class LoginUseCase #Inject internal constructor(
private val authRepository: AuthEmailRepository
) {
var loginResultLiveData = MutableLiveData<LoginResult>()
fun login(userName: String, password: String): MutableLiveData<LoginResult> {
authRepository.login(userName, password)
.addOnCompleteListener {
if (it.isSuccessful) {
loginResultLiveData.postValue(LoginResult.Success)
} else {
loginResultLiveData.postValue(LoginResult.Fail(it.exception.toString()))
}
}
return loginResultLiveData
}
}
The problem is that only after loginSignInButtonis clicked, the model creates a liveData object. But I'm starting to observe this object immediately after onClickListener is set. Also each time a button is clicked, this would create a new instance of viewModel.loginResultLiveData, which doesn’t make sense.
binding.loginSignInButton.setOnClickListener { viewModel.login(
username,
password
) }
viewModel.loginResultLiveData.observe(viewLifecycleOwner){
when(it){
is LoginResult.Success -> doSmth()
}
}
How could I properly implement MVVM architecture with LiveData in this case?
I could also move logic I now have in LoginUseCaseto ModelView and then have something like this, which avoids the problem described before. But then I cannot delegate mapping/error handling to use case.
class LoginViewModel #Inject internal constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
val loginResult: MutableLiveData<LoginResult> = MutableLiveData()
fun login(username: String, password: String) = loginUseCase.login(username, password)
.addOnCompleteListener {
if (it.isSuccessful) {
loginResult.postValue(LoginResult.Success)
} else {
loginResult.postValue(LoginResult.Fail(it.exception.toString()))
}
}
}
You are trying to observe a mutable LiveData that is only initialized after the onClickListener so you won't get it to work, also you have a lateinit property that is only initialized if you call the login method which will throw an exception.
To solve your problem you can have a MediatorLiveData that will observe your other live data and pass the result back to your fragment observer.
You can try the following:
class LoginViewModel #Inject internal constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
private var _loginResultLiveData = MediatorLiveData<LoginResult>()
val loginResultLiveData: LiveData<LoginResult> = _loginResultLiveData
fun login(username: String, password: String) {
val loginUseCaseLiveData = loginUseCase.login(username, password)
_loginResultLiveData.addSource(loginUseCaseLiveData) {
_loginResultLiveData.value = it
}
}
}
In this Factory I need to fetch my data from an api using Retrofit and store the cache with room, my Repository rules this app!
I have repository suspended functions that take care of getting my data and some that save/update data getting and saveing/updating require different values to function and I do not know (yet) how to configure it in Kodein
I lack the experience to solve this and there is nothing I found in Stackoverflow to assist me.
I have tried to add both the variables ID:String and the edited entity (CampaignEntry) to the Definition, it complies but crash on running with
No binding found for bind<CampaignEditViewModelFactory>() with ? { String -> ? }
My main Application the bind() is crashing the Application
class MarketingApplication : Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#MarketingApplication))
...
bind() from factory { id: String, campaignEntry: CampaignEntry ->
CampaignEditViewModelFactory(id, campaignEntry, instance()) }
...
My ViewModel - having to pass the variables id and campaignEntry that is consumed by different calls in one ViewModel might be the issue - but I cannot figure out the correct solution.
class CampaignEditViewModel(
private val id: String,
private val campaignEntry: CampaignEntry,
private val marketingRepository: MarketingRepository
) : ViewModel() {
val campaignToSave by lazyDeferred { marketingRepository.updateCampaign(campaignEntry) }
val campaignToEdit by lazyDeferred { marketingRepository.getCampaignById(id) }
}
my lazyDeferred for clarity
fun <T> lazyDeferred(block: suspend CoroutineScope.() -> T): Lazy<Deferred<T>> {
return lazy {
GlobalScope.async(start = CoroutineStart.LAZY) {
block.invoke(this)
}
}
}
The Repository snap
interface MarketingRepository {
...
suspend fun getCampaignById(campaignId: String): LiveData<CampaignEntry>
suspend fun updateCampaign(campaignEntry: CampaignEntry): LiveData<CampaignEntry>
...
I call the Viewmodel from my fragment like so
class CampaignEditFragment : ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val viewModelFactoryInstanceFactory: ((String) -> CampaignEditViewModelFactory) by factory()
...
private fun bindUI() = launch {
val campaignVM = campaignEditViewModel.campaignToEdit.await()
...
btn_edit_save.setOnClickListener {it: View
saveCampaign(it)
...
private fun saveCampaign(it: View) = launch {
campaignEditViewModel.campaignToSave.await()
}
And then lastly the ScopedFragment
abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
If you need any more code - please ask
Since you are binding with 2 arguments, you need to use factory2:
private val viewModelFactoryInstanceFactory: ((String, campaignEntry) -> CampaignEditViewModelFactory) by factory2()