Tarefa em segundo plano, diálogo de progresso, mudança de orientação - existe alguma solução 100% funcional?

235

Baixo alguns dados da Internet no thread de segundo plano (eu uso AsyncTask) e exibo uma caixa de diálogo de progresso durante o download. A orientação muda, a Atividade é reiniciada e, em seguida, minha AsyncTask é concluída - desejo encerrar a caixa de diálogo de progresso e iniciar uma nova Atividade. Mas, por vezes, chamar a opção unlockDialog gera uma exceção (provavelmente porque a Atividade foi destruída e a nova Atividade ainda não foi iniciada).

Qual é a melhor maneira de lidar com esse tipo de problema (atualizar a interface do usuário a partir do encadeamento em segundo plano que funciona mesmo que o usuário altere a orientação)? Alguém do Google forneceu alguma "solução oficial"?

fhucho
fonte
4
Minha postagem no blog sobre este tópico pode ajudar. Trata-se de reter tarefas de longa execução através de alterações na configuração.
Alex Lockwood
1
Esta questão também está relacionada.
Alex Lockwood
Apenas FTR há um mistério relacionado aqui .. stackoverflow.com/q/23742412/294884
Fattie

Respostas:

336

Etapa 1: crie AsyncTaskuma staticclasse aninhada ou uma classe totalmente separada, mas não uma classe interna (aninhada não estática).

Etapa 2: AsyncTaskSegure o Activityatravés de um membro de dados, definido através do construtor e de um configurador.

Etapa 3: Ao criar o AsyncTask, forneça a corrente Activityao construtor.

Etapa 4: onRetainNonConfigurationInstance()retorne a AsyncTask, depois de desconectá-la da atividade original que está indo embora agora.

Etapa 5: no onCreate()caso getLastNonConfigurationInstance()contrário null, transmita-o à sua AsyncTaskturma e ligue para o levantador para associar sua nova atividade à tarefa.

Etapa 6: Não consulte o membro de dados da atividade de doInBackground().

Se você seguir a receita acima, tudo funcionará. onProgressUpdate()e onPostExecute()são suspensos entre o início onRetainNonConfigurationInstance()e o final do subsequente onCreate().

Aqui está um projeto de amostra demonstrando a técnica.

Outra abordagem é abandonar o AsyncTaske mover seu trabalho para um IntentService. Isso é particularmente útil se o trabalho a ser feito puder ser longo e continuar independentemente do que o usuário faz em termos de atividades (por exemplo, baixar um arquivo grande). Você pode usar uma transmissão ordenada Intentpara que a atividade responda ao trabalho que está sendo realizado (se ainda estiver em primeiro plano) ou aumente a Notificationpara informar ao usuário se o trabalho foi realizado. Aqui está uma postagem de blog com mais informações sobre esse padrão.

CommonsWare
fonte
8
Muito obrigado pela sua ótima resposta a esse problema comum! Para ser mais completo, você pode adicionar à etapa 4 que precisamos desanexar (definida como nula) a atividade no AsyncTask. Isso está bem ilustrado no projeto de amostra.
precisa
3
Mas e se eu precisar ter acesso aos membros da Activity?
319 Eugene
3
@ Andrew: Crie uma classe interna estática ou algo que se apegue a vários objetos e retorne-a.
CommonsWare
11
onRetainNonConfigurationInstance()foi descontinuado e a alternativa sugerida é usar setRetainInstance(), mas não retorna um objeto. É possível lidar asyncTaskcom mudanças na configuração setRetainInstance()?
Indrek Kõue
10
@SYLARRR: Absolutamente. Ter o Fragmentporão AsyncTask. Tenha o Fragmentchamado setRetainInstance(true)em si. Tenha a AsyncTaskúnica conversa com o Fragment. Agora, em uma alteração na configuração, o Fragmentitem não é destruído e recriado (mesmo que a atividade seja) e, portanto, AsyncTaské retido na alteração da configuração.
CommonsWare
13

A resposta aceita foi muito útil, mas não possui uma caixa de diálogo de progresso.

Felizmente para você, leitor, criei um exemplo extremamente abrangente e funcional de um AsyncTask com uma caixa de diálogo de progresso !

  1. A rotação funciona e a caixa de diálogo sobrevive.
  2. Você pode cancelar a tarefa e a caixa de diálogo pressionando o botão Voltar (se desejar esse comportamento).
  3. Ele usa fragmentos.
  4. O layout do fragmento abaixo da atividade muda corretamente quando o dispositivo gira.
Timmmm
fonte
A resposta aceita é sobre classes estáticas (não membros). E isso é necessário para evitar que o AsyncTask tenha um ponteiro (oculto) para a instância da classe externa que se torna um vazamento de memória ao destruir a atividade.
Bananeweizen
Sim, não sei por que eu coloquei isso nos membros estáticos, já que eu também os usei ... estranhos. Resposta editada.
Timmmm
Você poderia atualizar seu link? Eu realmente preciso disso.
Romain Pellerin
Desculpe, ainda não tive a chance de restaurar meu site - em breve! Mas, nesse meio tempo, é basicamente o mesmo que o código nesta resposta: stackoverflow.com/questions/8417885/…
Timmmm
1
O link é falso; apenas leva a um índice inútil, sem indicação de onde está o código.
FractalBob 17/07/2015
9

