110 votes

le support FragmentPagerAdapter contient la référence aux anciens fragments

J'ai réduit mon problème à un problème avec le FragmentManager conservant des instances d'anciens fragments et mon viewpager n'étant pas synchronisé avec mon FragmentManager. Voir ce problème : http://code.google.com/p/Android/issues/detail?id=19211#makechanges . Je n'ai toujours aucune idée de la façon de résoudre ce problème. Avez-vous des suggestions ?

J'ai essayé de déboguer ce problème depuis longtemps et toute aide serait grandement appréciée. J'utilise un FragmentPagerAdapter qui accepte une liste de fragments comme suit :

List<Fragment> fragments = new Vector<Fragment>();
fragments.add(Fragment.instantiate(this, Fragment1.class.getName())); 
...
new PagerAdapter(getSupportFragmentManager(), fragments);

L'implémentation est standard. J'utilise ActionBarSherlock et la bibliothèque de calculabilité v4 pour Fragments.

Mon problème est qu'après avoir quitté l'application, ouvert plusieurs autres applications et être revenu, les fragments perdent leur référence à l'activité FragmentActivity (c'est-à-dire qu'ils ne peuvent plus être utilisés comme référence à l'activité Fragment). getActivity() == null ). Je n'arrive pas à comprendre pourquoi cela se produit. J'ai essayé de définir manuellement setRetainInstance(true); mais cela n'aide pas. Je me suis dit que cela se produisait lorsque mon FragmentActivity était détruit, mais cela se produit toujours si j'ouvre l'application avant d'obtenir le message du journal. Avez-vous une idée ?

@Override
protected void onDestroy(){
    Log.w(TAG, "DESTROYDESTROYDESTROYDESTROYDESTROYDESTROYDESTROY");
    super.onDestroy();
}

L'adaptateur :

public class PagerAdapter extends FragmentPagerAdapter {
    private List<Fragment> fragments;

    public PagerAdapter(FragmentManager fm, List<Fragment> fragments) {
        super(fm);

        this.fragments = fragments;

    }

    @Override
    public Fragment getItem(int position) {

        return this.fragments.get(position);

    }

    @Override
    public int getCount() {

        return this.fragments.size();

    }

}

Un de mes fragments a été supprimé mais j'ai commenté tout ce qui a été supprimé et ça ne fonctionne toujours pas.

public class MyFragment extends Fragment implements MyFragmentInterface, OnScrollListener {
...

@Override
public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    handler = new Handler();    
    setHasOptionsMenu(true);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    Log.w(TAG,"ATTACHATTACHATTACHATTACHATTACH");
    context = activity;
    if(context== null){
        Log.e("IS NULL", "NULLNULLNULLNULLNULLNULLNULLNULLNULLNULLNULL");
    }else{
        Log.d("IS NOT NULL", "NOTNOTNOTNOTNOTNOTNOTNOT");
    }

}

@Override
public void onActivityCreated(Bundle savedState) {
    super.onActivityCreated(savedState);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View v = inflater.inflate(R.layout.my_fragment,container, false);

    return v;
}

@Override
public void onResume(){
    super.onResume();
}

private void callService(){
    // do not call another service is already running
    if(startLoad || !canSet) return;
    // set flag
    startLoad = true;
    canSet = false;
    // show the bottom spinner
    addFooter();
    Intent intent = new Intent(context, MyService.class);
    intent.putExtra(MyService.STATUS_RECEIVER, resultReceiver);
    context.startService(intent);
}

private ResultReceiver resultReceiver = new ResultReceiver(null) {
    @Override
    protected void onReceiveResult(int resultCode, final Bundle resultData) {
        boolean isSet = false;
        if(resultData!=null)
        if(resultData.containsKey(MyService.STATUS_FINISHED_GET)){
            if(resultData.getBoolean(MyService.STATUS_FINISHED_GET)){
                removeFooter();
                startLoad = false;
                isSet = true;
            }
        }

        switch(resultCode){
        case MyService.STATUS_FINISHED: 
            stopSpinning();
            break;
        case SyncService.STATUS_RUNNING:
            break;
        case SyncService.STATUS_ERROR:
            break;
        }
    }
};

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    menu.clear();
    inflater.inflate(R.menu.activity, menu);
}

