I'm having a heap of strange problems with fragments that contain fragments embedded at design time in the axml.
My question is when I've finished with the fragment that I've loaded into a View with the FragmentManager, and then removed also using the FragmentManager, are the implicitly loaded fragments automatically destroyed? If not how should I clean up the parent fragment so that the embedded fragments are also drestroyed. Also when the parent fragment is destroyed do I need to call View.RemoveAllViews() to remove the fragments layout?
This seems to work. Capture the fragments as they are loaded into a list and then remove them when the main fragment is unloaded later. ( Must Dispose() )
public partial class MainActivity : Activity
{
private LinearLayout LoaderLayout;
private readonly List<Fragment> ActiveFragments = new List<Fragment>();
public override void OnAttachFragment( Fragment fragment ) { ActiveFragments.Add( fragment ); }
private async void ClearLoadFrame()
{
if( LoaderLayout == null )
LoaderLayout = FindViewById<LinearLayout>( Resource.Id.loaderLayout );
var Transaction = FragmentManager.BeginTransaction();
foreach( var Frag in ActiveFragments )
{
Transaction.Remove( Frag );
Frag.Dispose();
}
Transaction.CommitAllowingStateLoss();
ActiveFragments.Clear();
var Completed = false;
RunOnUiThread( () =>
{
LoaderLayout.RemoveAllViews();
Completed = true;
} );
await Task.Run( () =>
{
while( !Completed )
Thread.Sleep(50);
});
}
I discovered that what I'm trying to do isn't allowed.
From this article
Note that one limitation is that nested (or child) fragments must be dynamically added at runtime to their parent fragment and cannot be statically added using the tag.
Related
What is the proper way of navigating back from nested fragments of ViewPager2?
Despite using app:defaultNavHost="true"with FragmentContainerView pressing back button while in a nested fragment of a page calls Activity's back press instead of navigating back to previous fragment.
As per the Create a NavHostFragment documentation, app:defaultNavHost="true" calls setPrimaryNavigationFragment() when the Fragment is first added - it is setPrimaryNavigationFragment() that routes back button press events to that fragment automatically.
In a ViewPager2 though, it is the ViewPager2 that is responsible for creating and adding the Fragment. Since every level of the Fragment hierarchy needs to be the primary navigation fragment, adding a child fragment via XML still doesn't solve the missing link: that the Fragment in the ViewPager2 needs to be the primary navigation fragment.
Therefore, you need to hook into the callbacks for when a Fragment is made the active Fragment and call setPrimaryNavigationFragment(). ViewPager2 1.1.0-alpha01 adds exactly this API in the FragmentTransactionCallback, specifically, the onFragmentMaxLifecyclePreUpdated(), which is called whenever the Lifecycle state of a Fragment is changed: when it is changed to RESUMED, that Fragment is now the active fragment and should become the primary navigation Fragment as part of the onPost callback.
private class Adapter(parentFragment: Fragment) : FragmentStateAdapter(parentFragment) {
init {
// Add a FragmentTransactionCallback to handle changing
// the primary navigation fragment
registerFragmentTransactionCallback(object : FragmentTransactionCallback() {
override fun onFragmentMaxLifecyclePreUpdated(
fragment: Fragment,
maxLifecycleState: Lifecycle.State
) = if (maxLifecycleState == Lifecycle.State.RESUMED) {
// This fragment is becoming the active Fragment - set it to
// the primary navigation fragment in the OnPostEventListener
OnPostEventListener {
fragment.parentFragmentManager.commitNow {
setPrimaryNavigationFragment(fragment)
}
}
} else {
super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)
}
})
}
// The rest of your FragmentStateAdapter...
}
You need to override your parent activity's onBackPressed logic, you need to use https://developer.android.com/reference/androidx/navigation/NavController#popBackStack() to navigate up in your nav graph of nested fragment.
Here is the Java version of #ianhanniballake answer (yes im a luddite for not using kotlin yet - i need to know java inside out first before i learn anything else).
I havent unit tested this yet, but it "works"...
public class ViewPagerAdapter extends FragmentStateAdapter {
public ViewPagerAdapter(#NonNull FragmentManager fragmentManager,
#NonNull Lifecycle lifecycle) {
super(fragmentManager, lifecycle);
registerFragmentTransactionCallback(new FragmentTransactionCallback() {
#NonNull
#Override
public OnPostEventListener onFragmentMaxLifecyclePreUpdated(#NonNull Fragment fragmentArg,
#NonNull Lifecycle.State maxLifecycleState) {
if (maxLifecycleState == Lifecycle.State.RESUMED) {
return () -> fragmentArg.getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(fragmentArg).commitNow();
} else {
return super.onFragmentMaxLifecyclePreUpdated(fragmentArg, maxLifecycleState);
}
}
});
}
// remainder of your FragmentStateAdapter here
}
In the main fragment of my activity, I generate more fragments dependent on a config file.
When the device is rotated, I want to reuse the former generated fragments instead of creating them again.
This is the creation method, called in onCreateView of the main fragment:
private fun initChildFragments(elementsContainer: LinearLayout) {
val transaction = childFragmentManager.beginTransaction()
for (element in config.elements) {
val id = element.id
// create new container for fragment
addElementContainer(elementsContainer, id)
// try to re-use old fragment
var fragment = childFragmentManager.findFragmentByTag(element.getTag())
if (fragment == null) {
// create new fragment (should only happen at first run)
fragment = TestFragment.newInstance()
}
// add fragment to our container.
transaction.replace(id, fragment, element.getTag()
}
transaction.commitNow()
}
private fun addElementContainer(container: LinearLayout, id: Int) {
val childContainer = FrameLayout(context)
childContainer.id = id
container.addView(childContainer)
// for test purposes to make the container visible
val textView = TextView(context)
textView.setText("ID: " + id)
container.addView(textView)
}
When I first start the app, everything works fine. When I rotate the device, the fragments where found by findFragmentByTag and added to the containers. The element containers are visible on the screen (through the text view), but the fragments are not visible.
If I re-create the fragments every time the device rotates, they appear on the screen again, but then I loose all my ViewModel data.
While trying to reopen the application after onDestroy() is called, I can't seem to update my ListView within my two Fragments ( Anytime, and Upcoming ) as they're nested within a TabLayout within my MainScreen's ViewPager.
When I return to my application, I need to update the ListViews with data I'm getting from my server, and my networking logic is handled within my MainScreen class so I can tell it's children what to display.
However, when I call FeedPagerAdapter's update method, my AnytimeFragment's getActivity() call often returns null. It doesn't return null if I call it during onCreateView(...), but awhile after ( loading server data ), it does return null. Although, if I cause onPause and onResume to be called after this, it'll display correctly.
I feel like I'm doing something incorrectly with how I'm handling my Fragments within my TabLayout, or getting them back after onDestroy().
This is the flow I'm using when trying to update my AnytimeFragment after I have acquired the data from my server.
MainScreen -> Handle to Feed Fragment -> FeedPagerAdapter's update -> Anytime's update.
Thanks for your help.
Main Screen Pager with three Fragments ( Feed, Create and Home ).
private class MainPagerAdapter extends FragmentStatePagerAdapter {
public MainPagerAdapter ( FragmentManager fm ) {
super( fm );
}
#Override
public Fragment getItem( int position ) {
switch ( position ) {
default:
case FEED:
// Child with TabLayout and two Fragments I'm trying to update.
return new Feed();
case CREATE:
return new Create();
case HOME:
return new Home();
}
}
#Override
public int getCount() {
return 3;
}
}
Feed TabLayout with two Fragments ( Anytime, and Upcoming ).
public class FeedPagerAdapter extends FragmentPagerAdapter {
public FeedPagerAdapter ( FragmentManager fm, Context context ) {
super( fm );
this.context = context;
}
#Override
public Fragment getItem( int position ) {
switch ( position ) {
default:
case ANYTIME:
return new AnytimeFragment();
case UPCOMING:
return new UpcomingFragment();
}
}
#Override
public int getCount() {
return 2;
}
public void update() {
( ( AnytimeFragment) getItem( ANYTIME) ).update();
}
}
Within my Anytime Fragment.
public void update() {
// This often returns null.
if( getActivity() == null )
return;
// Update the ListView.
}
I found out how to fix my problem.
First, I was recreating my FeedPagerAdapter onResume within my Feed class. I noticed it would call Anytime.update() more times than I wanted.
Second, I changed my listView within the Anytime Fragment to be static, as it was sometimes null.
I'm using the support library v4 and my questions are, How to know if a Fragment is Visible? and How can I change the propierties of the Layout inflated in the Fragment?
I'm using fragments like in the android developers tutorial with a FragmentActivity.
You should be able to do the following:
MyFragmentClass test = (MyFragmentClass) getSupportFragmentManager().findFragmentByTag("testID");
if (test != null && test.isVisible()) {
//DO STUFF
}
else {
//Whatever
}
Both isVisible() and isAdded() return true as soon as the Fragment is created, and not even actually visible. The only solution that actually works is:
if (isAdded() && isVisible() && getUserVisibleHint()) {
// ... do your thing
}
This does the job. Period.
NOTICE:
getUserVisibleHint() is now deprecated. be careful.
If you want to know when use is looking at the fragment you should use
yourFragment.isResumed()
instead of
yourFragment.isVisible()
First of all isVisible() already checks for isAdded() so no need for calling both. Second, non-of these two means that user is actually seeing your fragment. Only isResumed() makes sure that your fragment is in front of the user and user can interact with it if thats whats you are looking for.
you can try this way:
Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragment_container);
or
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
In this if, you check if currentFragment is instance of YourFragment
if (currentFragment instanceof YourFragment) {
Log.v(TAG, "your Fragment is Visible");
}
You can override setMenuVisibility like this:
#Override
public void setMenuVisibility(final boolean visible) {
if (visible) {
//Do your stuff here
}
super.setMenuVisibility(visible);
}
getUserVisibleHint() comes as true only when the fragment is on the view and visible
One thing to be aware of, is that isVisible() returns the visible state of the current fragment. There is a problem in the support library, where if you have nested fragments, and you hide the parent fragment (and therefore all the children), the child still says it is visible.
isVisible() is final, so can't override unfortunately. My workaround was to create a BaseFragment class that all my fragments extend, and then create a method like so:
public boolean getIsVisible()
{
if (getParentFragment() != null && getParentFragment() instanceof BaseFragment)
{
return isVisible() && ((BaseFragment) getParentFragment()).getIsVisible();
}
else
{
return isVisible();
}
}
I do isVisible() && ((BaseFragment) getParentFragment()).getIsVisible(); because we want to return false if any of the parent fragments are hidden.
This seems to do the trick for me.
ArticleFragment articleFrag = (ArticleFragment)
getSupportFragmentManager().findFragmentById(R.id.article_fragment);
if (articleFrag != null && articleFrag.isVisible()) {
// Call a method in the ArticleFragment to update its content
articleFrag.updateArticleView(position);
}
see http://developer.android.com/training/basics/fragments/communicating.html
Just in case you use a Fragment layout with a ViewPager (TabLayout), you can easily ask for the current (in front) fragment by ViewPager.getCurrentItem() method. It will give you the page index.
Mapping from page index to fragment[class] should be easy as you did the mapping in your FragmentPagerAdapter derived Adapter already.
int i = pager.getCurrentItem();
You may register for page change notifications by
ViewPager pager = (ViewPager) findViewById(R.id.container);
pager.addOnPageChangeListener(this);
Of course you must implement interface ViewPager.OnPageChangeListener
public class MainActivity
extends AppCompatActivity
implements ViewPager.OnPageChangeListener
{
public void onPageSelected (int position)
{
// we get notified here when user scrolls/switches Fragment in ViewPager -- so
// we know which one is in front.
Toast toast = Toast.makeText(this, "current page " + String.valueOf(position), Toast.LENGTH_LONG);
toast.show();
}
public void onPageScrolled (int position, float positionOffset, int positionOffsetPixels) {
}
public void onPageScrollStateChanged (int state) {
}
}
My answer here might be a little off the question. But as a newbie to Android Apps I was just facing exactly this problem and did not find an answer anywhere. So worked out above solution and posting it here -- perhaps someone finds it useful.
Edit: You might combine this method with LiveData on which the fragments subscribe. Further on, if you give your Fragments a page index as constructor argument, you can make a simple amIvisible() function in your fragment class.
In MainActivity:
private final MutableLiveData<Integer> current_page_ld = new MutableLiveData<>();
public LiveData<Integer> getCurrentPageIdx() { return current_page_ld; }
public void onPageSelected(int position) {
current_page_ld.setValue(position);
}
public class MyPagerAdapter extends FragmentPagerAdapter
{
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page: But only on first
// creation -- not on restore state !!!
// see: https://stackoverflow.com/a/35677363/3290848
switch (position) {
case 0:
return MyFragment.newInstance(0);
case 1:
return OtherFragment.newInstance(1);
case 2:
return XYFragment.newInstance(2);
}
return null;
}
}
In Fragment:
public static MyFragment newInstance(int index) {
MyFragment fragment = new MyFragment();
Bundle args = new Bundle();
args.putInt("idx", index);
fragment.setArguments(args);
return fragment;
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mPageIndex = getArguments().getInt(ARG_PARAM1);
}
...
}
public void onAttach(Context context)
{
super.onAttach(context);
MyActivity mActivity = (MyActivity)context;
mActivity.getCurrentPageIdx().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer data) {
if (data == mPageIndex) {
// have focus
} else {
// not in front
}
}
});
}
Try this if you have only one Fragment
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
//TODO: Your Code Here
}
Adding some information here that I experienced:
fragment.isVisible is only working (true/false) when you replaceFragment() otherwise if you work with addFragment(), isVisible always returns true whether the fragment is in behind of some other fragment.
None of the above solutions worked for me.
The following however works like a charm:-
override fun setUserVisibleHint(isVisibleToUser: Boolean)
getUserVisibleHint is now deprecated, and I was having problems with isVisible being true when another fragment was added in front of it. This detects the fragment's visibility on the back stack using its view. This may be helpful if your issue is related to other fragments on the back stack.
View extension to detect if a view is being displayed on the screen: (see also How can you tell if a View is visible on screen in Android?)
fun View.isVisibleOnScreen(): Boolean {
if (!isShown) return false
val actualPosition = Rect().also { getGlobalVisibleRect(it) }
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
val screenHeight = Resources.getSystem().displayMetrics.heightPixels
val screen = Rect(0, 0, screenWidth, screenHeight)
return Rect.intersects(actualPosition, screen)
}
Then defined a back stack listener from the fragment, watching the top fragment on the stack (the one added last)
fun Fragment.setOnFragmentStackVisibilityListener(onVisible: () -> Unit) {
val renderDelayMillis = 300L
parentFragmentManager.addOnBackStackChangedListener {
Handler(Looper.getMainLooper()).postDelayed({
if (isAdded) {
val topStackFragment = parentFragmentManager.fragments[parentFragmentManager.fragments.size - 1]
if (topStackFragment.view == view && isVisible && view!!.isVisibleOnScreen()) {
onVisible.invoke()
}
}
}, renderDelayMillis)
}
}
The back stack listener is called before the view is ready so an arbitrarily small delay was needed. The lambda is called when the view becomes visible.
I was using Android's BottomNavigationView and managing fragments with FragmentTransactions.hide(frag) and FragmentTransaction.show(frag). So, to detect if a fragment is visible or not, I used following:
abstract class BaseFragment : Fragment() {
open fun onFragmentVisible(){
}
override fun onStart() {
super.onStart()
if (!isHidden){
onFragmentVisible()
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (!hidden){
onFragmentVisible()
}
}
}
You can extend BaseFragment in your fragment and implement it's onFragmentVisible function.
In Kotlin
if you use FragmentPagerAdapter and since getUserVisibleHint() is deprecated in api 29, I suggest you to add behaviour parameter BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT in your FragmentPagerAdapter like this:
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
then in your fragment you can check using their lifecycle state:
if(lifecycle.currentState == Lifecycle.State.RESUMED) {
// do something when fragment is visible
}
I have a ViewPager (extends FragmentPagerAdapter) which holds two Fragments. What I need is just refresh a ListView for each Fragment when I swipe among them. For this I have implemented ViewPager.OnPageChangeListener interface (namely onPageScrollStateChanged). In order to hold references to Fragments I use a HashTable. I store references to Fragments in HashTable in getItem() method:
#Override
public Fragment getItem(int num) {
if (num == 0) {
Fragment itemsListFragment = new ItemsListFragment();
mPageReferenceMap.put(num, itemsListFragment);
return itemsListFragment;
} else {
Fragment favsListFragment = new ItemsFavsListFragment();
mPageReferenceMap.put(num, favsListFragment);
return favsListFragment;
}
}
So when I swipe from one Fragment to another the onPageScrollStateChanged triggers where I use the HashTable to call required method in both Fragments (refresh):
public void refreshList() {
((ItemsListFragment) mPageReferenceMap.get(0)).refresh();
((ItemsFavsListFragment) mPageReferenceMap.get(1)).refresh();
}
Everything goes fine until orientation change event happens. After it the code in refresh() method, which is:
public void refresh() {
mAdapter.changeCursor(mDbHelper.getAll());
getListView().setItemChecked(-1, true); // The last row from a exception trace finishes here (my class).
}
results in IllegalStateException:
java.lang.IllegalStateException: Content view not yet created
at android.support.v4.app.ListFragment.ensureList(ListFragment.java:328)
at android.support.v4.app.ListFragment.getListView(ListFragment.java:222)
at ebeletskiy.gmail.com.passwords.ui.ItemsFavsListFragment.refresh(ItemsFavsListFragment.java:17)
Assuming the Content view is not created indeed I set the boolean variable in onActivityCreated() method to true and used if/else condition to call getListView() or not, which shown the activity and content view successfully created.
Then I was debugging to see when FragmentPagerAdapter invokes getItem() and it happens the method is not called after orientation change event. So looks like it ViewPager holds references to old Fragments. This is just my assumption.
So, is there any way to enforce the ViewPager to call getItem() again, so I can use proper references to current Fragments? May be some other solution? Thank you very much.
Then I was debugging to see when FragmentPagerAdapter invokes getItem() and it happens the method is not called after orientation change event. So looks like it ViewPager holds references to old Fragments.
The fragments should be automatically recreated, just like any fragment is on an configuration change. The exception would be if you used setRetainInstance(true), in which case they should be the same fragment objects as before.
So, is there any way to enforce the ViewPager to call getItem() again, so I can use proper references to current Fragments?
What is wrong with the fragments that are there?
I've spent some days searching for a solution for this problem, and many points was figured out:
use FragmentPagerAdapter instead of FragmentStatePagerAdapter
use FragmentStatePagerAdapter instead of FragmentPagerAdapter
return POSITION_NONE on getItemPosition override of FragmentPagerAdapter
don't use FragmentPagerAdapter if you need dynamic changes of Fragments
and many many many others...
In my app, like Eugene, I managed myself the instances of created fragments. I keep that in one HashMap<String,Fragment> inside some specialized class, so the fragments are never released, speeding up my app (but consuming more resources).
The problem was when I rotate my tablet (and phone). The getItem(int) wasn't called anymore for that fragment, and I couldn't change it.
I really spent many time until really found a solution, so I need share it with StackOverflow community, who helps me so many many times...
The solution for this problem, although the hard work to find it, is quite simple:
Just keep the reference to FragmentManager in the constructor of FragmentPagerAdapter extends:
public class Manager_Pager extends FragmentPagerAdapter {
private final FragmentManager mFragmentManager;
private final FragmentActivity mContext;
public Manager_Pager(FragmentActivity context) {
super( context.getSupportFragmentManager() );
this.mContext = context;
this.mFragmentManager = context.getSupportFragmentManager();
}
#Override
public int getItemPosition( Object object ) {
// here, check if this fragment is an instance of the
// ***FragmentClass_of_you_want_be_dynamic***
if (object instanceof FragmentClass_of_you_want_be_dynamic) {
// if true, remove from ***FragmentManager*** and return ***POSITION_NONE***
// to force a call to ***getItem***
mFragmentManager.beginTransaction().remove((Fragment) object).commit();
return POSITION_NONE;
}
//don't return POSITION_NONE, avoid fragment recreation.
return super.getItemPosition(object);
}
#Override
public Fragment getItem( int position ) {
if ( position == MY_DYNAMIC_FRAGMENT_INDEX){
Bundle args = new Bundle();
args.putString( "anything", position );
args.putString( "created_at", ALITEC.Utils.timeToStr() );
return Fragment.instantiate( mContext, FragmentClass_of_you_want_be_dynamic.class.getName(), args );
}else
if ( position == OTHER ){
//...
}else
return Fragment.instantiate( mContext, FragmentDefault.class.getName(), null );
}
}
Thats all. And it will work like a charm...
You can clear the saved instance state
protected void onCreate(Bundle savedInstanceState) {
clearBundle(savedInstanceState);
super.onCreate(savedInstanceState, R.layout.activity_car);
}
private void clearBundle(Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.remove("android:fragments");
savedInstanceState.remove("android:support:fragments");
savedInstanceState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
savedInstanceState.remove("android:lastAutofillId");
}
}