Como lidar com uma AsyncTask durante a rotação da tela?

88

Eu li muito sobre como salvar meu estado de instância ou como lidar com minha atividade sendo destruída durante a rotação da tela.

Parece haver muitas possibilidades, mas ainda não descobri qual funciona melhor para recuperar os resultados de uma AsyncTask.

Eu tenho alguns AsyncTasks que são simplesmente iniciados novamente e chamam o isFinishing()método da atividade e se a atividade estiver terminando eles não atualizarão nada.

O problema é que tenho uma tarefa que faz uma solicitação a um serviço da web que pode falhar ou ter sucesso e reiniciar a tarefa resultaria em perda financeira para o usuário.

Como você resolveria isso? Quais são as vantagens ou desvantagens das soluções possíveis?

Janusz
fonte
1
Veja minha resposta aqui . Você também pode encontrar essas informações sobre o que setRetainInstance(true)realmente é útil.
Timmmm
o que eu faria é simplesmente implementar um serviço local que realiza o processamento (em um thread) que sua asyncTask está fazendo. Para exibir os resultados, transmita os dados para sua atividade. Agora a atividade é responsável apenas por mostrar os dados e o processamento nunca é interrompido por uma rotação de tela.
Alguém em algum lugar
Que tal usar AsyncTaskLoader em vez de AsyncTask ??
Sourangshu Biswas

Respostas:

7

Minha primeira sugestão seria ter certeza de que você realmente precisa que sua atividade seja redefinida em uma rotação de tela (o comportamento padrão). Sempre que tive problemas com a rotação, adicionei esse atributo à minha <activity>tag no AndroidManifest.xml e estava tudo bem.

android:configChanges="keyboardHidden|orientation"

Parece estranho, mas o que ele faz é passar para o seu onConfigurationChanged()método, se você não fornecer um ele não faz nada além de medir novamente o layout, o que parece ser uma forma perfeitamente adequada de lidar com o giro na maioria das vezes .

Jim Blackler
fonte
5
Mas isso impedirá que a Activity mude o layout. E, portanto, força o usuário a utilizar seu dispositivo em uma orientação específica ditada pela sua aplicação e não pelas necessidades dele.
Janusz
77
Usar essa técnica impede que você use facilmente recursos específicos de configuração. Por exemplo, se você quiser que seu layout ou drawables ou cordas ou o que seja diferente em retratos e paisagens, você desejará o comportamento padrão. Substituir a mudança de configuração só deve ser feito em casos muito específicos (um jogo, um navegador da web, etc.) e não por preguiça ou conveniência porque você está se restringindo.
Romain Guy
38
Bem, é exatamente isso, Romain. "se você quiser que seu layout ou drawables ou strings ou o que seja diferente em retratos e paisagens, você vai querer o comportamento padrão", acredito que seja um caso de uso muito mais raro do que você imagina. O que você chama de "casos muito específicos" é a maioria dos desenvolvedores, eu acredito. Usar layouts relativos que funcionam em todas as dimensões é a melhor prática e não é tão difícil. Falar de preguiça é altamente equivocado, essas técnicas são para melhorar a experiência do usuário, não para reduzir o tempo de desenvolvimento.
Jim Blackler,
2
Eu descobri que isso funciona perfeitamente para LinearLayout, mas ao usar RelativeLayout, ele não redesenha o layout corretamente ao alternar para o modo paisagem (pelo menos não no N1). Veja estas perguntas: stackoverflow.com/questions/2987049/…
JohnRock
9
Concordo com Romain (ele sabe do que está falando, ele desenvolve o SO). O que acontece quando você deseja portar seu aplicativo para um tablet e sua IU fica horrível quando esticada? Se você seguir a abordagem desta resposta, precisará recodificar toda a sua solução porque optou por este hack preguiçoso.
Austyn Mahoney,
46

Você pode verificar como eu lidoAsyncTask com mudanças de orientação em code.google.com/p/shelves . Existem várias maneiras de fazer isso, a que escolhi neste aplicativo é cancelar qualquer tarefa em execução no momento, salvar seu estado e iniciar uma nova com o estado salvo quando a nova Activityfor criada. É fácil de fazer, funciona bem e, como bônus, cuida de interromper suas tarefas quando o usuário sai do aplicativo.

