Fragmentos de Android. Manter uma AsyncTask durante a rotação da tela ou alteração da configuração

86

Estou trabalhando em um aplicativo de smartphone / tablet, usando apenas um APK e carregando recursos conforme necessário, dependendo do tamanho da tela, a melhor escolha de design parece ser usar Fragments via ACL.

Este aplicativo tem funcionado bem até agora sendo apenas baseado em atividades. Esta é uma aula simulada de como eu lido com AsyncTasks e ProgressDialogs nas Atividades para que funcionem mesmo quando a tela é girada ou ocorre uma alteração na configuração durante a comunicação.

Não vou alterar o manifesto para evitar a recriação da Activity, há muitos motivos pelos quais não quero fazer isso, mas principalmente porque os documentos oficiais dizem que não é recomendado e eu consegui sem ele até agora, então por favor não recomende isso rota.

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

Este código está funcionando bem, tenho cerca de 10.000 usuários sem reclamação, então parecia lógico apenas copiar essa lógica para o novo Fragment Based Design, mas, é claro, não está funcionando.

Aqui está o LoginFragment:

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

Não posso usar onRetainNonConfigurationInstance()porque tem que ser chamado da Activity e não do Fragment, o mesmo acontece getLastNonConfigurationInstance(). Eu li algumas perguntas semelhantes aqui sem resposta.

Eu entendo que pode ser necessário algum trabalho ao redor para que essas coisas sejam organizadas adequadamente em fragmentos. Dito isso, gostaria de manter a mesma lógica básica de design.

Qual seria a maneira adequada de reter o AsyncTask durante uma alteração de configuração e, se ainda estiver em execução, mostrar um progressDialog, levando em consideração que o AsyncTask é uma classe interna ao Fragment e é o próprio Fragment que invoca o AsyncTask.execute ()?

blindstuff
fonte
1
Talvez a discussão sobre como lidar com a mudança de configuração com AsyncTask possa ajudar
rds
associar AsyncTask a um ciclo de vida do aplicativo ... assim, pode retomar quando a atividade for recriada
Fred Grott
Confira minha postagem sobre este tópico: Lidando com alterações de configuração com Fragments
Alex Lockwood

Respostas:

75

Na verdade, os fragmentos podem tornar isso muito mais fácil. Basta usar o método Fragment.setRetainInstance (boolean) para que sua instância de fragmento seja mantida durante as alterações de configuração. Observe que esta é a substituição recomendada para Activity.onRetainnonConfigurationInstance () nos documentos.

Se, por algum motivo, você realmente não quiser usar um fragmento retido, existem outras abordagens que você pode adotar. Observe que cada fragmento possui um identificador exclusivo retornado por Fragment.getId () . Você também pode descobrir se um fragmento está sendo dividido para uma alteração de configuração por meio de Fragment.getActivity (). IsChangingConfigurations () . Então, no ponto em que você decidir parar seu AsyncTask (em onStop () ou onDestroy () provavelmente), você pode, por exemplo, verificar se a configuração está mudando e, se assim for, colocá-la em um SparseArray estático sob o identificador do fragmento, e, em seguida, em seu onCreate () ou onStart (), verifique se você tem uma AsyncTask no array esparso disponível.

hackbod
fonte
Observe que setRetainInstance é apenas se você não estiver usando a pilha de retorno.
Neil
4
Não é possível que AsyncTask envie seu resultado de volta antes que o onCreateView do Fragment retido seja executado?
jakk
6
@jakk Os métodos de ciclo de vida para atividades, fragmentos, etc. são invocados sequencialmente pela fila de mensagens do thread da GUI principal, então, mesmo se a tarefa terminar simultaneamente em segundo plano antes que esses métodos de ciclo de vida sejam concluídos (ou mesmo invocados), o onPostExecutemétodo ainda precisa aguarde antes de ser finalmente processado pela fila de mensagens do thread principal.
Alex Lockwood
Esta abordagem (RetainInstance = true) não funcionará se você quiser carregar arquivos de layout diferentes para cada orientação.
Justin
Iniciar a asynctask no método onCreate de MainActivity parece funcionar apenas se a asynctask - dentro do fragmento "trabalhador" - for iniciada pela ação explícita do usuário. Porque o thread principal e a interface do usuário estão disponíveis. No entanto, iniciar a tarefa assíncrona logo após iniciar o aplicativo - sem uma ação do usuário como um clique de botão - oferece uma exceção. Nesse caso, a asynctask pode ser chamada no método onStart na MainActivity, não no método onCreate.
ʕ ᵔᴥᵔ ʔ
66

