Fragmento MyFragment não anexado à Atividade


393

Eu criei um pequeno aplicativo de teste que representa meu problema. Estou usando o ActionBarSherlock para implementar guias com fragmentos (Sherlock).

Meu 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();
    }
}

Eu adicionei a Thread.sleeppeça para simular o download de dados. O código no onPostExecuteé para simular o uso do Fragment.

Quando eu giro a tela muito rapidamente entre paisagem e retrato, recebo uma exceção no onPostExecutecódigo:

java.lang.IllegalStateException: fragmento MyFragment {410f6060} não anexado à atividade

Eu acho que é porque um novo MyFragmentfoi criado nesse meio tempo e foi anexado à Atividade antes do AsyncTasktérmino. O código onPostExecutechama um desanexado MyFragment.

Mas como posso corrigir isso?


11
Você deve usar a vista do inflador de fragmentos. mView = inflater.inflate(R.layout.my_layout, container, false) E agora usar este ponto de vista quando você deseja obter recursos: mView.getResources().***. Isso me ajudou a corrigir esse bug.
— foxis

@foxis Isso vaza o Contextque está anexado ao seu `mView`.
— Nhaarman 16/03/19

Pode ser que ainda não verifique. Para evitar vazamentos, que tal obter nulo mViewno onDestroy?
— foxis

Respostas:


774

Eu encontrei a resposta muito simples isAdded():

Retorne truese o fragmento estiver atualmente adicionado à sua atividade.

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

Para evitar onPostExecuteser chamado quando o Fragmentnão estiver conectado ao, Activityé para cancelar o AsyncTaskquando pausar ou parar o Fragment. Então isAdded()não seria mais necessário. No entanto, é aconselhável manter essa verificação no local.


No meu caso, quando estou iniciando outra aplicação Intent from ... então estou recebendo o mesmo erro ... alguma sugestão?
— CoDe 28/10

11
developer.android.com/reference/android/app/… ... também há isDetached(), que foi adicionado no nível 13 da API
— Lucas Jota

5
Quando na API <11, você está usando developer.android.com/reference/android/support/v4/app/… onde ele funcionará.
— Nhaarman 21/03

Eu enfrentei esse problema quando usei o DialogFragment. Depois de descartar o dialogFragment, tentei iniciar outra atividade. Então este erro ocorreu. Evitei esse erro chamando demitir () após startActivity. O problema era que o fragmento já estava desanexado da Activity.
— Ataru

28

O problema é que você está tentando acessar recursos (nesse caso, strings) usando getResources (). GetString (), que tentará obter os recursos da Activity. Veja este código fonte da classe 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 é o objeto que contém sua atividade.

Como a atividade pode não estar anexada, sua chamada getResources () lançará uma exceção.

A solução aceita IMHO não é o caminho a seguir, pois você está apenas escondendo o problema. A maneira correta é apenas obter os recursos de outro lugar que é sempre garantido que existe, como o contexto do aplicativo:

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

Eu usei essa solução porque precisava executar getString()quando meu fragmento foi pausado. Obrigado
— Geekarist

24

Eu enfrentei dois cenários diferentes aqui:

1) Quando eu quero que a tarefa assíncrona termine de qualquer maneira: imagine que o meu onPostExecute armazene dados recebidos e chame um ouvinte para atualizar as visualizações, para que, para ser mais eficiente, eu queira que a tarefa termine assim mesmo, para que eu tenha os dados prontos quando o usuário acessar de volta. Neste caso, eu costumo fazer isso:

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

2) Quando eu quero que a tarefa assíncrona termine apenas quando as visualizações puderem ser atualizadas: no caso que você está propondo aqui, a tarefa atualiza apenas as visualizações, não é necessário armazenamento de dados, portanto não há nenhuma pista para a tarefa terminar se as visualizações forem não está mais sendo mostrado. Eu faço isso:

@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();
}

Não encontrei nenhum problema com isso, embora eu também use uma maneira (talvez) mais complexa que inclua iniciar tarefas da atividade em vez de fragmentos.

Desejo que isso ajude alguém! :)


18

O problema com o seu código é o modo como você está usando o AsyncTask, porque quando você gira a tela durante o encadeamento de suspensão:

Thread.sleep(2000) 

