Zadanie w tle, okno dialogowe postępu, zmiana orientacji - czy istnieje jakieś 100% działające rozwiązanie?

235

Pobieram niektóre dane z Internetu w wątku w tle (używam AsyncTask) i wyświetlam okno dialogowe postępu podczas pobierania. Zmienia się orientacja, działanie jest ponownie uruchamiane, a następnie moja AsyncTask jest zakończona - chcę zamknąć okno dialogowe postępu i rozpocząć nowe działanie. Ale wywoływanie funkcjiISSUTDialog czasami generuje wyjątek (prawdopodobnie dlatego, że działanie zostało zniszczone, a nowe działanie jeszcze się nie rozpoczęło).

Jaki jest najlepszy sposób poradzenia sobie z tego rodzaju problemem (aktualizacja interfejsu użytkownika z wątku w tle, który działa, nawet jeśli użytkownik zmieni orientację)? Czy ktoś z Google przedstawił jakieś „oficjalne rozwiązanie”?

fucho
źródło
4
Mój post na blogu na ten temat może pomóc. Chodzi o zachowanie długotrwałych zadań w ramach zmian konfiguracji.
Alex Lockwood,
1
To pytanie jest również powiązane.
Alex Lockwood
Po prostu FTR wiąże się z tym tajemnica .. stackoverflow.com/q/23742412/294884
Fattie

Odpowiedzi:

336

Krok 1: Sprawdź swoją AsyncTaskdo staticzagnieżdżone klasy lub całkowicie odrębną klasę, tylko nie wewnętrzną (non-statycznego zagnieżdżone) klasę.

Krok # 2: AsyncTaskPrzytrzymaj Activityelement danych za pomocą konstruktora i setera.

Krok # 3: Podczas tworzenia AsyncTaskpodaj prąd Activitydo konstruktora.

Krok # 4: onRetainNonConfigurationInstance()Wróć AsyncTask, po odłączeniu go od oryginalnej, już nieobecnej aktywności.

Krok # 5: onCreate()Jeśli getLastNonConfigurationInstance()tak nie jest null, rzuć go na swoją AsyncTaskklasę i zadzwoń do setera, aby powiązał nową aktywność z zadaniem.

Krok # 6: Nie odnosić się do członka danych aktywności z doInBackground().

Jeśli zastosujesz się do powyższego przepisu, wszystko zadziała. onProgressUpdate()i onPostExecute()są zawieszone między początkiem onRetainNonConfigurationInstance()a końcem kolejnego onCreate().

Oto przykładowy projekt demonstrujący technikę.

Innym podejściem jest porzucenie AsyncTaski przeniesienie pracy do IntentService. Jest to szczególnie przydatne, jeśli praca do wykonania może być długa i powinna trwać bez względu na to, co użytkownik robi w zakresie działań (np. Pobieranie dużego pliku). Możesz użyć uporządkowanej transmisji, Intentaby albo aktywność reagowała na wykonywaną pracę (jeśli nadal jest na pierwszym planie), albo podnieść a, Notificationaby poinformować użytkownika, czy praca została wykonana. Oto post na blogu zawierający więcej informacji na temat tego wzoru.

CommonsWare
źródło
8
Bardzo dziękuję za wspaniałą odpowiedź na ten często występujący problem! Żeby być dokładnym, możesz dodać do kroku # 4, że musimy odłączyć (ustawić na zero) aktywność w AsyncTask. Jest to dobrze zilustrowane w przykładowym projekcie.
Kevin Gaudin
3
Ale co, jeśli potrzebuję dostępu do członków działania?
Eugene
3
@Andrew: Utwórz statyczną klasę wewnętrzną lub coś, co utrzymuje kilka obiektów i zwróć ją.
CommonsWare
11
onRetainNonConfigurationInstance()jest przestarzałe, a sugerowaną alternatywą jest użycie setRetainInstance(), ale nie zwraca obiektu. Czy można obsługiwać asyncTaskzmiany konfiguracji setRetainInstance()?
Indrek Kõue
10
@SYLARRR: Oczywiście. Mają Fragmentprzytrzymać AsyncTask. Miej Fragmentwezwanie setRetainInstance(true)na siebie. Porozmawiaj AsyncTasktylko z Fragment. Teraz, po zmianie konfiguracji, Fragmentnie jest niszczony i odtwarzany (nawet jeśli działanie jest), a więc AsyncTaskjest zachowywany przez zmianę konfiguracji.
CommonsWare
13