@Override
public void onPause(){
    super.onPause();
}

public void onScroll(AbsListView arg0, int firstVisible, int visibleCount, int totalCount) {
    boolean loadMore = /* maybe add a padding */
        firstVisible + visibleCount >= totalCount;

    boolean away = firstVisible+ visibleCount <= totalCount - visibleCount;

    if(away){
        // startLoad can now be set again
        canSet = true;
    }

    if(loadMore) 

}

public void onScrollStateChanged(AbsListView arg0, int state) {
    switch(state){
    case OnScrollListener.SCROLL_STATE_FLING: 
        adapter.setLoad(false); 
        lastState = OnScrollListener.SCROLL_STATE_FLING;
        break;
    case OnScrollListener.SCROLL_STATE_IDLE: 
        adapter.setLoad(true);
        if(lastState == SCROLL_STATE_FLING){
            // load the images on screen
        }

        lastState = OnScrollListener.SCROLL_STATE_IDLE;
        break;
    case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
        adapter.setLoad(true);
        if(lastState == SCROLL_STATE_FLING){
            // load the images on screen
        }

        lastState = OnScrollListener.SCROLL_STATE_TOUCH_SCROLL;
        break;
    }
}

@Override
public void onDetach(){
    super.onDetach();
    if(this.adapter!=null)
        this.adapter.clearContext();

    Log.w(TAG, "DETACHEDDETACHEDDETACHEDDETACHEDDETACHEDDETACHED");
}

public void update(final int id, String name) {
    if(name!=null){
        getActivity().getSupportActionBar().setTitle(name);
    }

}

}

La méthode de mise à jour est appelée lorsqu'un utilisateur interagit avec un autre fragment et que la méthode getActivity renvoie null. Voici la méthode que l'autre fragment appelle :

((MyFragment) pagerAdapter.getItem(1)).update(id, name);

Je crois que lorsque l'application est détruite puis recréée, au lieu de démarrer l'application sur le fragment par défaut, l'application démarre et le viewpager navigue sur la dernière page connue. Cela semble étrange, l'application ne devrait-elle pas simplement se charger sur le fragment par défaut ?

28 votes

Le Fragment d'Android est nul !

13 votes

Mon Dieu, j'adore vos messages de journal :D

121voto

antonyt Points 10649

Vous rencontrez un problème parce que vous instanciez et conservez des références à vos fragments en dehors de l'espace de travail de l'utilisateur. PagerAdapter.getItem et essayent d'utiliser ces références indépendamment du ViewPager. Comme le dit Seraph, vous avez des garanties qu'un fragment a été instancié/ajouté dans un ViewPager à un moment particulier - ceci devrait être considéré comme un détail d'implémentation. Un ViewPager effectue un chargement paresseux de ses pages ; par défaut, il ne charge que la page actuelle, ainsi que celles de gauche et de droite.

Si vous mettez votre application en arrière-plan, les fragments qui ont été ajoutés au gestionnaire de fragments sont sauvegardés automatiquement. Même si votre application est tuée, ces informations sont restaurées lorsque vous relancez votre application.

Considérons maintenant que vous avez consulté quelques pages, les fragments A, B et C. Vous savez que ceux-ci ont été ajoutés au gestionnaire de fragments. Comme vous utilisez FragmentPagerAdapter et non FragmentStatePagerAdapter ces fragments seront encore ajoutés (mais potentiellement détachés) lorsque vous ferez défiler d'autres pages.

Considérez que vous mettez ensuite votre application en arrière-plan, puis qu'elle est tuée. Lorsque vous revenez, Android se souvient que vous aviez les fragments A, B et C dans le gestionnaire de fragments et il les recrée pour vous et les ajoute. Cependant, ceux qui sont ajoutés au gestionnaire de fragments maintenant ne sont PAS ceux que vous avez dans votre liste de fragments dans votre activité.