Acho que você vai gostar do meu exemplo extremamente abrangente e funcional detalhado a seguir.

  1. A rotação funciona e o diálogo sobrevive.
  2. Você pode cancelar a tarefa e o diálogo pressionando o botão Voltar (se desejar este comportamento).
  3. Ele usa fragmentos.
  4. O layout do fragmento sob a atividade muda corretamente quando o dispositivo gira.
  5. Há um download completo do código-fonte e um APK pré-compilado para que você possa ver se o comportamento é o que deseja.

Editar

Conforme solicitado por Brad Larson, reproduzi a maior parte da solução vinculada abaixo. Além disso, desde que o postei, fui apontado AsyncTaskLoader. Não tenho certeza se é totalmente aplicável aos mesmos problemas, mas você deve dar uma olhada de qualquer maneira.

Usando AsyncTaskcom diálogos de progresso e rotação do dispositivo.

Uma solução funcional!

Finalmente coloquei tudo para funcionar. Meu código tem os seguintes recursos:

  1. Um Fragmentcujo layout muda com a orientação.
  2. Um AsyncTaskno qual você pode fazer algum trabalho.
  3. UMA DialogFragment que mostra o progresso da tarefa em uma barra de progresso (não apenas um botão giratório indeterminado).
  4. A rotação funciona sem interromper a tarefa ou dispensar a caixa de diálogo.
  5. O botão Voltar dispensa a caixa de diálogo e cancela a tarefa (você pode alterar esse comportamento com bastante facilidade).

Não acho que essa combinação de funcionalidade possa ser encontrada em nenhum outro lugar.

A ideia básica é a seguinte. Existe uma MainActivityclasse que contém um único fragmento - MainFragment. MainFragmenttem layouts diferentes para orientação horizontal e vertical e setRetainInstance()é falso para que o layout possa ser alterado. Isto significa que quando a orientação do dispositivo é alterada, ambos MainActivitye MainFragmentsão completamente destruídas e recriado.

Separadamente, temos MyTask(estendido de AsyncTask) que faz todo o trabalho. Não podemos armazená-lo MainFragmentporque será destruído e o Google suspendeu o uso de qualquer coisa parecida setRetainNonInstanceConfiguration(). Isso nem sempre está disponível e é um hack feio na melhor das hipóteses. Em vez disso, armazenaremos MyTaskem outro fragmento, um DialogFragmentchamado TaskFragment.Este fragmento irá ter setRetainInstance()definida como verdadeiro, de modo a rotação do dispositivo este fragmento não é destruída, eMyTask é retida.

Finalmente, precisamos dizer ao TaskFragment quem informar quando estiver concluído, e fazemos isso usando setTargetFragment(<the MainFragment>)quando o criamos. Quando o dispositivo é girado, o MainFragmenté destruído e uma nova instância é criada, usamos o FragmentManagerpara localizar a caixa de diálogo (com base em sua tag) e fazer setTargetFragment(<the new MainFragment>). É basicamente isso.

Havia duas outras coisas que eu precisava fazer: primeiro cancelar a tarefa quando a caixa de diálogo for descartada e, em segundo lugar, definir a mensagem de dispensar como nula, caso contrário, a caixa de diálogo será estranhamente descartada quando o dispositivo for girado.

O código

Não vou listar os layouts, eles são bastante óbvios e você pode encontrá-los no download do projeto abaixo.

