viewpager - using fragments in android




ViewPager e frammenti: qual รจ il modo giusto per archiviare lo stato di un frammento? (7)

I frammenti sembrano essere molto carini per la separazione della logica dell'interfaccia utente in alcuni moduli. Ma insieme a ViewPager suo ciclo di vita è ancora nebbioso. Quindi i pensieri del Guru sono assolutamente necessari!

modificare

Vedi la soluzione stupida qui sotto ;-)

Scopo

L'attività principale ha un ViewPager con frammenti. Questi frammenti potrebbero implementare una logica leggermente diversa per altre attività (sottomeniche), quindi i dati dei frammenti vengono riempiti tramite un'interfaccia di callback all'interno dell'attività. E tutto funziona bene al primo avvio, ma! ...

Problema

Quando l'attività viene ricreata (ad esempio sul cambiamento di orientamento), ViewPager i frammenti di ViewPager . Il codice (che trovi di seguito) dice che ogni volta che l'attività viene creata cerco di creare un nuovo adattatore per frammenti ViewPager uguale ai frammenti (forse questo è il problema) ma FragmentManager ha già tutti questi frammenti memorizzati da qualche parte (dove?) e avvia il meccanismo di ricreazione per quelli. Pertanto, il meccanismo di ricreazione richiama il "vecchio" frammento su Attach, onCreateView, ecc. Con la mia chiamata all'interfaccia di richiamata per l'avvio di dati tramite il metodo implementato di Activity. Ma questo metodo punta al frammento appena creato che viene creato tramite il metodo onCreate di Activity.

Problema

Forse sto usando schemi sbagliati ma anche il libro di Android 3 Pro non ha molto a riguardo. Quindi, per favore , dammi un pugno e sottolinea come farlo nel modo giusto. Grazie molto!

Codice

Attività principale

public class DashboardActivity extends BasePagerActivity implements OnMessageListActionListener {

private MessagesFragment mMessagesFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    setContentView(R.layout.viewpager_container);
    new DefaultToolbar(this);

    // create fragments to use
    mMessagesFragment = new MessagesFragment();
    mStreamsFragment = new StreamsFragment();

    // set titles and fragments for view pager
    Map<String, Fragment> screens = new LinkedHashMap<String, Fragment>();
    screens.put(getApplicationContext().getString(R.string.dashboard_title_dumb), new DumbFragment());
    screens.put(getApplicationContext().getString(R.string.dashboard_title_messages), mMessagesFragment);

    // instantiate view pager via adapter
    mPager = (ViewPager) findViewById(R.id.viewpager_pager);
    mPagerAdapter = new BasePagerAdapter(screens, getSupportFragmentManager());
    mPager.setAdapter(mPagerAdapter);

    // set title indicator
    TitlePageIndicator indicator = (TitlePageIndicator) findViewById(R.id.viewpager_titles);
    indicator.setViewPager(mPager, 1);

}

/* set of fragments callback interface implementations */

@Override
public void onMessageInitialisation() {

    Logger.d("Dash onMessageInitialisation");
    if (mMessagesFragment != null)
        mMessagesFragment.loadLastMessages();
}

@Override
public void onMessageSelected(Message selectedMessage) {

    Intent intent = new Intent(this, StreamActivity.class);
    intent.putExtra(Message.class.getName(), selectedMessage);
    startActivity(intent);
}

BasePagerActivity aka helper

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}

Adattatore

public class BasePagerAdapter extends FragmentPagerAdapter implements TitleProvider {

private Map<String, Fragment> mScreens;

public BasePagerAdapter(Map<String, Fragment> screenMap, FragmentManager fm) {

    super(fm);
    this.mScreens = screenMap;
}

@Override
public Fragment getItem(int position) {

    return mScreens.values().toArray(new Fragment[mScreens.size()])[position];
}

@Override
public int getCount() {

    return mScreens.size();
}

@Override
public String getTitle(int position) {

    return mScreens.keySet().toArray(new String[mScreens.size()])[position];
}

// hack. we don't want to destroy our fragments and re-initiate them after
@Override
public void destroyItem(View container, int position, Object object) {

    // TODO Auto-generated method stub
}

}

Frammento

