I am launching a coroutine that after a specified delay display a counter value on the screen.
job = launch(UI) {
var count= 0
while (true) {
textView.text = "${count++}"
delay(200L)
}
}
Now on screen rotation I want UI keeps getting updated with correct counter value. Does someone has any idea how to resume the job on configuration(e.g. screen rotation) change.
Does someone has any idea how to resume the job on configuration(e.g. screen rotation) change.
Your job never stopped running, but you keep holding on to and updating a TextView which is no longer showing on the screen. After the configuration changed, your activity and its entire view hierarchy got scraped.
While technically you can configure your app not to recreate the activity on rotation, Google strongly discourages you from doing that. The app will seem to work for the case of rotation, but then will break on another kind of config change like timezone, location etc. You just have to bite the bullet and make your app work across activity recreation events.
I made my coroutines work across activity recreation by relying an a Fragment in which I set
retainInstance = true
This means that your fragment instance survives the death of its parent activity and, when the new activity replaces it, Android injects your fragment into it instead of creating a new one. It does not prevent the destruction of the view hierarchy, you must write code that updates the fragment's state to reflect these changes. It helps because it allows you to keep the fragment's state instead of bothering with parcelization.
On configuration change, your fragment will go through these lifecycle events:
onDestroyView
onCreateView
It doesn't go through onPause/onResume, this only happens when you switch activities or exit the app. You can start your coroutine in onResume and cancel it in onPause.
As of the recently released version 0.23 of kotlinx.coroutines, launch became an extension function: you must call it in the context of some CoroutineScope which controls the resulting job's lifecycle. You should bind its lifecycle to the fragment, so let your fragment implement CoroutineScope. Another change is that the UI coroutine context is now deprecated in favor of Dispatchers.Main.
Here's a brief example that demonstrates all the points I mentioned:
class MyFragment : Fragment, CoroutineScope {
private var textView: TextView? = null
private var rootJob = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + rootJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val rootView = inflater.inflate(R.layout.frag_id, container, false)
this.textView = rootView.findViewById(R.id.textview)
return rootView
}
override fun onDestroyView() {
this.textView = null
}
override fun onResume() {
this.launch {
var count = 0
while (true) {
textView?.text = "$count"
count++
delay(200L)
}
}
}
override fun onPause() {
rootJob.cancel()
rootJob = Job()
}
}
Now, as the view hierarchy gets rebuilt, your coroutine will automatically fetch the current instance of textView. If a timer tick happens to occur at an inconvenient moment while the UI is being rebuilt, the coroutine will just silently skip updating the view and try again at the next tick.
By default rotation kills the activity and restarts it. That means your textview will no longer be the one on screen, it will be the one belonging to the old activity.
Your options are:
1)Add a configSettings to your manifest to turn off this behavior.
2)Use something that can persist around activity restarts like a View Model, a loader, an injected event bus, etc.
Personally unless you have a different layout for portrait and landscape I'd just go with number 1, its easier.
You can do it in ViewModel instead of Activity.
Related
I am really noob into kotlin and I was trying to implement applandeo calendar library o my project in kotlin. Everything works well if you use activities but when changing into fragments I don't know how to give context because "this" is not working as a Context. In te function openDatePicker() the first parameter should be the context, but no idea about how to get it.
Also I don't know if its possible to pass from a fragment to an activity. My project is structured as a main activity with a bottom navigation bar where every elements redirects to the fragment. This code is inside one of those fragments. Any help or idea will be great ! :)
class CalendarFragment : Fragment(), OnDayClickListener, OnSelectDateListener{
private lateinit var binding: CalendarViewFragmentBinding
private val notes = mutableMapOf<EventDay, String>()
private lateinit var appContext: Context
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val context = this.context
// Inflate the layout for this fragment
val calendar_view = inflater.inflate(R.layout.calendar_view_fragment, container, false)
binding = CalendarViewFragmentBinding.inflate(layoutInflater)
binding.fabButton.setOnClickListener { openDatePicker() }
binding.calendarView.setOnDayClickListener(this)
return calendar_view
}
private fun openDatePicker() {
DatePickerBuilder(************, this)
.pickerType(CalendarView.ONE_DAY_PICKER)
.headerColor(R.color.md_theme_light_primary)
.todayLabelColor(R.color.md_theme_light_primary)
.selectionColor(R.color.md_theme_light_secondary)
.dialogButtonsColor(R.color.md_theme_light_secondary)
.build()
.show()
}
I tried functions such as requireContext(), requireActivity(), requireContext().applicationContext, this.context, but no one working as I expect.
Your Fragment is attached to its context when its onAttach() callback is invoked. This happens before it reaches the CREATED lifecycle state:
When your fragment reaches the CREATED state, it has been added to a FragmentManager and the onAttach() method has already been called.
What this means is that by the time onCreate is called (or any lifecycle callbacks after that, including onCreateView, onViewCreated, onStart etc.) your fragment will have a context, and you can access it using requireContext().
You could also use getContext(), which you can access as a property with context, but that returns null if the Fragment isn't associated with a context yet - meaning you have to null-check and handle that possibility. requireContext() will throw an exception if you don't have that context yet, but if you're making sure to only call it when the fragment is in the CREATED state (or later) then it will be safe, and you won't need to check the return value. This is the recommended way of doing things.
So as long as you're accessing the Context in a lifecycle callback like onCreateView, requireContext() will work - that's how you get the Fragment's context. Because you're calling openDatePicker from inside onCreateView, you can just use requireContext() in that function - it's safe at that point!
But you can't define it as a normal top-level variable like this:
class MyFragment : Fragment {
var appContext: Context = requireContext()
}
because that variable is assigned at construction time, which happens way before the fragment reaches the CREATED state. You don't have access to the context at construction time, so any top-level stuff that requires it will end up throwing an exception. This goes for Activities too! That's why you have to assign stuff later, like in onCreate (and this is where lateinit comes in useful, you can have a top-level variable without having to assign it before you're ready)
And no, you can't pass this as a Context when you're in a Fragment, because a Fragment isn't a Context. It works for an Activity because that is a Context - to be more accurate it's able to provide one, but that only works after it's in the CREATED state. That's why you can get in trouble using it in top-level declarations like this
class MyActivity : AppCompatActivity {
val thing = ThingThatRequiresContext(this)
}
because like we just talked about, at construction time the Activity doesn't have access to a context, and the thing that requires it (if it tries to access it immediately) will end up throwing an exception. You see that error a lot, where an Activity can't be started because something like this is happening during initialisation.
Just use it from Fragment. Here is the documentation
private fun openDatePicker() {
DatePickerBuilder(requireContext(), this)
.pickerType(CalendarView.ONE_DAY_PICKER)
.headerColor(R.color.md_theme_light_primary)
.todayLabelColor(R.color.md_theme_light_primary)
.selectionColor(R.color.md_theme_light_secondary)
.dialogButtonsColor(R.color.md_theme_light_secondary)
.build()
.show()
}
Hi fragment has the context. You just have to use context instead of this, and if you want a context that is never null, use requireContext()
I have created one chatsheetfragment(Bottomsheetdialogfragment). Whenever I open it I'm calling all the chats and binding in RecyclerView. So, my problem is the chat sheet is always reloading from onCreate() which eventually results refreshing the fragment every time. how to stop it.
And I'm using viewmodel using dagger-hilt . Viewmodel instance is also creating every time.
Tried opening as singleton instance but not worked and now I'm opening like below
private fun chatButton() {
binding.chatIv.setOnClickListener {
ChatSheetFragment().show(
supportFragmentManager,
ChatSheetFragment::class.java.simpleName
)
}
}
With ChatSheetFragment(), a brand new fragment is getting created and therefore a brand new ViewModel in case that you bind this ViewModel to that fragment.
This can be solved by binding that ChatSheetFragment to the parent activity/fragment ViewModel that can host the updated list.
So, in short:
Change the ViewModel in the ChatSheetFragment to either the parent fragment/activity (according to your desgin):
i.e., instead of ViewModelProvider(this)[MyChatViewModel::class.java] you'd replace this with requireParentFragment() or requireActivity() and replace MyChatViewModel with the one of the parent fragment/activity.
Move the list logic that you want to maintain from the chat fragment ViewModel to the parent ViewModel.
Another solution is not to create a brand new fragment with ChatSheetFragment() and just show the existing one; but not sure if that can affect the performance to keep it alive while you don't need it.
Edit:
problem to me is bottomsheetfragment is detaching and destroying itself whenever it dismiss. what can i do so that it can not be destroyed
This is right; calling dismiss() or even setting the BottomSheetBehavior state to STATE_HIDDEN will destroy the fragment.
But there is a workaround to just hide the decorView of the dialogFragment window whenever you want to hide the chat fragment like the following:
val chatDialogFragment = ChatSheetFragment()
// Hide the bottom sheet dialog fragment
chatDialogFragment.dialog.hide(); // equivalent to dialog.window.decorView.visiblity = View.GONE
// Show the bottom sheet dialog fragment
chatDialogFragment.dialog.show // equivalent to dialog.window.decorView.visiblity = View.VISIBLE
But you need to handle the situations when the DialogFragment can hide; here is a couple ones:
Dismiss on back press
Customize the dialog in onCreateDialog():
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : BottomSheetDialog(requireContext(), theme) {
override fun onBackPressed() {
this#BottomSheetFragment.dialog?.hide()
}
}
}
Dismiss on touch outside:
#SuppressLint("ClickableViewAccessibility")
override fun onStart() {
super.onStart()
val outsideView =
requireDialog().findViewById<View>(com.google.android.material.R.id.touch_outside)
isCancelable = false
dialog?.setCanceledOnTouchOutside(false)
outsideView.setOnTouchListener { _: View?, event: MotionEvent ->
if (event.action == MotionEvent.ACTION_UP) dialog?.hide()
false
}
Whenever you want to show the fragment again; just show its dialog without re-instantiating it as described above
I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!
Currently, I am playing around Android Navigation Component with Bottom Navigation Bar. While playing I realized two facts:
Fragments are always recreated (onCreate, onViewCreated, onViewDestroyed are called as soon as the user navigates to another fragment)
savedInstanceState is always null (in onCreate, onViewCreated, etc.)
The first issue can be fixed by using custom FragmentNavigator, which will reuse fragment if it already exists
package am.chamich.apps.advancedbottomnavigation.navigator
import android.content.Context
import android.os.Bundle
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.fragment.FragmentNavigator
#Navigator.Name("retain_state_fragment")
class RetainStateFragmentNavigator(
private val context: Context,
private val manager: androidx.fragment.app.FragmentManager,
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
val className = destination.className
fragment = instantiateFragment(context, manager, className, args)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commit()
return destination
}
}
Question
For the second issue, I have no idea how to fix it, actually, I even didn't understand how the fragment is restoring its state (for example when you rotate the screen), I tied to use fragment.setInitialSavedState(savedState) to save and restore fragment state, but that doesn't help in this situation.
Actually what I need to know is when fragment view was recreated
Here is a link to my GitHub project, any help is welcome.
Fragment will save its state only when activity is recreated (e.g. screen rotation) and changing the fragment doesn't matter. From documentation:
There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state.
Source
Saving custom state:
Put this method inside fragment:
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("text", "some value")
super.onSaveInstanceState(outState)
}
And read the value, for example, inside onViewCreated:
val text = savedInstanceState?.getString("text")
You will receive desired value after screen rotation / phone language change or other config changes - when activity (and fragment) is being recreated.
Check this Blog, http://www.androiddevelopment.co.in/2019/05/how-to-save-android-activity-state.html, This Blog explain how to save activity state when the activity is destroyed.
For example, If you change the language of your phone while the activity was running (and so different resources from your project need to be loaded). Another very common scenario is when you rotate your phone to the side so that the activity is recreated and displayed in landscape. You can use this technique to store instance values for your application (selections, unsaved text, etc.).
Update: accepted answer points to explanation (bug) with a work-around, but also see my Kotlin based work-around attached as an answer below.
This code is in Kotlin, but I think it is a basic android fragment life-cycle issue.
I have a Fragment that holds a reference to an other "subfragment"
Here is basically what I am doing:
I have a main fragment that has retainInstance set to true
I have a field in the main fragment that will hold a reference to the subfragment, initially this field is null
In the main fragment's onCreateView, I check to see if the subfragment field is null, if so, I create an instance of the subFragment and assign it to the field
Finally I add the subfragment to a container in the layout of the main fragment.
If the field is not null, ie we are in onCreateView due to a configuration change, I don't re-create the subfragment, I just try to added it to the containter.
When the device is rotated, I do observe the onPaused() and onDestroyView() methods of the subfragment being called, but I don't see any lifecyle methods being called on the subfragment during the process of adding the retained reference to the subfragment, to the child_container when the main fragments view is re-created.
The net affect is that I don't see the subfragment view in the main fragment. If I comment out the if (subfragment == null) and just create a new subfragment everytime, i do see the subfragment in the view.
Update
The answer below does point out a bug, in which the childFragmentManager is not retained on configuration changes. This will ultimately break my intended usage, which was to preserve the backstack after rotation, however I think what I am seeing is something different.
I added code to the activities onWindowFocusChanged method and I see something like this when the app is first launched:
activity is in view
fm = FragmentManager{b13b9b18 in Tab1Fragment{b13b2b98}}
tab 1 fragments = [DefaultSubfragment{b13bb610 #0 id=0x7f0c0078}]
and then after rotation:
activity is in view
fm = FragmentManager{b13f9c30 in Tab1Fragment{b13b2b98}}
tab 1 fragments = null
here fm is the childFragmentManager, and as you can see, we still have the same instance of Tab1Fragment, but it has a new childFragmentManager, which I think is unwanted and due to the bug reported in the answer below.
The thing is that I did add the subfragment to this new childFragmentManger.
So it seems like the transaction never executes with the reference to the fragment that was retained, but does complete if I create a brand new fragment. (I did try calling executePendingTransactions on the new childFragmentManager)
class Tab1Fragment: Fragment() {
var subfragment: DefaultSubfragment? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLabel = "label 1"
subfragment!!.buttonText = "button 1"
}
addRootContentToContainer(R.id.child_container, content = subfragment!!)
return rootView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
Your problem looks similar to the issue described here:
https://code.google.com/p/android/issues/detail?id=74222
unfortunately this issue will probably not be fixed by google.
Using retained fragments for UI or nested fragments is not a good idea - they are recomended to be used in place of onRetainNonConfigurationInstance, so ie. for large collections/data structures. Also you could find Loaders better than retained fragments, they also are retained during config changes.
btw. I find retained fragments more of a hack - like using android:configChanges to "fix" problems caused by screen rotations. It all works until user presses home screen and android decides to kill your app process. Once user will like to go back to your app - your retained fragments will be destroyed - and you will still have to recreate it. So its always better to code everything like if your resources could be destroyed any time.
The accepted answer to my question above points out a reported bug in the support library v4 in which nested fragments (and child fragment managers) are no longer retained on configuration changes.
One of the posts provides a work-around (which seems to work well).
The work around involves creating a subclass of Fragment and uses reflection.
Since my original question used Kotlin code, I thought I would share my Kotlin version of the work around here in case anyone else hits this. In the end, I am not sure I will stick with this solution, since it is still somewhat of a hack, it still manipulates private fields, however if the field name is changed, the error will be found at compile time rather than runtime.
The way this works is this:
In your fragment that will contain child fragments you create a field retainedChildFragmentManager, that will hold the childFragmentManager that will be lost during the configuration change
In the onCreate callback for the same fragment, you set retainInstance to true
In the onAttach callback for the same fragment, you check to see if retainedChildFragmentManger is non-null, if so you call a Fragment extension function that re-attaches the retainedChildFragmentManager, otherwise you set the retainedChildFragmentManager to the current childFragmentManager.
Finally you need to fix the child fragments to point back to the newly created hosting activity (the bug leaves them referencing the old activity, which I think results in a memory leak).
Here is an example:
Kotlin Fragment extensions
// some convenience functions
inline fun Fragment.pushContentIntoContainer(containerId: Int, content: Fragment) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.addToBackStack("tag")
transaction.commit()
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
// here we address the bug
inline fun Fragment.reattachRetainedChildFragmentManager(childFragmentManager: FragmentManager) {
setChildFragmentManager(childFragmentManager)
updateChildFragmentsHost()
}
fun Fragment.setChildFragmentManager(childFragmentManager: FragmentManager) {
if (childFragmentManager is FragmentManagerImpl) {
mChildFragmentManager = childFragmentManager // mChildFragmentManager is private to Fragment, but the extension can touch it
}
}
fun Fragment.updateChildFragmentsHost() {
mChildFragmentManager.fragments.forEach { fragment -> // fragments is hidden in Fragment
fragment?.mHost = mHost // mHost is private also
}
}
The Fragment Hosting the child Fragments
class Tab1Fragment : Fragment() , TabRootFragment {
var subfragment: DefaultSubfragment? = null
var retainedChildFragmentManager: FragmentManager? = null
override val title = "Tab 1"
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLable = "label 1x"
subfragment!!.buttonText = "button 1"
addRootContentToContainer(R.id.child_container, content = subfragment!!)
}
return rootView
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (retainedChildFragmentManager != null) {
reattachRetainedChildFragmentManager(retainedChildFragmentManager!!)
} else {
retainedChildFragmentManager = childFragmentManager
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
}