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?

nhaarman
fonte
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.

nhaarman
fonte
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(...)
Tiago
fonte
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! :)

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

    }
}
Erick Reátegui Diaz
fonte
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


        }
    }
Vinayak
fonte
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

superusuário
fonte
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.

Aristo Michael
fonte
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!

ohgodnotanotherone
fonte
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.

Anthony Chuinard
fonte
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();
CoolMind
fonte
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);
}
Raz
fonte
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