Mettre à jour les données dans ListFragment dans le cadre de ViewPager

142

J'utilise la compatibilité v4 ViewPager sous Android. Mon FragmentActivity a un tas de données qui doivent être affichées de différentes manières sur différentes pages de mon ViewPager. Jusqu'à présent, je n'ai que 3 instances du même ListFragment, mais à l'avenir j'aurai 3 instances de ListFragments différents. Le ViewPager est sur un écran de téléphone vertical, les listes ne sont pas côte à côte.

Maintenant, un bouton sur le ListFragment démarre une activité pleine page distincte (via FragmentActivity), qui retourne et FragmentActivity modifie les données, les enregistre, puis tente de mettre à jour toutes les vues dans son ViewPager. C'est ici, où je suis coincé.

public class ProgressMainActivity extends FragmentActivity
{
    MyAdapter mAdapter;
    ViewPager mPager;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
    ...
        mAdapter = new MyAdapter(getSupportFragmentManager());

        mPager = (ViewPager) findViewById(R.id.viewpager);
        mPager.setAdapter(mAdapter);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        ...
        updateFragments();
        ...
    }
    public void updateFragments()
    {
        //Attempt 1:
        //mAdapter.notifyDataSetChanged();
        //mPager.setAdapter(mAdapter);

        //Attempt 2:
        //HomeListFragment fragment = (HomeListFragment) getSupportFragmentManager().findFragmentById(mAdapter.fragId[0]);
        //fragment.updateDisplay();
    }

    public static class MyAdapter extends FragmentPagerAdapter implements
         TitleProvider
    {
      int[] fragId = {0,0,0,0,0};
      public MyAdapter(FragmentManager fm)
      {
         super(fm);
      }
      @Override
      public String getTitle(int position){
         return titles[position];
      }
      @Override
      public int getCount(){
         return titles.length;
      }

      @Override
      public Fragment getItem(int position)
      {

         Fragment frag = HomeListFragment.newInstance(position);
         //Attempt 2:
         //fragId[position] = frag.getId();
         return frag;
      }

      @Override
      public int getItemPosition(Object object) {
         return POSITION_NONE; //To make notifyDataSetChanged() do something
     }
   }

    public class HomeListFragment extends ListFragment
    {
    ...
        public static HomeListFragment newInstance(int num)
        {
            HomeListFragment f = new HomeListFragment();
            ...
            return f;
        }
   ...

Maintenant, comme vous pouvez le voir, ma première tentative a été de notifierDataSetChanged sur l'ensemble du FragmentPagerAdapter, et cela a montré qu'il fallait parfois mettre à jour les données, mais d'autres j'ai eu une IllegalStateException: impossible d'effectuer cette action après onSaveInstanceState.

Ma deuxième tentative consistait à essayer d'appeler une fonction de mise à jour dans mon ListFragment, mais getId dans getItem a renvoyé 0. Selon les documents que j'ai essayés par

acquérir une référence au Fragment à partir de FragmentManager, en utilisant findFragmentById () ou findFragmentByTag ()

mais je ne connais pas la balise ou l'identifiant de mes fragments! J'ai un android: id = "@ + id / viewpager" pour ViewPager, et un android: id = "@ android: id / list" pour mon ListView dans la mise en page ListFragment, mais je ne pense pas que ceux-ci soient utiles.

Alors, comment puis-je: a) mettre à jour l'intégralité du ViewPager en toute sécurité en une seule fois (idéalement en renvoyant l'utilisateur à la page sur laquelle il se trouvait auparavant) - il est normal que l'utilisateur voit la vue changer. Ou de préférence, b) appelez une fonction dans chaque ListFragment affecté pour mettre à jour le ListView manuellement.

Toute aide serait acceptée avec gratitude!

barkside
la source

Réponses:

58

Essayez d'enregistrer la balise chaque fois qu'un Fragement est instancié.

public class MPagerAdapter extends FragmentPagerAdapter {
    private Map<Integer, String> mFragmentTags;
    private FragmentManager mFragmentManager;

