Fragment doesn't receive channel event sent from ViewModel - android

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

Related

Observing data from Base ViewModel in extended Fragment is not working

I have a BaseFragment and BaseViewModel. I created HomeFragment that extended from BaseFragment. Now I observe a LiveData of BaseViewModel but observing data is not working.
// BaseFragment
abstract class BaseFragment<VDB: ViewDataBinding, VM: BaseViewModel>: Fragment() {
lateinit var mBinding: VDB
lateinit var mViewModel: VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val parameterizedType = javaClass.genericSuperclass as? ParameterizedType
#Suppress("UNCHECKED_CAST")
val vmClass = parameterizedType?.actualTypeArguments?.getOrNull(1) as? Class<VM>?
if (vmClass != null) mViewModel = ViewModelProvider(this)[vmClass]
}
......
}
//BaseViewModel
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
val isMeet = MutableLiveData(true)
//.... code to change isMeet ....
//HomeFragment
class MainFragment: BaseFragment<FragmentMainBinding, MainViewModel>() {
override fun onResume() {
super.onResume()
mViewModel.isMeet.observe(viewLifecycleOwner) {
Log.d(TAG, "setupViews: a $it") // there isn't any happened
}
Log.d(TAG, "onResume: ")
}
Could you please try this?
val isMeet = MutableLiveData<Boolean>().apply { value = true }

java.lang.RuntimeException: Cannot create an instance of class com.example.mvvmapp.NoteViewModel in koltlin

Error: can't create an instance of the view model class
here is how I'm trying to create it
class MainActivity : AppCompatActivity() {
lateinit var noteViewModel: NoteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noteViewModel = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(this.application)
).get(
NoteViewModel::class.java
)
noteViewModel.getAllNotes().observe(this, object : Observer<List<Note>> {
override fun onChanged(t: List<Note>?) {
Toast.makeText(this#MainActivity, t.toString(), Toast.LENGTH_LONG).show()
}
})
}
}
And here is my view model class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository = NoteRepository(application)
private val allNotes: LiveData<List<Note>> = repository.getAllNotes()
fun insert(note: Note) {
repository.insert(note)
}
fun delete(note: Note) {
repository.delete(note)
}
fun update(note: Note) {
repository.update(note)
}
fun deleteAll() {
repository.deleteAllNotes()
}
fun getAllNotes(): LiveData<List<Note>> = allNotes
}
everything looks fine, I don't know what's causing the error
You can use the kotlin property delegate "viewModels()" to instantiate your viewmodel
class MainActivity : AppCompatActivity() {
var noteViewModel: NoteViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noteViewModel.getAllNotes().observe(this, object : Observer<List<Note>> {
override fun onChanged(t: List<Note>?) {
Toast.makeText(this#MainActivity, t.toString(), Toast.LENGTH_LONG).show()
}
})
}
}
Try with the below dependency then you will be able to get kotlin property delegate "viewModels()" in your activity.
dependencies {
val lifecycle_version = "2.3.1"
val arch_version = "2.1.0"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// Lifecycles only (without ViewModel or LiveData)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")
// Saved state module for ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")
implementation "androidx.activity:activity-ktx:1.3.0-beta01"
}
//write below code in your activity for instantiate viewModel
private val noteViewModel: NoteViewModel by viewModels()
//For more detail go through below link
https://developer.android.com/jetpack/androidx/releases/lifecycle#declaring_dependencies
I had the same problem(in java). This solved it:
// old code
mWordViewModel = new ViewModelProvider(this).get(WordViewModel.class);
// new code
mWordViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(WordViewModel.class);
if anyone is having this problem one solution to fix this is just make ViewModel constructor public (Class that extends AndroidViewModel)

Is there a way to achieve this rx flow in Kotlin with coroutines/Flow/Channels?

