Fragmento MyFragment no asociado a la actividad

393

He creado una pequeña aplicación de prueba que representa mi problema. Estoy usando ActionBarSherlock para implementar pestañas con (Sherlock) Fragments.

Mi código: TestActivity.java

public class TestActivity extends SherlockFragmentActivity {
    private ActionBar actionBar;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupTabs(savedInstanceState);
    }

    private void setupTabs(Bundle savedInstanceState) {
        actionBar = getSupportActionBar();
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

        addTab1();
        addTab2();
    }

    private void addTab1() {
        Tab tab1 = actionBar.newTab();
        tab1.setTag("1");
        String tabText = "1";
        tab1.setText(tabText);
        tab1.setTabListener(new TabListener<MyFragment>(TestActivity.this, "1", MyFragment.class));

        actionBar.addTab(tab1);
    }

    private void addTab2() {
        Tab tab1 = actionBar.newTab();
        tab1.setTag("2");
        String tabText = "2";
        tab1.setText(tabText);
        tab1.setTabListener(new TabListener<MyFragment>(TestActivity.this, "2", MyFragment.class));

        actionBar.addTab(tab1);
    }
}

TabListener.java

public class TabListener<T extends SherlockFragment> implements com.actionbarsherlock.app.ActionBar.TabListener {
    private final SherlockFragmentActivity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(SherlockFragmentActivity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
    }

    /* The following are each of the ActionBar.TabListener callbacks */

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        SherlockFragment preInitializedFragment = (SherlockFragment) mActivity.getSupportFragmentManager().findFragmentByTag(mTag);

        // Check if the fragment is already initialized
        if (preInitializedFragment == null) {
            // If not, instantiate and add it to the activity
            SherlockFragment mFragment = (SherlockFragment) SherlockFragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            ft.attach(preInitializedFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        SherlockFragment preInitializedFragment = (SherlockFragment) mActivity.getSupportFragmentManager().findFragmentByTag(mTag);

        if (preInitializedFragment != null) {
            // Detach the fragment, because another one is being attached
            ft.detach(preInitializedFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // User selected the already selected tab. Usually do nothing.
    }
}

MyFragment.java

public class MyFragment extends SherlockFragment {

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

        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result){
                getResources().getString(R.string.app_name);
            }

        }.execute();
    }
}

He agregado la Thread.sleepparte para simular la descarga de datos. El código en el onPostExecutees simular el uso de Fragment.

Cuando giro la pantalla muy rápido entre horizontal y vertical, aparece una excepción en el onPostExecutecódigo:

java.lang.IllegalStateException: Fragment MyFragment {410f6060} no adjunto a la actividad

Creo que es porque MyFragmentse ha creado una nueva mientras tanto, y se adjuntó a la Actividad antes de AsyncTaskfinalizar. El código en onPostExecutellamadas a un desapegado MyFragment.

Pero, ¿cómo puedo solucionar esto?

nhaarman
fuente
1
Debe usar la vista desde el fragmentador inflador. mView = inflater.inflate(R.layout.my_layout, container, false) Y ahora utilizar este punto de vista cuando se quiere obtener recursos: mView.getResources().***. Me ayudó a solucionar este error.
foxis
@foxis Eso filtra el Contextque está adjunto a su `mView`.
nhaarman
Puede ser que aún no lo verifique. Para evitar fugas, ¿qué tal si mViewqueda nulo en onDestroy?
foxis

Respuestas:

774

He encontrado la respuesta muy simple isAdded():

Devuelve truesi el fragmento se agrega actualmente a su actividad.

@Override
protected void onPostExecute(Void result){
    if(isAdded()){
        getResources().getString(R.string.app_name);
    }
}

Para evitar onPostExecuteser llamado cuando el Fragmentno está conectado al Activityes cancelar el AsyncTaskal pausar o detener el Fragment. Entonces isAdded()ya no sería necesario. Sin embargo, es recomendable mantener esta verificación en su lugar.

nhaarman
fuente
En mi caso, cuando estoy iniciando Otro intento de aplicación desde ... entonces recibo el mismo error ... ¿alguna sugerencia?
Código
1
developer.android.com/reference/android/app/… ... también isDetached()se agregó en el nivel 13 de API
Lucas Jota
55
Cuando está en API <11, está utilizando developer.android.com/reference/android/support/v4/app/… donde funcionará.
nhaarman
Me enfrenté a este problema cuando usé DialogFragment. Después de descartar dialogFragment, intenté iniciar otra actividad. Entonces se produjo este error. Evité este error llamando a despedir () después de startActivity. El problema era que el fragmento ya estaba separado de la Actividad.
Ataru
28

