Fragmenty Androida. Zachowywanie AsyncTask podczas obracania ekranu lub zmiany konfiguracji

86

Pracuję nad aplikacją na smartfony / tablety, używam tylko jednego pliku APK i ładuję zasoby w zależności od rozmiaru ekranu, najlepszym wyborem projektu wydaje się użycie fragmentów za pośrednictwem listy ACL.

Ta aplikacja działała dobrze, do tej pory była oparta tylko na aktywności. To jest próbna klasa pokazująca, jak obsługuję AsyncTasks i ProgressDialogs w działaniach, aby działały nawet po obróceniu ekranu lub zmianie konfiguracji w trakcie komunikacji.

Nie zmienię manifestu, aby uniknąć odtworzenia działania, jest wiele powodów, dla których nie chcę tego robić, ale głównie dlatego, że oficjalni doktorzy mówią, że nie jest to zalecane i jak dotąd radziłem sobie bez tego, więc nie polecaj tego trasa.

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

Ten kod działa dobrze, mam około 10.000 użytkowników bez skarg, więc logiczne wydawało się skopiowanie tej logiki do nowego projektu opartego na fragmentach, ale oczywiście nie działa.

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

Nie mogę go użyć, onRetainNonConfigurationInstance()ponieważ musi być wywołany z działania, a nie z fragmentu, to samo dotyczy getLastNonConfigurationInstance(). Przeczytałem tutaj kilka podobnych pytań bez odpowiedzi.

Rozumiem, że może to wymagać trochę pracy, aby uporządkować te rzeczy we fragmentach, to powiedziawszy, chciałbym zachować tę samą podstawową logikę projektowania.

Jaki byłby właściwy sposób zachowania AsyncTask podczas zmiany konfiguracji i jeśli nadal działa, pokaż progressDialog, biorąc pod uwagę, że AsyncTask jest klasą wewnętrzną fragmentu i to sam fragment wywołuje AsyncTask. Execute ()?

ślepy
źródło
skojarzyć AsyncTask z cyklem życia aplikacji, dlatego można je wznowić po odtworzeniu aktywności
Fred Grott,
Sprawdź mój post na ten temat: Obsługa zmian konfiguracji z Fragments
Alex Lockwood

Odpowiedzi:

75

Fragmenty mogą to znacznie ułatwić. Po prostu użyj metody Fragment.setRetainInstance (boolean), aby Twoja instancja fragmentu była zachowywana po zmianie konfiguracji. Zauważ, że jest to zalecany zamiennik dla Activity.onRetainnonConfigurationInstance () w dokumentacji.

Jeśli z jakiegoś powodu naprawdę nie chcesz używać zachowanego fragmentu, możesz zastosować inne metody. Zauważ, że każdy fragment ma unikalny identyfikator zwracany przez Fragment.getId () . Możesz również dowiedzieć się, czy fragment jest usuwany w celu zmiany konfiguracji za pomocą Fragment.getActivity (). IsChangingConfigurations () . Tak więc w momencie, w którym zdecydowałbyś się zatrzymać AsyncTask (najprawdopodobniej w onStop () lub onDestroy ()), możesz na przykład sprawdzić, czy konfiguracja się zmienia, a jeśli tak, umieścić ją w statycznej tablicy SparseArray pod identyfikatorem fragmentu, a następnie w swoim onCreate () lub onStart () sprawdź, czy masz AsyncTask w dostępnej tablicy rzadkiej.

hackbod
źródło
Zauważ, że setRetainInstance jest tylko wtedy, gdy nie używasz stosu wstecznego.
Neil
4
Czy nie jest możliwe, że AsyncTask odsyła swój wynik z powrotem przed uruchomieniem onCreateView zachowanego fragmentu?
jakk
6
@jakk Metody cyklu życia dla działań, fragmentów itp. są wywoływane sekwencyjnie przez kolejkę komunikatów głównego wątku GUI, więc nawet jeśli zadanie zakończyło się jednocześnie w tle przed zakończeniem (lub nawet wywołaniem) tych metod cyklu życia, onPostExecutemetoda nadal musiałaby poczekaj, zanim ostatecznie zostanie przetworzony przez kolejkę komunikatów wątku głównego.
Alex Lockwood
To podejście (RetainInstance = true) nie zadziała, jeśli chcesz załadować różne pliki układu dla każdej orientacji.
Justin
Uruchomienie asynctask w metodzie onCreate MainActivity wydaje się działać tylko wtedy, gdy asynctask - wewnątrz fragmentu „workera” - jest uruchamiane przez jawną akcję użytkownika. Ponieważ główny wątek i interfejs użytkownika są dostępne. Jednak rozpoczęcie asynctask zaraz po uruchomieniu aplikacji - bez działania użytkownika, takiego jak kliknięcie przycisku - stanowi wyjątek. W takim przypadku asynctask można wywołać w metodzie onStart w MainActivity, a nie w metodzie onCreate.
ʕ ᵔᴥᵔ ʔ
66

