Frammenti Android. Conservazione di un AsyncTask durante la rotazione dello schermo o la modifica della configurazione


86

Sto lavorando su un'app per smartphone / tablet, utilizzando un solo APK e caricando le risorse in base alle dimensioni dello schermo, la scelta migliore per il design sembrava utilizzare i frammenti tramite l'ACL.

Questa app ha funzionato bene fino ad ora essendo basata solo sull'attività. Questa è una classe fittizia di come gestisco AsyncTasks e ProgressDialogs nelle attività in modo che funzionino anche quando lo schermo viene ruotato o si verifica una modifica della configurazione durante la comunicazione.

Non cambierò il manifest per evitare di ricreare l'attività, ci sono molte ragioni per cui non voglio farlo, ma principalmente perché i documenti ufficiali dicono che non è raccomandato e sono riuscito senza di esso fino ad ora, quindi per favore non lo consiglio itinerario.

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

Questo codice funziona bene, ho circa 10.000 utenti senza lamentele, quindi mi è sembrato logico copiare questa logica nel nuovo Fragment Based Design, ma, ovviamente, non funziona.

Ecco il 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
                }
            }
        }
    }
}

Non posso usarlo onRetainNonConfigurationInstance()poiché deve essere chiamato dall'attività e non dal frammento, lo stesso vale per getLastNonConfigurationInstance(). Ho letto alcune domande simili qui senza risposta.

Capisco che potrebbe richiedere un po 'di lavoro per organizzare correttamente questa roba in frammenti, detto questo, vorrei mantenere la stessa logica di progettazione di base.

Quale sarebbe il modo corretto per mantenere l'AsyncTask durante una modifica della configurazione e, se è ancora in esecuzione, mostrare un progressDialog, tenendo in considerazione che l'AsyncTask è una classe interna al Fragment ed è il Fragment stesso che richiama l'AsyncTask.execute ()?



associare AsyncTask a un ciclo di vita dell'applicazione .. quindi può riprendere quando l'attività viene ricreata
Fred Grott

Risposte:


75

I frammenti possono effettivamente rendere tutto più semplice. Usa semplicemente il metodo Fragment.setRetainInstance (boolean) per mantenere l'istanza di frammento durante le modifiche alla configurazione. Notare che questa è la sostituzione consigliata per Activity.onRetainnonConfigurationInstance () nella documentazione.

Se per qualche motivo non vuoi davvero usare un frammento conservato, ci sono altri approcci che puoi adottare. Notare che ogni frammento ha un identificatore univoco restituito da Fragment.getId () . Puoi anche scoprire se un frammento viene abbattuto per una modifica della configurazione tramite Fragment.getActivity (). IsChangingConfigurations () . Quindi, nel punto in cui decideresti di interrompere il tuo AsyncTask (in onStop () o onDestroy () molto probabilmente), potresti per esempio controllare se la configurazione sta cambiando e in tal caso inserirla in uno SparseArray statico sotto l'identificatore del frammento, e poi nel tuo onCreate () o onStart () guarda per vedere se hai un AsyncTask nell'array sparse disponibile.


Nota che setRetainInstance è solo se non stai usando lo stack indietro.
Neil

4
Non è possibile che AsyncTask invii il suo risultato prima che venga eseguito onCreateView del frammento conservato?
jakk

6
@jakk I metodi del ciclo di vita per attività, frammenti, ecc. vengono richiamati in sequenza dalla coda dei messaggi del thread della GUI principale, quindi anche se l'attività fosse terminata contemporaneamente in background prima che questi metodi del ciclo di vita onPostExecutefossero completati (o addirittura richiamati), il metodo avrebbe comunque bisogno di attendere prima di essere finalmente elaborato dalla coda dei messaggi del thread principale.
Alex Lockwood

Questo approccio (RetainInstance = true) non funzionerà se desideri caricare file di layout diversi per ogni orientamento.
Justin

