android - Fragmento MiFragmento no adjunto a la Actividad




android-fragments actionbarsherlock (8)

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

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 añadido la parte Thread.sleep para simular la descarga de datos. El código en onPostExecute es simular el uso del Fragment .

Cuando roto la pantalla muy rápido entre horizontal y vertical, obtengo una excepción en el código onPostExecute :

java.lang.IllegalStateException: Fragmento MyFragment {410f6060} no adjunto a la Actividad

Creo que se debe a que se ha creado un nuevo MyFragment mientras tanto, y se adjuntó a la Actividad antes de que finalizara la AsyncTask . El código en onPostExecute llama a un MyFragment sin MyFragment .

Pero, ¿cómo puedo solucionar esto?


El problema con tu código es la forma en que estás usando la AsyncTask, porque cuando giras la pantalla durante el hilo de suspensión:

Thread.sleep(2000) 

AsyncTask sigue funcionando, es porque no canceló la instancia de AsyncTask correctamente en onDestroy () antes de que el fragmento se reconstruya (cuando se gire) y cuando esta misma instancia de AsyncTask (después de rotate) se ejecuta enPostExecute (), esto intenta encontrar los recursos con getResources () con la instancia de fragmento anterior (una instancia no válida):

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

que es equivalente a:

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

Así que la solución final es administrar la instancia de AsyncTask (para cancelar si esto todavía funciona) antes de que el fragmento se vuelva a generar cuando gire 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;
        }

    }
}

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

 /**
  * 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 () lanzará una Excepción.

La solución aceptada IMHO no es el camino a seguir, ya que simplemente está ocultando el problema. La forma correcta es obtener los recursos de otro lugar que siempre se garantiza que exista, como el contexto de la aplicación:

youApplicationObject.getResources().getString(...)

He encontrado la respuesta muy simple: isAdded() :

Devuelva true si el fragmento se agrega actualmente a su actividad.

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

Para evitar que se onPostExecute a onPostExecute cuando el Fragment no está adjunto a la Activity se debe cancelar la AsyncTask cuando se detiene o detiene el Fragment . Entonces isAdded() ya no sería necesario. Sin embargo, es recomendable mantener este control en su lugar.


He enfrentado dos escenarios diferentes aquí:

1) Cuando quiero que la tarea asincrónica termine de todos modos: imagina que onPostExecute almacena los datos recibidos y luego llama a un oyente para que actualice las vistas, para que sea más eficiente, quiero que la tarea termine de todos modos, así que tengo la información lista cuando el usuario lo indique. atrás. En este caso suelo hacer 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 asíncrona solo termine cuando las vistas pueden actualizarse: en el caso que está proponiendo aquí, la tarea solo actualiza las vistas, no se necesita almacenamiento de datos, por lo que no tiene la menor 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 forma (quizás) más compleja que incluye el inicio de tareas desde la actividad en lugar de los fragmentos.

¡Ojalá esto ayude a alguien! :)


Me enfrenté con el mismo problema, solo agregué la instancia de singletone para obtener el recurso 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.


Si extiende la clase de Application y mantiene un objeto de contexto 'global' estático, de la siguiente manera, 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 Toast y la carga de recursos sin preocuparse por los ciclos de vida.


Son una solución bastante difícil para esto y la pérdida de fragmentos de la actividad.

Por lo tanto, en el caso de getResource o cualquier otro que dependa del contexto de actividad al que se acceda 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


        }
    }

if (getActivity() == null) return;

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





actionbarsherlock