I came across with problem when i'm trying to open BottomSheetDialogFragment in Fragment, using callback result from another fragment, that is nested in another activity.
All further demonstrations is abstractions of real case in the project with established application architecture which can't be change. Let's i will explain you.
I have the main host Activity called "MainActivity" which contains BaseFragment
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.flContainer.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
}
override fun onStart() {
super.onStart()
supportFragmentManager.beginTransaction()
.add(R.id.flContainer, BaseFragment())
.addToBackStack(BaseFragment.TAG)
.commitAllowingStateLoss()
}
override fun onResume() {
super.onResume()
Log.e("VadymTag", "MainActivity onResume")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log.e("VadymTag", "MainActivity onSaveInstanceState")
}
override fun onPause() {
super.onPause()
Log.e("VadymTag", "MainActivity onPause")
}
}
This BaseFragment open login screen using LoginActivity which contains LoginFragment because its necessary to authorize user.
class BaseFragment : Fragment() {
private var _binding: FragmentBaseBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentBaseBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding?.bOpenLogin?.setOnClickListener {
startActivity(Intent(requireContext(), LoginActivity::class.java))
}
MainNavigator.openBottomSheet = ::openBottomSheet
}
override fun onResume() {
super.onResume()
Log.e("VadymTag", "BaseFragment onResume")
}
fun openBottomSheet() {
val bottomSheetFragment = MyBottomSheetDialog()
bottomSheetFragment.show(childFragmentManager, MyBottomSheetDialog.TAG)
}
override fun onPause() {
super.onPause()
Log.e("VadymTag", "BaseFragment onPause")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log.e("VadymTag", "BaseFragment onSaveInstanceState")
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
val TAG = BaseFragment::class.java.simpleName
}
}
Also LoginActivity handles result of sucess / failed login using supportFragmentManager.setFragmentResultListener (...
. For this example FragmentResultListener handles any changes successfull.
LoginActivity asks MainNavigator to open BottomSheetDialogFragment from BaseFragment where was called login, for authorization user and finishes.
class LoginActivity : AppCompatActivity() {
private lateinit var binding: LoginMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LoginMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
override fun onStart() {
super.onStart()
supportFragmentManager.beginTransaction()
.add(R.id.flLoginContainer, LoginFragment())
.addToBackStack(LoginFragment.TAG)
.commit()
initLoginListener()
}
fun initLoginListener() {
supportFragmentManager
.setFragmentResultListener(LOGIN_KEY, this) { _, bundle ->
MainNavigator.openBottomSheet()
finish()
}
}
companion object {
const val LOGIN_KEY = "login_key"
const val LOGIN_FIELD = "login_key"
}
}
LoginFragment
class LoginFragment : Fragment() {
private var _binding: LoginFragmentBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = LoginFragmentBinding.inflate(inflater, container, false)
return _binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding?.bLoginSuccess?.setOnClickListener {
parentFragmentManager.setFragmentResult(LOGIN_KEY, bundleOf(LOGIN_FIELD to true))
}
}
companion object {
val TAG = LoginFragment::class.java.simpleName
}
}
MainNavigator is abstraction used for navigation cross tho whole application.
object MainNavigator {
var openBottomSheet: () -> Unit = {}
}
MainNavigator calls BaseFragment to open BottomSheetDialogFragment.
class MyBottomSheetDialog : BottomSheetDialogFragment() {
private var _binding: FragmentMyBottomSheetBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMyBottomSheetBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
val TAG = MyBottomSheetDialog::class.java.simpleName
}
}
When LoginActivity calls MainNavigator to open BottomSheetDialogFragment. - happens 2022-02-08 19:49:58.285 20135-20135/com.vadim.stackoverflowquestion E/VadymTag: BaseFragment onPause 2022-02-08 19:49:58.286 20135-20135/com.vadim.stackoverflowquestion E/VadymTag: MainActivity onPause 2022-02-08 19:49:58.790 0-0/? E/init: updatable process 'console' exited 4 times in 4 minutes 2022-02-08 19:49:59.027 20135-20135/com.vadim.stackoverflowquestion E/VadymTag: BaseFragment onSaveInstanceState 2022-02-08 19:49:59.031 20135-20135/com.vadim.stackoverflowquestion E/VadymTag: MainActivity onSaveInstanceState 2022-02-08 19:49:59.799 20135-20135/com.vadim.stackoverflowquestion E/AndroidRuntime: FATAL EXCEPTION: main Process: com.galazjukvadim.stackoverflowquestion, PID: 20135 java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:1844) at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1884) at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:329) at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:294) at androidx.fragment.app.DialogFragment.show(DialogFragment.java:260) at com.galazjukvadim.stackoverflowquestion.BaseFragment.openBottomSheet(BaseFragment.kt:52) at com.galazjukvadim.stackoverflowquestion.BaseFragment$onViewCreated$2.invoke(BaseFragment.kt:39) at com.galazjukvadim.stackoverflowquestion.BaseFragment$onViewCreated$2.invoke(BaseFragment.kt:39) at com.galazjukvadim.stackoverflowquestion.LoginActivity.initLoginListener$lambda-0(LoginActivity.kt:33) at com.galazjukvadim.stackoverflowquestion.LoginActivity.$r8$lambda$3dwuINVTP3WL69H0HgmUiCWJ7Dw(Unknown Source:0) at com.galazjukvadim.stackoverflowquestion.LoginActivity$$ExternalSyntheticLambda0.onFragmentResult(Unknown Source:2) at androidx.fragment.app.FragmentManager$LifecycleAwareResultListener.onFragmentResult(FragmentManager.java:256) at androidx.fragment.app.FragmentManager.setFragmentResult(FragmentManager.java:865) at com.galazjukvadim.stackoverflowquestion.LoginFragment.onViewCreated$lambda-0(LoginFragment.kt:30) at com.galazjukvadim.stackoverflowquestion.LoginFragment.$r8$lambda$PNHKtYyi4mi0uK7kLsV6wOurKW4(Unknown Source:0)
Its caused following problem called Activity state loss .
Reading following articless: 1, 2 i watched expected behavior .
My BaseFragment and MainActivity have called onPause
and after onSaveInstanceState
that triggered throw IllegalStateException: Can not perform this action after onSaveInstanceState .
In this case its affordable to use .commitAllowingStateLoss()
to show BottomSheetDialogFragment. But under hood of bottomSheetFragment.show(childFragmentManager, MyBottomSheetDialog.TAG)
is used usual ft.commit();
Does anybody know the solution for this?