Atividade principal

Isso é muito simples. Eu adicionei um retorno de chamada a esta atividade para que ela saiba quando a tarefa for concluída, mas você pode não precisar disso. Principalmente, eu só queria mostrar o mecanismo de retorno de chamada de atividade de fragmento porque é muito legal e você pode não ter visto antes.

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment

É longo, mas vale a pena!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

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

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

TaskFragment

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

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

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

Minha tarefa

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

Baixe o projeto de exemplo

Aqui está o código-fonte e o APK . Desculpe, o ADT insistiu em adicionar a biblioteca de suporte antes de permitir que eu fizesse um projeto. Tenho certeza que você pode removê-lo.

Timmmm
fonte
4
Eu evitaria manter a barra de progresso DialogFragment, porque ela tem elementos de interface do usuário que contêm referências ao contexto antigo. Em vez disso, armazenaria AsyncTaskem outro fragmento vazio e definiria DialogFragmentcomo seu destino.
SD
Essas referências não serão apagadas quando o dispositivo for girado e onCreateView()chamado novamente? O antigo mProgressBarserá substituído pelo menos por um novo.
Timmmm
Não explicitamente, mas tenho certeza disso. Você pode adicionar mProgressBar = null;em onDestroyView()se você quiser ser extra certeza. O método da Singularidade pode ser uma boa ideia, mas aumentará ainda mais a complexidade do código!
Timmmm
1
A referência que você mantém na asynctask é o fragmento progressdialog, certo? então 2 questões: 1- e se eu quiser alterar o fragmento real que chama o progressdialog; 2- e se eu quiser passar parâmetros para asynctask? Atenciosamente,
Maxrunner
1
@Maxrunner, Para passar parâmetros, provavelmente a coisa mais fácil é mudar mTask.execute()para MainFragment.onClick(). Alternativamente, você pode permitir que parâmetros sejam passados setTask()ou até mesmo armazená-los nele MyTaskmesmo. Não tenho certeza do que sua primeira pergunta significa, mas talvez você queira usar TaskFragment.getTargetFragment()? Tenho certeza de que funcionará usando um ViewPager. Mas ViewPagersnão são muito bem compreendidos ou documentados, então boa sorte! Lembre-se de que seu fragmento não é criado até que seja visível pela primeira vez.
Timmmm
16

Publiquei recentemente um artigo que descreve como lidar com alterações de configuração usando Fragments retidos . Ele resolve o problema de reter umAsyncTask mudança em uma rotação bem.

O TL; DR é usar o host AsyncTaskdentro de um Fragment, chamar setRetainInstance(true)o Fragmente relatar o AsyncTaskprogresso / resultados de volta a ele Activity(ou a seu destino Fragment, se você escolher usar a abordagem descrita por @Timmmm) por meio do retido Fragment.

Alex Lockwood
fonte
5
Como você faria sobre os fragmentos aninhados? Como um AsyncTask iniciado a partir de um RetainedFragment dentro de outro Fragment (Tab).
Rekin de
Se o fragmento já foi retido, por que não apenas executar a tarefa assíncrona de dentro desse fragmento retido? Se já estiver retido, a tarefa assíncrona poderá reportar de volta a ela, mesmo se ocorrer uma alteração na configuração.
Alex Lockwood de
@AlexLockwood Obrigado pelo blog. Em vez de ter que manipular onAttache onDetach, será melhor se estiver dentro TaskFragment, apenas getActivityligamos sempre que precisamos disparar um retorno de chamada. (Verificando instaceof TaskCallbacks)
Cheok Yan Cheng
1
Você pode fazer isso de qualquer maneira. Acabei de fazer isso onAttach()e onDetach()pude evitar lançar constantemente a atividade para TaskCallbackstodas as vezes que eu quisesse usá-la.
Alex Lockwood
1
@AlexLockwood Se meu aplicativo está seguindo uma única atividade - design de múltiplos fragmentos, devo ter um fragmento de tarefa separado para cada um dos meus fragmentos de IU? Então, basicamente, o ciclo de vida de cada fragmento de tarefa será gerenciado por seu fragmento de destino e não haverá comunicação com a atividade.
Manas Bajaj
13