    public MPagerAdapter(FragmentManager fm) {
        super(fm);
        mFragmentManager = fm;
        mFragmentTags = new HashMap<Integer, String>();
    }

    @Override
    public int getCount() {
        return 10;
    }

    @Override
    public Fragment getItem(int position) {
        return Fragment.instantiate(mContext, AFragment.class.getName(), null);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object obj = super.instantiateItem(container, position);
        if (obj instanceof Fragment) {
            // record the fragment tag here.
            Fragment f = (Fragment) obj;
            String tag = f.getTag();
            mFragmentTags.put(position, tag);
        }
        return obj;
    }

    public Fragment getFragment(int position) {
        String tag = mFragmentTags.get(position);
        if (tag == null)
            return null;
        return mFragmentManager.findFragmentByTag(tag);
    }
}
faylon
la source
Cela semble correct mais cela ne fonctionne pas dans ma classe. Cela fonctionne-t-il avec vous? Je reçois toujours un nullfragment.
sandalone du
1
Cela fonctionne pour moi, je l'ai utilisé dans plusieurs projets. Vous pouvez essayer de lire le code source de FragmentPagerAdapter et imprimer certains journaux à déboguer.
faylon
Merci fonctionne comme un charme même après la rotation des écrans.Même j'ai pu changer le contenu de FragmentA en utilisant MyActivity de FragmentB, en utilisant cette solution.
Mehdi Fanai
5
Cela fonctionne FragmentPagerAdapermais échoue pour FragmentStatePagerAdaper( Fragment.getTag()toujours revenir null.
Engine Bai
1
J'ai essayé cette solution, créé une méthode pour mettre à jour les données du fragment. Mais maintenant, je ne suis pas en mesure de voir les vues de fragment. Je peux voir que les données vont se fragmenter mais les vues ne sont pas visibles. De l'aide?
Arun Badole
256

La réponse de Barkside fonctionne avec FragmentPagerAdaptermais ne fonctionne pas avec FragmentStatePagerAdapter, car elle ne définit pas de balises sur les fragments auxquels elle passe FragmentManager.

Avec FragmentStatePagerAdapteril semble que nous pouvons nous en sortir, en utilisant son instantiateItem(ViewGroup container, int position)appel. Il renvoie la référence au fragment à la position position. Si FragmentStatePagerAdaptercontient déjà la référence au fragment en question, instantiateItemrenvoie simplement la référence à ce fragment et n'appelle pas getItem()pour l'instancier à nouveau.

Supposons donc que je regarde actuellement le fragment # 50 et que je souhaite accéder au fragment # 49. Comme ils sont proches, il y a de fortes chances que le # 49 soit déjà instancié. Alors,

ViewPager pager = findViewById(R.id.viewpager);
FragmentStatePagerAdapter a = (FragmentStatePagerAdapter) pager.getAdapter();
MyFragment f49 = (MyFragment) a.instantiateItem(pager, 49)
Pēteris Caune
la source
5
Je simplifierais cela en faisant simplement "a" un PagerAdapter (PagerAdapter a = pager.getAdapter ();). Ou même MyFragment f49 = (MyFragment) pager.getAdapter (). InstantiateItem (pager, 49); instantiateItem () provient de PagerAdapter, vous n'avez donc pas à le transtyper pour appeler instantiateItem ()
Dandre Allison
12
... et juste au cas où quelqu'un se demanderait, le ViewPager garde une trace de l'index du fragment visualisé (ViewPager.getCurrentItem ()), qui est le 49 dans l'exemple ci-dessus ... mais je dois dire que je Je suis étonné que l'API ViewPager ne renvoie pas directement une référence au fragment réel.
mblackwell8
6
Avec FragmentStatePagerAdapter, utiliser le code ci-dessus avec a.instantiateItem(viewPager, viewPager.getCurrentItem());(pour se débarrasser du 49) comme proposé par @ mblackwell8 fonctionne parfaitement.
Cedric Simon
1
Attendez, donc je dois passer le ViewPager à chaque fragment qu'il contient juste pour pouvoir faire des choses aux vues dans le fragment? Actuellement, tout ce que je fais dans onCreate sur la page actuelle se passe sur la page suivante. Cela semble extrêmement louche en termes de fuite.
G_V
il est important de mentionner que tous les appels à instantiateItemdoivent être entourés de startUpdate/ finishUpdatecalls. les détails sont dans ma réponse à une question similaire: stackoverflow.com/questions/14035090/…
morgwai
154

OK, je pense avoir trouvé un moyen d'exécuter la demande b) dans ma propre question afin que je partage pour le bénéfice des autres. La balise des fragments à l'intérieur d'un ViewPager est dans le formulaire "android:switcher:VIEWPAGER_ID:INDEX", où VIEWPAGER_IDest la mise R.id.viewpageren page XML, et INDEX est la position dans le viewpager. Donc si la position est connue (par exemple 0), je peux jouer dans updateFragments():

      HomeListFragment fragment = 
          (HomeListFragment) getSupportFragmentManager().findFragmentByTag(
                       "android:switcher:"+R.id.viewpager+":0");
      if(fragment != null)  // could be null if not instantiated yet
      {
         if(fragment.getView() != null) 
         {
            // no need to call if fragment's onDestroyView() 
            //has since been called.
            fragment.updateDisplay(); // do what updates are required
         }
      }

Je ne sais pas si c'est une façon valable de le faire, mais cela fonctionnera jusqu'à ce que quelque chose de mieux soit suggéré.

barkside
la source
4
Je me suis connecté juste pour attribuer +1 à votre réponse et question.
SuitUp
5
puisque ce n'est pas dans la documentation officielle, je ne l'utiliserais pas. Et s'ils modifiaient le tag dans une prochaine version?
Thierry-Dimitri Roy
4
FragmentPagerAdapter est livré avec une méthode statique makeFragmentNameutilisée pour générer la balise que vous pouvez utiliser à la place pour une approche légèrement moins hacky: HomeListFragment fragment = (HomeListFragment) getSupportFragmentManager().findFragmentByTag(FragmentPagerAdapter.makeFragmentName(R.id.viewpager, 0));
dgmltn
8
@dgmltn, makeFragmentNameest une private staticméthode, vous ne pouvez donc pas y accéder.
StenaviN
1
Douce mère de Jésus, cela fonctionne! Je vais juste garder mon doigt croisé et l'utiliser.
Bogdan Alexandru
35

Si vous me le demandez, la deuxième solution sur la page ci-dessous, garder une trace de toutes les pages de fragments "actives", est meilleure: http://tamsler.blogspot.nl/2011/11/android-viewpager-and-fragments-part -ii.html

La réponse de barkside est trop hacky pour moi.

vous gardez une trace de toutes les pages de fragments "actives". Dans ce cas, vous effectuez le suivi des pages de fragment dans le FragmentStatePagerAdapter, qui est utilisé par le ViewPager.

private final SparseArray<Fragment> mPageReferences = new SparseArray<Fragment>();

public Fragment getItem(int index) {
    Fragment myFragment = MyFragment.newInstance();
    mPageReferences.put(index, myFragment);
    return myFragment;
}

Pour éviter de conserver une référence à des pages de fragment "inactives", nous devons implémenter la méthode destroyItem (...) de FragmentStatePagerAdapter:

public void destroyItem(View container, int position, Object object) {
    super.destroyItem(container, position, object);
    mPageReferences.remove(position);
}

... et lorsque vous avez besoin d'accéder à la page actuellement visible, vous appelez alors:

int index = mViewPager.getCurrentItem();
MyAdapter adapter = ((MyAdapter)mViewPager.getAdapter());
MyFragment fragment = adapter.getFragment(index);

... où la méthode getFragment (int) de MyAdapter ressemble à ceci:

public MyFragment getFragment(int key) {
    return mPageReferences.get(key);
}

"

Franc
la source
@Frank deuxième solution dans le lien est simple ...! Merci .. :)
TheFlash
3
Mieux vaut si vous utilisez un SparseArray au lieu d'une carte
GaRRaPeTa
a mis à jour ma réponse pour qu'elle utilise un SparseArray, si vous ciblez <11, utilisez plutôt un SparseArrayCompat car la classe a été mise à jour dans la version 11.
Frank
2
Cela sera interrompu lors des événements du cycle de vie. Voir stackoverflow.com/a/24662049/236743
Dori
13

