I create ViewModel class at fragment, but viewModel is not saving after rotation - every time i got new Viewmodel instance. Where is problem?
Acrivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.addToBackStack(null)
.replace(R.id.main_container, VideoFragment())
.commit()
}
}
Fragment:
class VideoFragment: Fragment() {
lateinit var viewModel: VideoViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(this).get(VideoViewModel::class.java)
return inflater.inflate(R.layout.fragment_video, container, false)
}
}
ViewModel:
class VideoViewModel: ViewModel() {
init {
Log.i("XXX", "$this ")
}
}
if i will use "requireActivity()" - as ViewModelStoreOwner - viewModel isnt recreate, but its will bound to activity lifecycle.
viewModel = ViewModelProvider(requireActivity()).get(VideoViewModel::class.java)
This is because you are replacing your Fragment on every configuration change when the Activity is recreated. The FragmentManager already retains your Fragment for you. You should commit the transaction only on the initial creation:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.addToBackStack(null)
.replace(R.id.main_container, VideoFragment())
.commit()
}
}
}
Your problem is most likely caused due to the activity being destroyed and then created again after app is rotated.
To fix this you can give your fragment an id/tag when navigating and then when activity is rotated call your supportFragmentManager if there already exists an instance of your old fragment, if it does, navigate to old fragment instance, otherwise create a new instance just like you are doing now.
Wax911 (commented on 9 may 2020) answer to this question:
https://github.com/InsertKoinIO/koin/issues/693
He explains the problem with activities and their lifecycle when rotating the screen
Related
I am using Hilt to inject a ViewModel into a fragment. But on screen rotation, ViewModel.onCleared() is called. Is this expected behaviour? I always thought ViewModel survives screen rotation.
Due to this, the ViewModel is recreated which I verified by comparing the ViewModel hash code on screen rotation.
Here is my fragment code:
#AndroidEntryPoint
class DashboardFragment : BaseFragment() {
private val dashboardViewModel: DashboardViewModel by viewModels()
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_dashboard, container, false
)
binding.viewModel = dashboardViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.adapter = adapter
setObservers()
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
and the ViewModel looks like this:
#HiltViewModel
class DashboardViewModel #Inject constructor() : ViewModel() {
init {
//Some code
}
override fun onCleared() {
super.onCleared()
Timber.e("cleared")
}
}
Maybe you missed .addToBackStack when you run this fragment.
But most importantly: make sure that the fragment is not recreated again when you rotate the screen, for example, using
if (savedInstanceState == null) {
...
}
inside of onCreate, onCreateView and so on
This is not what should be happening with a ViewModel. As you said they are designed to survive recreation due to an orientation change.
What seems to be happening here is that you're recreating your Fragment whenever the screen is rotated, in onCreate of an activity or something similar. This then causes the ViewModel to get cleared and re-instantiated.
I need to call a fragment method in the activity....
FirstFragment.kt
class FirstFragment: Fragment() {
fun getToast(context: Context) {
return Toast.makeText(context, "hello", Toast.LENGTH_SHORT).show()
}
}
MainActivity.kt
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, FirstFragment, FistFragmentTAG_NAME)
transaction.commit()
//expecting to use fragment method...
parameter.getToast() //parameter is the code neede to call FirstFragment method .getToast()
}
}
Ive tried these 3 cases without success
Ive tried: (it crashes the app)
FirstFragment().getToast(this#MainActivity)
Also tried, but the function .getToast() appears red
val myToast = supportFragmentManager.findFragmentByTag(FistFragmentTAG_NAME).getToast(this#MainActivity)
Also tried, but the funtion .getToast() appears red as well
supportFragmentManager.fragments.forEach { it: Fragment
val getToast = it.getToast(this#MainActivity)
the problem here is not the context, because in the real, Im trying to call a method that doesnt use context at all... Context was set in this example just to call a Toast.
How do I call the fragment method??*
This is activity class
class MyActivity : AppCompatActivity() {
private lateinit var myFragment: FirstFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myFragment = FirstFragment()
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_container, myFragment , "MyFragment")
.commit()
//this is fragment method, we call it from activity
myFragment.getToast(this)
}
}
This is fragment class
class FirstFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
fun getToast(context: Context) {
Toast.makeText(context, "Hello", Toast.LENGTH_SHORT).show()
}
}
Example of what i'm trying to achieve https://i.stack.imgur.com/lhp10.png
I'm stuck on the part where i need to switch to createUserFragment from defaultFragment
i attached this to my button, but nothing happens when i press it, not sure what's wrong there
MainActivity
class MainActivity : AppCompatActivity() {
lateinit var appBarConfiguration: AppBarConfiguration
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navController = findNavController(R.id.hostFragment)
appBarConfiguration = AppBarConfiguration(navController.graph,drawer_layout)
NavigationUI.setupActionBarWithNavController(this, navController,drawer_layout)
NavigationUI.setupWithNavController(navigation_drawer,navController)
}
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController,appBarConfiguration)
}
defaultFragment
class defaultFragment : Fragment() {
private lateinit var binding: defaultFragmentBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = defaultFragmentBinding.inflate(inflater)
return (binding.root)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.createNewBlankFAB.setOnClickListener{
val transaction = (activity as MainActivity).supportFragmentManager.beginTransaction()
transaction.replace(R.id.defaultFragment, createUserFragment())
transaction.disallowAddToBackStack()
transaction.commit()
}
}
}
My questions are:
1)How can i fix my thing?
2)Will it work properly? I.e no memory leaks or some other funky stuff
3)Do i have to use another fragment for data input or maybe there's another way?
UPDATE
I stil have no idea how this works, but apparently i was replacing wrong fragment, i switched this R.id.defaultFragment in transaction.replace to R.id.hostFragment from which, i assume, all other fragments spawn, but now it just spawn on top of my existing fragment and the drawer button doesn't change its state, i guess i have to either do all of this differently or somehow pass to the drawer navigation information that current fragment was changed?
This is how we navigate within fragments in Navigation component library . We use navigate to then id of the destination Fragment which is defined in navgraph
binding.createNewBlankFAB.setOnClickListener{
Navigation.findNavController(view).navigate(R.id.createUserFragment);
}
I think you have to call "transaction.add" instead of "replace" since you are calling your fragment from an activity. Replace is called when you call a fragment from another fragment, I believe.
i am currently programming an app in Android Studio and i am having a big issue. The main problem is, that i want an activity with a fragment in it and this fragment has got a spinner. I wanted to find the spinner by id, but it always returned null and i read that i can't use findViewById if it is not in the ContentView i just set. So i am currently trying to find the fragment that contains the spinner, but i also can't find the fragment, i tried findFragmentById and findViewById from the FragmentManager. I always get a TypeCastException and if i try findFragmentById(...)!! it throws a NullPointer.
This is my MainActivity:
class MainActivity : AppCompatActivity() {
private val manager: FragmentManager? = supportFragmentManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showDeviceFragment()
val fragment = manager!!.findFragmentById(R.id.fragment_holder) as DeviceFragment
val options = arrayOf("Wandhalterung (Arm)", "Gestellhalterung (Arm)", "Gestellhalterung")
fragment.option.adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, options)
}
fun showDeviceFragment() {
val transaction = manager!!.beginTransaction()
val fragment = DeviceFragment()
transaction.add(R.id.fragment_holder, fragment, "DEVICE_FRAGMENT")
transaction.addToBackStack(null)
transaction.commit()
}
}
And this is the DeviceFragment:
class DeviceFragment : Fragment() {
lateinit var option : Spinner
companion object {
fun newInstance(): DeviceFragment {
return DeviceFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_device, container, false)
option = view.spinner
return inflater?.inflate(R.layout.fragment_device, container, false)
}
}
The fragment_holder just is a FrameLayout.
Thanks in advance
Few things:
1) No need to hold reference for supportFragmentManager, use it directly (because i am not sure if it will be null when Activity is initialized)
2) Try removing addToBackStack(null) and using findFragmentByTag("DEVICE_FRAGMENT")
3) Most importantly, Don't try to access "things" of Fragment from Activity, do those Adapter initialization/fill in the Fragment itself. Because Fragment has its own lifecycle and you may try to access "things" at wrong lifecycle
I have the single activity with several fragments on top, as Google recommends. In one fragment I wish to place a switch, and I wish to still know it's state when I come back from other fragments. Example: I am in fragment one, then I turn on the switch, navigate to fragment two or three, go back to fragment one and I wish to load that fragment with that switch in the on position as I left it.
I have tried to copy the examples provided by google advocates, just to see the code to fail hard and do nothing.
/////////////////////////////////////////////////////////////////
//Inside the first fragment:
class myFragment : Fragment() {
companion object {
fun newInstance() = myFragment()
}
private lateinit var viewModel: myViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
**viewModel = ViewModelProvider(this, SavedStateVMFactory(this)).get(myViewModel::class.java)
//Here I was hoping to read the state when I come back.
switch_on_off.isChecked = viewModel.getSwRoundTimerInit()**
subscribeToLiveData() //To read liveData
switch_on_off.setOnCheckedChangeListener { _, isChecked ->
viewModel.setOnOff(isChecked)
}
}//End of onActivityCreated
//other code...
/////////////////////////////////////////////////////////////////////////
//On the fragment ViewModel
class myViewModel(private val **mState: SavedStateHandle**) : ViewModel() {
//SavedStateHandle Keys to save and restore states in the App
private val swStateKey = "SW_STATE_KEY"
private var otherSwitch:Boolean //other internal states.
//Init for the other internal states
init {
otherSwitch = false
}
fun getSwRoundTimerInit():Boolean{
val state = mState[swStateKey] ?: "false"
return state.toBoolean()
}
fun setOnOff(swValue:Boolean){
mState.set(swStateKey, swValue.toString())
}
}
This does not work. It always loads the default (off) value, as if the savedState is null all the time.
change
//fragment scope
viewModel = ViewModelProvider(thisSavedStateVMFactory(this)).get(myViewModel::class.java)
to
//activity scope
viewModel = activity?.let { ViewModelProviders.of(it,SavedStateVMFactory(this)).get(myViewModel::class.java) }
https://developer.android.com/topic/libraries/architecture/viewmodel#sharing