I am trying out Kotlin Coroutines and Flow for the first time and I am trying to reproduce a certain flow I use on Android with RxJava with an MVI-ish approach, but I am having difficulties getting it right and I am essentially stuck at this point.
The RxJava app looks essentially like this:
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.None
)
sealed class RenderEvent {
object None : RenderEvent()
class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
MainActivity has an instance of a PublishSubject with a Event type. Ie MainActivityView.Event.OnViewInitialised, MainActivityView.Event.OnError etc. The initial Event is sent in onCreate() via the subjects's .onNext(Event) call.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedSubject: PublishSubject<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
Observable.mergeArray(
onViewInitialisedSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
).observeOn(
Schedulers.io()
).compose(
viewModel()
).observeOn(
AndroidSchedulers.mainThread()
).subscribe(
::render
).addTo(
subscriptions
)
onViewInitialisedSubject
.onNext(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
MainActivityViewModel.kt
These Event's are then picked up by a MainActivityViewModel class which is invoked by .compose(viewModel()) which then transform the received Event into a sort of a new State via ObservableTransformer<Event, State>. The viewmodel returns a new state with a renderEvent in it, which can then be acted upon in the MainActivity again via render(state: MainActivityView.State)function.
#MainActivityScope
class MainActivityViewModel #Inject constructor(
private var state: MainActivityView.State
) {
operator fun invoke(): ObservableTransformer<MainActivityView.Event, MainActivityView.State> = onEvent
private val onEvent = ObservableTransformer<MainActivityView.Event,
MainActivityView.State> { upstream: Observable<MainActivityView.Event> ->
upstream.publish { shared: Observable<MainActivityView.Event> ->
Observable.mergeArray(
shared.ofType(MainActivityView.Event.OnViewInitialised::class.java)
).compose(
eventToViewState
)
}
}
private val eventToViewState = ObservableTransformer<MainActivityView.Event, MainActivityView.State> { upstream ->
upstream.flatMap { event ->
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
}
}
}
private fun onViewInitialisedEvent(): Observable<MainActivityView.State> {
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
return state.asObservable()
}
}
Could I achieve sort of the same flow with coroutines/Flow/Channels? Possibly a bit simplified even?
EDIT:
I have since found a solution that works for me, I haven't found any issues thus far. However this solution uses ConflatedBroadcastChannel<T> which eventually will be deprecated, it will likely be possible to replace it with (at the time of writing) not yet released SharedFlow api (more on that here.
The way it works is that the Activity and viewmodel shares
a ConflatedBroadcastChannel<MainActivity.Event> which is used to send or offer events from the Activity (or an adapter). The viewmodel reduce the event to a new State which is then emitted. The Activity is collecting on the Flow<State> returned by viewModel.invoke(), and ultimately renders the emitted State.
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
data class OnButtonClicked(val idOfItemClicked: Int) : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.Idle
)
sealed class RenderEvent {
object Idle : RenderEvent()
data class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>
private var isInitialised: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
}
private fun init() {
if (!isInitialised) {
lifecycleScope.launch {
viewModel()
.flowOn(
Dispatchers.IO
).collect(::render)
}
eventChannel
.offer(
MainActivityView.Event.OnViewInitialised
)
isInitialised = true
}
}
private suspend fun render(state: MainActivityView.State): Unit =
when (state.renderEvent) {
MainActivityView.RenderEvent.Idle -> Unit
is MainActivityView.RenderEvent.DisplayText ->
renderDisplayText(text = state.renderEvent.text)
}
private val renderDisplayText(text: String) {
// render text
}
}
MainActivityViewModel.kt
class MainActivityViewModel constructor(
private var state: MainActivityView.State = MainActivityView.State(),
private val eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>,
) {
suspend fun invoke(): Flow<MainActivityView.State> =
eventChannel
.asFlow()
.flatMapLatest { event: MainActivityView.Event ->
reduce(event)
}
private fun reduce(event: MainActivityView.Event): Flow<MainActivityView.State> =
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
MainActivityView.Event.OnButtonClicked -> onButtonClickedEvent(event.idOfItemClicked)
}
private fun onViewInitialisedEvent(): Flow<MainActivityView.State> = flow
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
emit(state)
}
private fun onButtonClickedEvent(idOfItemClicked: Int): Flow<MainActivityView.State> = flow
// do something to handle click
println("item clicked: $idOfItemClicked")
emit(state)
}
}
Similiar questions:
publishsubject-with-kotlin-coroutines-flow
Your MainActivity can look something like this.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedChannel: BroadcastChannel<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
onViewInitialisedChannel.asFlow()
.buffer()
.flowOn(Dispatchers.IO)
.onEach(::render)
.launchIn(GlobalScope)
onViewInitialisedChannel
.offer(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
I think what you're looking for is the Flow version of compose and ObservableTransformer and as far as I can tell there isn't one. What you can use instead is the let operator and do something like this:
MainActivity:
yourFlow
.let(viewModel::invoke)
.onEach(::render)
.launchIn(lifecycleScope) // or viewLifecycleOwner.lifecycleScope if you're in a fragment
ViewModel:
operator fun invoke(viewEventFlow: Flow<Event>): Flow<State> = viewEventFlow.flatMapLatest { event ->
when (event) {
Event.OnViewInitialised -> flowOf(onViewInitialisedEvent())
}
}
As far as sharing a flow I would watch these issues:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
https://github.com/Kotlin/kotlinx.coroutines/issues/2047
Dominic's answer might work for replacing the publish subjects but I think the coroutines team is moving away from BroadcastChannel and intends to deprecate it in the near future.
kotlinx-coroutines-core provides a transform function.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
it isn't quite the same as what we are used to in RxJava but should be usable for achieving the same result.

Kotling :: Android :: java.lang.ClassCastException: java.lang.Class cannot be cast to androidx.lifecycle.ViewModel

I am getting an exception in the first line of the code below
viewModel.homeLiveData.observe(this, Observer { list ->
list?.let {
mList.addAll(list)
adapter.notifyDataSetChanged()
}
})
java.lang.ClassCastException: java.lang.Class cannot be cast to androidx.lifecycle.ViewModel
The Whole code is Below
what is wrong with the cast ? is there anything wrong of I am creating my ViewModel?
My BaseActivity
abstract class BaseActivity<V : ViewModel> : DaggerAppCompatActivity(), HasSupportFragmentInjector {
#Inject
lateinit var fragmentAndroidInjector: DispatchingAndroidInjector<Fragment>
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
#LayoutRes
abstract fun layoutRes(): Int
protected lateinit var viewModel : V
protected abstract fun getViewModel() : Class<V>
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home)
onBackPressed()
return super.onOptionsItemSelected(item)
}
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(layoutRes())
viewModel = ViewModelProviders.of(this, viewModelFactory).get(getViewModel())
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentAndroidInjector
}
then My Activity
class MainActivity : BaseActivity<MainViewModel>() {
override fun layoutRes(): Int = R.layout.activity_main
override fun getViewModel(): Class<MainViewModel> = MainViewModel::class.java
private val mList = mutableListOf<Any>()
private lateinit var adapter: DataAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setSupportActionBar(toolbar)
recyclerView.apply {
layoutManager = GridLayoutManager(applicationContext, 2)
adapter = DataAdapter(context, mList)
}
viewModel.homeLiveData.observe(this, Observer { list ->
list?.let {
mList.addAll(list)
adapter.notifyDataSetChanged()
}
})
viewModel.getHomeItems()
}
and this is my ViewModel
class MainViewModel #Inject constructor() : ViewModel() {
val homeLiveData: MutableLiveData<List<HomeScreenModel>> = MutableLiveData()
fun getHomeItems() {
Handler().post {
val homeModleList = listOf(
HomeScreenModel(R.drawable.ic_launcher_background, MyApplication.instance.getString(R.string.settings))
)
homeLiveData.setValue(homeModleList)
}
}
}
In my opinion, your viewModel property, which you try to access in viewModel.homeLiveData is shadowed by getViewModel() abstract function that you declare in BaseActivity. This is because Kotlin thinks that getXyZ() is a getter for the field xyZ and thus when you access viewModel, the compiler thinks you want to call getViewModel, which is of type Class<V>. I suggest renaming either the function or the property and it should work then.
Check your viewModel factory if you implemented it correctly