Você também pode usar onRetainNonConfigurationInstance()para passar seu AsyncTaskpara o novo Activity(no Activityentanto, tome cuidado para não vazar o anterior dessa forma.)

Romain Guy
fonte
1
Eu tentei girar durante as interrupções de busca de livros e me deu menos resultados do que quando não girava, uma pena
max4ever
1
Não consegui encontrar um único uso de AsyncTask nesse código. Existe uma classe UserTask que parece semelhante. Este projeto é anterior a AsyncTask?
devconsole
7
AsyncTask veio de UserTask. Eu originalmente escrevi UserTask para meus próprios aplicativos e depois o transformei em AsyncTask. Desculpe, esqueci que foi renomeado.
Romain Guy
@RomainGuy Olá, espero que você esteja bem. De acordo com seu código 2, os pedidos são enviados ao servidor, embora na primeira tarefa seja cancelada, mas não foi cancelada com sucesso. Não sei por quê. Você poderia me dizer se existe alguma maneira de resolver isso.
iamcrypticcoder
10

Essa é a pergunta mais interessante que já vi em relação ao Android !!! Na verdade, já estou procurando a solução nos últimos meses. Ainda não resolvi.

Tenha cuidado, simplesmente substituindo o

android:configChanges="keyboardHidden|orientation"

coisas não são suficientes.

Considere o caso em que o usuário recebe uma chamada telefônica enquanto o AsyncTask está em execução. Sua solicitação já está sendo processada pelo servidor, portanto, AsyncTask está aguardando resposta. Nesse momento, seu aplicativo entra em segundo plano, pois o aplicativo Telefone acaba de entrar em primeiro plano. OS pode matar sua atividade, pois está em segundo plano.

Vit Khudenko
fonte
6

Por que você não sempre mantém uma referência à AsyncTask atual no Singleton fornecido pelo Android?

Sempre que uma tarefa é iniciada, no PreExecute ou no construtor, você define:

((Application) getApplication()).setCurrentTask(asyncTask);

Sempre que terminar, você o define como nulo.

Dessa forma, você sempre tem uma referência que permite fazer algo como onCreate ou onResume conforme apropriado para sua lógica específica:

this.asyncTaskReference = ((Application) getApplication()).getCurrentTask();

Se for null, você sabe que não há nenhum em execução no momento!

:-)

neteinstein
fonte
Isso vai funcionar? Alguém testou isso? A tarefa ainda será interrompida pelo sistema se ocorrer uma interrupção de chamada telefônica ou se navegarmos para uma nova atividade e depois voltarmos?
Robert
6
ApplicationA instância tem seu próprio ciclo de vida - ela também pode ser eliminada pelo sistema operacional, portanto, essa solução pode causar um bug difícil de reproduzir.
Vit Khudenko de
7
Eu pensei: se o aplicativo for encerrado, todo o aplicativo será encerrado (e, portanto, todos os AsyncTasks também).
manmal
Acho que esse aplicativo pode ser encerrado sem que todas as tarefas assíncronas sejam (muito raras). Mas @Arhimed com algumas verificações fáceis de fazer no início e no final de cada tarefa assíncrona, você pode evitar os bugs.
neteinstein
3

Em Pro android 4. autor sugeriu uma maneira legal, que você deveria usar weak reference.

Nota de referência fraca

hqt
fonte
3

No meu ponto de vista, é melhor armazenar asynctask onRetainNonConfigurationInstancedesacoplando-o do objeto Activity atual e ligando-o a um novo objeto Activity após a mudança de orientação. Aqui encontrei um exemplo muito bom de como trabalhar com AsyncTask e ProgressDialog.

Yury
fonte
2

Android: processamento em segundo plano / operação assíncrona com alteração de configuração

Para manter os estados de operação assíncrona durante o processo em segundo plano: você pode obter a ajuda de fragmentos.

Veja as seguintes etapas:

Etapa 1: crie um fragmento sem cabeçalho, digamos, tarefa em segundo plano e adicione uma classe de tarefa assíncrona privada com ele.

Etapa 2 (etapa opcional): se você deseja colocar um cursor de carregamento no topo de sua atividade, use o código abaixo:

Etapa 3: em sua atividade principal, implemente a interface BackgroundTaskCallbacks definida na etapa 1