Minha primeira sugestão é evitar AsyncTasks internas , você pode ler uma pergunta que fiz sobre isso e as respostas: Android: Recomendações AsyncTask: classe privada ou classe pública?

Depois disso, comecei a usar o não-interno e ... agora vejo MUITOS benefícios.

A segunda é manter uma referência de sua AsyncTask em execução na Applicationclasse - http://developer.android.com/reference/android/app/Application.html

Sempre que você iniciar uma AsyncTask, defina-a no aplicativo e, quando terminar, defina-a como nula.

Quando um fragmento / atividade começa, você pode verificar se alguma AsyncTask está em execução (verificando se é nula ou não no aplicativo) e, em seguida, definir a referência interna para o que quiser (atividade, fragmento etc para que você possa fazer callbacks).

Isso resolverá seu problema: Se você tiver apenas 1 AsyncTask em execução em um determinado momento, poderá adicionar uma referência simples:

AsyncTask<?,?,?> asyncTask = null;

Caso contrário, tenha na aplicação um HashMap com referências a eles.

A caixa de diálogo de progresso pode seguir exatamente o mesmo princípio.

neteinstein
fonte
2
Eu concordei, contanto que você vincule o ciclo de vida de AsyncTask ao seu pai (definindo AsyncTask como uma classe interna de Activity / Fragment), é bastante difícil fazer seu AsyncTask escapar da recriação do ciclo de vida de seu pai, no entanto, não gosto de seu solução, parece tão hacky.
yorkw
A questão é ... você tem uma solução melhor?
neteinstein
1
Vou ter que concordar com @yorkw aqui, esta solução foi apresentada a mim há um tempo, quando eu estava lidando com esse problema sem usar fragmentos (aplicativo baseado em atividades). Esta pergunta: stackoverflow.com/questions/2620917/… tem a mesma resposta, e concordo com um dos comentários que diz "A instância do aplicativo tem seu próprio ciclo de vida - pode ser eliminada pelo sistema operacional também, então esta solução pode causar um bug difícil de reproduzir "
blindstuff
1
Ainda não vejo nenhuma outra maneira que seja menos "hacky" como disse @yorkw. Estou usando em vários aplicativos e, com alguma atenção aos possíveis problemas, tudo funciona muito bem.
neteinstein
Talvez a solução @hackbod seja mais adequada para você.
neteinstein
4

Eu vim com um método de usar AsyncTaskLoaders para isso. É muito fácil de usar e requer menos sobrecarga de IMO.

Basicamente, você cria um AsyncTaskLoader como este:

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

Então, em sua atividade que usa o AsyncTaskLoader acima quando um botão é clicado:

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

Isso parece lidar bem com as mudanças de orientação e sua tarefa em segundo plano continuará durante a rotação.

Algumas coisas a serem observadas:

  1. Se em onCreate você se reconectar ao asynctaskloader, será chamado de volta em onLoadFinished () com o resultado anterior (mesmo se já tiver sido informado que a solicitação foi concluída). Na verdade, esse é um bom comportamento na maioria das vezes, mas às vezes pode ser difícil de lidar. Embora eu imagine que existem muitas maneiras de lidar com isso, o que fiz foi chamar loader.abandon () em onLoadFinished. Em seguida, adicionei check-in onCreate para apenas reconectar ao carregador se ele já não tiver sido abandonado. Se você precisar dos dados resultantes novamente, não vai querer fazer isso. Na maioria dos casos, você deseja os dados.

Tenho mais detalhes sobre como usar isso para chamadas http aqui

Matt Wolfe
fonte
Claro que getSupportLoaderManager().getLoader(0);não retornará nulo (porque o carregador com esse id, 0, ainda não existe)?
EmmanuelMess
1
Sim, será nulo, a menos que uma alteração na configuração tenha causado o reinício da atividade enquanto o carregador estava em andamento. É por isso que fiz a verificação de nulo.
Matt Wolfe
3