public class MessagesFragment extends ListFragment {

private boolean mIsLastMessages;

private List<Message> mMessagesList;
private MessageArrayAdapter mAdapter;

private LoadMessagesTask mLoadMessagesTask;
private OnMessageListActionListener mListener;

// define callback interface
public interface OnMessageListActionListener {
    public void onMessageInitialisation();
    public void onMessageSelected(Message selectedMessage);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    // setting callback
    mListener = (OnMessageListActionListener) activity;
    mIsLastMessages = activity instanceof DashboardActivity;

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    inflater.inflate(R.layout.fragment_listview, container);
    mProgressView = inflater.inflate(R.layout.listrow_progress, null);
    mEmptyView = inflater.inflate(R.layout.fragment_nodata, null);
    return super.onCreateView(inflater, container, savedInstanceState);
}

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

    // instantiate loading task
    mLoadMessagesTask = new LoadMessagesTask();

    // instantiate list of messages
    mMessagesList = new ArrayList<Message>();
    mAdapter = new MessageArrayAdapter(getActivity(), mMessagesList);
    setListAdapter(mAdapter);
}

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

public void onListItemClick(ListView l, View v, int position, long id) {
    Message selectedMessage = (Message) getListAdapter().getItem(position);
    mListener.onMessageSelected(selectedMessage);
    super.onListItemClick(l, v, position, id);
}

/* public methods to load messages from host acitivity, etc... */
}

Soluzione

La soluzione stupida consiste nel salvare i frammenti all'interno di onSaveInstanceState (di Attività host) con putFragment e scaricarli all'interno di onCreate tramite getFragment. Ma ho ancora la strana sensazione che le cose non dovrebbero funzionare così ... Vedi il codice qui sotto:

    @Override
protected void onSaveInstanceState(Bundle outState) {

    super.onSaveInstanceState(outState);
    getSupportFragmentManager()
            .putFragment(outState, MessagesFragment.class.getName(), mMessagesFragment);
}

protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    ...
    // create fragments to use
    if (savedInstanceState != null) {
        mMessagesFragment = (MessagesFragment) getSupportFragmentManager().getFragment(
                savedInstanceState, MessagesFragment.class.getName());
                StreamsFragment.class.getName());
    }
    if (mMessagesFragment == null)
        mMessagesFragment = new MessagesFragment();
    ...
}

Cos'è il BasePagerAdapter ? È necessario utilizzare uno degli adattatori cercapersone standard, FragmentPagerAdapter o FragmentStatePagerAdapter , a seconda che si desideri Frammenti non più necessari per il ViewPager da conservare (il primo) o per il loro stato salvato (quest'ultimo) e -creato se necessario di nuovo.

Il codice di esempio per l'utilizzo di ViewPager può essere trovato here

È vero che la gestione dei frammenti in un visualizzatore di pagine attraverso istanze di attività è un po 'complicata, perché il FragmentManager nel framework si occupa di salvare lo stato e ripristinare eventuali frammenti attivi creati dal cercapersone. Tutto ciò significa in realtà che l'adattatore durante l'inizializzazione deve essere sicuro che si ricolleghi con qualsiasi frammento ripristinato. Puoi vedere il codice di FragmentPagerAdapter o FragmentStatePagerAdapter per vedere come è fatto.


Ho trovato questa soluzione semplice ed elegante. Presume che l'attività è responsabile della creazione dei frammenti e che l'adattatore li serve solo loro.

Questo è il codice dell'adattatore (niente di strano qui, eccetto il fatto che mFragments è una lista di frammenti mantenuti mFragments )

class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {

    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

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

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        TabFragment fragment = (TabFragment)mFragments.get(position);
        return fragment.getTitle();
    }
} 

L'intero problema di questo thread è ottenere un riferimento dei "vecchi" frammenti, quindi utilizzo questo codice nel onCreate di Activity.

    if (savedInstanceState!=null) {
        if (getSupportFragmentManager().getFragments()!=null) {
            for (Fragment fragment : getSupportFragmentManager().getFragments()) {
                mFragments.add(fragment);
            }
        }
    }

Ovviamente è possibile perfezionare ulteriormente questo codice, se necessario, ad esempio assicurandosi che i frammenti siano istanze di una particolare classe.