Le FragmentPagerAdapter n'essaiera pas d'appeler getPosition s'il existe déjà un fragment ajouté pour cette position particulière de la page. En fait, puisque le fragment recréé par Android ne sera jamais supprimé, vous n'avez aucun espoir de le remplacer par un appel à getPosition . Il est également assez difficile d'en obtenir une référence car il a été ajouté avec une balise qui vous est inconnue. C'est à dessein ; on vous décourage d'intervenir sur les fragments gérés par le visualisateur. Vous devriez effectuer toutes vos actions dans un fragment, communiquer avec l'activité et demander à passer à une page particulière, si nécessaire.

Maintenant, revenons à votre problème avec l'activité manquante. Appeler pagerAdapter.getItem(1)).update(id, name) après que tout cela se soit produit, vous renvoie le fragment dans votre liste, qui doit encore être ajouté au gestionnaire de fragments Il n'y aura donc pas de référence à l'activité. Je suggère que votre méthode de mise à jour modifie une structure de données partagée (éventuellement gérée par l'activité), et que lorsque vous vous déplacez vers une page particulière, elle puisse se dessiner sur la base de ces données mises à jour.

1 votes

J'aime votre solution car elle est très élégante et va probablement remanier mon code. Cependant, comme vous l'avez dit, je voulais garder 100% de la logique et des données dans le fragment. Votre solution nécessiterait de garder toutes les données dans le FragmentActivity et d'utiliser chaque Fragment simplement pour gérer la logique d'affichage. Comme je l'ai dit, je préfère cette solution, mais l'interaction entre les fragments est très lourde et cela pourrait devenir ennuyeux à gérer. Quoi qu'il en soit, je vous remercie pour votre explication détaillée. Vous l'avez expliqué bien mieux que je ne pourrais le faire.

30 votes

En bref : ne jamais détenir une référence à un fragment en dehors de l'adaptateur.

2 votes

Alors comment une instance d'Activity peut-elle accéder à l'instance de FragmentPagerAdapter si elle n'a pas instancié le FragmentPagerAdapter ? Les instances futures ne vont-elles pas simplement réinstancier le FragmentPagerAdapter et toutes ses instances de fragment ? Le FragmentPagerAdapter doit-il implémenter toutes les interfaces de fragment pour gérer la communication entre les fragments ?

108voto

Mik Points 588

J'ai trouvé une solution simple qui a fonctionné pour moi.

Faites en sorte que votre adaptateur de fragment étende FragmentStatePagerAdapter au lieu de FragmentPagerAdapter et surchargez la méthode onSave pour qu'elle renvoie null.

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

Cela empêche Android de recréer le fragment


Un jour plus tard, j'ai trouvé une autre et meilleure solution.

Appelez setRetainInstance(true) pour tous vos fragments et enregistrez les références à ceux-ci quelque part. Je l'ai fait dans une variable statique dans mon activité, car elle est déclarée comme singleTask et les fragments peuvent rester les mêmes tout le temps.

De cette façon, Android ne recrée pas de fragments mais utilise les mêmes instances.

4 votes

Merci, merci, merci beaucoup, je n'ai vraiment pas de mots pour vous remercier Mik, j'étais à la recherche de ce problème depuis 10 jours et j'ai essayé tant de méthodes, mais ces quatre lignes magiques m'ont sauvé la vie :)

7 votes

Il est très risqué d'avoir une référence statique aux fragments et/ou aux activités, car cela peut provoquer très facilement des fuites de mémoire. Bien sûr, si vous êtes prudent, vous pouvez gérer cela assez facilement en les mettant à null lorsqu'ils ne sont plus nécessaires.

0 votes

Cela a fonctionné pour moi. Les fragments et leurs vues respectives conservent leurs liens après le plantage et le redémarrage de l'application. Merci !