Error "Can't create ViewModelProvider for detached fragment" when i called method from Presenter

I have a fragment with updateToolbar() function. When i try to get my ViewModel, calles UserViewModel on this method i get error:
Can't create ViewModelProvider for detached fragment
When i try to get UserViewModel inside onViewCreated(), all works fine. Why it happens? I call updateToolbar() after onCreateView() and I'm not create any fragment transactions before function called.
I'm started to learn Clean Architecture, and intuitively i think the reason of error can be on it, so i add this code too. I think problem about presenter, but i can't understand where exactly.
PacksFragment:
class PacksFragment : BaseCompatFragment() {
#Inject
lateinit var presenter: PacksFragmentPresenter
private var userViewModel: UserViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
LibApp.get().injector.inject(this)
...
presenter.openNewPack(packId)
}
fun updateToolbar() {
Timber.e((userViewModel == null).toString())
Timber.e(userViewModel?.getData()?.value?.coins.toString())
}
}
PacksFragmentPresenter:
class PacksFragmentPresenter #Inject constructor(
private val packsFragment: PacksFragment,
private val getCoinsFromUserCase: GetCoinsFromUserCase
) {
fun openNewPack(packId: Int) {
if (getCoinsFromUserCase.getCoinsFromUser()){
packsFragment.updateToolbar()
}
}
}
GetCoinsFromUserCase:
class GetCoinsFromUserCase {
fun getCoinsFromUser(): Boolean {
val userViewModel = UserViewModel()
userViewModel.takeCoins(10)
return true
}
}
userViewModel:
class UserViewModel : ViewModel(), UserApi {
private val data = MutableLiveData<User>()
fun setData(user: User) {
//Logged fine
Timber.e("User setted")
data.value = user
}
fun getData(): LiveData<User> {
if (data.value == null) {
val user = User()
user.coins = 200
data.value = user
}
//Logged fine, "false"
Timber.e("User getted")
Timber.e("Is user == null? %s", (data.value == null).toString())
return data
}
override fun takeCoins(value: Int) {
//Specially commented it
// getCoins(value)
}
}
UPD:
I make some changes to prevent crush - make userViewModel nullable(update PacksFragment code on the top).
But userViewModel is always null when i call updateToolbar(). Main thing fragment not removed/deleted/invisible/..., it's active fragment with button which called updateToolbar() function.

Categories

Resources