Eu criei uma pequena biblioteca de tarefas em segundo plano de código aberto que é fortemente baseada no Marshmallow, AsyncTaskmas com funcionalidades adicionais, como:

  1. Retenção automática de tarefas nas alterações de configuração;
  2. Retorno de chamada da IU (ouvintes);
  3. Não reinicia ou cancela a tarefa quando o dispositivo gira (como os carregadores fariam);

A biblioteca usa internamente um Fragmentsem qualquer interface de usuário, que é mantida durante as alterações de configuração (setRetainInstance(true) ).

Você pode encontrá-lo no GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Exemplo mais básico (versão 0.2.0):

Este exemplo retém totalmente a tarefa, usando uma quantidade muito limitada de código.

Tarefa:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

Atividade:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}
Rolf ツ
fonte
1

Minha abordagem é usar o padrão de design de delegação, em geral, podemos isolar a lógica de negócios real (ler dados da Internet ou banco de dados ou qualquer outro) de AsyncTask (o delegador) para BusinessDAO (o delegado), em seu método AysncTask.doInBackground () , delegue a tarefa real ao BusinessDAO e, em seguida, implemente um mecanismo de processo singleton no BusinessDAO, de modo que várias chamadas para BusinessDAO.doSomething () acionarão apenas uma tarefa real em execução a cada vez e aguardando o resultado da tarefa. A ideia é reter o delegado (ou seja, BusinessDAO) durante a alteração da configuração, em vez do delegador (ou seja, AsyncTask).

  1. Crie / implemente nosso próprio aplicativo, o objetivo é criar / inicializar o BusinessDAO aqui, de modo que o ciclo de vida do nosso BusinessDAO tenha o escopo do aplicativo, não do escopo da atividade, observe que você precisa alterar o AndroidManifest.xml para usar MyApplication:

    public class MyApplication extends android.app.Application {
      private BusinessDAO businessDAO;
    
      @Override
      public void onCreate() {
        super.onCreate();
        businessDAO = new BusinessDAO();
      }
    
      pubilc BusinessDAO getBusinessDAO() {
        return businessDAO;
      }
    
    }
    
  2. Nosso Activity / Fragement existente está praticamente inalterado, ainda implementa AsyncTask como uma classe interna e envolve AsyncTask.execute () de Activity / Fragement, a diferença agora é que AsyncTask delegará a tarefa real ao BusinessDAO, portanto, durante a mudança de configuração, uma segunda AsyncTask será inicializado e executado, e chamará BusinessDAO.doSomething () segunda vez; no entanto, a segunda chamada para BusinessDAO.doSomething () não acionará uma nova tarefa em execução, em vez disso, aguardará a conclusão da tarefa em execução atual:

    public class LoginFragment extends Fragment {
      ... ...
    
      public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
        // get a reference of BusinessDAO from application scope.
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //Handle task result and update UI stuff.
        }
      }
    
      ... ...
    }
    
  3. Dentro do BusinessDAO, implemente o mecanismo do processo singleton, por exemplo:

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          // nothing running at the moment, submit a new callable task to run.
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        // Task already submitted and running, waiting for the running task to finish.
        myFutureTask.get();
      }
    
      // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          // do your job here.
          return this;
        }
      }
    
    }
    

Não tenho 100% de certeza se isso vai funcionar, além disso, o trecho de código de amostra deve ser considerado como pseudocódigo. Estou apenas tentando dar-lhes algumas pistas do nível de design. Quaisquer comentários ou sugestões são bem-vindos e apreciados.

yorkw
fonte
Parece uma solução muito boa. Desde que você respondeu a cerca de 2 anos e meio atrás, você testou desde então ?! Você está dizendo que não tenho certeza se está funcionando, a coisa não é nem eu !! Estou buscando uma solução bem testada para este problema. Você tem alguma sugestão?
Alireza A. Ahmadi
1

