Android How Lifecycle-Aware Components Detect Configuration Change inside ViewModel - android

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.

Related

Koin's getStateViewModel is deprecated - what is the alternative?

I have the following implementation on my Fragment -
class HeroesDetailsFragment : Fragment() {
private val navArgs: HeroesDetailsFragmentArgs by navArgs()
private val heroesDetailsViewModel: HeroesDetailsViewModel by stateViewModel(state = { navArgs.toBundle() })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentHeroDetailsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initListeners()
observeUiState()
observeUiAction()
}
}
My HeroesDetailsViewModel looks like this -
class HeroesDetailsViewModel(
private val savedStateHandle: SavedStateHandle,
private val heroesDetailsRepository: HeroesDetailsRepository
) : ViewModel() {
private fun getArgsModel() = HeroesDetailsFragmentArgs.fromSavedStateHandle(savedStateHandle)
init {
val navArgs = getArgsModel()
getAdditionalHeroDetails(navArgs.heroModel.id)
observeUiEvents()
}
}
And in my ViewModelModule I declare the following
val viewModelModule = module {
// ...
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
As you can see, I utilized the stateViewModel extension for Fragments that allows me to create a StateViewModel. The issue is that when trying to use the same functionality in Compose:
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = getStateViewModel() //provides deprecation error
) {
}
I get the following deprecation message -
getStateViewModel will be merged to sharedViewModel - no need anymore of state parameter
I did not find any good references on this topic, and it seems weird for me because the Fragment extension stateViewModel is completely fine and not deprecated so I am missing information on what should I do to replace it.
My goal is to inject a ViewModel with state parameters that will initialize the SavedStateHandle object. Currently I am using Koin DI, will switch in the future to Dagger-Hilt so it would be also a nice bonus to see the solution both in Koin and in Dagger-Hilt.
So I finally found a way to inject the ViewModel with dynamic information coming from the Fragment. I was looking at the old Fragment / Activity way which includes handling bundles, but in Compose it's much easier as we don't need to use the SavedStateHandle object because we can handle process death by the rememberSaveable { } block, which decouples the need to inject a ViewModel with dynamic information and the need to save information for process death.
This leaves the ViewModel to only ask for the relevant model and not bother handling process death. Just pure information.
class HeroesDetailsViewModel(
heroListModel : HeroesListModel,
private val heroesDetailsRepositoryImpl: HeroesDetailsRepositoryImpl
) : ViewModel() {
init {
getAdditionalHeroDetails(heroListModel.id)
observeUiEvents()
}
}
So I added a model that will be injected when needed via the parameters field -
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = koinViewModel(parameters = { ParametersHolder(mutableListOf(model)) })
) {
}
And in my DI module the implementation actually is left the same -
val viewModelModule = module {
viewModelOf(::HeroesViewModel)
viewModelOf(::HeroesListItemViewModel)
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
Hopefully this saves some time for other people in the future 💪🙂

Fragment Never Receives LiveData updates from Shared ViewModel

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

Difference between by viewmodels and viewmodel creation using Factory?

I am studying ViewModel to apply it to MVVM design pattern.
There was a method using by viemodels() and a method using ViewModelProvider.Factory in view model creation.
by viewModels() creates a ViewModel object.
ViewModelProvider.Factory also creates Viewmodel objects.
What is the difference between these two?
In addition, in some sample code, I saw the code in comment 3, which uses by viewModels() and factory together. What does this mean?
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels() // 1
private lateinit var viewModelFactory: WriteRoutineViewModelFactory
// private val viewModel: WriteRoutineViewModel by viewModels(
// factoryProducer = { viewModelFactory } // 3.What does this code mean?
// )
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWritingRoutineBinding.inflate(inflater, container, false)
viewModelFactory = WriteRoutineViewModelFactory()
// viewModel = ViewModelProvider(this, viewModelFactory).get(WriteRoutineViewModel::class.java) // 2
return binding.root
}
If your ViewModel has a zero-argument constructor, or if it has a constructor where its only argument is of type Application and it's a subclass of AndroidViewModel, then you do not need a factory. (Or if your constructor is either of the above plus SavedStateHandle.) A view model factory is a class that is able to instantiate your ViewModel that has a more complicated constructor.
When instantiating your ViewModel without using a delegate, you have to use a lateinit var for the property because you can't instantiate it until onCreateView.
If your ViewModel had no need for a factory, the process of doing it without a delegate would look like this:
class WritingRoutineFragment : Fragment() {
private lateinit var viewModel: WriteRoutineViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
viewModel = ViewModelProvider(this, viewModelFactory).get(WriteRoutineViewModel::class.java)
//...
}
}
and if it did need a factory, it would look like this, where you have to instantiate a factory and pass it to the ViewModelProvider constructor:
class WritingRoutineFragment : Fragment() {
private lateinit var viewModel: WriteRoutineViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
viewModel = ViewModelProvider(this, WriteRoutineViewModelFactory()).get(WriteRoutineViewModel::class.java)
//...
}
}
The delegate allows you to do this more concisely in a val right at the declaration site so you don't have to do any setup of your view model property in onCreateView. It will lazily create the ViewModel the first time the property is used. The advantage is more concise and clearer code (lateinit var splits the property from its declaration and makes it mutable even though it will never change).
So the above code when no factory is needed looks like:
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels()
}
and if you do need a factory it will look like this. You pass it a function that instantiates the factory, which is easily done with a lambda:
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
}
The code in your example has an extra property just to hold the factory, which is an unnecessary complication since you'll never need to access it directly. It's also quite odd that the factory in your example has an empty constructor, because if the factory doesn't have any state, then it has no data to pass to the ViewModel constructor.

How to use viewmodel + room with fragments?

I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.
MyApplication
class myApplication : Application() {
companion object {
var database: myDatabase? = null
var repository: myRepository? = null
}
override fun onCreate() {
super.onCreate()
database = MyDatabase.getInstance(this)
repository = MyRepository(database!!.myDatabaseDao)
}
}
MyViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
HomeFragment
class HomeFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var rootView = inflater.inflate(R.layout.fragment_home, container, false)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.allWords.observe(viewLifecycleOwner) { words ->
// Update the cached copy of the words in the adapter.
words.let { Log.d("fragment", it.toString()) }
}
}
}
I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.
Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?
Would appreciate any help, or documentation that explains this more clearly.
The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:
ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)
This constructor takes two parameters:
The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.
Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).
private val myViewModel: MyViewModel by activityViewModels() {
MyViewModelFactory(MyApplication.repository!!)
}
As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.

how to use a dagger fragment viewmodel in injected dagger fragment?

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

Categories

Resources