L'avvio dell'asynctask all'interno del metodo onCreate di MainActivity sembra funzionare solo se l'asynctask - all'interno del frammento "worker" - viene avviato da un'azione esplicita dell'utente. Perché il thread principale e l'interfaccia utente sono disponibili. Tuttavia, l'avvio dell'asynctask subito dopo l'avvio dell'app, senza un'azione dell'utente come il clic di un pulsante, rappresenta un'eccezione. In tal caso, l'asynctask può essere chiamato nel metodo onStart su MainActivity, non nel metodo onCreate.
ʕ ᵔᴥᵔ ʔ

66

Penso che apprezzerai il mio esempio estremamente completo e funzionante descritto di seguito.

  1. La rotazione funziona e il dialogo sopravvive.
  2. È possibile annullare l'attività e la finestra di dialogo premendo il pulsante Indietro (se si desidera questo comportamento).
  3. Usa frammenti.
  4. Il layout del frammento sotto l'attività cambia correttamente quando il dispositivo ruota.
  5. C'è un download completo del codice sorgente e un APK precompilato in modo da poter vedere se il comportamento è quello che desideri.

modificare

Come richiesto da Brad Larson, ho riprodotto la maggior parte della soluzione collegata di seguito. Inoltre da quando l'ho postata mi hanno segnalato AsyncTaskLoader. Non sono sicuro che sia totalmente applicabile agli stessi problemi, ma dovresti comunque verificarlo.

Utilizzo AsyncTaskcon finestre di dialogo di avanzamento e rotazione del dispositivo.

Una soluzione funzionante!

Finalmente ho tutto per funzionare. Il mio codice ha le seguenti caratteristiche:

  1. Un il Fragmentcui layout cambia con l'orientamento.
  2. E AsyncTaskin cui puoi lavorare.
  3. A DialogFragmentche mostra l'avanzamento dell'attività in una barra di avanzamento (non solo uno spinner indeterminato).
  4. La rotazione funziona senza interrompere l'attività o chiudere la finestra di dialogo.
  5. Il pulsante Indietro chiude la finestra di dialogo e annulla l'attività (puoi modificare questo comportamento abbastanza facilmente).

Non credo che la combinazione di funzionalità possa essere trovata da nessun'altra parte.

L'idea di base è la seguente. C'è una MainActivityclasse che contiene un singolo frammento - MainFragment. MainFragmentha layout diversi per l'orientamento orizzontale e verticale ed setRetainInstance()è falso in modo che il layout possa cambiare. Ciò significa che quando si modifica l'orientamento del dispositivo, entrambi MainActivitye MainFragmentvengono completamente distrutti e ricreati.

Separatamente abbiamo MyTask(esteso da AsyncTask) che fa tutto il lavoro. Non possiamo archiviarlo MainFragmentperché verrà distrutto e Google ha deprecato l'utilizzo di qualcosa di simile setRetainNonInstanceConfiguration(). Non è sempre disponibile comunque ed è un brutto trucco nel migliore dei casi. Invece memorizzeremo MyTaskin un altro frammento, un DialogFragmentchiamato TaskFragment. Questo frammento si ha setRetainInstance()impostato su vero, così come il dispositivo di ruota questo frammento non è distrutta, e MyTaskviene mantenuta.

Infine dobbiamo dire a TaskFragmentchi informare quando è finito, e lo facciamo usando setTargetFragment(<the MainFragment>)quando lo creiamo. Quando il dispositivo viene ruotato e MainFragmentviene distrutto e viene creata una nuova istanza, utilizziamo FragmentManagerper trovare la finestra di dialogo (in base al suo tag) e fare setTargetFragment(<the new MainFragment>). Questo è praticamente tutto.

C'erano altre due cose che dovevo fare: prima annullare l'attività quando la finestra di dialogo viene chiusa e la seconda impostare il messaggio di eliminazione su null, altrimenti la finestra di dialogo viene stranamente ignorata quando il dispositivo viene ruotato.