o AsyncTask ainda está funcionando, é porque você não cancelou a instância do AsyncTask corretamente em onDestroy () antes da reconstrução do fragmento (quando você gira) e quando essa mesma instância do AsyncTask (após a rotação) é executada emPostExecute (), isso tenta encontrar os recursos com getResources () com a instância antiga do fragmento (uma instância inválida):

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

que é equivalente a:

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

Portanto, a solução final é gerenciar a instância do AsyncTask (para cancelar se ainda estiver funcionando) antes que o fragmento seja reconstruído quando você girar a tela e, se cancelado durante a transição, reinicie o AsyncTask após a reconstrução com o auxílio de um sinalizador booleano:

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;
        }

    }
}

em vez se getResources().***usando Fragments.this.getResource().***ajudou
— Prabs

17

São uma solução bastante complicada para isso e vazamento de fragmento de atividade.

Portanto, no caso de getResource ou qualquer coisa que dependa do contexto de atividade acessado pelo Fragment, verifique sempre o status da atividade e o status dos fragmentos da seguinte maneira

 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


        }
    }

7
isAdded () é suficiente porque: booleano público final isAdded () {return mHost! = null && mAdded; }
— NguyenDat 30/11/16

No meu caso, essas verificações não são suficientes, ainda tendo travamentos, apesar de eu as ter adicionado.
— David

@ David, isAddedé o suficiente. Eu nunca vi uma situação quando getString()havia caído se isAdded == true. Tem certeza de que uma atividade foi mostrada e um fragmento foi anexado?
— CoolMind

14
if (getActivity() == null) return;

funciona também em alguns casos. Apenas interrompe a execução do código e verifique se o aplicativo não falha


10

Enfrentei o mesmo problema: basta adicionar a instância singletone para obter o recurso, conforme referido por Erick

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

você também pode usar

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

Espero que isso ajude.


2

Eu enfrentei problemas semelhantes quando a atividade de configurações do aplicativo com as preferências carregadas estava visível. Se eu alterasse uma das preferências e, em seguida, fizesse o conteúdo de exibição girar e alterasse a preferência novamente, haveria uma falha na mensagem de que o fragmento (minha classe Preferences) não estava anexado a uma atividade.

Ao depurar, parecia que o método onCreate () do PreferencesFragment estava sendo chamado duas vezes quando o conteúdo da exibição girou. Isso já era estranho o suficiente. Em seguida, adicionei a verificação isAdded () fora do bloco, onde indicava a falha e resolvia o problema.

Aqui está o código do ouvinte que atualiza o resumo das preferências para mostrar a nova entrada. Está localizado no método onCreate () da minha classe Preferences, que estende a classe 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 isso ajude os outros!


0

Se você estender a Applicationclasse e manter um objeto de Contexto 'global' estático, conforme a seguir, poderá usá-lo em vez da atividade para carregar um recurso String.

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

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

Se você usar isso, poderá se safar Toaste carregar recursos sem se preocupar com os ciclos de vida.


5
Estou com voto negativo, mas ninguém explicou o porquê. Contextos estáticos geralmente são ruins, mas eu pensava que não seria um vazamento de memória se você tiver uma referência de aplicativo estática.
— Anthony Chuinard

Sua resposta foi negada porque esta é apenas uma solução inadequada para hackear. Verifique a solução compartilhada por @nhaarman
— Vivek Kumar Srivastava

0

No meu caso, métodos de fragmento foram chamados após

getActivity().onBackPressed();

0

Um post antigo, mas fiquei surpreso com a resposta mais votada.

A solução adequada para isso deve ser cancelar a asynctask no onStop (ou onde for apropriado no seu fragmento). Dessa forma, você não introduz um vazamento de memória (uma assíncrona mantém uma referência ao seu fragmento destruído) e tem um controle melhor do que está acontecendo no seu fragmento.

@Override
public void onStop() {
    super.onStop();
    mYourAsyncTask.cancel(true);
}

11
A resposta mais votada inclui isso. Além disso, cancelpode não impedir a onPostExecuteinvocação.
— Nhaarman 5/01

Chamando cancelar não garante OnPostExecute nunca será chamado, ambas as chamadas executar no mesmo segmento, portanto, você está garantido que não será invocado depois de chamar cancelar
— Raz
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.