Przyjęta odpowiedź była bardzo pomocna, ale nie ma okna dialogowego postępu.

Na szczęście dla ciebie, czytelniku, stworzyłem niezwykle obszerny i działający przykład AsyncTask z oknem postępu !

  1. Rotacja działa, a dialog przetrwa.
  2. Możesz anulować zadanie i okno dialogowe, naciskając przycisk Wstecz (jeśli chcesz tego zachowania).
  3. Wykorzystuje fragmenty.
  4. Układ fragmentu pod działaniem zmienia się prawidłowo, gdy urządzenie się obraca.
Timmmm
źródło
Akceptowana odpowiedź dotyczy klas statycznych (nie członków). I są one konieczne, aby uniknąć sytuacji, w której AsyncTask ma (ukryty) wskaźnik do instancji klasy zewnętrznej, która staje się przeciekiem pamięci po zniszczeniu działania.
Bananeweizen
Tak, nie jestem pewien, dlaczego umieszczam to w elementach statycznych, skoro tak naprawdę ich też użyłem ... dziwne. Edytowana odpowiedź.
Timmmm,
Czy możesz zaktualizować swój link? Naprawdę tego potrzebuję.
Romain Pellerin
Przepraszam, nie przywróciłem mojej strony - zrobię to wkrótce! Ale w międzyczasie jest to w zasadzie taki sam kod jak w tej odpowiedzi: stackoverflow.com/questions/8417885/…
Timmmm
1
Link jest fałszywy; prowadzi tylko do bezużytecznego indeksu bez wskazania, gdzie znajduje się kod.
FractalBob,
9

Pracowałem przez tydzień, aby znaleźć rozwiązanie tego dylematu, bez uciekania się do edycji pliku manifestu. Założenia tego rozwiązania są następujące:

  1. Zawsze musisz użyć okna dialogowego postępu
  2. Jednocześnie wykonywane jest tylko jedno zadanie
  3. Musisz utrzymać zadanie, gdy telefon zostanie obrócony, a okno dialogowe postępu zostanie automatycznie odrzucone.

Realizacja

Musisz skopiować dwa pliki znajdujące się na dole tego postu do swojego obszaru roboczego. Upewnij się tylko, że:

  1. Wszystkie twoje Activitypowinny się przedłużyćBaseActivity

  2. W onCreate(), super.onCreate()powinien być nazywany po zainicjować żadnych elementów, które muszą być dostępne przez twoje ASyncTasks. Zastąp także, getContentViewId()aby podać identyfikator układu formularza.

  3. Zastąp onCreateDialog() jak zwykle, aby utworzyć okna dialogowe zarządzane przez działanie.

  4. Zobacz kod poniżej przykładowej statycznej klasy wewnętrznej, aby Twoje AsyncTasks. Możesz zapisać swój wynik w mResult, aby uzyskać do niego dostęp później.


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

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

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

        // your task code

        return null;
    }

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

I wreszcie, aby uruchomić nowe zadanie:

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

Otóż ​​to! Mam nadzieję, że to solidne rozwiązanie pomoże komuś.

BaseActivity.java (sam organizuj import)

protected abstract int getContentViewId();

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

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

        setContentView(getContentViewId());

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

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

        mDialogMap.put(id, true);
    }

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

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

        return super.onRetainNonConfigurationInstance();
    }

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

SuperAsyncTask.java

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

    protected abstract void onAfterExecute();

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

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

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

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

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

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

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

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
Oleg Waszkiewicz
źródło
4

Czy ktoś z Google przedstawił jakieś „oficjalne rozwiązanie”?

Tak.

Rozwiązaniem jest raczej propozycja architektury aplikacji niż tylko trochę kodu .