D'accord, après avoir testé la méthode par @barkside ci-dessus, je n'ai pas pu la faire fonctionner avec mon application. Ensuite, je me suis souvenu que l' application IOSched2012 utilisait également un viewpager, et c'est là que j'ai trouvé ma solution. Il n'utilise aucun ID de fragment ou balise car ceux-ci ne sont pas stockés de viewpagermanière facilement accessible.

Voici les parties importantes des applications IOSched HomeActivity. Portez une attention particulière au commentaire , car c'est là que réside la clé:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    // Since the pager fragments don't have known tags or IDs, the only way to persist the
    // reference is to use putFragment/getFragment. Remember, we're not persisting the exact
    // Fragment instance. This mechanism simply gives us a way to persist access to the
    // 'current' fragment instance for the given fragment (which changes across orientation
    // changes).
    //
    // The outcome of all this is that the "Refresh" menu button refreshes the stream across
    // orientation changes.
    if (mSocialStreamFragment != null) {
        getSupportFragmentManager().putFragment(outState, "stream_fragment",
                mSocialStreamFragment);
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if (mSocialStreamFragment == null) {
        mSocialStreamFragment = (SocialStreamFragment) getSupportFragmentManager()
                .getFragment(savedInstanceState, "stream_fragment");
    }
}