Il codice

Non elencherò i layout, sono piuttosto evidenti e puoi trovarli nel download del progetto qui sotto.

Attività principale

Questo è piuttosto semplice. Ho aggiunto una richiamata a questa attività in modo che sappia quando l'attività è terminata, ma potresti non averne bisogno. Principalmente volevo solo mostrare il meccanismo di callback dell'attività dei frammenti perché è abbastanza pulito e potresti non averlo visto prima.

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

È lungo ma ne vale la 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);
        }
    }

Il mio compito

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

Scarica il progetto di esempio

Ecco il codice sorgente e l'APK . Spiacenti, l'ADT ha insistito per aggiungere la libreria di supporto prima che mi permettesse di realizzare un progetto. Sono sicuro che puoi rimuoverlo.


4
Eviterei di mantenere la barra di avanzamento DialogFragment, perché ha elementi dell'interfaccia utente che contengono riferimenti al vecchio contesto. Invece, memorizzerei AsyncTaskin un altro frammento vuoto e lo impostare DialogFragmentcome destinazione.
SD

Questi riferimenti non verranno cancellati quando il dispositivo viene ruotato e onCreateView()viene richiamato di nuovo? Il vecchio mProgressBarverrà almeno sovrascritto con uno nuovo.
Timmmm

Non esplicitamente, ma ne sono abbastanza sicuro. È possibile aggiungere mProgressBar = null;in onDestroyView(), se si vuole essere più sicuri. Il metodo di Singularity può essere una buona idea, ma aumenterà ulteriormente la complessità del codice!
Timmmm

1
Il riferimento che trattieni nell'asincronia è il frammento di progressdialog, giusto? quindi 2 domande: 1- e se volessi modificare il frammento reale che chiama il progressdialog; 2- cosa succede se voglio passare i parametri all'asynctask? Saluti,
Maxrunner

1
@ Maxrunner, Per passare i parametri, la cosa più semplice è probabilmente passare mTask.execute()a MainFragment.onClick(). In alternativa è possibile consentire il passaggio dei parametri setTask()o persino memorizzarli in MyTasksé. Non sono esattamente sicuro di cosa significhi la tua prima domanda, ma forse vuoi usare TaskFragment.getTargetFragment()? Sono abbastanza sicuro che funzionerà usando un file ViewPager. Ma ViewPagersnon sono molto ben compresi o documentati, quindi buona fortuna! Ricorda che il tuo frammento non viene creato fino a quando non è visibile la prima volta.
Timmmm

16

Di recente ho pubblicato un articolo che descrive come gestire le modifiche alla configurazione utilizzando i messaggi di posta elettronica mantenuti Fragment. Risolve bene il problema di mantenere un AsyncTaskcambiamento attraverso una rotazione.