Zaproponowali 3 wzorce projektowe, które pozwalają aplikacji na synchronizację z serwerem, niezależnie od stanu aplikacji (zadziała nawet, jeśli użytkownik zakończy aplikację, użytkownik zmieni ekran, aplikacja zostanie zakończona, co drugi możliwy stan, w którym operacja danych w tle może zostać naruszona, obejmuje to)

Propozycję wyjaśniono w wystąpieniu aplikacji klienckich REST dla systemu Android podczas Google I / O 2010 autorstwa Virgila Dobjanschi. Ma 1 godzinę, ale bardzo warto go obejrzeć.

Podstawą jest wyodrębnienie operacji sieciowych do działania, Servicektóre działa niezależnie od dowolnej Activityaplikacji. Jeśli pracujesz z bazami danych, stosowanie ContentResolveri Cursornie daje out-of-the-box wzorca Obserwator , który jest wygodny do aktualizacji UI bez aditional logiki, po zaktualizowaniu lokalną bazę danych z pobranego danych zdalnych. Każdy inny kod po operacji byłby uruchamiany przez wywołanie zwrotne przekazywane do Service(używam ResultReceiverdo tego podklasy).

W każdym razie moje wyjaśnienie jest dość niejasne, zdecydowanie powinieneś obejrzeć mowę.

Christopher Francisco
źródło
2

Chociaż odpowiedź Marka (CommonsWare) rzeczywiście działa na zmiany orientacji, nie powiedzie się, jeśli Aktywność zostanie zniszczona bezpośrednio (jak w przypadku rozmowy telefonicznej).

Możesz obsługiwać zmiany orientacji ORAZ rzadkie zniszczone zdarzenia Activity, używając obiektu Application do odwołania się do ASyncTask.

Jest to doskonałe wyjaśnienie problemu i rozwiązania tutaj :

Podziękowania należą się całkowicie Ryanowi za wymyślenie tego.

Scott Biggs
źródło
1

Po 4 latach Google rozwiązało problem, wywołując setRetainInstance (true) w Activity onCreate. Pozwoli to zachować instancję aktywności podczas obracania urządzenia. Mam również proste rozwiązanie dla starszego Androida.

Singagirl
źródło
1
Problem, który obserwują ludzie, ponieważ Android niszczy klasę aktywności podczas rotacji, rozszerzenia klawiatury i innych zdarzeń, ale zadanie asynchroniczne nadal zachowuje odwołanie do zniszczonej instancji i próbuje użyć go do aktualizacji interfejsu użytkownika. Możesz poinstruować Androida, aby nie niszczył aktywności w sposób jawny lub pragmatyczny. W takim przypadku odwołanie do zadania asynchronicznego pozostaje ważne i nie zaobserwowano problemu. Ponieważ rotacja może wymagać dodatkowej pracy, takiej jak ponowne ładowanie widoków itp., Google nie zaleca zachowania aktywności. Więc decydujesz.
Singagirl
Dzięki, wiedziałem o sytuacji, ale nie o setRetainInstance (). Nie rozumiem twojego twierdzenia, że ​​Google wykorzystało to do rozwiązania problemów zadanych w pytaniu. Czy możesz połączyć źródło informacji? Dzięki.
jj_
onRetainNonConfigurationInstance () Ta funkcja jest wywoływana wyłącznie jako optymalizacja i nie można polegać na jej wywołaniu. <Z tego samego źródła: developer.android.com/reference/android/app/…
Dhananjay M
0

powinieneś wywoływać wszystkie akcje aktywności za pomocą modułu obsługi aktywności. Więc jeśli jesteś w jakimś wątku, powinieneś utworzyć Runnable i opublikować go za pomocą Handler Activitie. W przeciwnym razie twoja aplikacja ulegnie awarii czasami ze śmiertelnym wyjątkiem.

xpepermint
źródło
0

To jest moje rozwiązanie: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Zasadniczo kroki są następujące:

  1. Używam onSaveInstanceStatedo zapisania zadania, jeśli jest ono nadal przetwarzane.
  2. W onCreatedostaję zadanie, jeśli został zapisany.
  3. W onPauseOdrzucam, ProgressDialogjeśli to jest pokazane.
  4. W onResumePokaż, ProgressDialogjeśli zadanie jest nadal przetwarzane.
Gotcha
źródło