Et stockez les instances de vous Fragmentsde la FragmentPagerAdaptermême manière:

    private class HomePagerAdapter extends FragmentPagerAdapter {
    public HomePagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return (mMyScheduleFragment = new MyScheduleFragment());

            case 1:
                return (mExploreFragment = new ExploreFragment());

            case 2:
                return (mSocialStreamFragment = new SocialStreamFragment());
        }
        return null;
    }

N'oubliez pas non plus de garder vos Fragmentappels comme ceci:

    if (mSocialStreamFragment != null) {
        mSocialStreamFragment.refresh();
    }
Ryan R
la source
Ryan, cela veut-il dire que vous surchargez les fonctions que le ViewPager utilise pour «conserver» vos instances de fragment et les définissez sur une variable publique accessible? Pourquoi sur l'instance de restauration avez-vous if (mSocialStreamFragment == null) {?
Thomas Clowes
1
c'est une bonne réponse et +1 pour la référence de l'application io12 - je ne sais pas si cela fonctionnera tout au long des changements de cycle de vie, mais en raison de getItem()ne pas être appelé après la recréation du parent en raison d'un changement de cycle de vie. Voir stackoverflow.com/a/24662049/236743 . Dans l'exemple, cela est en partie protégé pour un fragment mais pas pour les deux autres
Dori
2

Vous pouvez copier FragmentPagerAdapteret modifier du code source, ajouter une getTag()méthode

par exemple

public abstract class AppFragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;

private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;

public AppFragmentPagerAdapter(FragmentManager fm) {
    mFragmentManager = fm;
}


public abstract Fragment getItem(int position);

@Override
public void startUpdate(ViewGroup container) {
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);


    String name = getTag(position);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);

        mCurTransaction.add(container.getId(), fragment,
                getTag(position));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment) object).getView());
    mCurTransaction.detach((Fragment) object);
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment) object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitAllowingStateLoss();
        mCurTransaction = null;
        mFragmentManager.executePendingTransactions();
    }
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return ((Fragment) object).getView() == view;
}

@Override
public Parcelable saveState() {
    return null;
}

@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}


public long getItemId(int position) {
    return position;
}

private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

protected abstract String getTag(int position);
}

puis étendez-le, remplacez ces méthodes abstraites, n'avez pas besoin d'avoir peur du changement de groupe Android