Myślę, że spodoba ci się mój niezwykle obszerny i działający przykład opisany poniżej.

  1. Rotacja działa, a dialog pozostaje.
  2. Możesz anulować zadanie i okno dialogowe, naciskając przycisk Wstecz (jeśli chcesz to zachowanie).
  3. Używa fragmentów.
  4. Układ fragmentu pod aktywnością zmienia się prawidłowo, gdy urządzenie się obraca.
  5. Istnieje możliwość pobrania pełnego kodu źródłowego i wstępnie skompilowanego pliku APK, dzięki czemu można sprawdzić, czy zachowanie jest zgodne z oczekiwaniami.

Edytować

Zgodnie z prośbą Brada Larsona, poniżej odtworzyłem większość połączonych rozwiązań. Również odkąd to opublikowałem, zostałem wskazanyAsyncTaskLoader . Nie jestem pewien, czy można to w pełni zastosować do tych samych problemów, ale i tak powinieneś to sprawdzić.

Za pomocą AsyncTask z oknami dialogowymi postępu i obrotem urządzenia.

Działające rozwiązanie!

Wreszcie mam wszystko do pracy. Mój kod ma następujące funkcje:

  1. ZA FragmentKtórego układ zmienia się z orientacją.
  2. Na AsyncTask którym możesz popracować.
  3. ZA DialogFragment który pokazuje postęp zadania na pasku postępu (a nie tylko nieokreślonym pokrętle).
  4. Rotacja działa bez przerywania zadania lub zamykania okna dialogowego.
  5. Przycisk Wstecz zamyka okno dialogowe i anuluje zadanie (możesz jednak dość łatwo zmienić to zachowanie).

Nie sądzę, aby takie połączenie funkcjonalności można było znaleźć nigdzie indziej.

Podstawowa idea jest następująca. Istnieje MainActivityklasa, która zawiera pojedynczy fragment - MainFragment. MainFragmentma różne układy dla orientacji poziomej i pionowej i setRetainInstance()jest fałszywa, więc układ może się zmienić. Oznacza to, że gdy orientacja urządzenia zmienia się, zarówno MainActivityi MainFragmentsą całkowicie zniszczone i odtworzone.

Oddzielnie mamy MyTask(rozszerzony AsyncTask), który wykonuje całą pracę. Nie możemy go przechowywać, MainFragmentponieważ zostanie zniszczony, a firma Google wycofała się z użycia czegoś podobnego setRetainNonInstanceConfiguration(). To i tak nie zawsze jest dostępne i jest w najlepszym przypadku brzydkim hackem. Zamiast tego zapiszemy MyTaskw innym fragmencie, DialogFragmenttzw TaskFragment. Ten fragment będzie mieć setRetainInstance()wartość true, tak jak podczas obracania urządzenia ten fragment nie jest zniszczony, aMyTask jest zachowana.

Na koniec musimy powiedzieć, TaskFragmentkogo poinformować, kiedy jest skończona, i robimy to, setTargetFragment(<the MainFragment>)kiedy ją tworzymy. Po obróceniu urządzenia, MainFragmentzniszczeniu go i utworzeniu nowej instancji, używamy FragmentManagerdo znalezienia okna dialogowego (na podstawie jego tagu) i wykonujemy setTargetFragment(<the new MainFragment>). To prawie wszystko.

Musiałem zrobić jeszcze dwie rzeczy: najpierw anulować zadanie po zamknięciu okna dialogowego, a po drugie ustawić komunikat o odrzuceniu na wartość null, w przeciwnym razie okno dialogowe jest dziwnie zamykane po obróceniu urządzenia.

Kod

Nie będę wymieniać układów, są one dość oczywiste i można je znaleźć w pliku do pobrania projektu poniżej.