Inserisci:

   @SuppressLint("ValidFragment")

prima della tua lezione

non funziona in questo modo:

@SuppressLint({ "ValidFragment", "HandlerLeak" })

La mia soluzione è molto scortese ma funziona: essendo i miei frammenti creati dinamicamente da dati conservati, rimuovo semplicemente tutti i frammenti dal PageAdapter prima di chiamare super.onSaveInstanceState() e quindi ricrearli nella creazione di attività:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
    mSectionsPagerAdapter.removeAllfragments();
    super.onSaveInstanceState(outState);
}

Non puoi rimuoverli in onDestroy() , altrimenti ottieni questa eccezione:

java.lang.IllegalStateException: impossibile eseguire questa azione dopo onSaveInstanceState

Qui il codice nell'adattatore di pagina:

public void removeAllfragments()
{
    if ( mFragmentList != null ) {
        for ( Fragment fragment : mFragmentList ) {
            mFm.beginTransaction().remove(fragment).commit();
        }
        mFragmentList.clear();
        notifyDataSetChanged();
    }
}

Io salvo solo la pagina corrente e la ripristino in onCreate() , dopo che i frammenti sono stati creati.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  

Quando FragmentPagerAdapter aggiunge un frammento a FragmentManager, utilizza un tag speciale in base alla posizione particolare in cui verrà inserito il frammento. FragmentPagerAdapter.getItem(int position) viene chiamato solo quando non esiste un frammento per quella posizione. Dopo la rotazione, Android noterà che ha già creato / salvato un frammento per questa particolare posizione e quindi tenta semplicemente di riconnettersi con FragmentManager.findFragmentByTag() , invece di crearne uno nuovo. Tutto questo è gratuito quando si usa FragmentPagerAdapter ed è il motivo per cui è normale avere il codice di inizializzazione del frammento nel metodo getItem(int) .

Anche se non utilizzassimo FragmentPagerAdapter , non è una buona idea creare un nuovo frammento ogni volta in Activity.onCreate(Bundle) . Come hai notato, quando un frammento viene aggiunto a FragmentManager, verrà ricreato per te dopo la rotazione e non è necessario aggiungerlo di nuovo. Farlo è una causa comune di errori quando si lavora con i frammenti.

Un approccio usuale quando si lavora con i frammenti è questo:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ...

    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit(); 
    }

    ...

}

Quando si utilizza FragmentPagerAdapter , abbandoniamo la gestione dei frammenti nell'adattatore e non è necessario eseguire i passaggi precedenti. Per impostazione predefinita, precaricherà solo un frammento davanti e dietro la posizione corrente (sebbene non li distrugga a meno che non si utilizzi FragmentStatePagerAdapter ). Questo è controllato da ViewPager.setOffscreenPageLimit(int) . Per questo motivo, non è garantito che i metodi di chiamata diretta sui frammenti esterni all'adattatore siano validi, poiché potrebbero non essere nemmeno vivi.

Per putFragment breve, la soluzione per utilizzare putFragment per poter ottenere un riferimento in seguito non è così folle, e non è così diversa dal solito modo di utilizzare i frammenti in ogni caso (sopra). È difficile ottenere un riferimento altrimenti, poiché il frammento viene aggiunto dall'adattatore e non tu personalmente. Assicurati solo che offscreenPageLimit sia abbastanza alto da caricare i frammenti desiderati in ogni momento, dal momento che ti affidi alla presenza. Ciò aggira le capacità di caricamento pigro del ViewPager, ma sembra essere quello che desideri per la tua applicazione.

Un altro approccio è quello di scavalcare FragmentPageAdapter.instantiateItem(View, int) e salvare un riferimento al frammento restituito dalla super-call prima di restituirlo (ha la logica per trovare il frammento, se già presente).

Per un'immagine più completa, ViewPager un'occhiata ad alcuni dei sorgenti di FragmentPagerAdapter (breve) e ViewPager (long).


Se qualcuno sta avendo problemi con il loro FragmentStatePagerAdapter non ripristina correttamente lo stato dei suoi frammenti ... cioè ... nuovi frammenti vengono creati dal FragmentStatePagerAdapter invece di ripristinarli dallo stato ...