FragmentPageAdapter code source dans le futur

 class TimeLinePagerAdapter extends AppFragmentPagerAdapter {


    List<Fragment> list = new ArrayList<Fragment>();


    public TimeLinePagerAdapter(FragmentManager fm) {
        super(fm);
        list.add(new FriendsTimeLineFragment());
        list.add(new MentionsTimeLineFragment());
        list.add(new CommentsTimeLineFragment());
    }


    public Fragment getItem(int position) {
        return list.get(position);
    }

    @Override
    protected String getTag(int position) {
        List<String> tagList = new ArrayList<String>();
        tagList.add(FriendsTimeLineFragment.class.getName());
        tagList.add(MentionsTimeLineFragment.class.getName());
        tagList.add(CommentsTimeLineFragment.class.getName());
        return tagList.get(position);
    }


    @Override
    public int getCount() {
        return list.size();
    }


}
Jiang Qi
la source
1

Fonctionne également sans problèmes:

quelque part dans la mise en page du fragment de page:

<FrameLayout android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone" android:id="@+id/fragment_reference">
     <View android:layout_width="0dp" android:layout_height="0dp" android:visibility="gone"/>
</FrameLayout>

dans onCreateView () du fragment:

...
View root = inflater.inflate(R.layout.fragment_page, container, false);
ViewGroup ref = (ViewGroup)root.findViewById(R.id.fragment_reference);
ref.setTag(this);
ref.getChildAt(0).setTag("fragment:" + pageIndex);
return root;

et méthode pour renvoyer Fragment depuis ViewPager, s'il existe:

public Fragment getFragment(int pageIndex) {        
        View w = mViewPager.findViewWithTag("fragment:" + pageIndex);
        if (w == null) return null;
        View r = (View) w.getParent();
        return (Fragment) r.getTag();
}
Hox
la source
1

Vous pouvez également remplacer la setPrimaryItemméthode FragmentPagerAdaptercomme suit:

public void setPrimaryItem(ViewGroup container, int position, Object object) { 
    if (mCurrentFragment != object) {
        mCurrentFragment = (Fragment) object; //Keep reference to object
        ((MyInterface)mCurrentFragment).viewDidAppear();//Or call a method on the fragment
    }

    super.setPrimaryItem(container, position, object);
}

public Fragment getCurrentFragment(){
    return mCurrentFragment;
}
Vlad Spreys
la source
0

Je veux donner mon approche au cas où cela pourrait aider quelqu'un d'autre:

Voici mon adaptateur de téléavertisseur:

 public class CustomPagerAdapter extends FragmentStatePagerAdapter{
    private Fragment[] fragments;

    public CustomPagerAdapter(FragmentManager fm) {
        super(fm);
        fragments = new Fragment[]{
                new FragmentA(),
                new FragmentB()
        };
    }

    @Override
    public Fragment getItem(int arg0) {
        return fragments[arg0];
    }

    @Override
    public int getCount() {
        return fragments.length;
    }

}

Dans mon activité j'ai:

public class MainActivity {
        private ViewPager view_pager;
        private CustomPagerAdapter adapter;


        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        adapter = new CustomPagerAdapter(getSupportFragmentManager());
        view_pager = (ViewPager) findViewById(R.id.pager);
        view_pager.setAdapter(adapter);
        view_pager.setOnPageChangeListener(this);
          ...
         }

}

Ensuite, pour obtenir le fragment actuel, ce que je fais est:

int index = view_pager.getCurrentItem();
Fragment currentFragment = adapter.getItem(index);
kiduxa
la source
1
cela se cassera après les méthodes de cycle de vie d'activité / fragment
Dori
0

C'est ma solution car je n'ai pas besoin de garder une trace de mes onglets et de les actualiser de toute façon.

    Bundle b = new Bundle();
    b.putInt(Constants.SharedPreferenceKeys.NUM_QUERY_DAYS,numQueryDays);
    for(android.support.v4.app.Fragment f:getSupportFragmentManager().getFragments()){
        if(f instanceof HomeTermFragment){
            ((HomeTermFragment) f).restartLoader(b);
        }
    }
abhi08638
la source