Główna aktywność

To jest całkiem proste. Dodałem wywołanie zwrotne do tego działania, aby wiedział, kiedy zadanie jest zakończone, ale możesz tego nie potrzebować. Przede wszystkim chciałem pokazać mechanizm wywołania zwrotnego aktywności fragmentu, ponieważ jest całkiem zgrabny i być może nie widziałeś go wcześniej.

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

To długo, ale warto!

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

Moje zadanie

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

Pobierz przykładowy projekt

Oto kod źródłowy i plik APK . Przepraszam, ADT nalegał na dodanie biblioteki wsparcia, zanim pozwoliłoby mi to zrobić projekt. Jestem pewien, że możesz to usunąć.

Timmmm
źródło
4
Unikałbym zachowywania paska postępu DialogFragment, ponieważ zawiera elementy interfejsu użytkownika, które przechowują odniesienia do starego kontekstu. Zamiast tego przechowuję AsyncTaskw innym pustym fragmencie i ustawiam DialogFragmentjako cel.
SD
Czy te odniesienia nie zostaną wyczyszczone po obróceniu urządzenia i onCreateView()ponownym wywołaniu? Stary mProgressBarzostanie przynajmniej nadpisany nowym.
Timmmm
Nie wprost, ale jestem tego całkiem pewien. Można dodać mProgressBar = null;w onDestroyView()jeśli chcesz być bardzo pewny. Metoda osobliwości może być dobrym pomysłem, ale jeszcze bardziej zwiększy złożoność kodu!
Timmmm,
1
Odniesienie, które trzymasz w asynctask, to fragment dialogu progresywnego, prawda? więc 2 pytania: 1- co jeśli chcę zmienić rzeczywisty fragment, który wywołuje okno dialogowe postępu; 2- A co jeśli chcę przekazać parametry do zadania asynchronicznego? Pozdrawiam,
Maxrunner,
1
@Maxrunner, Aby przekazać parametry, prawdopodobnie najłatwiej jest przejść mTask.execute()do MainFragment.onClick(). Alternatywnie możesz zezwolić na przekazywanie parametrów setTask()lub nawet przechowywać je w MyTasksobie. Nie jestem do końca pewien, co oznacza twoje pierwsze pytanie, ale może chcesz użyć TaskFragment.getTargetFragment()? Jestem prawie pewien, że zadziała przy użyciu ViewPager. Ale ViewPagersnie są zbyt dobrze zrozumiane ani udokumentowane, więc powodzenia! Pamiętaj, że twój fragment nie jest tworzony, dopóki nie będzie widoczny po raz pierwszy.
Timmmm
16

Niedawno opublikowałem artykuł opisujący, jak radzić sobie ze zmianami konfiguracji przy użyciu zachowanych plików Fragments. AsyncTaskŁadnie rozwiązuje problem utrzymywania zmiany w poprzek obrotu.

TL; DR polega na korzystaniu z hosta AsyncTaskwewnątrz a Fragment, dzwonieniu setRetainInstance(true)do niego Fragmenti zgłaszaniu AsyncTaskpostępów / wyników z powrotem do niego Activity(lub jego celu Fragment, jeśli zdecydujesz się użyć podejścia opisanego przez @Timmmm) za pośrednictwem zachowanego Fragment.

Alex Lockwood
źródło
5
Jak zabrałbyś się do zagnieżdżonych fragmentów? Podobnie jak AsyncTask rozpoczęty od RetainedFragment wewnątrz innego fragmentu (Tab).
Rekin
Jeśli fragment jest już zachowany, dlaczego nie wykonać zadania asynchronicznego z poziomu tego zachowanego fragmentu? Jeśli jest już zachowany, zadanie asynchroniczne będzie mogło wysłać do niego raport, nawet jeśli nastąpi zmiana konfiguracji.
Alex Lockwood,
@AlexLockwood Dzięki za blog. Zamiast zajmować się onAttachi onDetach, czy będzie lepiej, jeśli będzie w środku TaskFragment, po prostu dzwonimy, getActivitygdy potrzebujemy wywołania zwrotnego. (Sprawdzając instaceof TaskCallbacks)
Cheok Yan Cheng
1
Możesz to zrobić tak czy inaczej. I właśnie to zrobił w onAttach()i onDetach()tak mógłbym uniknąć stale odlewania aktywność na TaskCallbackskażdym razem chciałem go używać.
Alex Lockwood
1
@AlexLockwood Jeśli moja aplikacja wykonuje jedno działanie - projekt z wieloma fragmentami, czy powinienem mieć osobny fragment zadania dla każdego z moich fragmentów interfejsu użytkownika? Tak więc w zasadzie cykl życia każdego fragmentu zadania będzie zarządzany przez jego docelowy fragment i nie będzie komunikacji z działaniem.
Manas Bajaj
13