Assicurati di chiamare ViewPager.setOffscreenPageLimit() PRIMA di chiamare ViewPager.setAdapeter(fragmentStatePagerAdapter)

Dopo aver chiamato ViewPager.setOffscreenPageLimit() ... il ViewPager cercherà immediatamente il suo adattatore e cercherà di ottenere i suoi frammenti. Questo potrebbe accadere prima che ViewPager abbia la possibilità di ripristinare i Fragments da savedInstanceState (creando così nuovi Fragments che non possono essere reinizializzati da SavedInstanceState perché sono nuovi).


Voglio offrire una soluzione che espanda la meravigliosa risposta di antonyt e che menzioni l'override di FragmentPageAdapter.instantiateItem(View, int) per salvare i riferimenti ai Fragments creati in modo che possiate lavorare su di essi in seguito. Questo dovrebbe funzionare anche con FragmentStatePagerAdapter ; vedere le note per i dettagli.

Ecco un semplice esempio di come ottenere un riferimento ai Fragments restituiti da FragmentPagerAdapter che non si basa sui tags interni impostati su Fragments . La chiave è sovrascrivere instantiateItem() e salvare i riferimenti in là anziché in getItem() .

public class SomeActivity extends Activity {
    private FragmentA m1stFragment;
    private FragmentB m2ndFragment;

    // other code in your Activity...

    private class CustomPagerAdapter extends FragmentPagerAdapter {
        // other code in your custom FragmentPagerAdapter...

        public CustomPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            // Do NOT try to save references to the Fragments in getItem(),
            // because getItem() is not always called. If the Fragment
            // was already created then it will be retrieved from the FragmentManger
            // and not here (i.e. getItem() won't be called again).
            switch (position) {
                case 0:
                    return new FragmentA();
                case 1:
                    return new FragmentB();
                default:
                    // This should never happen. Always account for each position above
                    return null;
            }
        }

        // Here we can finally safely save a reference to the created
        // Fragment, no matter where it came from (either getItem() or
        // FragmentManger). Simply save the returned Fragment from
        // super.instantiateItem() into an appropriate reference depending
        // on the ViewPager position.
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
            // save the appropriate reference depending on position
            switch (position) {
                case 0:
                    m1stFragment = (FragmentA) createdFragment;
                    break;
                case 1:
                    m2ndFragment = (FragmentB) createdFragment;
                    break;
            }
            return createdFragment;
        }
    }

    public void someMethod() {
        // do work on the referenced Fragments, but first check if they
        // even exist yet, otherwise you'll get an NPE.

        if (m1stFragment != null) {
            // m1stFragment.doWork();
        }

        if (m2ndFragment != null) {
            // m2ndFragment.doSomeWorkToo();
        }
    }
}

oppure se preferisci lavorare con i tags posto delle variabili membro / riferimenti ai Fragments puoi anche prendere i tags impostati da FragmentPagerAdapter nello stesso modo: NOTA: questo non si applica a FragmentStatePagerAdapter dal momento che non imposta i tags durante la creazione i suoi Fragments .

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
    // get the tags set by FragmentPagerAdapter
    switch (position) {
        case 0:
            String firstTag = createdFragment.getTag();
            break;
        case 1:
            String secondTag = createdFragment.getTag();
            break;
    }
    // ... save the tags somewhere so you can reference them later
    return createdFragment;
}

Si noti che questo metodo NON si basa sul mimare il set di tag interno da FragmentPagerAdapter e utilizza invece API appropriate per recuperarli. In questo modo, anche se il tag cambia nelle future versioni di SupportLibrary , sarai comunque al sicuro.

Non dimenticare che, a seconda del design della tua Activity , i Fragments cui stai lavorando potrebbero essere o non essere ancora disponibili, quindi devi tenerne conto effettuando controlli null prima di utilizzare i tuoi riferimenti.

Inoltre, se invece stai lavorando con FragmentStatePagerAdapter , allora non vuoi mantenere dei riferimenti duri ai tuoi Fragments perché potresti averne molti e dei riferimenti difficili li terrebbero inutilmente in memoria. Salvate invece i riferimenti Fragment nelle variabili WeakReference invece di quelli standard. Come questo:

WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
    // reference hasn't been cleared yet; do work...
}






android-viewpager