class BackgroundTask extends Fragment {
public BackgroundTask() {

}

// Add a static interface 

static interface BackgroundTaskCallbacks {
    void onPreExecute();

    void onCancelled();

    void onPostExecute();

    void doInBackground();
}

private BackgroundTaskCallbacks callbacks;
private PerformAsyncOpeation asyncOperation;
private boolean isRunning;
private final String TAG = BackgroundTask.class.getSimpleName();

/**
 * Start the async operation.
 */
public void start() {
    Log.d(TAG, "********* BACKGROUND TASK START OPERATION ENTER *********");
    if (!isRunning) {
        asyncOperation = new PerformAsyncOpeation();
        asyncOperation.execute();
        isRunning = true;
    }
    Log.d(TAG, "********* BACKGROUND TASK START OPERATION EXIT *********");
}

/**
 * Cancel the background task.
 */
public void cancel() {
    Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION ENTER *********");
    if (isRunning) {
        asyncOperation.cancel(false);
        asyncOperation = null;
        isRunning = false;
    }
    Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION EXIT *********");
}

/**
 * Returns the current state of the background task.
 */
public boolean isRunning() {
    return isRunning;
}

/**
 * Android passes us a reference to the newly created Activity by calling
 * this method after each configuration change.
 */
public void onAttach(Activity activity) {
    Log.d(TAG, "********* BACKGROUND TASK ON ATTACH ENTER *********");
    super.onAttach(activity);
    if (!(activity instanceof BackgroundTaskCallbacks)) {
        throw new IllegalStateException(
                "Activity must implement the LoginCallbacks interface.");
    }

    // Hold a reference to the parent Activity so we can report back the
    // task's
    // current progress and results.
    callbacks = (BackgroundTaskCallbacks) activity;
    Log.d(TAG, "********* BACKGROUND TASK ON ATTACH EXIT *********");
}

public void onCreate(Bundle savedInstanceState) {
    Log.d(TAG, "********* BACKGROUND TASK ON CREATE ENTER *********");
    super.onCreate(savedInstanceState);
    // Retain this fragment across configuration changes.
    setRetainInstance(true);
    Log.d(TAG, "********* BACKGROUND TASK ON CREATE EXIT *********");
}

public void onDetach() {
    super.onDetach();
    callbacks = null;
}

private class PerformAsyncOpeation extends AsyncTask<Void, Void, Void> {
    protected void onPreExecute() {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE ENTER *********");
        if (callbacks != null) {
            callbacks.onPreExecute();
        }
        isRunning = true;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE EXIT *********");
    }

    protected Void doInBackground(Void... params) {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND ENTER *********");
        if (callbacks != null) {
            callbacks.doInBackground();
        }
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND EXIT *********");
        return null;
    }

    protected void onCancelled() {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL ENTER *********");
        if (callbacks != null) {
            callbacks.onCancelled();
        }
        isRunning = false;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL EXIT *********");
    }

    protected void onPostExecute(Void ignore) {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE ENTER *********");
        if (callbacks != null) {
            callbacks.onPostExecute();
        }
        isRunning = false;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE EXIT *********");
    }
}

public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    setRetainInstance(true);
}

public void onStart() {
    super.onStart();
}

public void onResume() {
    super.onResume();
}

public void onPause() {
    super.onPause();
}

public void onStop() {
    super.onStop();
}

public class ProgressIndicator extends Dialog {

public ProgressIndicator(Context context, int theme) {
    super(context, theme);
}

private ProgressBar progressBar;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.progress_indicator);
    this.setCancelable(false);
    progressBar = (ProgressBar) findViewById(R.id.progressBar);
    progressBar.getIndeterminateDrawable().setColorFilter(R.color.DarkBlue, android.graphics.PorterDuff.Mode.SCREEN);
}

@Override
public void show() {
    super.show();
}

@Override
public void dismiss() {
    super.dismiss();
}

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