Moja pierwsza sugestia to unikanie wewnętrznych AsyncTasks , możesz przeczytać pytanie, które zadałem na ten temat i odpowiedzi: Android: Rekomendacje AsyncTask: zajęcia prywatne czy publiczne?

Potem zacząłem używać czegoś innego niż wewnętrzne i ... teraz widzę DUŻO korzyści.

Po drugie, zachowaj odniesienie do uruchomionego AsyncTask w Applicationklasie - http://developer.android.com/reference/android/app/Application.html

Za każdym razem, gdy uruchamiasz AsyncTask, ustaw go w aplikacji i po zakończeniu ustaw go na null.

Po uruchomieniu fragmentu / aktywności możesz sprawdzić, czy jakikolwiek AsyncTask jest uruchomiony (sprawdzając, czy jest pusty, czy nie w Aplikacji), a następnie ustawić odniesienie wewnątrz na cokolwiek chcesz (aktywność, fragment itp., Aby móc wykonywać wywołania zwrotne).

To rozwiąże Twój problem: Jeśli masz uruchomione tylko 1 AsyncTask w określonym czasie, możesz dodać proste odniesienie:

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

W innym przypadku, miej w aplikacji HashMap z odniesieniami do nich.

Okno dialogowe postępu może działać na tej samej zasadzie.

neteinstein
źródło
2
Zgodziłem się, o ile powiążesz cykl życia AsyncTask z jego elementem nadrzędnym (definiując AsyncTask jako wewnętrzną klasę Activity / Fragment), dość trudno jest sprawić, aby AsyncTask uciec z odtwarzania cyklu życia jego rodzica, jednak nie podoba mi się rozwiązanie, wygląda po prostu tak zepsuty.
yorkw
Pytanie brzmi… czy masz lepsze rozwiązanie?
neteinstein
1
Muszę się tutaj zgodzić z @yorkw, to rozwiązanie, które przedstawiłem mi jakiś czas temu, kiedy miałem do czynienia z tym problemem bez użycia fragmentów (aplikacja Activity based). To pytanie: stackoverflow.com/questions/2620917/… ma tę samą odpowiedź i zgadzam się z jednym z komentarzy, który mówi: „Instancja aplikacji ma swój własny cykl życia - może zostać zabita również przez system operacyjny, więc to rozwiązanie może spowodować trudny do odtworzenia błąd ”
blindstuff
1
Nadal nie widzę innego sposobu, który byłby mniej „hacky”, jak powiedział @yorkw. Używam go w kilku aplikacjach i po zwróceniu uwagi na możliwe problemy wszystko działa świetnie.
neteinstein
Może rozwiązanie @hackbod bardziej Ci odpowiada.
neteinstein
4

Wymyśliłem metodę wykorzystania do tego AsyncTaskLoaders. Jest dość łatwy w użyciu i wymaga mniejszych nakładów IMO.

Zasadniczo tworzysz AsyncTaskLoader w następujący sposób:

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

Następnie w Twojej aktywności używającej powyższego AsyncTaskLoader po kliknięciu przycisku:

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

    }
}

Wydaje się, że dobrze radzi sobie ze zmianami orientacji, a zadanie w tle będzie kontynuowane podczas obracania.

Kilka uwag:

  1. Jeśli w onCreate ponownie podłączysz się do asynctaskloadera, zostaniesz wywołany z powrotem w onLoadFinished () z poprzednim wynikiem (nawet jeśli już powiedziano ci, że żądanie zostało zakończone). W rzeczywistości jest to dobre zachowanie przez większość czasu, ale czasami może być trudne do opanowania. Chociaż wyobrażam sobie, że istnieje wiele sposobów radzenia sobie z tym, co zrobiłem, nazwałem loader.abandon () w onLoadFinished. Następnie dodałem check in onCreate, aby ponownie podłączyć się do programu ładującego tylko wtedy, gdy nie został już porzucony. Jeśli będziesz ponownie potrzebować wynikowych danych, nie będziesz chciał tego robić. W większości przypadków potrzebujesz danych.