30voto

Maurycy Points 1398

J'ai résolu ce problème en accédant à mes fragments directement par le FragmentManager plutôt que par le FragmentPagerAdapter comme suit. Tout d'abord, je dois déterminer le tag du fragment généré automatiquement par le FragmentPagerAdapter...

private String getFragmentTag(int pos){
    return "android:switcher:"+R.id.viewpager+":"+pos;
}

Ensuite, j'obtiens simplement une référence à ce fragment et je fais ce dont j'ai besoin comme ceci...

Fragment f = this.getSupportFragmentManager().findFragmentByTag(getFragmentTag(1));
((MyFragmentInterface) f).update(id, name);
viewPager.setCurrentItem(1, true);

A l'intérieur de mes fragments, j'ai mis le setRetainInstance(false); afin que je puisse ajouter manuellement des valeurs au paquet savedInstanceState.

@Override
public void onSaveInstanceState(Bundle outState) {
    if(this.my !=null)
        outState.putInt("myId", this.my.getId());

    super.onSaveInstanceState(outState);
}

et ensuite, dans le OnCreate, je saisis cette clé et je restaure l'état du fragment si nécessaire. Une solution facile qui était difficile (pour moi du moins) à comprendre.

0 votes

J'ai un problème similaire au vôtre, mais je ne comprends pas bien votre explication, pouvez-vous fournir plus de détails ? J'ai également un adaptateur qui stocke les fragments dans une liste, mais lorsque je reprends mon application à partir des applications récentes, l'application se bloque parce qu'il y a un fragment détaché. Le problème est le suivant stackoverflow.com/questions/11631408/

3 votes

Quel est votre "mon" dans le fragment de référence ?

0 votes

Nous savons tous que cette solution n'est pas une bonne approche, mais c'est la manière la plus simple d'utiliser le système. FragmentByTag dans le ViewPager.

7voto

Seraph Points 342

N'essayez pas d'interagir entre les fragments dans ViewPager. Vous ne pouvez pas garantir que l'autre fragment est attaché ou même existe. Au lieu de changer le titre de la barre d'action depuis le fragment, vous pouvez le faire depuis votre activité. Utilisez le modèle d'interface standard pour cela :

public interface UpdateCallback
{
    void update(String name);
}

public class MyActivity extends FragmentActivity implements UpdateCallback
{
    @Override
    public void update(String name)
    {
        getSupportActionBar().setTitle(name);
    }

}

public class MyFragment extends Fragment
{
    private UpdateCallback callback;

    @Override
    public void onAttach(SupportActivity activity)
    {
        super.onAttach(activity);
        callback = (UpdateCallback) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        callback = null;
    }

    public void updateActionbar(String name)
    {
        if(callback != null)
            callback.update(name);
    }
}

0 votes

Je vais certainement inclure le code maintenant... Je suis déjà en train d'enregistrer ces méthodes. Le onDetach est appelé lorsque le onStop est appelé dans le fragmentActivity. Le onAttach est appelé juste avant le onCreate du FragmentActivity.

0 votes

Hmm merci mais le problème persiste. J'ai fait de la définition du nom un callback à la place. Ne puis-je pas mettre de la logique dans le fragment ? Mes fragments déclenchent des services et d'autres éléments qui nécessitent une référence à l'activité. Je devrais déplacer toute la logique de mes applications vers l'activité principale pour éviter le problème que je rencontre. Cela semble un peu inutile.

0 votes

Ok, je viens de tester tout cela et j'ai commenté tout mon code... sauf le callback pour changer le titre. Lorsque l'application est lancée pour la première fois et que je me déplace vers cet écran, le nom du titre n'est pas modifié. Après avoir chargé plusieurs applications puis être revenu, le titre ne change plus. Il semble que le callback soit reçu sur une ancienne instance d'activité. Je ne vois pas d'autre explication.

0voto

saulpower Points 695