Il TL; DR consiste nell'usare l'host del tuo AsyncTaskinterno a Fragment, chiamare setRetainInstance(true)il Fragmente riportare AsyncTaski progressi / risultati di al suo Activity(o al suo obiettivo Fragment, se scegli di utilizzare l'approccio descritto da @Timmmm) attraverso il mantenuto Fragment.


5
Come gestireste i frammenti annidati? Come un AsyncTask avviato da un RetainedFragment all'interno di un altro Fragment (Tab).
Rekin

Se il frammento è già conservato, perché non eseguire semplicemente l'attività asincrona dall'interno di quel frammento conservato? Se è già conservata, l'attività asincrona sarà in grado di riportarla anche se si verifica una modifica alla configurazione.
Alex Lockwood

@AlexLockwood Grazie per il blog. Invece di dover gestire onAttache onDetach, sarà meglio se all'interno TaskFragment, chiamiamo semplicemente getActivityogni volta che abbiamo bisogno di attivare una richiamata. (Controllando instaceof TaskCallbacks)
Cheok Yan Cheng

1
Lo puoi fare in entrambi i modi. L'ho appena fatto onAttach()e onDetach()così ho potuto evitare di trasmettere costantemente l'attività TaskCallbacksogni volta che volevo usarla.
Alex Lockwood

1
@AlexLockwood Se la mia applicazione segue una singola attività - progettazione di più frammenti, dovrei avere un frammento di attività separato per ciascuno dei miei frammenti dell'interfaccia utente? Quindi, in pratica, il ciclo di vita di ogni frammento di attività sarà gestito dal frammento di destinazione e non ci sarebbe comunicazione con l'attività.
Manas Bajaj,

13

Il mio primo suggerimento è di evitare AsyncTasks interni , puoi leggere una domanda che ho posto su questo e le risposte: Android: consigli AsyncTask: classe privata o lezione pubblica?

Dopo di che ho iniziato a usare il non-interno e ... ora vedo MOLTI vantaggi.

Il secondo è, mantieni un riferimento del tuo AsyncTask in esecuzione nella Applicationclasse - http://developer.android.com/reference/android/app/Application.html

Ogni volta che avvii un AsyncTask, impostalo sull'applicazione e quando finisce impostalo su null.

Quando si avvia un frammento / attività, è possibile verificare se è in esecuzione un AsyncTask (verificando se è nullo o meno sull'applicazione) e quindi impostare il riferimento all'interno di ciò che si desidera (attività, frammento ecc. In modo da poter eseguire callback).

Questo risolverà il tuo problema: se hai solo 1 AsyncTask in esecuzione in un determinato momento, puoi aggiungere un semplice riferimento:

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

Altrimenti, avere nell'applicazione una HashMap con riferimenti ad essi.

La finestra di dialogo di avanzamento può seguire lo stesso identico principio.


2
Sono d'accordo fintanto che leghi il ciclo di vita di AsyncTask al suo genitore (definendo AsyncTask come una classe interna di Activity / Fragment), è abbastanza difficile far sfuggire il tuo AsyncTask dalla ricreazione del ciclo di vita del suo genitore, tuttavia, non mi piace il tuo soluzione, sembra così hacky.
yorkw

La domanda è .. hai una soluzione migliore?
neteinstein

1
Dovrò essere d'accordo con @yorkw qui, questa soluzione mi è stata presentata tempo fa quando stavo affrontando questo problema senza utilizzare frammenti (app basata su attività). Questa domanda: stackoverflow.com/questions/2620917/… ha la stessa risposta e sono d'accordo con uno dei commenti che dice "L'istanza dell'applicazione ha il proprio ciclo di vita - può essere interrotta anche dal sistema operativo, quindi questa soluzione potrebbe causare un bug difficile da riprodurre "
blindstuff

1
Ancora non vedo nessun altro modo che sia meno "hacky" come ha detto @yorkw. Lo sto usando in diverse app e con una certa attenzione ai possibili problemi funziona tutto alla grande.
neteinstein

Forse la soluzione @hackbod ti si addice di più.
neteinstein

4

Ho escogitato un metodo per utilizzare AsyncTaskLoaders per questo. È abbastanza facile da usare e richiede meno spese generali IMO.

Fondamentalmente crei un AsyncTaskLoader come questo:

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

Quindi nella tua attività che utilizza AsyncTaskLoader sopra quando viene fatto clic su un pulsante:

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

    }
}

Questo sembra gestire bene i cambiamenti di orientamento e l'attività in background continuerà durante la rotazione.