El problema es que está intentando acceder a los recursos (en este caso, cadenas) usando getResources (). GetString (), que intentará obtener los recursos de la Actividad. Vea este código fuente de la clase Fragment:

 /**
  * Return <code>getActivity().getResources()</code>.
  */
 final public Resources getResources() {
     if (mHost == null) {
         throw new IllegalStateException("Fragment " + this + " not attached to Activity");
     }
     return mHost.getContext().getResources();
 }

mHost es el objeto que contiene tu actividad.

Debido a que la Actividad podría no estar adjunta, su llamada a getResources () generará una Excepción.

La solución aceptada en mi humilde opinión no es el camino a seguir, ya que solo está ocultando el problema. La forma correcta es obtener los recursos de otro lugar que siempre se garantiza que existe, como el contexto de la aplicación:

youApplicationObject.getResources().getString(...)
Tiago
fuente
Utilicé esta solución porque necesitaba ejecutar getString()cuando mi fragmento estaba en pausa. Gracias
Geekarist
24

Me he enfrentado a dos escenarios diferentes aquí:

1) Cuando quiero que la tarea asincrónica termine de todos modos: imagina que mi onPostExecute almacena los datos recibidos y luego llama a un oyente para actualizar las vistas, por lo que, para ser más eficiente, quiero que la tarea termine de todos modos para tener los datos listos cuando el usuario llegue espalda. En este caso, generalmente hago esto:

@Override
protected void onPostExecute(void result) {
    // do whatever you do to save data
    if (this.getView() != null) {
        // update views
    }
}

2) Cuando quiero que la tarea asincrónica solo termine cuando las vistas se pueden actualizar: en el caso que está proponiendo aquí, la tarea solo actualiza las vistas, no se necesita almacenamiento de datos, por lo que no tiene idea de que la tarea finalice si las vistas son ya no se muestra. Hago esto:

@Override
protected void onStop() {
    // notice here that I keep a reference to the task being executed as a class member:
    if (this.myTask != null && this.myTask.getStatus() == Status.RUNNING) this.myTask.cancel(true);
    super.onStop();
}

No he encontrado ningún problema con esto, aunque también uso una (quizás) forma más compleja que incluye iniciar tareas desde la actividad en lugar de los fragmentos.

Ojalá esto ayude a alguien! :)

luixal
fuente
18

El problema con su código es la forma en que está utilizando AsyncTask, porque cuando gira la pantalla durante el hilo de suspensión:

Thread.sleep(2000) 

AsyncTask todavía funciona, es porque no canceló la instancia de AsyncTask correctamente en onDestroy () antes de que se reconstruya el fragmento (cuando gira) y cuando esta misma instancia de AsyncTask (después de girar) se ejecuta en PostExecute (), intenta encontrar los recursos con getResources () con la antigua instancia de fragmento (una instancia no válida):

getResources().getString(R.string.app_name)

que es equivalente a:

MyFragment.this.getResources().getString(R.string.app_name)

Entonces, la solución final es administrar la instancia de AsyncTask (para cancelar si aún funciona) antes de que el fragmento se reconstruya cuando gira la pantalla, y si se cancela durante la transición, reinicie AsyncTask después de la reconstrucción con la ayuda de una bandera booleana:

public class MyFragment extends SherlockFragment {

    private MyAsyncTask myAsyncTask = null;
    private boolean myAsyncTaskIsRunning = true;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(savedInstanceState!=null) {
            myAsyncTaskIsRunning = savedInstanceState.getBoolean("myAsyncTaskIsRunning");
        }
        if(myAsyncTaskIsRunning) {
            myAsyncTask = new MyAsyncTask();
            myAsyncTask.execute();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("myAsyncTaskIsRunning",myAsyncTaskIsRunning);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(myAsyncTask!=null) myAsyncTask.cancel(true);
        myAsyncTask = null;

    }

    public class MyAsyncTask extends AsyncTask<Void, Void, Void>() {

        public MyAsyncTask(){}

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            myAsyncTaskIsRunning = true;
        }
        @Override
        protected Void doInBackground(Void... params) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {}
            return null;
        }

        @Override
        protected void onPostExecute(Void result){
            getResources().getString(R.string.app_name);
            myAsyncTaskIsRunning = false;
            myAsyncTask = null;
        }

    }
}
Erick Reátegui Diaz
fuente
en cambio si getResources().***usa Fragments.this.getResource().***ayuda
Prabs
17

Son una solución bastante engañosa para esto y una fuga de fragmentos de la actividad.

Entonces, en el caso de getResource o cualquier cosa que dependa del acceso al contexto de actividad desde Fragment, siempre se verifica el estado de la actividad y el estado de los fragmentos de la siguiente manera

 Activity activity = getActivity(); 
    if(activity != null && isAdded())

         getResources().getString(R.string.no_internet_error_msg);