Trabalhei por uma semana para encontrar uma solução para esse dilema sem recorrer à edição do arquivo de manifesto. As suposições para esta solução são:

  1. Você sempre precisa usar uma caixa de diálogo de progresso
  2. Apenas uma tarefa é executada por vez
  3. Você precisa que a tarefa persista quando o telefone é girado e a caixa de diálogo de progresso é descartada automaticamente.

Implementação

Você precisará copiar os dois arquivos encontrados na parte inferior desta postagem no seu espaço de trabalho. Apenas certifique-se de que:

  1. Todos os seus Activitys devem se estenderBaseActivity

  2. Em onCreate(), super.onCreate()deve ser chamado depois que você inicializar qualquer membro que precise ser acessado por seus ASyncTasks. Substitua também getContentViewId()para fornecer o ID do layout do formulário.

  3. Substitua onCreateDialog() como de costume para criar caixas de diálogo gerenciadas pela atividade.

  4. Veja o código abaixo para obter uma amostra de classe interna estática para criar suas AsyncTasks. Você pode armazenar seu resultado no mResult para acessar mais tarde.


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

E, finalmente, para iniciar sua nova tarefa:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

É isso aí! Espero que esta solução robusta ajude alguém.

BaseActivity.java (organize você mesmo as importações)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

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

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
Oleg Vaskevich
fonte
4

Alguém do Google forneceu alguma "solução oficial"?

Sim.

A solução é mais uma proposta de arquitetura de aplicativo do que apenas algum código .

Eles propuseram 3 padrões de design que permitem que um aplicativo funcione em sincronia com um servidor, independentemente do estado do aplicativo (ele funcionará mesmo que o usuário termine o aplicativo, o usuário altere a tela, o aplicativo seja encerrado, todos os outros estados possíveis em que uma operação de dados em segundo plano pode ser interrompida, isso abrange)

A proposta é explicada no discurso sobre aplicativos clientes REST Android durante o Google I / O 2010 por Virgil Dobjanschi. Tem uma hora de duração, mas vale a pena assistir.

A base é abstrair as operações de rede para uma Serviceque funcione independentemente de qualquer Activityaplicação. Se você estiver trabalhando com bancos de dados, use ContentResolvere Cursordaria a você um padrão Observador pronto para uso que é conveniente para atualizar a interface do usuário sem lógica adicional, depois que você atualiza seu banco de dados local com os dados remotos buscados. Qualquer outro código pós-operação seria executado por meio de um retorno de chamada passado para o Service(eu uso uma ResultReceiversubclasse para isso).

De qualquer forma, minha explicação é bastante vaga, você definitivamente deve assistir ao discurso.

Christopher Francisco
fonte
2

Embora a resposta de Mark (CommonsWare) realmente funcione para alterações de orientação, ela falha se a Atividade for destruída diretamente (como no caso de uma ligação telefônica).

Você pode lidar com as alterações de orientação E os raros eventos de Atividade destruídos usando um objeto Aplicativo para referenciar sua ASyncTask.

Há uma excelente explicação do problema e da solução aqui :

Crédito é totalmente para Ryan por descobrir isso.

Scott Biggs
fonte
1

Após 4 anos, o Google resolveu o problema chamando setRetainInstance (true) em Activity onCreate. Ele preservará sua instância de atividade durante a rotação do dispositivo. Eu também tenho uma solução simples para Android mais antigo.

Singagirl
fonte
1
O problema que as pessoas observam acontece porque o Android destrói uma classe de atividade na rotação, extensão do teclado e outros eventos, mas uma tarefa assíncrona ainda mantém uma referência para a instância destruída e tenta usá-la para atualizações da interface do usuário. Você pode instruir o Android a não destruir a atividade de forma manifesta ou pragmática. Nesse caso, uma referência de tarefa assíncrona permanece válida e nenhum problema é observado. Como a rotação pode exigir algum trabalho adicional como recarregar visualizações e assim por diante, o Google não recomenda preservar a atividade. Então você decide.
Singagirl
Obrigado, eu sabia sobre a situação, mas não sobre setRetainInstance (). O que não entendo é a sua afirmação de que o Google usou isso para resolver os problemas da pergunta. Você pode vincular a fonte de informação? Obrigado.
jj_
onRetainNonConfigurationInstance () Essa função é chamada puramente como uma otimização e você não deve confiar nela. <Da ​​mesma fonte: developer.android.com/reference/android/app/…
Dhananjay M
0

você deve chamar todas as ações de atividade usando o manipulador de atividades. Portanto, se você estiver em algum segmento, crie um Runnable e publique usando o Manipulador da Activitie. Caso contrário, seu aplicativo falhará às vezes, com exceção fatal.

xpepermint
fonte
0

Esta é a minha solução: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Basicamente, as etapas são:

  1. Eu uso onSaveInstanceStatepara salvar a tarefa se ela ainda estiver em processamento.
  2. Em onCreateeu recebo a tarefa se ela foi salva.
  3. Em onPausedescartar o ProgressDialogse é mostrado.
  4. Em onResumeeu mostro ProgressDialogse a tarefa ainda está sendo processada.
Peguei vocês
fonte