Alcune cose da notare:

  1. Se in onCreate ti ricolleghi all'asynctaskloader verrai richiamato in onLoadFinished () con il risultato precedente (anche se ti era già stato detto che la richiesta era completa). Questo è in realtà un buon comportamento il più delle volte, ma a volte può essere difficile da gestire. Mentre immagino che ci siano molti modi per gestire questo, quello che ho fatto è stato chiamato loader.abandon () in onLoadFinished. Quindi ho aggiunto il check-in su Create per ricollegarlo al caricatore solo se non era già abbandonato. Se hai bisogno di nuovo dei dati risultanti, non vorrai farlo. Nella maggior parte dei casi vuoi i dati.

Ho maggiori dettagli sull'utilizzo di questo per le chiamate http qui


Sicuro che getSupportLoaderManager().getLoader(0);non restituirà null (perché il caricatore con quell'id, 0, non esiste ancora)?
EmmanuelMess

1
Sì, sarà nullo a meno che una modifica alla configurazione non abbia causato il riavvio dell'attività mentre il caricatore era in corso .. Ecco perché ho verificato la presenza di null.
Matt Wolfe

3

Ho creato una minuscola libreria di attività in background open source fortemente basata su Marshmallow AsyncTaskma con funzionalità aggiuntive come:

  1. Conservazione automatica delle attività tra le modifiche alla configurazione;
  2. Callback dell'interfaccia utente (ascoltatori);
  3. Non riavvia o annulla l'attività quando il dispositivo ruota (come farebbe Loaders);

La libreria utilizza internamente una Fragmentsenza alcuna interfaccia utente, che viene mantenuta durante le modifiche alla configurazione ( setRetainInstance(true)).

Puoi trovarlo su GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

L'esempio più semplice (versione 0.2.0):

Questo esempio conserva completamente l'attività, utilizzando una quantità di codice molto limitata.

Compito:

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

Attività:

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

1

Il mio approccio consiste nell'utilizzare il modello di progettazione della delega, in generale, possiamo isolare la logica aziendale effettiva (leggere i dati da Internet o dal database o altro) da AsyncTask (il delegante) a BusinessDAO (il delegato), nel metodo AysncTask.doInBackground () , delegare l'attività effettiva a BusinessDAO, quindi implementare un meccanismo di processo singleton in BusinessDAO, in modo che più chiamate a BusinessDAO.doSomething () attiveranno solo un'attività effettiva in esecuzione ogni volta e in attesa del risultato dell'attività. L'idea è di mantenere il delegato (cioè BusinessDAO) durante la modifica della configurazione, invece del delegante (cioè AsyncTask).

  1. Crea / implementa la nostra applicazione, lo scopo è creare / inizializzare BusinessDAO qui, in modo che il ciclo di vita del nostro BusinessDAO sia limitato all'ambito dell'applicazione, non all'ambito dell'attività, nota che è necessario modificare AndroidManifest.xml per utilizzare 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. Le nostre attività / frazioni esistenti sono per lo più invariate, implementano ancora AsyncTask come classe interna e coinvolgono AsyncTask.execute () da Activity / Fragement, la differenza ora è che AsyncTask delegherà l'attività effettiva a BusinessDAO, quindi durante la modifica della configurazione, un secondo AsyncTask verrà inizializzato ed eseguito e chiamerà BusinessDAO.doSomething () una seconda volta, tuttavia, la seconda chiamata a BusinessDAO.doSomething () non attiverà una nuova attività in esecuzione, ma attenderà che l'attività in esecuzione corrente finisca:

    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. All'interno di BusinessDAO, implementare il meccanismo di processo singleton, ad esempio:

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

Non sono sicuro al 100% se funzionerà, inoltre, lo snippet di codice di esempio dovrebbe essere considerato come pseudocodice. Sto solo cercando di darti qualche indizio dal livello di progettazione. Eventuali commenti o suggerimenti sono benvenuti e apprezzati.


Sembra una soluzione molto bella. Dato che hai risposto circa 2 anni e mezzo fa, l'hai provato da allora ?! Stai dicendo che non sono sicuro che funzioni, ma neanche io !! Sto cercando una soluzione ben testata per questo problema. Hai qualche suggerimento?
Alireza A. Ahmadi

1

Potresti rendere AsyncTask un campo statico. Se hai bisogno di un contesto, dovresti spedire il contesto dell'applicazione. Ciò eviterà perdite di memoria, altrimenti manterrai un riferimento all'intera attività.


1

Se qualcuno trova la strada per questo thread, ho scoperto che un approccio pulito era quello di eseguire l'attività Async da un app.Service(avviato con START_STICKY) e quindi ricreare iterare sui servizi in esecuzione per scoprire se il servizio (e quindi l'attività asincrona) è ancora in esecuzione;

    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 lo è, aggiungi di nuovo DialogFragment(o qualsiasi altra cosa) e in caso contrario assicurati che la finestra di dialogo sia stata chiusa.

