Handling BottomSheetDialogFragment State - android

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

Related

Change view via ViewModel

I'm new to MVVM. I'm trying to figure out easiest way to change view from ViewModel. In fragment part I have navigation to next fragment
fun nextFragment(){
findNavController().navigate(R.id.action_memory_to_memoryEnd)
}
But I cannot call it from ViewModel. AFAIK it is not even possible and it destroys the conception of ViewModel.
I wanted to call fun nextFragment() when this condition in ViewModel is True
if (listOfCheckedButtonsId.size >= 18){
Memory.endGame()
}
Is there any simple way to change Views depending on values in ViewModel?
Thanks to Gorky's respond I figured out how to do that.
In Fragment I created observer
sharedViewModel.changeView.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
if (hasFinished) nextFragment()
})
I created changeView variable in ViewModel. When
var changeView = MutableLiveData<Boolean>()
change to true, observer call function.
source:
https://developer.android.com/codelabs/kotlin-android-training-live-data#6

android callback to dialogfragment from fragment through viewpager

Most of the Q/A deal with fragment -> dialog fragment communication. My scenario is just the reverse and I've been unsuccessful in finding an idea or way to be able to communicate from a fragment to a dialog fragment that uses a viewpager.
The dialog fragment has a viewpager and it uses a FragmentStatePagerAdapter to load the different fragments. There are only 2 fragments. Fragment 1 asks some questions and fragment 2 shows the results. Fragment 1 needs to send a command to the dialog fragment when it is done with the questions to load fragment 2.
I've tried using a interface but the app crashes as the listener is null in onAttach for the fragments. I've also tried inheriting a base dialog fragment class for all of the others but that also proved unsuccessful.
I'm probably missing something obvious or my process is is incorrect. Anyone have an idea or a suggestion on doing this?
In case anyone else comes along and is wondering how to go about this. I solved this by passing the dialog fragment (FragmentNewTrips) class through the viewpager and then into the child fragment where I set it as the listener.
So, when I defined the adapter I passed the listener which was the dialog fragment
AdapterPagerTrips(childFragmentManager, tripIds, this)
Then within the adapter I passed the dialog fragment class to the corresponding fragment that needed it like so
class AdapterPagerTrips (fragmentManager: FragmentManager, val tripIds: ArrayList<String?>,
val listener: FragmentNewTrips):FragmentStatePagerAdapter(fragmentManager,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
// 2
override fun getItem(position: Int): Fragment {
if (position == 0) return FragmentNewTripsTypes.newInstance(listener)
return FragmentNewTripsResults.newInstance(tripIds, listener)
}
// 3
override fun getCount(): Int {
return 2
}
}
And each fragment had a companion object that would set the listener
companion object {
fun newInstance(tripIds: ArrayList<String?>, listener: FragmentNewTrips) = FragmentNewTripsResults().apply {
arguments = Bundle().apply { putStringArrayList(TRIPIDS, tripIds) }
this.listener = listener
}
}

Android Kotlin Coroutine on screen rotation

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.

Navigate to fragment on FAB click (Navigation Architecture Components)

I have no idea how to, using the new navigation architecture component, navigate from my main screen (with a FloatingActionButton attatched to a BottomAppBar) to another screen without the app bar.
When I click the fab I want my next screen (fragment?) to slide in from the right. The problem is where do I put my BottomAppBar? If I put it in my MainActivity then I have the issue of the FloatingActionButton not having a NavController set. I also cannot put my BottomAppBar in my Fragment. I am at a loss.
Ran into this issue today and I found out that there is a simple and elegant solution for it.
val navController = findNavController(R.id.navHostFragment)
fabAdd.setOnClickListener {
navController.navigate(R.id.yourFragment)
}
This takes care of the navigation. Then you must control the visibility of your BottomAppBar inside your Activity.
You could have your BottomAppBar in MainActivity and access your FloatingActionButton in your fragment as follows
activity?.fab?.setOnClickListener {
/*...*/
findNavController().navigate(R.id.action_firstFragment_to_secondFragment, mDataBundle)
}
You could hide the BottomAppBar from another activity as follows
(activity as AppCompatActivity).supportActionBar?.hide()
Make sure you .show() the BottomAppBar while returning to previous fragment
Put it in MainActivity and setOnClickListener in onStart() of the activity and it will work fine.
override fun onStart() {
super.onStart()
floatingActionButton.setOnClickListener {
it.findNavController().navigate(R.id.yourFragment)
}
}
Note:This solution is like and hack and better is to follow Activity LifeCycle and setUp OnClickListener when the activity is ready to interact.
Similar question [SOLVED]
if you wanted to navigate to certain fragment (not the star one) in the beginning for some reason, and also you have to graphs for one activity, here is what I suggest:
this method will start activity
companion object {
const val REQUEST_OR_CONFIRM = "request_or_confirm"
const val IS_JUST_VIEW = "IS_JUST_VIEW"
const val MODEL = "model"
fun open(activity: Activity, isRequestOrConfirm: Boolean, isJustView: Boolean = false, model: DataModel? = null) {
val intent = Intent(activity, HostActivity::class.java)
intent.putExtra(REQUEST_OR_CONFIRM, isRequestOrConfirm)
intent.putExtra(IS_JUST_VIEW, isJustView)
intent.putExtra(MODEL, model)
activity.startActivity(intent)
}
}
and then in, onCreate method of Host Activity, first decide which graph to use and then pass the intent extras bundle so the start fragment can decide what to do:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_purchase_nav)
if (intent.getBooleanExtra(REQUEST_OR_CONFIRM, true)) {
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_first_scenario, intent.extras)
} else {
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_second_scenario, intent.extras)
}
}
and here's how you can decide what to do in start fragment:
if (arguments != null && arguments!!.getBoolean(HostActivity.IS_JUST_VIEW)){
navigateToYourDestinationFrag(arguments!!.getParcelable<DataModel>(HostActivity.MODEL))
}
and then navigate like you would do normally:
private fun navigateToYourDestinationFrag(model: DataModel) {
val action = StartFragmentDirections.actionStartFragmentToOtherFragment(model)
findNavController().navigate(action)
}
here's how your graph might look in case you wanted to jump to the third fragment in the beginning
PS: make sure you will handle back button on the third fragment, here's a solution
UPDATE:
as EpicPandaForce mentioned, you can also start activities using Navigation Components:
to do that, first add the Activity to your existing graph, either by the + icon (which didn't work for me) or by manually adding in the xml:
<activity
android:id="#+id/secondActivity"
tools:layout="#layout/activity_second"
android:name="com.amin.SecondActivity" >
</activity>
you can also add arguments and use them just like you would in a fragment, with navArgs()
<activity
android:id="#+id/secondActivity"
tools:layout="#layout/activity_second"
android:name="com.amin.SecondActivity" >
<argument
android:name="testArgument"
app:argType="string"
android:defaultValue="helloWorld" />
</activity>
in koltin,here's how you would use the argument, First declare args with the type of generated class named after you activity, in this case SecondActivityArgs in top of your activity class:
val args: SecondActivityArgsby by navArgs()
and then you can use it like this:
print(args.testArgument)
This doesn't destroy BottomAppBar. Add this to MainActivity only and don't do anything else
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
fabAdd.setOnClickListener {
findNavController(navHostFragment).navigate(R.id.fab)
}

Fragment instance is retained but child fragment is not re-attached

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
}
}

Categories

Resources