Você pode tornar o AsyncTask um campo estático. Se você precisa de um contexto, deve enviar o contexto do seu aplicativo. Isso evitará vazamentos de memória, caso contrário, você manterá uma referência para toda a sua atividade.

Boude
fonte
1

Se alguém encontrar o caminho para este thread, eu descobri que uma abordagem limpa seria executar a tarefa assíncrona de um app.Service(iniciado com START_STICKY) e, em seguida, recriar a iteração sobre os serviços em execução para descobrir se o serviço (e, portanto, a tarefa assíncrona) ainda está corrida;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

Se estiver, adicione novamente DialogFragment(ou qualquer outra coisa) e, se não estiver, certifique-se de que a caixa de diálogo foi descartada.

Isso é particularmente pertinente se você estiver usando as v4.support.*bibliotecas, pois (no momento da escrita) elas têm problemas conhecidos com o setRetainInstancemétodo e a paginação de visualizações. Além disso, ao não reter a instância, você pode recriar sua atividade usando um conjunto diferente de recursos (ou seja, um layout de visualização diferente para a nova orientação)

BrantApps
fonte
Não é um exagero executar um serviço apenas para manter uma AsyncTask? Um serviço é executado em seu próprio processo e isso não vem sem custos adicionais.
WeNeigh
Vinay interessante. Não percebi que o aplicativo tem mais recursos (é muito leve de qualquer maneira no momento). O que você encontrou? Eu vejo um serviço como um ambiente previsível para permitir que o sistema execute algum trabalho pesado ou E / S, independentemente do estado da IU. Comunicar-se com o serviço para ver quando algo foi concluído parecia "certo". Os serviços que começo a executar param um pouco na conclusão da tarefa, então geralmente sobrevivem por volta de 10-30s.
BrantApps
A resposta do Commonsware aqui parece sugerir que os serviços são uma má ideia. Agora estou pensando em AsyncTaskLoaders, mas eles parecem vir com problemas próprios (inflexibilidade, apenas para carregamento de dados, etc.)
WeNeigh
1
Entendo. Visivelmente, este serviço ao qual você se vinculou estava explicitamente sendo configurado para ser executado em seu próprio processo. O mestre não pareceu gostar desse padrão sendo usado com freqüência. Não forneci explicitamente essas propriedades "execute um novo processo a cada vez", então espero estar isolado dessa parte das críticas. Vou me esforçar para quantificar os efeitos. Os serviços como um conceito, é claro, não são "idéias ruins" e são fundamentais para que todo aplicativo faça algo remotamente interessante, sem trocadilhos. O JDoc deles fornece mais orientações sobre seu uso, caso você ainda não tenha certeza.
BrantApps de
0

Eu escrevo o mesmo código para resolver este problema

A primeira etapa é criar a classe Application:

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

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

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

Em AndroidManifest.xml

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

Código em atividade:

public class MainActivity extends Activity {

private Task1 mTask1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

Quando a orientação da atividade muda, a variável mTask é iniciada no contexto do aplicativo. Quando a tarefa é concluída, a variável é definida como nula e removida da memória.

Para mim é o suficiente.

Kenumir
fonte
0

Dê uma olhada no exemplo abaixo, como usar o fragmento retido para reter a tarefa em segundo plano:

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

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

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mListener != null) {
                mListener.onRequestProgressUpdate(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}
DeepakPanwar
fonte
-1

Dê uma olhada aqui .

Existe uma solução baseada em Timmmm's solução .

Mas eu melhorei:

  • Agora a solução é extensível - você só precisa estender FragmentAbleToStartTask

  • Você pode continuar executando várias tarefas ao mesmo tempo.

    E na minha opinião é tão fácil quanto startActivityForResult e receber o resultado

  • Você também pode interromper uma tarefa em execução e verificar se determinada tarefa está sendo executada

Desculpe pelo meu Inglês

l3onid-clean-coder
fonte
o primeiro link está quebrado
Tejzeratul