public class MyActivity extends FragmentActivity implements BackgroundTaskCallbacks,{

private static final String KEY_CURRENT_PROGRESS = "current_progress";

ProgressIndicator progressIndicator = null;

private final static String TAG = MyActivity.class.getSimpleName();

private BackgroundTask task = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(//"set your layout here");
    initialize your views and widget here .............



    FragmentManager fm = getSupportFragmentManager();
    task = (BackgroundTask) fm.findFragmentByTag("login");

    // If the Fragment is non-null, then it is currently being
    // retained across a configuration change.
    if (task == null) {
        task = new BackgroundTask();
        fm.beginTransaction().add(task, "login").commit();
    }

    // Restore saved state
    if (savedInstanceState != null) {
        Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON CREATE :: "
                + task.isRunning());
        if (task.isRunning()) {
            progressIndicator = new ProgressIndicator(this,
                    R.style.TransparentDialog);
            if (progressIndicator != null) {
                progressIndicator.show();
            }
        }
    }
}

@Override
protected void onPause() {
    // TODO Auto-generated method stub
    super.onPause();

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    // save the current state of your operation here by saying this 

    super.onSaveInstanceState(outState);
    Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON SAVE INSTANCE :: "
            + task.isRunning());
    outState.putBoolean(KEY_CURRENT_PROGRESS, task.isRunning());
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
    }
    progressIndicator = null;
}


private void performOperation() {

            if (!task.isRunning() && progressIndicator == null) {
                progressIndicator = new ProgressIndicator(this,
                        R.style.TransparentDialog);
                progressIndicator.show();
            }
            if (task.isRunning()) {
                task.cancel();
            } else {
                task.start();
            }
        }


@Override
protected void onDestroy() {
    super.onDestroy();
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
    }
    progressIndicator = null;
}

@Override
public void onPreExecute() {
    Log.i(TAG, "CALLING ON PRE EXECUTE");
}

@Override
public void onCancelled() {
    Log.i(TAG, "CALLING ON CANCELLED");
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();

}

public void onPostExecute() {
    Log.i(TAG, "CALLING ON POST EXECUTE");
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
        progressIndicator = null;
    }
}

@Override
public void doInBackground() {
    // put your code here for background operation
}

}

Piyush Gupta
fonte
1

Uma coisa a se considerar é se o resultado da AsyncTask deve estar disponível apenas para a atividade que iniciou a tarefa. Se sim, então a resposta de Romain Guy é a melhor. Se ele deve estar disponível para outras atividades do seu aplicativo, onPostExecutevocê pode usá-lo LocalBroadcastManager.

LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent("finished"));

Você também precisará se certificar de que a atividade lida corretamente com a situação quando a transmissão é enviada enquanto a atividade está pausada.

Juozas Kontvainis
fonte
1

Dê uma olhada neste post . Este Post envolve AsyncTask realizando uma operação de longa duração e vazamento de memória quando a rotação da tela ocorre em um aplicativo de amostra. O aplicativo de amostra está disponível no forge de origem

Vahid
fonte
0

Minha solução.

No meu caso, tenho uma cadeia de AsyncTasks com o mesmo contexto. A atividade teve acesso apenas ao primeiro. Para cancelar qualquer tarefa em execução, fiz o seguinte:

public final class TaskLoader {

private static AsyncTask task;

     private TaskLoader() {
         throw new UnsupportedOperationException();
     }

     public static void setTask(AsyncTask task) {
         TaskLoader.task = task;
     }

    public static void cancel() {
         TaskLoader.task.cancel(true);
     }
}

Tarefa doInBackground():

protected Void doInBackground(Params... params) {
    TaskLoader.setTask(this);
    ....
}

Atividade onStop()ou onPause():

protected void onStop() {
    super.onStop();
    TaskLoader.cancel();
}
Pitt90
fonte
0
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    final AddTask task = mAddTask;
    if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
        final String bookId = task.getBookId();
        task.cancel(true);

        if (bookId != null) {
            outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
            outState.putString(STATE_ADD_BOOK, bookId);
        }

        mAddTask = null;
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) {
        final String id = savedInstanceState.getString(STATE_ADD_BOOK);
        if (!BooksManager.bookExists(getContentResolver(), id)) {
            mAddTask = (AddTask) new AddTask().execute(id);
        }
    }
}
Atif Mahmood
fonte
0

você também pode adicionar android: configChanges = "keyboardHidden |idance | screenSize"

ao seu exemplo manifesto, espero que ajude

 <application
    android:name=".AppController"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:configChanges="keyboardHidden|orientation|screenSize"
    android:theme="@style/AppTheme">
eng mohamed emam
fonte