Więcej informacji na temat korzystania z tego do połączeń http można znaleźć tutaj

Matt Wolfe
źródło
Na pewno to getSupportLoaderManager().getLoader(0);nie zwróci wartości null (ponieważ moduł ładujący o tym identyfikatorze 0 jeszcze nie istnieje)?
EmmanuelMess
1
Tak, będzie zerowa, chyba że zmiana konfiguracji spowodowała ponowne uruchomienie działania, gdy program ładujący był w toku. Dlatego sprawdziłem, czy nie ma null.
Matt Wolfe,
3

Stworzyłem bardzo małą bibliotekę zadań w tle typu open source, która jest w dużym stopniu oparta na Marshmallow, AsyncTaskale z dodatkową funkcjonalnością, taką jak:

  1. Automatyczne zachowywanie zadań po zmianach konfiguracji;
  2. Wywołanie zwrotne interfejsu użytkownika (słuchacze);
  3. Nie restartuje ani nie anuluje zadania, gdy urządzenie się obraca (tak jak robią to programy ładujące);

Biblioteka wewnętrznie używa Fragmentinterfejsu użytkownika bez żadnego interfejsu użytkownika, który jest zachowywany przy zmianach konfiguracji (setRetainInstance(true) ).

Możesz go znaleźć na GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Najbardziej podstawowy przykład (wersja 0.2.0):

Ten przykład w pełni zachowuje zadanie, używając bardzo ograniczonej ilości kodu.

Zadanie:

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

Czynność:

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 ツ
źródło
1

Moje podejście polega na użyciu wzorca projektowego delegowania, generalnie możemy wyodrębnić rzeczywistą logikę biznesową (odczytać dane z Internetu lub bazy danych lub cokolwiek innego) z AsyncTask (delegator) do BusinessDAO (delegat), w metodzie AysncTask.doInBackground () , deleguj rzeczywiste zadanie do BusinessDAO, a następnie zaimplementuj mechanizm pojedynczego procesu w BusinessDAO, tak aby wielokrotne wywołanie BusinessDAO.doSomething () wyzwoliło za każdym razem jedno rzeczywiste zadanie, które będzie się uruchamiało i czekało na wynik zadania. Chodzi o to, aby podczas zmiany konfiguracji zachować delegata (tj. BusinessDAO) zamiast delegatora (tj. AsyncTask).

  1. Utwórz / zaimplementuj naszą własną aplikację, celem jest utworzenie / zainicjowanie BusinessDAO tutaj, aby cykl życia naszego BusinessDAO obejmował zakres aplikacji, a nie zakres działań, pamiętaj, że musisz zmienić AndroidManifest.xml, aby używać 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. Nasze istniejące Activity / Fragement są w większości niezmienione, nadal implementujemy AsyncTask jako klasę wewnętrzną i obejmują AsyncTask.execute () z Activity / Fragement, różnica jest teraz taka, że ​​AsyncTask deleguje rzeczywiste zadanie do BusinessDAO, więc podczas zmiany konfiguracji drugie AsyncTask zostanie zainicjowany i wykonany, a następnie wywoła BusinessDAO.doSomething () po raz drugi, jednak drugie wywołanie BusinessDAO.doSomething () nie wyzwoli nowego uruchomionego zadania, zamiast tego będzie czekało na zakończenie bieżącego uruchomionego zadania:

    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. Wewnątrz BusinessDAO zaimplementuj mechanizm pojedynczego procesu, na przykład:

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

Nie jestem w 100% pewien, czy to zadziała, ponadto przykładowy fragment kodu należy traktować jako pseudokod. Próbuję tylko dać ci wskazówkę na poziomie projektowania. Wszelkie uwagi i sugestie są mile widziane i doceniane.

yorkw
źródło
Wydaje się bardzo fajne rozwiązanie. Od kiedy odpowiedziałeś na to jakieś 2 i pół roku temu, czy testowałeś to od tego czasu ?! Mówisz, że nie jestem pewien, czy to działa, ale ja też nie! Zamierzam znaleźć dobrze przetestowane rozwiązanie tego problemu. Masz jakieś sugestie?
Alireza A. Ahmadi
1