//Or any other depends on activity context to be live like dailog


        }
    }
Vinayak
fuente
77
isAdded () es suficiente porque: boolean público final isAdded () {return mHost! = null && mAdded; }
NguyenDat
En mi caso, estas comprobaciones no son suficientes, todavía se cuelgan a pesar de que agregué estas.
David
@David, isAddedes suficiente. Nunca vi una situación cuando se getString()había estrellado si isAdded == true. ¿Estás seguro de que se mostró una actividad y se adjuntó un fragmento?
CoolMind
14
if (getActivity() == null) return;

Funciona también en algunos casos. Simplemente interrumpe la ejecución del código y asegúrese de que la aplicación no se bloquee

superusuario
fuente
10

Me enfrenté al mismo problema, solo agregué la instancia de singletone para obtener el recurso según lo referido por Erick

MainFragmentActivity.defaultInstance().getResources().getString(R.string.app_name);

también puedes usar

getActivity().getResources().getString(R.string.app_name);

Espero que esto sea de ayuda.

Aristo Michael
fuente
2

Me enfrenté a problemas similares cuando la actividad de configuración de la aplicación con las preferencias cargadas era visible. Si cambiara una de las preferencias y luego hiciera girar el contenido de la pantalla y volviera a cambiar la preferencia, se bloquearía con un mensaje de que el fragmento (mi clase de Preferencias) no estaba adjunto a una actividad.

Al depurar, parecía que el método onCreate () del PreferencesFragment se llamaba dos veces cuando el contenido de la pantalla giraba. Eso ya era bastante extraño. Luego agregué la verificación isAdded () fuera del bloque donde indicaría el bloqueo y resolvió el problema.

Aquí está el código del oyente que actualiza el resumen de preferencias para mostrar la nueva entrada. Se encuentra en el método onCreate () de mi clase de Preferencias que extiende la clase PreferenceFragment:

public static class Preferences extends PreferenceFragment {
    SharedPreferences.OnSharedPreferenceChangeListener listener;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // ...
        listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                // check if the fragment has been added to the activity yet (necessary to avoid crashes)
                if (isAdded()) {
                    // for the preferences of type "list" set the summary to be the entry of the selected item
                    if (key.equals(getString(R.string.pref_fileviewer_textsize))) {
                        ListPreference listPref = (ListPreference) findPreference(key);
                        listPref.setSummary("Display file content with a text size of " + listPref.getEntry());
                    } else if (key.equals(getString(R.string.pref_fileviewer_segmentsize))) {
                        ListPreference listPref = (ListPreference) findPreference(key);
                        listPref.setSummary("Show " + listPref.getEntry() + " bytes of a file at once");
                    }
                }
            }
        };
        // ...
    }

¡Espero que esto ayude a otros!

oh dios no otra
fuente
0

Si extiende la Applicationclase y mantiene un objeto de contexto 'global' estático, como se indica a continuación, puede usar eso en lugar de la actividad para cargar un recurso de cadena.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

    @Override
    public void onCreate() {
        super.onCreate();
        GLOBAL_APP_CONTEXT = this;
    }
}

Si usa esto, puede salirse con la suya Toasty cargar recursos sin preocuparse por los ciclos de vida.

Anthony Chuinard
fuente
55
Estoy siendo rechazado pero nadie ha explicado por qué. Los contextos estáticos suelen ser malos, pero pensé que no es una pérdida de memoria si tienes una referencia de aplicación estática.
Anthony Chuinard
Su respuesta es rechazada porque esto es solo un truco, no una solución adecuada. Compruebe la solución compartida por @nhaarman
Vivek Kumar Srivastava
0

En mi caso, se han llamado métodos de fragmentos después

getActivity().onBackPressed();
CoolMind
fuente
0

Una publicación anterior, pero me sorprendió la respuesta más votada.

La solución adecuada para esto debería ser cancelar el asinctask en onStop (o donde sea apropiado en su fragmento). De esta manera, no introduce una pérdida de memoria (un asinctask que mantiene una referencia a su fragmento destruido) y tiene un mejor control de lo que está sucediendo en su fragmento.

@Override
public void onStop() {
    super.onStop();
    mYourAsyncTask.cancel(true);
}
Raz
fuente
1
La respuesta más votada incluye esto. Además, cancelno puede evitar que onPostExecutese invoque.
nhaarman el
Llamar a cancelar garantiza que onPostExecute nunca será llamado, ambas llamadas se ejecutan en el mismo hilo, por lo tanto, tiene la garantía de que no se invocará después de llamar a cancelar
Raz