I stumbled on a problem when ViewModel.OnCleared() is not being called when the app goes to background (even if Don't keep activities is enabled) but I can see that Fragment.onDestroy() is actually being called.
What could be wrong in the following code? How can I make ViewModel.OnCleared() to be actually called in this scenario?
ViewModel:
class ViewModelFirst(application: Application) : AndroidViewModel(application) {
companion object {
private const val TAG = "ViewModelFirst"
}
init {
Log.v(TAG, "Created")
}
override fun onCleared() {
super.onCleared()
Log.v(TAG, "onCleared")
}
}
Fragment:
class FragmentFirst : Fragment() {
companion object {
private const val TAG = "FragmentFirst"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
ViewModelProviders.of(this).get(ViewModelFirst::class.java)
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onDestroy() {
super.onDestroy()
Log.v(TAG, "onDestroy")
}
}
Activity:
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction().replace(R.id.container, FragmentFirst()).commit()
}
}
override fun onDestroy() {
super.onDestroy()
Log.v(TAG, "onDestroy")
}
}
Answer myself:
It's a bug of com.android.support:appcompat-v7:27.1.0
I am experiencing this issue if I use the following dependencies:
implementation 'com.android.support:appcompat-v7:27.1.0'
implementation "android.arch.lifecycle:extensions:1.1.0"
If I change the version of appcompat-v7 27.1.0 -> 27.0.2, then ViewModel.OnCleared()works as expected (I have the call when the app goes to background).
appcompat-v7:28.0.0-alpha1 works as well, looks like this is a problem only of appcompat-v7:27.1.0
Update (June 2018)
As #Akshay said, the bug was kinda fixed on 27.1.1. But not fully unfortunately.
The following scenario is still unfixed:
Have Don't keep activities enabled.
Start the app.
Push home button.
On 27.0.2 I have the following output at logcat:
V/ViewModelFirst: Created
V/ViewModelFirst: onCleared
V/FragmentFirst: onDestroy
V/MainActivity: onDestroy
Which totally correct.
But on 27.1.1 till 28.0.0-alpha3 I have the following output at logcat:
V/ViewModelFirst: Created
V/FragmentFirst: onDestroy
V/MainActivity: onDestroy
As we can see activity and fragment was destroyed but viewModel was not notified with onCleared.
I suspect that in case if the Don't keep activities will be disabled and the app at background will be naturally unloaded by Android (due another app claiming for a bunch of resources) at some moment of time the viewModel.onCleared() will not be called which is very sad.
P.S. I have pushed the code here: https://github.com/allco/onClearedInvestigation
And have reported the issue to Google here: https://issuetracker.google.com/issues/110285295
Update (August 2018)
28.0.0-rc01 has this problem solved. Yay!
It was an issue in Support Library v27.1.0 and has been fixed in Support Library v27.1.1.
There is a bug reported previously at https://issuetracker.google.com/issues/74139250.
Refer this link for more details: https://developer.android.com/topic/libraries/support-library/revisions
Related
I am getting a random crash "lateinit property binding has not been initialized". Most of the time it's working fine but a few time randomly we are getting this crash on crashlytics.
Please let me know what's wrong here
I have a BaseActivity with following code
abstract class BaseActivity<D : ViewDataBinding> : AppComptActivity() {
abstract val layoutId: Int
lateinit val binding: D
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
binding = DataBindingUtil.setContentView(this, layoutId)
....
}
}
I have a HomeActivity which override BaseActivity with following code
class HomeActivity : BaseActivity<ActivityHomeBinding>() {
override val layoutId: Int get() = R.layout.activity_home
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
....
}
}
I am using bottomNavigation menu and one of the fragment is HomeFragment
class HomeFragment : BaseFragment<FragmenntHomeBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState:Bundle)
(activity as HomeActivity).binding.appBarHome.visible(false)
//HERE I AM GETTING lateinit property binding has not been initialized crash
}
}
I don't want to use isInitialized property of lateinit as this will not solve my issue
As mentioned in the comment, I'd suggest instead of calling parent container (Activity) objects directly, register a listener to a navigation change like this in HomeActivity:
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if(destination.id = R.id.homeFragment) {
// TODO hide/show your view here
}
}
In that case, you are sure that the view gets hidden/shown when it should be without relying on the HomeFragment being only in HomeActivity as this can change in the future and your app will start crashing
If you have an orientation change or other config change, or the OS process is killed while in the background and the user returns to the app, Android will recreate the Activity and the Fragments.
Unfortunately, it creates the Fragments first, before creating the Activity. So you cannot rely on the existence of the Activity until the Fragment has been attached to the Activity. You should move code that relies on the existence of the Activity to
onActivityCreated().
Note: I also agree with the comment about not doing it this way. Your Fragment should not make assumptions like this (that it is hosted by HomeActivity), but instead should make some callback to the hosting Activity and let the hosting Activity set the visibility of the app bar (or whatever else it wants to do).
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.
I am using KoinDI and I have a login screen. Here is my code -
My AppModule code which shows LoginViewModel DI definition -
private val viewModelModules = module {
viewModel { LoginViewModel(get()) }
}
My LoginFragment code -
private val viewModel: LoginViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.login_button?.setOnClickListener {
onLoginButtonPressed()
}
}
private fun onLoginButtonPressed() {
val email = view?.email_value?.text.toString()
val password = view?.password_value?.text.toString()
viewModel.onLoginPressed(email, password).observe(this, Observer {
if (it.userLoggedIn) {
//...
}
handleError(it.error)
})
}
The problem is when I click login and immediately put the app in background and API call fails (I fail it on purpose
for testing from the backend side) and when I bring the app in foreground I see
that the viewmodel continues to observe resulting in API call happening again and again until it succeeds. Why does it happen?
Why cannot my viewmodel observe only on login button click?
When you say viewModel.onLoginPressed.observe the activity/fragment will receive events when it is started or resumed state and when it is destroyed the observer will automatically be removed.
You seem to have a retry logic inside the viewModel that keep retrying.
Hi in a multi modules app, I am loading child modules using loadKoinModules() and unloading it using unloadKoinModules() in feature module my code looks like
class FeatureActivity:AppCompatActivity(){
private val loadFeatures by lazy { loadKoinModules(featureModule) }
private fun injectFeatures() = loadFeatures
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectFeatures()
}
override fun onDestroy() {
super.onDestroy()
unloadKoinModules(featureModule)
}
}
Everything works fine but problem start when another instance on same activity is loaded. While current activity is in background. App crash due to error below
org.koin.error.BeanOverrideException: Try to override definition with Factory
Is there a way to avoid this error
It is somehow correct what you are doing, you can unload dynamically as you do this is why unloadKoinModules has been added link
but why aren't you unloading onStop? according to android lifecycle and what you want to do, you have to unload in onStop
When activity gets focus onCreate will occur (and you will load modules), later when activity loses focus, onStop will occurs (and you will unload modules) and the circle between the events...
class FeatureActivity:AppCompatActivity(){
private val loadFeatures by lazy { loadKoinModules(featureModule) }
private fun injectFeatures() = loadFeatures
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectFeatures()
}
override fun onStop() {
super.onStop()
unloadKoinModules(featureModule)
}
}
Koin won't let to you redefine an already existing definition (type,name,path …). You will run into an error.
You need to allow definition override :-
val featureModule = module {
// override for this definition
single<yourType>(override=true) { YourClass() }
}
ALSO you can override on module level instead of overriding on definition level only:-
val featureModule = module(override=true) {
single<yourType> { YourClass() }
}
Important:-
Order matters when listing modules and overriding definitions. You must have your overriding definitions in last of your module list.
Some possibilities:
Load your feature module in the top-level application level and don't scope it to any activity lifecycle.
Add a reference-counting wrapper around your module load/unload so the module is not reloaded if it is already loaded, and it is only unloaded when the usage count is zero. (You can simplify this by not caring about unloading and change the count to just a "initialised" boolean.)
I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.