Puisque le FragmentManager se chargera de restaurer vos fragments pour vous dès que la méthode onResume() sera appelée, je fais en sorte que le fragment appelle l'activité et s'ajoute à une liste. Dans mon instance, je stocke tout cela dans l'implémentation de mon PagerAdapter. Chaque fragment connaît sa position car elle est ajoutée aux arguments du fragment lors de sa création. Maintenant, quand j'ai besoin de manipuler un fragment à un index spécifique, tout ce que j'ai à faire est d'utiliser la liste de mon adaptateur.

Voici un exemple d'adaptateur pour un ViewPager personnalisé qui agrandira le fragment lorsqu'il sera au centre de l'attention, et le réduira lorsqu'il sera hors de l'attention. Outre les classes Adapter et Fragment que j'ai présentées ici, il suffit que l'activité parente puisse faire référence à la variable de l'adaptateur et le tour est joué.

Adaptateur

public class GrowPagerAdapter extends FragmentPagerAdapter implements OnPageChangeListener, OnScrollChangedListener {

public final String TAG = this.getClass().getSimpleName();

private final int COUNT = 4;

public static final float BASE_SIZE = 0.8f;
public static final float BASE_ALPHA = 0.8f;

private int mCurrentPage = 0;
private boolean mScrollingLeft;

private List<SummaryTabletFragment> mFragments;

public int getCurrentPage() {
    return mCurrentPage;
}

public void addFragment(SummaryTabletFragment fragment) {
    mFragments.add(fragment.getPosition(), fragment);
}

public GrowPagerAdapter(FragmentManager fm) {
    super(fm);

    mFragments = new ArrayList<SummaryTabletFragment>();
}

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

@Override
public Fragment getItem(int position) {
    return SummaryTabletFragment.newInstance(position);
}

@Override
public void onPageScrollStateChanged(int state) {}

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    adjustSize(position, positionOffset);
}

@Override
public void onPageSelected(int position) {
    mCurrentPage = position;
}

/**
 * Used to adjust the size of each view in the viewpager as the user
 * scrolls.  This provides the effect of children scaling down as they
 * are moved out and back to full size as they come into focus.
 * 
 * @param position
 * @param percent
 */
private void adjustSize(int position, float percent) {

    position += (mScrollingLeft ? 1 : 0);
    int secondary = position + (mScrollingLeft ? -1 : 1);
    int tertiary = position + (mScrollingLeft ? 1 : -1);

    float scaleUp = mScrollingLeft ? percent : 1.0f - percent;
    float scaleDown = mScrollingLeft ? 1.0f - percent : percent;

    float percentOut = scaleUp > BASE_ALPHA ? BASE_ALPHA : scaleUp;
    float percentIn = scaleDown > BASE_ALPHA ? BASE_ALPHA : scaleDown;

    if (scaleUp < BASE_SIZE)
        scaleUp = BASE_SIZE;

    if (scaleDown < BASE_SIZE)
        scaleDown = BASE_SIZE;

    // Adjust the fragments that are, or will be, on screen
    SummaryTabletFragment current = (position < mFragments.size()) ? mFragments.get(position) : null;
    SummaryTabletFragment next = (secondary < mFragments.size() && secondary > -1) ? mFragments.get(secondary) : null;
    SummaryTabletFragment afterNext = (tertiary < mFragments.size() && tertiary > -1) ? mFragments.get(tertiary) : null;

    if (current != null && next != null) {

        // Apply the adjustments to each fragment
        current.transitionFragment(percentIn, scaleUp);
        next.transitionFragment(percentOut, scaleDown);

        if (afterNext != null) {
            afterNext.transitionFragment(BASE_ALPHA, BASE_SIZE);
        }
    }
}

@Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {

    // Keep track of which direction we are scrolling
    mScrollingLeft = (oldl - l) < 0;
}
}

Fragment

