I've been doing some with a fragment that has a view model with a dependency on the WorkManager. I used to get the WorkManager using the now deprecated method WorkManager.getInstance(), so I refactored the code and followed the same method of getting the WorkManager instance as that done in the Sunflower project (which has since changed). The Sunflower sample project now uses NavArgs() and no longer does this: InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
My question is, can IllegalStateException be thrown when assigning the viewModel variable by injection because of getting a WorkManager instance using requireActivity() like in my code below? Is it possible for an activity to not be attached/get destroyed at the time this variable is assigned? Should I refactor and use the application context instead of requireActivity()?
class DetailFragment : Fragment() {
private val viewModel by inject<ViewModel> { parametersOf(WorkManager.getInstance(requireActivity())) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = DetailFragmentBinding.inflate(inflater, container, false).apply {
vm = viewModel.apply {
event.observe(this#DetailFragment, Observer {
viewDataBinding.refreshLayout.isRefreshing = false
})
}
}
return viewDataBinding.root
}
}
You should be mostly safe. But it only depends on where you get viewModel for the first time. You could make sure viewModel gets initialized in onCreate or onStart before setting up any handler accessing it.
In your case it's called in onCreateView only, thus it should be attached to an Activity and for that reason safe.
Related
My app uses MVVM architecture. I have a ViewModel shared by both an Activity and one of its child fragments. The ViewModel contains a simple string that I want to update from the Activity and observe in the fragment.
My issue is simple: the observe callback is never reached in my fragment after the LiveData updates. For testing, I tried observing the data in MainActivity, but that works fine. Additionally, observing LiveData variables in my fragment declared in other ViewModels works fine too. Only this ViewModel's LiveData seems to pose a problem for my fragment, strangely.
I'm declaring the ViewModel and injecting it into my Activity and Fragment via Koin. What am I doing incorrectly to never get updates in my fragment for this ViewModel's data?
ViewModel
class RFIDTagViewModel: ViewModel() {
private val _rfidTagUUID = MutableLiveData<String>()
val rfidTagUUID: LiveData<String> = _rfidTagUUID
fun tagUUIDScanned(tagUUID: String) {
_rfidTagUUID.postValue(tagUUID)
}
}
Activity
class MainActivity : AppCompatActivity(), Readers.RFIDReaderEventHandler,
RFIDSledEventHandler.TagScanInterface {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
rfidViewModel.rfidTagUUID.observe(this, {
Timber.d("I'm ALWAYS reached")
})
}
override fun onResume() {
rfidViewModel.tagUUIDScanned(uuid) //TODO: data passed in here, never makes it to Fragment observer, only observed by Activity successfully
}
}
Fragment
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
rfidViewModel.rfidTagUUID.observe(viewLifecycleOwner, { tagUUID ->
Timber.d("I'm NEVER reached")
})
}}
Koin DI Config
val appModule = module {
viewModel { RFIDTagViewModel() }
}
In your Fragment I see you are using viewModels(). viewModels() here will be attached to the Fragment, not to the Activity.
If you want to shareViewModel between Fragment and Activity, then in Fragment you use activityViewModels(). Now, in the Fragment, your shareViewModel will be attached to the Activity containing your Fragment.
Edit as follows:
PickingItemFragment.kt
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
}
More information: Communicating with fragments
You need to use the same viewmodel, aka, sharedViewModel, the way you are doing you are using two different instances of the same viewmodel.
To fix it.
On both activity and fragment:
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=pt-br
My Fragment:
class FirstFragment : Fragment() {
private lateinit var binding: FragmentFirstBinding
private lateinit var viewModelFactory: FirstViewModelFactory
private lateinit var viewModel: FirstViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_first, container, false)
viewModelFactory = FirstViewModelFactory(requireActivity().application, this.lifecycle) //<- Lifecycle object
viewModel = ViewModelProvider(this, viewModelFactory).get(FirstViewModel::class.java)
return binding.root
}
}
My ViewModel:
class FirstViewModel(application: Application, lifecycle: Lifecycle) : AndroidViewModel(application), LifecycleObserver {
init {
lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun showOnStopMessage() {
Log.v("xxx", "onStop called!!")
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
private fun showOnStartMessage() {
Log.v("xxx", "onStart called!!")
}
}
The above setup works well in no-configuration-change environment, showOnStopMessage() gets called when app goes to the background, and showOnStartMessage() gets called when the app is brought back to the foreground.
The problem is, when configuration-change happens (like rotating the screen), those functions are not being called any more.
Why this happens? How to detect and "survive" configuration-change? Thanks in advance.
As far as I understand, the problem is that your ViewModel is created only once (as it should be) and it only adds the lifecycle of the first fragment as a LifecycleObserver. When you rotate the screen, the same ViewModel is returned and it'll still try to react to the changes of the old Fragment, which won't happen.
I'd suggest not dealing with lifecycle inside the ViewModel at all (remove the related code from the Factory and from the ViewModel). Just call:
lifecycle.addObserver(viewModel)
right after the ViewModel is obtained, inside onCreateView.
It's hard to understand what the problem is from the headline - I'll try my best explaining:
I'm using Koin for dependency injection. I'm injecting my HomeViewModelinto my HomeFragment (the viewModel has parameters, but that should be unrelated to the problem):
// fragment code
private var viewModelParameters: ParametersDefinition? = null
lateinit var viewModel: VM
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layout, container, false)
binding?.lifecycleOwner = viewLifecycleOwner
viewModel = getViewModel(HomeViewModel::class, parameters = viewModelParameters)
return binding?.root ?: inflater.inflate(layout, container, false)
}
The fragment contains a RecyclerView. The recycler's ViewHolder declares an interface, that is injected via Koins by inject()`:
class MyRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), KoinComponent {
private val callback by inject<Callback>()
fun bind(item: MyItemType) {
itemView.setOnClickListener { callback.myCallbackFunction(item) }
}
interface Callback {
fun myCallbackFunction(item: MyItemType)
}
}
My HomeviewModel implements this interface, and I bind it to the viewModel in my KoinGraph module via Koin's bind DSL method:
private val baseModule = module {
single { androidApplication().resources }
single { PermissionHelper(androidApplication()) }
...
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
}
Now, when I click on my recycler item, the callback's myCallBackFunction is called, which should trigger the implementation in my HomeViewModel. Which it does, but: It is not the same instance, but a new HomeViewmodel.
My understanding is that Android's ViewModelclass, if used in the typical way (currently using, without Koin, by viewModels() - see here), should only exist once. But with Koin's viewModel{} call, I can create multiple instances, which I think I shouldn't be able to? Or should I (and if yes, why)?
Anyway, I'd like to bind my callback to the view model I already have (the one the fragment knows of) and not a new instance my fragment doesn't know about.
How can I achieve that using Koin and its injection patterns?
By the way, If I use
single { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
instead of
viewModel { HomeViewModel() } bind MyRecyclerviewHolder.Callback::class
my code works as intended - since I'm forcing my view model to be a singleton that way - which is what I want. But what is the point of the viewModel{} command then? And are there any downsides to it? It doesn't feel like what I should be supposed to do but maybe it's totally fine?
I'm using dagger2 for DI, and developing for a single activity. So I did inject a fragment when start main activity, and the fragment also inject this viewmodel. But the problem is occured when I inject a viewmodel in dagger fragment. If I don't use a constuctor #Inject in dagger fragment, ViewModel is working well But can't Inject at MainActivity. If I use a constuctor #Inject in dagger fragment, ViewModel is not working and got the error like this
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
Should I give up one?
MainActivity
#Inject
lateinit var myFolderFragment:MyFolderFragment
myFolderFragment:MyFolderFragment
class MyFolderFragment #Inject constructor(): DaggerFragment() {
#Inject
lateinit var viewModelFactory : ViewModelProvider.Factory
private val viewModel by viewModels<MyFolderViewModel> { viewModelFactory }
private lateinit var binding : FragmentMyfolderBinding
private var mActivity:Activity?=null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentMyfolderBinding.inflate(layoutInflater, container, false).apply {
viewmodel = viewModel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this.viewLifecycleOwner
}
You cannot inject dependencies into Fragment via it's constructor. Activities, BroadcastReceivers, Services, ContentProviders and Fragments require default constructor because OS creates instances of that classes using Reflection API.
In my opinion you have 3 ways to solve this :
way 1 (Hardest One) - Use FragmentFactory
way 2 (Easy to Undestand) - Direct inject in onCreateView method, it will look like this
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentMyfolderBinding.inflate(layoutInflater, container, false).apply {
(context as MyApplication).component.inject(this)
viewmodel = viewModel
}
return binding.root
}
way 3 (Less code) - Instead of Dagger2 you can use Hilt that will hide all magic that you you manually writing when use way 2
What is the proper way to keep the actual app state in MVVM? A simple example to describe what I mean: I have two fragments and one global variable or object of my Class. I can change this variable or object on both fragments. Where should I keep this in code?
The most easiest way is to use KTX extension function activityViewModels<VM : ViewModel>
see here.
From the doc:
Returns a property delegate to access parent activity's ViewModel ...
It will retrieve the ViewModel instance provided by the ViewModelProviders of the activity the fragments are attached to.
So any change on the view model instance will be reflected on all fragments.
Here a simple example:
class MVModel: ViewModel() {
var count = MutableLiveData(0)
fun increment() {
count.value = count.value!!.plus(1)
}
}
class MFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMBinding.inflate(inflater, container, false)
val viewModel by activityViewModels<MVModel>()
binding.lifecycleOwner = this // <-- this enables MutableLiveData update the UI
binding.vm = viewModel
return binding.root
}
}
You can make shared view model where all your components will access easily