Ciò è particolarmente pertinente se si utilizzano le v4.support.*librerie poiché (al momento della scrittura) hanno problemi con il setRetainInstancemetodo e la visualizzazione della paginazione. Inoltre, non conservando l'istanza puoi ricreare la tua attività utilizzando un diverso set di risorse (cioè un diverso layout di visualizzazione per il nuovo orientamento)


Non è eccessivo eseguire un servizio solo per conservare un AsyncTask? Un servizio viene eseguito nel proprio processo e ciò non viene fornito senza costi aggiuntivi.
WeNeigh

Interessante Vinay. Non ho notato che l'app è più pesante in termini di risorse (è comunque piuttosto leggera al momento). Cosa hai trovato? Considero un servizio come un ambiente prevedibile per consentire al sistema di eseguire operazioni pesanti o operazioni di I / O indipendentemente dallo stato dell'interfaccia utente. Comunicare con il servizio per vedere quando qualcosa è stato completato sembrava "giusto". I servizi che inizio per eseguire un po 'di lavoro si interrompono al completamento dell'attività, quindi in genere sopravvivono intorno ai 10-30 secondi.
BrantApps

La risposta di Commonsware qui sembra suggerire che i servizi sono una cattiva idea. Sto ora considerando AsyncTaskLoaders ma sembrano avere problemi propri (rigidità, solo per il caricamento dei dati, ecc.)
WeNeigh

1
Vedo. È evidente che questo servizio a cui ti sei collegato è stato impostato esplicitamente per essere eseguito nel proprio processo. Il maestro non sembrava apprezzare questo modello usato spesso. Non ho fornito esplicitamente quelle proprietà "eseguite in un nuovo processo ogni volta", quindi spero di essere isolato da quella parte di critica. Cercherò di quantificare gli effetti. I servizi come concetto ovviamente non sono "cattive idee" e sono fondamentali per ogni app che fa qualcosa di remotamente interessante, senza giochi di parole. Il loro JDoc fornisce ulteriori indicazioni sul loro utilizzo se non sei ancora sicuro.
BrantApps

0

Scrivo il codice samepl per risolvere questo problema

Il primo passo è creare la 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);
}
}

In 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">

Codice in attività:

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 l'orientamento dell'attività cambia, la variabile mTask viene avviata dal contesto dell'app. Quando l'attività è terminata, la variabile è impostata su null e rimossa dalla memoria.

Per me è abbastanza.


0

Dai un'occhiata all'esempio seguente, come utilizzare il frammento conservato per conservare l'attività in background:

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

    }
}

-1

Dai un'occhiata qui .

C'è una soluzione basata sulla soluzione di Timmmm .

Ma l'ho migliorato:

  • Ora la soluzione è estendibile: devi solo estenderla FragmentAbleToStartTask

  • Puoi continuare a eseguire più attività contemporaneamente.

    E secondo me è facile come startActivityForResult e ricevere il risultato

  • È inoltre possibile interrompere un'attività in esecuzione e verificare se una determinata attività è in esecuzione

Mi scusi per il mio inglese


primo collegamento è interrotto
Tejzeratul
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.