public class SummaryTabletFragment extends BaseTabletFragment {

public final String TAG = this.getClass().getSimpleName();

private final float SCALE_SIZE = 0.8f;

private RelativeLayout mBackground, mCover;
private TextView mTitle;
private VerticalTextView mLeft, mRight;

private String mTitleText;
private Integer mColor;

private boolean mInit = false;
private Float mScale, mPercent;

private GrowPagerAdapter mAdapter;
private int mCurrentPosition = 0;

public String getTitleText() {
    return mTitleText;
}

public void setTitleText(String titleText) {
    this.mTitleText = titleText;
}

public static SummaryTabletFragment newInstance(int position) {

    SummaryTabletFragment fragment = new SummaryTabletFragment();
    fragment.setRetainInstance(true);

    Bundle args = new Bundle();
    args.putInt("position", position);
    fragment.setArguments(args);

    return fragment;
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);

    mRoot = inflater.inflate(R.layout.tablet_dummy_view, null);

    setupViews();
    configureView();

    return mRoot;
}

@Override
public void onViewStateRestored(Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);

    if (savedInstanceState != null) {
        mColor = savedInstanceState.getInt("color", Color.BLACK);
    }

    configureView();
}

@Override
public void onSaveInstanceState(Bundle outState)  {

    outState.putInt("color", mColor);

    super.onSaveInstanceState(outState);
}

@Override
public int getPosition() {
    return getArguments().getInt("position", -1);
}

@Override
public void setPosition(int position) {
    getArguments().putInt("position", position);
}

public void onResume() {
    super.onResume();

    mAdapter = mActivity.getPagerAdapter();
    mAdapter.addFragment(this);
    mCurrentPosition = mAdapter.getCurrentPage();

    if ((getPosition() == (mCurrentPosition + 1) || getPosition() == (mCurrentPosition - 1)) && !mInit) {
        mInit = true;
        transitionFragment(GrowPagerAdapter.BASE_ALPHA, GrowPagerAdapter.BASE_SIZE);
        return;
    }

    if (getPosition() == mCurrentPosition && !mInit) {
        mInit = true;
        transitionFragment(0.00f, 1.0f);
    }
}

private void setupViews() {

    mCover = (RelativeLayout) mRoot.findViewById(R.id.cover);
    mLeft = (VerticalTextView) mRoot.findViewById(R.id.title_left);
    mRight = (VerticalTextView) mRoot.findViewById(R.id.title_right);
    mBackground = (RelativeLayout) mRoot.findViewById(R.id.root);
    mTitle = (TextView) mRoot.findViewById(R.id.title);
}

private void configureView() {

    Fonts.applyPrimaryBoldFont(mLeft, 15);
    Fonts.applyPrimaryBoldFont(mRight, 15);

    float[] size = UiUtils.getScreenMeasurements(mActivity);
    int width = (int) (size[0] * SCALE_SIZE);
    int height = (int) (size[1] * SCALE_SIZE);

    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height);
    mBackground.setLayoutParams(params);

    if (mScale != null)
        transitionFragment(mPercent, mScale);

    setRandomBackground();

    setTitleText("Fragment " + getPosition());

    mTitle.setText(getTitleText().toUpperCase());
    mLeft.setText(getTitleText().toUpperCase());
    mRight.setText(getTitleText().toUpperCase());

    mLeft.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {

            mActivity.showNextPage();
        }
    });

    mRight.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {

            mActivity.showPrevPage();
        }
    });
}

private void setRandomBackground() {

    if (mColor == null) {
        Random r = new Random();
        mColor = Color.rgb(r.nextInt(255), r.nextInt(255), r.nextInt(255));
    }

    mBackground.setBackgroundColor(mColor);
}

public void transitionFragment(float percent, float scale) {

    this.mScale = scale;
    this.mPercent = percent;

    if (getView() != null && mCover != null) {

        getView().setScaleX(scale);
        getView().setScaleY(scale);

        mCover.setAlpha(percent);
        mCover.setVisibility((percent <= 0.05f) ? View.GONE : View.VISIBLE);
    }
}

@Override
public String getFragmentTitle() {
    return null;
}
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X