Możesz ustawić AsyncTask jako pole statyczne. Jeśli potrzebujesz kontekstu, powinieneś wysłać kontekst aplikacji. Pozwoli to uniknąć wycieków pamięci, w przeciwnym razie zachowasz odniesienie do całej swojej aktywności.

Boude
źródło
1

Jeśli ktoś znajdzie drogę do tego wątku, to odkryłem, że czystym podejściem było uruchomienie zadania Async z app.Service(rozpoczęło się od START_STICKY), a następnie ponowne utworzenie iteracji przez uruchomione usługi, aby dowiedzieć się, czy usługa (a tym samym zadanie asynchroniczne) jest nadal bieganie;

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

Jeśli tak, dodaj ponownie DialogFragment(lub cokolwiek), a jeśli nie, upewnij się, że okno dialogowe zostało zamknięte.

Jest to szczególnie istotne, jeśli korzystasz z v4.support.*bibliotek, ponieważ (w chwili pisania tego tekstu) mają one znane problemy z setRetainInstancemetodą i stronicowaniem widoku. Ponadto, nie zachowując instancji, możesz odtworzyć swoją aktywność przy użyciu innego zestawu zasobów (tj. Innego układu widoku dla nowej orientacji)

BrantApps
źródło
Czy nie jest przesadą uruchamianie usługi tylko po to, aby zachować AsyncTask? Usługa działa w ramach własnego procesu, a to nie wiąże się z dodatkowymi kosztami.
WeNeigh
Ciekawe Vinay. Nie zauważyłem, że aplikacja jest bardziej obciążona zasobami (i tak jest w tej chwili dość lekka). Co znalazłeś? Postrzegam usługę jako przewidywalne środowisko, w którym system może działać z pewnym obciążeniem lub operacjami we / wy, niezależnie od stanu interfejsu użytkownika. Komunikowanie się z usługą, aby zobaczyć, kiedy coś się zakończyło, wydawało się „właściwe”. Usługi, które zaczynam wykonywać, zatrzymują się po zakończeniu zadania, więc zazwyczaj przetrwają około 10-30 sekund.
BrantApps
Odpowiedź Commonsware tutaj wydaje się sugerować, że Usługi to zły pomysł. Rozważam teraz AsyncTaskLoaders, ale wydaje się, że mają własne problemy (brak elastyczności, tylko do ładowania danych itp.)
WeNeigh
1
Widzę. Warto zauważyć, że ta usługa, z którą się łączyłeś, została wyraźnie skonfigurowana do działania we własnym procesie. Mistrzowi nie podobało się częste używanie tego wzoru. Nie podałem wyraźnie tych właściwości "uruchamianych w nowym procesie za każdym razem", więc mam nadzieję, że jestem odizolowany od tej części krytyki. Postaram się oszacować efekty. Usługi jako koncepcja nie są oczywiście „złymi pomysłami” i mają fundamentalne znaczenie dla każdej aplikacji, która robi coś zdalnie interesującego, bez zamierzonej gry słów. Ich JDoc zawiera więcej wskazówek dotyczących ich stosowania, jeśli nadal nie masz pewności.
BrantApps
0

Piszę kod samepl, aby rozwiązać ten problem

Pierwszym krokiem jest utworzenie klasy aplikacji:

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

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

Kod w działaniu:

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

}

Gdy zmienia się orientacja działania, zmienna mTask jest inicjowana z kontekstu aplikacji. Po zakończeniu zadania zmienna jest ustawiana na null i usuwana z pamięci.

Dla mnie to wystarczy.

Kenumir
źródło
0

Spójrz na poniższy przykład, jak użyć zachowanego fragmentu, aby zachować zadanie w tle:

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
źródło
-1

Zajrzyj tutaj .

Istnieje rozwiązanie oparte na Timmmm rozwiązaniu .

Ale poprawiłem to:

  • Teraz rozwiązanie jest rozszerzalne - wystarczy rozszerzyć FragmentAbleToStartTask

  • Możesz jednocześnie wykonywać kilka zadań.

    Moim zdaniem jest to tak proste, jak startActivityForResult i otrzymanie wyniku

  • Możesz również zatrzymać uruchomione zadanie i sprawdzić, czy jest uruchomione

Przepraszam za mój angielski

l3onid-clean-coder
źródło
pierwszy link jest uszkodzony
Tejzeratul