Jak obsługiwać zmianę orientacji ekranu, gdy aktywne jest okno dialogowe postępu i wątek w tle?


524

Mój program wykonuje pewną aktywność sieciową w wątku w tle. Przed rozpoczęciem pojawia się okno dialogowe postępu. Okno dialogowe zostaje zamknięte w module obsługi. To wszystko działa dobrze, z wyjątkiem sytuacji, gdy orientacja ekranu zmienia się, gdy okno dialogowe jest uruchomione (a wątek w tle się rozwija). W tym momencie aplikacja ulega awarii, blokuje się lub przechodzi w dziwny etap, w którym aplikacja w ogóle nie działa, dopóki wszystkie wątki nie zostaną zabite.

Jak mogę płynnie obsługiwać zmianę orientacji ekranu?

Przykładowy kod poniżej odpowiada mniej więcej temu, co robi mój prawdziwy program:

public class MyAct extends Activity implements Runnable {
    public ProgressDialog mProgress;

    // UI has a button that when pressed calls send

    public void send() {
         mProgress = ProgressDialog.show(this, "Please wait", 
                      "Please wait", 
                      true, true);
        Thread thread = new Thread(this);
        thread.start();
    }

    public void run() {
        Thread.sleep(10000);
        Message msg = new Message();
        mHandler.sendMessage(msg);
    }

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            mProgress.dismiss();
        }
    };
}

Stos:

E/WindowManager(  244): Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244): android.view.WindowLeaked: Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244):     at android.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager(  244):     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager(  244):     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager(  244):     at android.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager(  244):     at android.app.Dialog.show(Dialog.java:212)
E/WindowManager(  244):     at android.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager(  244):     at android.app.ProgressDialog.show(ProgressDialog.java:91)
E/WindowManager(  244):     at MyAct.send(MyAct.java:294)
E/WindowManager(  244):     at MyAct$4.onClick(MyAct.java:174)
E/WindowManager(  244):     at android.view.View.performClick(View.java:2129)
E/WindowManager(  244):     at android.view.View.onTouchEvent(View.java:3543)
E/WindowManager(  244):     at android.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager(  244):     at android.view.View.dispatchTouchEvent(View.java:3198)

Próbowałem zamknąć okno dialogowe postępu w onSaveInstanceState, ale to po prostu zapobiega natychmiastowej awarii. Wątek w tle nadal działa, a interfejs użytkownika jest częściowo narysowany. Musisz zabić całą aplikację, zanim zacznie ponownie działać.


1
Biorąc pod uwagę odpowiedzi, które otrzymałeś, powinieneś zmienić przyjętą odpowiedź na najlepszą, prawda?
rds

Zobacz także poprzednie pytanie stackoverflow.com/questions/456211/…
rds

3
Wszystko, mam naprawdę świetne wyjaśnienie i możliwe rozwiązania tego problemu. Przejdź przez http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/ Pozwól mi wiedzieć, czy to pomogło.
arcamax

2
W tym wpisie na blogu znajduje się dość pełne wyjaśnienie, jak zachować asynchroniczne zadania w tle dla różnych orientacji ekranu . Sprawdź to!
Adrian Monk

Po prostu ustaw Androida: configChanges = "orientacja | screenSize" na Manifest w działaniu. Zatrzyma to Androida, aby odtworzyć swoją aktywność
Jawad Zeb

Odpowiedzi:


155

Po zmianie orientacji system Android utworzy nowy widok. Prawdopodobnie masz awarie, ponieważ twój wątek w tle próbuje zmienić stan na stary. (Może to również powodować problemy, ponieważ wątek w tle nie znajduje się w wątku interfejsu użytkownika)

Sugerowałbym uczynienie tego mHandlera niestabilnym i aktualizowanie go, gdy zmienia się orientacja.


14
Możliwe, że wskazałeś przyczynę awarii. Pozbyłem się awarii, ale wciąż nie zastanawiałem się, jak przywrócić interfejs użytkownika do stanu, w jakim był przed zmianą orientacji w niezawodny sposób. Ale twoja odpowiedź posunęła mnie naprzód, więc przyznanie jej jako odpowiedzi.
Heikki Toivonen

4
Powinieneś zacząć od rozpoczęcia swojej aktywności, gdy zmieni się orientacja. Zasadniczo musisz ponownie skonfigurować widok przy użyciu starych danych. Proponuję więc poprosić o udoskonalenie statusu liczbowego z paska postępu i przebudować nowy widok, gdy pojawi się nowy „start” Nie pamiętam od razu, jeśli dostaniesz także nową aktywność, ale trochę przeszukiwania dokumentacji powinno pomóc.
haseman

6
Niedawno się z tym bawiłem, mogę przekazać, że otrzymujesz nową aktywność, gdy aplikacja zmieni orientację. (Otrzymasz również nowy widok) Jeśli spróbujesz zaktualizować stary widok, otrzymasz wyjątek, ponieważ stary widok ma nieprawidłowy kontekst aplikacji (twoja stara aktywność) Możesz to obejść, przekazując myActivity.getApplicationContext () zamiast wskaźnika do samego działania.
haseman

1
Czy ktoś może wytłumaczyć wykorzystanie / korzyść niestabilności w tym kontekście
Jawad Zeb

2
@Nepster Tak, też się nad tym zastanawiałem. Byłoby wspaniale, gdyby ktoś wyjaśnił o niestabilności.
RestInPeace

261

Edycja: inżynierowie Google nie zalecają takiego podejścia, zgodnie z opisem Dianne Hackborn (alias hackbod ) w tym poście StackOverflow . Sprawdź ten post na blogu, aby uzyskać więcej informacji.


Musisz dodać to do deklaracji aktywności w manifeście:

android:configChanges="orientation|screenSize"

tak to wygląda

<activity android:label="@string/app_name" 
        android:configChanges="orientation|screenSize|keyboardHidden" 
        android:name=".your.package">

Chodzi o to, że system niszczy działanie, gdy nastąpi zmiana konfiguracji. Zobacz Zmiana konfiguracji .

Umieszczenie tego w pliku konfiguracyjnym pozwala uniknąć zniszczenia systemu przez system. Zamiast tego wywołuje onConfigurationChanged(Configuration)metodę.


21
To zdecydowanie najlepsze rozwiązanie; ponieważ po prostu obraca układ (oczekiwane zachowanie na pierwszym miejscu). Pamiętaj tylko, aby umieścić Androida: configChanges = "orientacja | keyboardHidden" (z powodu telefonów z klawiaturą poziomą)
nikib3ro,

24
To wydaje się być zachowanie, którego się spodziewam. Dokumentacja wskazuje jednak, że działanie zostało zniszczone „ponieważ dowolny zasób aplikacji, w tym pliki układu, można zmienić w zależności od dowolnej wartości konfiguracji. Dlatego jedynym bezpiecznym sposobem obsługi zmiany konfiguracji jest odzyskanie wszystkich zasobów”. Poza tym orientationistnieje wiele innych powodów, dla których należy zmienić konfigurację: keyboardHidden(Już edytowałem odpowiedź wiki), uiMode(Na przykład wchodzenie lub wychodzenie z trybu samochodowego; zmiana trybu nocnego) itp. Teraz zastanawiam się, czy tak naprawdę jest dobra odpowiedź.
rds

116
To nie jest akceptowalne rozwiązanie. To po prostu maskuje prawdziwy problem.
rf43

18
Działa, ale nie jest zalecane przez Google.
Ed Burnette,

21
Nie stosuj tego podejścia tutaj. DDosAttack ma całkowitą rację. Wyobraź sobie, że tworzysz okno dialogowe postępu pobierania lub coś, co zajmuje dużo czasu. Jako użytkownik nie będziesz kontynuował tej aktywności i nie będziesz się na nią gapił. Możesz przełączyć się na ekran główny lub inną aplikację, na przykład grę lub połączenie telefoniczne, albo coś innego, co może pochłonąć zasoby, które ostatecznie zniszczą Twoją aktywność. I co potem? Masz do czynienia z tym samym starym problemem, który NIE został rozwiązany za pomocą tej zgrabnej sztuczki. Aktywność zostanie odtworzona ponownie, gdy użytkownik wróci.
tiguchi

68

Wymyśliłem solidne rozwiązanie tych problemów, które jest zgodne z „Android Way” rzeczy. Mam wszystkie moje długotrwałe operacje przy użyciu wzorca IntentService.

Oznacza to, że moje działania nadają intencje, IntentService wykonuje pracę, zapisuje dane w bazie danych, a następnie rozgłasza lepkie intencje. Ta lepka część jest ważna, ponieważ nawet jeśli działanie zostało wstrzymane w czasie, gdy użytkownik zainicjował pracę i przegapi transmisję w czasie rzeczywistym z usługi IntentService, nadal możemy odpowiedzieć i odebrać dane z wywołania działania. ProgressDialogs może całkiem dobrze pracować z tym wzorem onSaveInstanceState().

Zasadniczo musisz zapisać flagę, w której oknie dialogowym postępu uruchomiono pakiet zapisanej instancji. Nie zapisuj obiektu dialogowego postępu, ponieważ spowoduje to wyciek całej aktywności. Aby mieć trwały uchwyt okna dialogowego postępu, przechowuję go jako słabe odwołanie w obiekcie aplikacji. Po zmianie orientacji lub cokolwiek innego, co powoduje wstrzymanie działania (połączenie telefoniczne, użytkownik trafia do domu itp.), A następnie wznowienie, zamykam stare okno dialogowe i odtwarzam nowe okno dialogowe w nowo utworzonym działaniu.

W przypadku okien dialogowych postępu na czas nieokreślony jest to łatwe. Aby uzyskać styl paska postępu, musisz umieścić ostatni znany postęp w pakiecie i wszelkie informacje, których używasz lokalnie w działaniu, aby śledzić postęp. Po przywróceniu postępów wykorzystasz te informacje do ponownego pojawienia się paska postępu w tym samym stanie, co poprzednio, a następnie zaktualizujesz na podstawie aktualnego stanu rzeczy.

Podsumowując, umieszczenie długoterminowych zadań w usłudze IntentService w połączeniu z rozsądnym użyciem onSaveInstanceState()pozwala efektywnie śledzić okna dialogowe i przywracać je w trakcie zdarzeń cyklu życia działania. Odpowiednie fragmenty kodu aktywności znajdują się poniżej. Będziesz także potrzebował logiki w swoim BroadcastReceiver, aby odpowiednio obsługiwać Lepkie zamiary, ale to wykracza poza zakres tego.

public void doSignIn(View view) {
    waiting=true;
    AppClass app=(AppClass) getApplication();
    String logingon=getString(R.string.signon);
    app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this, "", logingon, true));
    ...
}

@Override
protected void onSaveInstanceState(Bundle saveState) {
    super.onSaveInstanceState(saveState);
    saveState.putBoolean("waiting",waiting);
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if(savedInstanceState!=null) {
        restoreProgress(savedInstanceState);    
    }
    ...
}

private void restoreProgress(Bundle savedInstanceState) {
    waiting=savedInstanceState.getBoolean("waiting");
    if (waiting) {
        AppClass app=(AppClass) getApplication();
        ProgressDialog refresher=(ProgressDialog) app.Dialog.get();
        refresher.dismiss();
        String logingon=getString(R.string.signon);
        app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this, "", logingon, true));
    }
}

wydaje się fajnym rozwiązaniem
Derekyy

„Mam wszystkie moje długotrwałe operacje przy użyciu wzorca IntentService.” To nie jest idealne rozwiązanie, ponieważ to jest jak strzelanie z armaty do wróbli i dużo kodu bojlera, więcej można obejrzeć na youtube.com/watch?v=NJsq0TU0qeg
Kamil Nekanowicz

28

Spotkałem ten sam problem. Moja aktywność musi analizować niektóre dane z adresu URL i jest powolna. Dlatego tworzę wątek, aby to zrobić, a następnie wyświetlam okno dialogowe postępu. Pozwalam, aby wątek wysłał wiadomość z powrotem do wątku interfejsu użytkownika, Handlergdy jest ona ukończona. W Handler.handleMessageotrzymuję obiekt danych (teraz gotowy) z wątku i wypełniam go w interfejsie użytkownika. Więc jest bardzo podobny do twojego przykładu.

Po wielu próbach i błędach wygląda na to, że znalazłem rozwiązanie. Przynajmniej teraz mogę obrócić ekran w dowolnym momencie, przed lub po zakończeniu nici. We wszystkich testach okno dialogowe jest poprawnie zamknięte, a wszystkie zachowania są zgodne z oczekiwaniami.

To, co zrobiłem, pokazano poniżej. Celem jest wypełnienie mojego modelu danych ( mDataObject), a następnie wypełnienie go w interfejsie użytkownika. Powinien umożliwiać obracanie ekranu w dowolnym momencie bez zaskoczenia.

class MyActivity {

    private MyDataObject mDataObject = null;
    private static MyThread mParserThread = null; // static, or make it singleton

    OnCreate() {
        ...
        Object retained = this.getLastNonConfigurationInstance();
        if(retained != null) {
            // data is already completely obtained before config change
            // by my previous self.
            // no need to create thread or show dialog at all
            mDataObject = (MyDataObject) retained;
            populateUI();
        } else if(mParserThread != null && mParserThread.isAlive()){
            // note: mParserThread is a static member or singleton object.
            // config changed during parsing in previous instance. swap handler
            // then wait for it to finish.
            mParserThread.setHandler(new MyHandler());
        } else {
            // no data and no thread. likely initial run
            // create thread, show dialog
            mParserThread = new MyThread(..., new MyHandler());
            mParserThread.start();
            showDialog(DIALOG_PROGRESS);
        }
    }

    // http://android-developers.blogspot.com/2009/02/faster-screen-orientation-change.html
    public Object onRetainNonConfigurationInstance() {
        // my future self can get this without re-downloading
        // if it's already ready.
        return mDataObject;
    }

    // use Activity.showDialog instead of ProgressDialog.show
    // so the dialog can be automatically managed across config change
    @Override
    protected Dialog onCreateDialog(int id) {
        // show progress dialog here
    }

    // inner class of MyActivity
    private class MyHandler extends Handler {
        public void handleMessage(msg) {
            mDataObject = mParserThread.getDataObject();
            populateUI();
            dismissDialog(DIALOG_PROGRESS);
        }
    }
}

class MyThread extends Thread {
    Handler mHandler;
    MyDataObject mDataObject;

    // constructor with handler param
    public MyHandler(..., Handler h) {
        ...
        mHandler = h;
    }

    public void setHandler(Handler h) { mHandler = h; } // for handler swapping after config change
    public MyDataObject getDataObject() { return mDataObject; } // return data object (completed) to caller

    public void run() {
        mDataObject = new MyDataObject();
        // do the lengthy task to fill mDataObject with data
        lengthyTask(mDataObject);
        // done. notify activity
        mHandler.sendEmptyMessage(0); // tell activity: i'm ready. come pick up the data.
    }
}

To działa dla mnie. Nie wiem, czy jest to „poprawna” metoda zaprojektowana przez Androida - twierdzą, że „niszczenie / odtwarzanie aktywności podczas obracania ekranu” faktycznie ułatwia rzeczy, więc myślę, że nie powinno to być zbyt trudne.

Daj mi znać, jeśli zauważysz problem w moim kodzie. Jak powiedziano powyżej, tak naprawdę nie wiem, czy jest jakiś efekt uboczny.


1
Wielkie dzięki! Wskazówka onRetainNonConfigurationInstance()i getLastNonConfigurationInstance()pomogła mi rozwiązać problem. Kciuki w górę!
sven

15

Pierwotnie postrzegany problem polegał na tym, że kod nie przetrwał zmiany orientacji ekranu. Najwyraźniej zostało to „rozwiązane”, ponieważ program sam obsługiwał zmianę orientacji ekranu, zamiast pozwalać na to interfejsowi użytkownika (poprzez wywołanie onDestroy).

Chciałbym twierdzić, że jeśli podstawowym problemem jest to, że program nie przetrwa onDestroy (), wówczas przyjęte rozwiązanie jest tylko obejściem, które pozostawia program z poważnymi innymi problemami i lukami. Pamiętaj, że system Android wyraźnie stwierdza, że ​​twoja aktywność jest zagrożona zniszczeniem prawie w dowolnym momencie z powodu okoliczności poza Twoją kontrolą. Dlatego twoja aktywność musi być w stanie przetrwać onDestroy () i kolejne onCreate () z dowolnego powodu, a nie tylko zmiany orientacji ekranu.

Jeśli zamierzasz samodzielnie zaakceptować obsługę zmian orientacji ekranu w celu rozwiązania problemu PO, musisz sprawdzić, czy inne przyczyny onDestroy () nie powodują tego samego błędu. Czy potrafisz to zrobić? Jeśli nie, zapytałbym, czy „zaakceptowana” odpowiedź jest naprawdę bardzo dobra.


14

Moim rozwiązaniem było rozszerzenie ProgressDialogklasy, aby uzyskać własne MyProgressDialog.
Przedefiniowałem show()i dismiss()metody blokowania orientacji przed wyświetleniem Dialogi odblokowania, kiedyDialog zwolnieniu. Tak więc, gdy Dialogpokazuje się i zmienia się orientacja urządzenia, orientacja ekranu pozostaje do momentu dismiss()wywołania, a następnie orientacja ekranu zmienia się zgodnie z wartościami czujnika / orientacją urządzenia.

Oto mój kod:

public class MyProgressDialog extends ProgressDialog {
private Context mContext;

public MyProgressDialog(Context context) {
    super(context);
    mContext = context;
}

public MyProgressDialog(Context context, int theme) {
    super(context, theme);
    mContext = context;
}

public void show() {
    if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
        ((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    else
        ((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    super.show();
}

public void dismiss() {
    super.dismiss();
    ((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}

}

8

Napotkałem ten sam problem i wymyśliłem rozwiązanie, które nie ingerowało w program ProgressDialog i uzyskuję szybsze wyniki.

Stworzyłem układ z paskiem postępu.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ProgressBar
    android:id="@+id/progressImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    />
</RelativeLayout>

Następnie w metodzie onCreate wykonaj następujące czynności

public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.progress);
}

Następnie wykonaj długie zadanie w wątku, a kiedy to się skończy, ustaw Runnable w widoku zawartości prawdziwy układ, którego chcesz użyć dla tego działania.

Na przykład:

mHandler.post(new Runnable(){

public void run() {
        setContentView(R.layout.my_layout);
    } 
});

To właśnie zrobiłem i przekonałem się, że działa szybciej niż pokazuje ProgressDialog i jest mniej inwazyjny i moim zdaniem lepiej wygląda.

Jeśli jednak chcesz użyć ProgressDialog, ta odpowiedź nie jest dla Ciebie.


To rozwiązanie jest eleganckie w prostej obudowie, ale ma wady. Musisz odbudować pełny widok zawartości. setContentView(R.layout.my_layout);nie jest wystarczający; musisz ustawić wszystkich słuchaczy, zresetować dane itp.
rds

@rds masz rację. To naprawdę tylko rozwiązanie prostego przypadku lub jeśli musisz wykonać ciężkie podnoszenie w metodzie onCreate przed wyświetleniem widoku.
Pzanno,

Nie do końca to rozumiem. Zamiast ustawień detektorów w onCreate (), jak to zwykle byśmy robili, moglibyśmy ustawić je w run (). Czy coś mi umyka?
Code Poet,

7

Odkryłem rozwiązanie tego, czego jeszcze nie widziałem. Możesz użyć niestandardowego obiektu aplikacji, który wie, czy wykonujesz zadania w tle, zamiast próbować to zrobić w działaniu, które jest niszczone i odtwarzane po zmianie orientacji. Napisałem o tym tutaj blog .


1
Utworzenie niestandardowego Applicationjest zwykle używane do utrzymania globalnego stanu aplikacji. Nie twierdzę, że to nie działa, ale wydaje się to zbyt skomplikowane. Z dokumentu „Zwykle nie ma potrzeby podklasowania aplikacji.”. W dużej mierze wolę odpowiedź sonxurxo.
rds

7

Mam zamiar przyczynić się do rozwiązania tego problemu z rotacją. Może to nie dotyczyć OP, ponieważ nie używa AsyncTask, ale może inni uznają to za przydatne. To dość proste, ale wydaje mi się, że wykonuje to za mnie:

Mam aktywność logowania przy użyciu zagnieżdżonej AsyncTaskklasy o nazwie BackgroundLoginTask.

W moim BackgroundLoginTaskprzypadku nie robię nic niezwykłego oprócz dodania zerowej kontroli po ProgressDialogodrzuceniu połączenia:

@Override
protected void onPostExecute(Boolean result)
{    
if (pleaseWaitDialog != null)
            pleaseWaitDialog.dismiss();
[...]
}

Ma to na celu obsłużenie przypadku, gdy zadanie w tle kończy się, gdy Activitynie jest widoczne, a zatem okno dialogowe postępu zostało już odrzucone przezonPause() metodę.

Następnie w mojej Activityklasie nadrzędnej tworzę globalne uchwyty statyczne do mojej AsyncTaskklasy i moja ProgressDialog( AsyncTaskzagnieżdżona może uzyskać dostęp do tych zmiennych):

private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

Służy to dwóm celom: po pierwsze, pozwala mi Activityzawsze uzyskać dostęp do AsyncTaskobiektu, nawet z nowej, post-obróconej aktywności. Po drugie, pozwala mi BackgroundLoginTaskuzyskać dostęp i zamknąć ProgressDialognawet po rotacji.

Następnie dodaję to do onPause(), powodując zniknięcie okna postępu, gdy Activityopuszczamy pierwszy plan (zapobiegając temu brzydkiemu „przymusowemu zamknięciu” awarii):

    if (pleaseWaitDialog != null)
    pleaseWaitDialog.dismiss();

Wreszcie mam następujące onResume()metody:

if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
        {
           if (pleaseWaitDialog != null)
             pleaseWaitDialog.show();
        }

Umożliwia Dialogto ponowne pojawienie się po Activityodtworzeniu.

Oto cała klasa:

public class NSFkioskLoginActivity extends NSFkioskBaseActivity {
    private static BackgroundLoginTask backgroundLoginTask;
    private static ProgressDialog pleaseWaitDialog;
    private Controller cont;

    // This is the app entry point.
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (CredentialsAvailableAndValidated())
        {
        //Go to main menu and don't run rest of onCreate method.
            gotoMainMenu();
            return;
        }
        setContentView(R.layout.login);
        populateStoredCredentials();   
    }

    //Save current progress to options when app is leaving foreground
    @Override
    public void onPause()
    {
        super.onPause();
        saveCredentialsToPreferences(false);
        //Get rid of progress dialog in the event of a screen rotation. Prevents a crash.
        if (pleaseWaitDialog != null)
        pleaseWaitDialog.dismiss();
    }

    @Override
    public void onResume()
    {
        super.onResume();
        if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
        {
           if (pleaseWaitDialog != null)
             pleaseWaitDialog.show();
        }
    }

    /**
     * Go to main menu, finishing this activity
     */
    private void gotoMainMenu()
    {
        startActivity(new Intent(getApplicationContext(), NSFkioskMainMenuActivity.class));
        finish();
    }

    /**
     * 
     * @param setValidatedBooleanTrue If set true, method will set CREDS_HAVE_BEEN_VALIDATED to true in addition to saving username/password.
     */
    private void saveCredentialsToPreferences(boolean setValidatedBooleanTrue)
    {
        SharedPreferences settings = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE);
        SharedPreferences.Editor prefEditor = settings.edit();
        EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
        EditText pswText = (EditText) findViewById(R.id.editTextPassword);
        prefEditor.putString(USERNAME, usernameText.getText().toString());
        prefEditor.putString(PASSWORD, pswText.getText().toString());
        if (setValidatedBooleanTrue)
        prefEditor.putBoolean(CREDS_HAVE_BEEN_VALIDATED, true);
        prefEditor.commit();
    }

    /**
     * Checks if user is already signed in
     */
    private boolean CredentialsAvailableAndValidated() {
        SharedPreferences settings = getSharedPreferences(APP_PREFERENCES,
                MODE_PRIVATE);
        if (settings.contains(USERNAME) && settings.contains(PASSWORD) && settings.getBoolean(CREDS_HAVE_BEEN_VALIDATED, false) == true)
         return true;   
        else
        return false;
    }

    //Populate stored credentials, if any available
    private void populateStoredCredentials()
    {
        SharedPreferences settings = getSharedPreferences(APP_PREFERENCES,
            MODE_PRIVATE);
        settings.getString(USERNAME, "");
       EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
       usernameText.setText(settings.getString(USERNAME, ""));
       EditText pswText = (EditText) findViewById(R.id.editTextPassword);
       pswText.setText(settings.getString(PASSWORD, ""));
    }

    /**
     * Validate credentials in a seperate thread, displaying a progress circle in the meantime
     * If successful, save credentials in preferences and proceed to main menu activity
     * If not, display an error message
     */
    public void loginButtonClick(View view)
    {
        if (phoneIsOnline())
        {
        EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
        EditText pswText = (EditText) findViewById(R.id.editTextPassword);
           //Call background task worker with username and password params
           backgroundLoginTask = new BackgroundLoginTask();
           backgroundLoginTask.execute(usernameText.getText().toString(), pswText.getText().toString());
        }
        else
        {
        //Display toast informing of no internet access
        String notOnlineMessage = getResources().getString(R.string.noNetworkAccessAvailable);
        Toast toast = Toast.makeText(getApplicationContext(), notOnlineMessage, Toast.LENGTH_SHORT);
        toast.show();
        }
    }

    /**
     * 
     * Takes two params: username and password
     *
     */
    public class BackgroundLoginTask extends AsyncTask<Object, String, Boolean>
    {       
       private Exception e = null;

       @Override
       protected void onPreExecute()
       {
           cont = Controller.getInstance();
           //Show progress dialog
           String pleaseWait = getResources().getString(R.string.pleaseWait);
           String commWithServer = getResources().getString(R.string.communicatingWithServer);
            if (pleaseWaitDialog == null)
              pleaseWaitDialog= ProgressDialog.show(NSFkioskLoginActivity.this, pleaseWait, commWithServer, true);

       }

        @Override
        protected Boolean doInBackground(Object... params)
        {
        try {
            //Returns true if credentials were valid. False if not. Exception if server could not be reached.
            return cont.validateCredentials((String)params[0], (String)params[1]);
        } catch (Exception e) {
            this.e=e;
            return false;
        }
        }

        /**
         * result is passed from doInBackground. Indicates whether credentials were validated.
         */
        @Override
        protected void onPostExecute(Boolean result)
        {
        //Hide progress dialog and handle exceptions
        //Progress dialog may be null if rotation has been switched
        if (pleaseWaitDialog != null)
             {
            pleaseWaitDialog.dismiss();
                pleaseWaitDialog = null;
             }

        if (e != null)
        {
         //Show toast with exception text
                String networkError = getResources().getString(R.string.serverErrorException);
                Toast toast = Toast.makeText(getApplicationContext(), networkError, Toast.LENGTH_SHORT);
            toast.show();
        }
        else
        {
            if (result == true)
            {
            saveCredentialsToPreferences(true);
            gotoMainMenu();
            }
            else
            {
            String toastText = getResources().getString(R.string.invalidCredentialsEntered);
                Toast toast = Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_SHORT);
            toast.show();
            } 
        }
        }

    }
}

Nie jestem bynajmniej doświadczonym programistą Androida, więc nie krępuj się komentować.


1
Ciekawy! Specjalnie dla tych z nas, którzy korzystają z AsyncTask. Właśnie wypróbowałem swoje rozwiązanie i wydaje się, że w większości działa. Jest jeden problem: wydaje się, że ProgressDialog kończy się nieco wcześniej po obrocie, GDY ProgressDialog jest nadal aktywny. Zamierzam się pobawić, aby zobaczyć dokładnie, co się dzieje i jak temu zaradzić. Ale już nie mam tych awarii!
Scott Biggs,

1
Znaleziono poprawkę. Wygląda na to, że problemem jest tutaj statyczny ProgressDialog. Kiedy rotacje przerywają ProgressDialog, czasami wywoływana jest jej metoda .dismiss () po ponownym uruchomieniu w nowej działalności. Tworząc ProgressDialog utworzony dla każdego działania, zapewniamy, że ten nowy ProgressDialog nie zostanie zabity wraz ze starym działaniem. Upewniłem się również, że ProgressDialog ma wartość null za każdym razem, gdy jest odrzucane (aby ułatwić odśmiecanie). Mamy tutaj rozwiązanie! Pozdrawiam osoby korzystające z AsyncTask!
Scott Biggs,

4

Przenieś długie zadanie do oddzielnej klasy. Zaimplementuj go jako wzorzec podmiot-obserwator. Za każdym razem, gdy działanie zostanie utworzone, zarejestruj się i podczas zamykania wyrejestruj się z klasą zadania. Klasa zadania może korzystać z AsyncTask.


1
Nie rozumiem, jak to by pomogło. Czy możesz wyjaśnić bardziej szczegółowo, w jaki sposób zapobiega to występującym problemom.
Heikki Toivonen,

1
Jak powiedział Haseman, uniemożliwia backendowi dostęp do elementów interfejsu użytkownika i możemy oddzielić interfejs użytkownika od backendu, backend działa w osobnym wątku i nadal działa nawet po zmianie orientacji ekranu i rejestracji - wyrejestruj się z zadaniem backendu w celu aktualizacji statusu . Prawdziwym przykładem, który rozwiązałem za pomocą tego, jest to, że mam zadanie pobierania, przeniosłem je do osobnego wątku, za każdym razem, gdy wątek jest tworzony, rejestruję się przy nim.
Vinay,

Ok, ponownie przeglądam ten problem i nie sądzę, że nadal w pełni rozumiem tę odpowiedź. Załóżmy, że głównym zadaniem jest uruchomienie AsyncTask w celu wykonania długotrwałej operacji sieciowej, której nie chcemy przerywać podczas zmiany orientacji ekranu. Nie widzę, jak nowa aktywność może wysłać wiadomość do AsyncTask rozpoczętej przez starą aktywność. Czy możesz podać przykład kodu?
Heikki Toivonen

@Heikki, czy moja implementacja jest poniżej tego, co masz na myśli?
beetstra

4

Sztuką jest pokazanie / zamknięcie okna dialogowego w AsyncTask podczas onPreExecute / onPostExecute jak zwykle, jednak w przypadku zmiany orientacji utwórz / pokaż nowe wystąpienie okna dialogowego w działaniu i przekaż jego odwołanie do zadania.

public class MainActivity extends Activity {
    private Button mButton;
    private MyTask mTask = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        MyTask task = (MyTask) getLastNonConfigurationInstance();
        if(task != null){
            mTask = task;
            mTask.mContext = this;
            mTask.mDialog = ProgressDialog.show(this, "", "", true);        
        }

        mButton = (Button) findViewById(R.id.button1);
        mButton.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                mTask = new MyTask(MainActivity.this);
                mTask.execute();
            }
        });
    }


    @Override
    public Object onRetainNonConfigurationInstance() {
        String str = "null";
        if(mTask != null){
            str = mTask.toString();
            mTask.mDialog.dismiss();
        }
        Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
        return mTask;
    }



    private class MyTask extends AsyncTask<Void, Void, Void>{
        private ProgressDialog mDialog;
        private MainActivity mContext;


        public MyTask(MainActivity context){
            super();
            mContext = context;
        }


        protected void onPreExecute() {
            mDialog = ProgressDialog.show(MainActivity.this, "", "", true);
        }

        protected void onPostExecute(Void result) {
            mContext.mTask = null;
            mDialog.dismiss();
        }


        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(5000);
            return null;
        }       
    }
}

4

Zrobiłem to w ten sposób:

    package com.palewar;
    import android.app.Activity;
    import android.app.ProgressDialog;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;

    public class ThreadActivity extends Activity {


        static ProgressDialog dialog;
        private Thread downloadThread;
        final static Handler handler = new Handler() {

            @Override
            public void handleMessage(Message msg) {

                super.handleMessage(msg);

                dialog.dismiss();

            }

        };

        protected void onDestroy() {
    super.onDestroy();
            if (dialog != null && dialog.isShowing()) {
                dialog.dismiss();
                dialog = null;
            }

        }

        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);

            downloadThread = (Thread) getLastNonConfigurationInstance();
            if (downloadThread != null && downloadThread.isAlive()) {
                dialog = ProgressDialog.show(ThreadActivity.this, "",
                        "Signing in...", false);
            }

            dialog = ProgressDialog.show(ThreadActivity.this, "",
                    "Signing in ...", false);

            downloadThread = new MyThread();
            downloadThread.start();
            // processThread();
        }

        // Save the thread
        @Override
        public Object onRetainNonConfigurationInstance() {
            return downloadThread;
        }


        static public class MyThread extends Thread {
            @Override
            public void run() {

                try {
                    // Simulate a slow network
                    try {
                        new Thread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.sendEmptyMessage(0);

                } finally {

                }
            }
        }

    }

Możesz także spróbować dać mi znać, czy to działa dla Ciebie, czy nie


Kod onDestroy może w ogóle nie zostać wykonany ze strony programisty : „Zwróć uwagę na kolumnę„ Killable ”w powyższej tabeli - dla metod oznaczonych jako możliwe do wywołania, po zwróceniu tej metody proces hostujący działanie może zostać zabity przez system w dowolnym momencie bez wykonywania kolejnego wiersza kodu
ilomambo

Mocno wierzę, że „onRetainNonConfigurationInstance ()” jest metodą stosowaną w takich przypadkach ... nyc praca
Nitin Bansal

Mam do czynienia z podobnym problemem, ponieważ Sachin Gurnani używa deklaracji statycznej, aby naprawić mój problem. stackoverflow.com/questions/12058774/...
Steven Du

2

Jeśli utworzysz tło, Servicektóre wykonuje wszystkie ciężkie operacje podnoszenia (żądania / odpowiedzi Tcp, odebranie), Viewi Activitymożna je zniszczyć i odtworzyć bez wycieku okna lub utraty danych. Pozwala to na zachowanie zalecane przez Androida, polegające na niszczeniu działania przy każdej zmianie konfiguracji (np. Przy każdej zmianie orientacji).

Jest to nieco bardziej złożone, ale jest to najlepszy sposób na wywołanie żądania serwera, wstępne / końcowe przetwarzanie danych itp.

Możesz nawet użyć swojego Servicedo kolejkowania każdego żądania do serwera, dzięki czemu łatwo i wydajnie poradzisz sobie z tymi rzeczami.

Przewodnik dla deweloperów zawiera pełny rozdziałServices .


A Serviceto więcej pracy niż, AsyncTaskale może być lepszym podejściem w niektórych sytuacjach. To niekoniecznie lepsze, prawda? Biorąc to pod uwagę, nie rozumiem, jak to rozwiązuje problem tego, ProgressDialogco wyciekło z głównego Activity. Gdzie zakładasz ProgressDialog? Gdzie to odrzucasz?
rds

2

Mam implementację, która pozwala zniszczyć działanie po zmianie orientacji ekranu, ale nadal skutecznie niszczy okno dialogowe w odtworzonej aktywności. Używam, ...NonConfigurationInstanceaby dołączyć zadanie w tle do odtworzonego działania. Normalna struktura systemu Android obsługuje odtwarzanie okna dialogowego, nic się tam nie zmienia.

Podklasowałem funkcję AsyncTask, dodając pole dla działania „będącego właścicielem” i metodę aktualizacji tego właściciela.

class MyBackgroundTask extends AsyncTask<...> {
  MyBackgroundTask (Activity a, ...) {
    super();
    this.ownerActivity = a;
  }

  public void attach(Activity a) {
    ownerActivity = a;
  }

  protected void onPostExecute(Integer result) {
    super.onPostExecute(result);
    ownerActivity.dismissDialog(DIALOG_PROGRESS);
  }

  ...
}

W mojej klasie aktywności dodałem pole backgroundTaskodnoszące się do „posiadanego” zadania tła i aktualizuję to pole za pomocą onRetainNonConfigurationInstancei getLastNonConfigurationInstance.

class MyActivity extends Activity {
  public void onCreate(Bundle savedInstanceState) {
    ...
    if (getLastNonConfigurationInstance() != null) {
      backgroundTask = (MyBackgroundTask) getLastNonConfigurationInstance();
      backgroundTask.attach(this);
    }
  }

  void startBackgroundTask() {
    backgroundTask = new MyBackgroundTask(this, ...);
    showDialog(DIALOG_PROGRESS);
    backgroundTask.execute(...);
  }

  public Object onRetainNonConfigurationInstance() {
    if (backgroundTask != null && backgroundTask.getStatus() != Status.FINISHED)
      return backgroundTask;
    return null;
  }
  ...
}

Sugestie dotyczące dalszej poprawy:

  • Wyczyść backgroundTaskodwołanie w działaniu po zakończeniu zadania, aby zwolnić pamięć lub inne związane z nim zasoby.
  • Wyczyść ownerActivity odniesienie w zadaniu w tle, zanim działanie zostanie zniszczone, na wypadek, gdyby nie zostało natychmiast odtworzone.
  • Utwórz BackgroundTaskinterfejs i / lub kolekcję, aby umożliwić uruchamianie różnych typów zadań z tego samego działania będącego właścicielem.

2

Jeśli utrzymasz dwa układy, wszystkie wątki interfejsu użytkownika powinny zostać zakończone.

Jeśli używasz AsynTask, możesz łatwo wywołać .cancel()metodę wewnątrz onDestroy()metody bieżącej aktywności.

@Override
protected void onDestroy (){
    removeDialog(DIALOG_LOGIN_ID); // remove loading dialog
    if (loginTask != null){
        if (loginTask.getStatus() != AsyncTask.Status.FINISHED)
            loginTask.cancel(true); //cancel AsyncTask
    }
    super.onDestroy();
}

Więcej informacji na temat AsyncTask można znaleźć w sekcji „Anulowanie zadania” tutaj .

Aktualizacja: Dodano warunek sprawdzania stanu, ponieważ można go anulować tylko wtedy, gdy jest w stanie uruchomionym. Należy również pamiętać, że AsyncTask można wykonać tylko raz.


2

Próbowałem wdrożyć rozwiązanie jfelectron , ponieważ jest to „ solidne rozwiązanie tych problemów, które jest zgodne z„ Android Way ”rzeczy, ale zajęło to trochę czasu, aby spojrzeć i zebrać wszystkie wymienione elementy. Skończyło się na tym nieco innym i myślę, że bardziej eleganckie, rozwiązanie zamieszczone tutaj w całości.

Używa usługi IntentService uruchomionej z działania, aby wykonać długo działające zadanie w osobnym wątku. Usługa odpala lepkie cele transmisji do działania, które aktualizuje okno dialogowe. Działanie wykorzystuje metody showDialog (), onCreateDialog () i onPrepareDialog () w celu wyeliminowania konieczności przekazywania trwałych danych w obiekcie aplikacji lub pakiecie saveInstanceState. Powinno to działać bez względu na przerwanie aplikacji.

Klasa aktywności:

public class TesterActivity extends Activity {
private ProgressDialog mProgressDialog;
private static final int PROGRESS_DIALOG = 0;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button b = (Button) this.findViewById(R.id.test_button);
    b.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            buttonClick();
        }
    });
}

private void buttonClick(){
    clearPriorBroadcast();
    showDialog(PROGRESS_DIALOG);
    Intent svc = new Intent(this, MyService.class);
    startService(svc);
}

protected Dialog onCreateDialog(int id) {
    switch(id) {
    case PROGRESS_DIALOG:
        mProgressDialog = new ProgressDialog(TesterActivity.this);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mProgressDialog.setMax(MyService.MAX_COUNTER);
        mProgressDialog.setMessage("Processing...");
        return mProgressDialog;
    default:
        return null;
    }
}

@Override
protected void onPrepareDialog(int id, Dialog dialog) {
    switch(id) {
    case PROGRESS_DIALOG:
        // setup a broadcast receiver to receive update events from the long running process
        IntentFilter filter = new IntentFilter();
        filter.addAction(MyService.BG_PROCESS_INTENT);
        registerReceiver(new MyBroadcastReceiver(), filter);
        break;
    }
}

public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.hasExtra(MyService.KEY_COUNTER)){
            int count = intent.getIntExtra(MyService.KEY_COUNTER, 0);
            mProgressDialog.setProgress(count);
            if (count >= MyService.MAX_COUNTER){
                dismissDialog(PROGRESS_DIALOG);
            }
        }
    }
}

/*
 * Sticky broadcasts persist and any prior broadcast will trigger in the 
 * broadcast receiver as soon as it is registered.
 * To clear any prior broadcast this code sends a blank broadcast to clear 
 * the last sticky broadcast.
 * This broadcast has no extras it will be ignored in the broadcast receiver 
 * setup in onPrepareDialog()
 */
private void clearPriorBroadcast(){
    Intent broadcastIntent = new Intent();
    broadcastIntent.setAction(MyService.BG_PROCESS_INTENT);
    sendStickyBroadcast(broadcastIntent);
}}

Klasa IntentService:

public class MyService extends IntentService {

public static final String BG_PROCESS_INTENT = "com.mindspiker.Tester.MyService.TEST";
public static final String KEY_COUNTER = "counter";
public static final int MAX_COUNTER = 100;

public MyService() {
  super("");
}

@Override
protected void onHandleIntent(Intent intent) {
    for (int i = 0; i <= MAX_COUNTER; i++) {
        Log.e("Service Example", " " + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(BG_PROCESS_INTENT);
        broadcastIntent.putExtra(KEY_COUNTER, i);
        sendStickyBroadcast(broadcastIntent);
    }
}}

Wpisy pliku manifestu:

przed sekcją aplikacji:

uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"

wewnątrz sekcji aplikacji

service android:name=".MyService"

2

Oto moje proponowane rozwiązanie:

  • Przenieś AsyncTask lub wątek do zachowanego fragmentu, jak wyjaśniono tutaj . Uważam, że dobrą praktyką jest przenoszenie wszystkich połączeń sieciowych do fragmentów. Jeśli używasz już fragmentów, jeden z nich może zostać pociągnięty do odpowiedzialności za połączenia. W przeciwnym razie możesz utworzyć fragment tylko do wykonania żądania, zgodnie z propozycją połączonego artykułu.
  • Fragment użyje interfejsu odbiornika do zasygnalizowania zakończenia / niepowodzenia zadania. Tam nie musisz się martwić o zmiany orientacji. Fragment zawsze będzie miał poprawny link do bieżącej aktywności, a okno dialogowe postępu można bezpiecznie wznowić.
  • Uczyń okno dialogowe postępu członkiem swojej klasy. W rzeczywistości powinieneś to zrobić dla wszystkich okien dialogowych. W metodzie onPause powinieneś je odrzucić, w przeciwnym razie otworzysz okno zmiany konfiguracji. Fragment powinien utrzymywać stan zajętości. Gdy fragment zostanie dołączony do działania, możesz ponownie wyświetlić okno dialogowe postępu, jeśli połączenie nadal działa. W tym void showProgressDialog()celu można dodać metodę do interfejsu detektora aktywności fragmentu.

Idealne rozwiązanie, ale nie rozumiem, dlaczego ta odpowiedź jest w cieniu innych!
blackkara

2

Napotkałem tę samą sytuację. Zrobiłem tylko jedną instancję dla mojego okna postępu w całej aplikacji.

Najpierw utworzyłem klasę DialogSingleton, aby uzyskać tylko jedną instancję (wzorzec Singleton)

public class DialogSingleton
{
    private static Dialog dialog;

    private static final Object mLock = new Object();
    private static DialogSingleton instance;

    private DialogSingleton()
    {

    }

    public static DialogSingleton GetInstance()
    {
        synchronized (mLock)
        {
            if(instance == null)
            {
                instance = new DialogSingleton();
            }

            return instance;
        }
    }

    public void DialogShow(Context context, String title)
    {
        if(!((Activity)context).isFinishing())
        {
            dialog = new ProgressDialog(context, 2);

            dialog.setCanceledOnTouchOutside(false);

            dialog.setTitle(title);

            dialog.show();
        }
    }

    public void DialogDismiss(Context context)
    {
        if(!((Activity)context).isFinishing() && dialog.isShowing())
        {
            dialog.dismiss();
        }
    }
}

Jak pokazuję w tej klasie, mam okno dialogowe postępu jako atrybut. Za każdym razem, gdy muszę wyświetlić okno dialogowe postępu, otrzymuję unikalną instancję i tworzę nowy ProgressDialog.

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

Kiedy skończę zadanie w tle, ponownie wywołuję unikalną instancję i zamykam jej okno dialogowe.

DialogSingleton.GetInstance().DialogDismiss(this);

Zapisuję status zadania w tle w moich wspólnych preferencjach. Kiedy obracam ekran, pytam, czy mam zadanie uruchomione dla tego działania: (onCreate)

if(Boolean.parseBoolean(preference.GetValue(IS_TASK_NAME_EXECUTED_KEY, "boolean").toString()))
{
    DialogSingleton.GetInstance().DialogShow(this, "Checking credentials!");
} // preference object gets the info from shared preferences (my own implementation to get and put data to shared preferences) and IS_TASK_NAME_EXECUTED_KEY is the key to save this flag (flag to know if this activity has a background task already running).

Kiedy zaczynam uruchamiać zadanie w tle:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true, "boolean");

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

Po zakończeniu uruchamiania zadania w tle:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false, "boolean");

DialogSingleton.GetInstance().DialogDismiss(ActivityName.this);

Mam nadzieję, że to pomoże.


2

To bardzo stare pytanie, które z jakiegoś powodu pojawiło się na pasku bocznym.

Jeśli zadanie w tle musi przetrwać tylko wtedy, gdy aktywność jest na pierwszym planie, „nowym” rozwiązaniem jest umieszczenie wątku w tle (lub najlepiej AsyncTask) w zachowanym fragmencie , jak opisano w tym przewodniku dla programistów oraz w licznych pytaniach i odpowiedziach .

Zachowany fragment przetrwa, jeśli aktywność zostanie zniszczona w wyniku zmiany konfiguracji, ale nie w przypadku, gdy aktywność zostanie zniszczona w tle lub na stosie. Dlatego zadanie w tle powinno być nadal przerywane, jeśli isChangingConfigurations()ma wartość false onPause().


2

Jestem świeższy w Androidzie i próbowałem tego i działa.

public class loadTotalMemberByBranch extends AsyncTask<Void, Void,Void> {
        ProgressDialog progressDialog = new ProgressDialog(Login.this);
        int ranSucess=0;
        @Override
        protected void onPreExecute() {
            // TODO Auto-generated method stub
            super.onPreExecute();
            progressDialog.setTitle("");    
            progressDialog.isIndeterminate();
            progressDialog.setCancelable(false);
            progressDialog.show();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);

        }
        @Override
        protected Void doInBackground(Void... params) {
            // TODO Auto-generated method stub

            return null;
        }
        @Override
        protected void onPostExecute(Void result) {
            // TODO Auto-generated method stub
            super.onPostExecute(result);
            progressDialog.dismiss();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        }
}

1

Próbowałem WSZYSTKO. Spędziłem dni eksperymentując. Nie chciałem blokować rotacji aktywności. Mój scenariusz to:

  1. Okno dialogowe postępu pokazujące użytkownikowi informacje dynamiczne. Np .: „Łączenie z serwerem ...”, „Pobieranie danych ...” itp.
  2. Wątek robi ciężkie rzeczy i aktualizuje okno dialogowe
  3. Aktualizacja interfejsu użytkownika z wynikami na końcu.

Problem polegał na tym, że podczas obracania ekranu każde rozwiązanie w książce zawiodło. Nawet z klasą AsyncTask, która jest prawidłowym sposobem radzenia sobie z tymi sytuacjami na Androidzie. Po obróceniu ekranu bieżący kontekst, z którym pracuje wątek początkowy, zniknie, co zakłóca wyświetlane okno dialogowe. Problemem zawsze był Dialog, bez względu na to, ile sztuczek dodałem do kodu (przekazywanie nowych kontekstów do uruchomionych wątków, utrzymywanie stanów wątków poprzez rotację itp.). Złożoność kodu na końcu była zawsze ogromna i zawsze było coś, co mogło pójść nie tak.

Jedynym rozwiązaniem, które działało dla mnie, była sztuczka Activity / Dialog. To proste i genialne, a wszystko to odporne na rotację:

  1. Zamiast tworzyć okno dialogowe i poprosić o jego pokazanie, utwórz działanie ustawione w manifeście za pomocą Androida: theme = "@ android: style / Theme.Dialog". Wygląda więc jak okno dialogowe.

  2. Zamień showDialog (DIALOG_ID) na startActivityForResult (yourActivityDialog, yourCode);

  3. Użyj onActivityResult w działaniu wywołującym, aby uzyskać wyniki z wykonanego wątku (nawet błędy) i zaktualizować interfejs użytkownika.

  4. W swoim „ActivityDialog” użyj wątków lub AsyncTask do wykonywania długich zadań, a onRetainNonConfigurationInstance, aby zapisać stan „okna dialogowego” podczas obracania ekranu.

Jest to szybkie i działa dobrze. Nadal używam okien dialogowych do innych zadań i AsyncTask do czegoś, co nie wymaga stałego okna dialogowego na ekranie. Ale w tym scenariuszu zawsze wybieram wzorzec Activity / Dialog.

I nie próbowałem tego, ale nawet można zablokować obracanie tego działania / okna dialogowego, gdy wątek jest uruchomiony, przyspieszając rzeczy, jednocześnie umożliwiając obracanie wywoływanego działania.


Fajne, z wyjątkiem tego, że parametry muszą być przekazywane przez Intent, co jest bardziej restrykcyjne niż Objectdozwolone przezAsyncTask
rds

@Rui Ja również użyłem tej metody przez ostatni rok. Teraz przyszło mi do głowy, że to również jest nieprawidłowe. Dlaczego Google miałoby w ogóle mieć okno dialogowe, gdyby to był sposób na „naprawienie” tego problemu? Problem, jaki widzę, polega na tym, że jeśli otworzysz ActivityB (Theme.Dialog) z ActivityA, wówczas ActivityA zostanie przesunięty w dół na stosie Activity, a zatem jest oznaczony jako gotowy do zabicia przez system operacyjny, jeśli to konieczne. Dlatego jeśli masz długotrwały proces i wyświetlasz jakieś „fałszywe” okno dialogowe postępu, a trwało to zbyt długo, a pamięć się kończyła ... Aktywność A została zabita i nie ma już nic do powrotu po zakończeniu postępu.
rf43

1

Obecnie istnieje znacznie wyraźniejszy sposób radzenia sobie z tego rodzaju problemami. Typowe podejście to:

1. Upewnij się, że Twoje dane są odpowiednio oddzielone od interfejsu użytkownika:

Wszystko, co jest procesem w tle, powinno być zachowane Fragment(ustaw to za pomocą Fragment.setRetainInstance(). Staje się to „trwałym przechowywaniem danych”, w którym przechowywane są wszystkie dane, które chcesz zachować. Po zdarzeniu zmiany orientacji Fragmentbędzie to nadal dostępne w oryginalnym stan przez FragmentManager.findFragmentByTag()połączenie (podczas jego tworzenia powinieneś nadać mu tag, a nie identyfikator, ponieważ nie jest dołączony do a View).

Zobacz opracowany przewodnik Obsługa zmian w środowisku wykonawczym, aby uzyskać informacje na temat robienia tego poprawnie i dlaczego jest to najlepsza opcja.

2. Upewnij się, że prawidłowo i bezpiecznie łączysz się między procesami w tle a interfejsem użytkownika:

Musisz odwrócić proces łączenia. W tej chwili proces w tle przyłącza się do procesu View- zamiast tego Viewpowinieneś dołączyć się do procesu w tle. To ma więcej sensu, prawda? W View„s działanie jest uzależnione od procesu w tle, podczas gdy proces w tle nie jest zależna od View.To środków zmieniających się link do standardowego Listenerinterfejsu. Powiedz, że twój proces (bez względu na to, jaką jest klasę - czy to jest AsyncTask, Runnableczy cokolwiek) definiuje a OnProcessFinishedListener, kiedy proces zostanie zakończony, powinien wywołać tego odbiornika, jeśli istnieje.

Ta odpowiedź to ładny zwięzły opis tego, jak zrobić niestandardowe obiekty nasłuchujące.

3. Połącz swój interfejs użytkownika z procesem danych, gdy interfejs użytkownika zostanie utworzony (w tym zmiany orientacji):

Teraz musisz się martwić o powiązanie zadania w tle z obecną Viewstrukturą. Jeśli właściwie postępujesz ze zmianami orientacji (a nie configChangeshack, który ludzie zawsze zalecają), Dialogsystem odtworzy je ponownie. Jest to ważne, oznacza to, że przy zmianie orientacji Dialogprzywoływane są wszystkie metody cyklu życia. Tak więc w dowolnej z tych metod ( onCreateDialogzazwyczaj jest to dobre miejsce) możesz wykonać połączenie takie jak:

DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
    f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
        public void onProcessFinished() {
            dismiss();
        }
    });
 }

Zobacz cykl życia Fragmentu, aby zdecydować, gdzie ustawienie detektora najlepiej pasuje do Twojej indywidualnej implementacji.

Jest to ogólne podejście do zapewnienia solidnego i kompletnego rozwiązania ogólnego problemu zadanego w tym pytaniu. W tej odpowiedzi prawdopodobnie brakuje kilku drobnych elementów, w zależności od indywidualnego scenariusza, ale jest to zazwyczaj najbardziej poprawne podejście do prawidłowej obsługi zdarzeń zmiany orientacji.


1

znalazłem i łatwiejsze rozwiązanie do obsługi wątków przy zmianie orientacji. Możesz po prostu zachować statyczne odniesienie do swojej aktywności / fragmentu i sprawdzić, czy jest ono puste przed działaniem na interfejsie użytkownika. Sugeruję też użycie try catch:

 public class DashListFragment extends Fragment {
     private static DashListFragment ACTIVE_INSTANCE;

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

        ACTIVE_INSTANCE = this;

        new Handler().postDelayed(new Runnable() {
            public void run() {
                try {
                        if (ACTIVE_INSTANCE != null) {
                            setAdapter(); // this method do something on ui or use context
                        }
                }
                catch (Exception e) {}


            }
        }, 1500l);

    }

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

        ACTIVE_INSTANCE = null;
    }


}

1

Jeśli masz problem z wykryciem zdarzeń zmiany orientacji w oknie dialogowym NIEZALEŻNYM OD REFERENCJI AKTYWNOŚCI , ta metoda działa wyjątkowo dobrze. Używam tego, ponieważ mam własną klasę dialogową, która może być wyświetlana w wielu różnych działaniach, więc nie zawsze wiem, w której aktywności jest pokazywana. Dzięki tej metodzie nie musisz zmieniać AndroidManifest, martw się o referencje do działań, i nie potrzebujesz niestandardowego okna dialogowego (tak jak ja). Potrzebny jest jednak niestandardowy widok zawartości, aby można było wykryć zmiany orientacji za pomocą tego konkretnego widoku. Oto mój przykład:

Ustawiać

public class MyContentView extends View{
    public MyContentView(Context context){
        super(context);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig){
        super.onConfigurationChanged(newConfig);

        //DO SOMETHING HERE!! :D
    }
}

Realizacja 1 - Dialog

Dialog dialog = new Dialog(context);
//set up dialog
dialog.setContentView(new MyContentView(context));
dialog.show();

Implementacja 2 - AlertDialog.Builder

AlertDialog.Builder builder = new AlertDialog.Builder(context);
//set up dialog builder
builder.setView(new MyContentView(context));        //Can use this method
builder.setCustomTitle(new MycontentView(context)); // or this method
builder.build().show();

Realizacja 3 - ProgressDialog / AlertDialog

ProgressDialog progress = new ProgressDialog(context);
//set up progress dialog
progress.setView(new MyContentView(context));        //Can use this method
progress.setCustomTitle(new MyContentView(context)); // or this method
progress.show();

1

Oto moje rozwiązanie, gdy stawiłem temu czoła: ProgressDialognie jest Fragmentdzieckiem, więc moja niestandardowa klasa „ ProgressDialogFragment” może się rozszerzyć DialogFragment, aby zachować okno dialogowe zmian konfiguracji.

import androidx.annotation.NonNull;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle; 
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;

 /**
 * Usage:
 * To display the dialog:
 *     >>> ProgressDialogFragment.showProgressDialogFragment(
 *              getSupportFragmentManager(), 
 *              "fragment_tag", 
 *              "my dialog title", 
 *              "my dialog message");
 *              
 * To hide the dialog
 *     >>> ProgressDialogFragment.hideProgressDialogFragment();
 */ 


public class ProgressDialogFragment extends DialogFragment {

    private static String sTitle, sMessage;
    private static ProgressDialogFragment sProgressDialogFragment;

    public ProgressDialogFragment() {
    }

    private ProgressDialogFragment(String title, String message) {
        sTitle = title;
        sMessage = message;
    }


    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return ProgressDialog.show(getActivity(), sTitle, sMessage);
    }

    public static void showProgressDialogFragment(FragmentManager fragmentManager, String fragmentTag, String title, String message) {
        if (sProgressDialogFragment == null) {
            sProgressDialogFragment = new ProgressDialogFragment(title, message);
            sProgressDialogFragment.show(fragmentManager, fragmentTag);

        } else { // case of config change (device rotation)
            sProgressDialogFragment = (ProgressDialogFragment) fragmentManager.findFragmentByTag(fragmentTag); // sProgressDialogFragment will try to survive its state on configuration as much as it can, but when calling .dismiss() it returns NPE, so we have to reset it on each config change
            sTitle = title;
            sMessage = message;
        }

    }

    public static void hideProgressDialogFragment() {
        if (sProgressDialogFragment != null) {
            sProgressDialogFragment.dismiss();
        }
    }
}

Wyzwanie polegało na zachowaniu tytułu i komunikatu okna dialogowego podczas obracania ekranu podczas resetowania do domyślnego pustego ciągu, chociaż okno dialogowe nadal jest wyświetlane

Istnieją 2 podejścia do rozwiązania tego:

Pierwsze podejście: wykonaj działanie, które wykorzystuje okno dialogowe do zachowania stanu podczas zmiany konfiguracji w pliku manifestu:

android:configChanges="orientation|screenSize|keyboardHidden"

To podejście nie jest preferowane przez Google.

Drugie podejście: w onCreate()metodzie działania musisz zachować swoją DialogFragment, odbudowując ProgressDialogFragmentponownie z tytułem i komunikatem w następujący sposób, jeśli savedInstanceStatewartość nie jest zerowa:

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

 if (savedInstanceState != null) {
      ProgressDialogFragment saveProgressDialog = (ProgressDialogFragment) getSupportFragmentManager()
              .findFragmentByTag("fragment_tag");
      if (saveProgressDialog != null) {
          showProgressDialogFragment(getSupportFragmentManager(), "fragment_tag", "my dialog title", "my dialog message");
      }
  }
}

0

Wydaje się, że jest to zbyt „szybkie i brudne”, aby mogło być prawdą, więc proszę wskazać wady, ale znalazłem, że zadziałało ...

W ramach metody onPostExecute mojej AsyncTask po prostu zawinąłem „.dismiss” dla okna dialogowego postępu w bloku try / catch (z pustym zapisem), a następnie po prostu zignorowałem zgłoszony wyjątek. Wydaje się, że jest to niewłaściwe, ale wydaje się, że nie ma żadnych złych skutków (przynajmniej dla tego, co robię później, czyli rozpoczęcia innej aktywności przechodzącej w wyniku mojego długiego zapytania jako Extra)


Czy mówisz, że w rzeczywistości nie ma wycieku pamięci, gdy platformy Android informują, że wyciekło okno?
rds

Moje okno dialogowe postępu jest zmienną składową klasy aktywności, więc założyłem, że kiedy działanie zostanie zniszczone i ponownie utworzone, zostanie ono wyrzucone do pamięci i nie będzie wycieków. Czy się mylę?
Simon

Tak, myślę, że to źle. Jak mówisz, Activityma odniesienie do Dialog. Po zmianie konfiguracji pierwsza Activityjest niszczona, co oznacza, że ​​wszystkie pola są ustawione na null. Ale niski poziom WindowManagerma również odniesienie do Dialog(ponieważ nie został jeszcze odrzucony). Nowy Activitypróbuje utworzyć nowy Dialog(w preExecute()), a menedżer okien zgłasza krytyczny wyjątek, który ci to uniemożliwia. Rzeczywiście, jeśli to zrobi, nie byłoby sposobu, aby czysto zniszczyć Dialogstąd zachowanie odniesienia do inicjału Activity. Czy mam rację?
rds

0

Najprostszym i najbardziej elastycznym rozwiązaniem jest użycie AsyncTask ze statycznym odniesieniem do ProgressBar . Zapewnia to zamknięte, a zatem wielokrotnego użytku rozwiązanie problemów zmiany orientacji. To rozwiązanie dobrze mi służyło do wykonywania różnorodnych zadań asynchronicznych, w tym pobierania Internetu, komunikacji z usługami i skanowania systemu plików. Rozwiązanie zostało dobrze przetestowane na wielu wersjach Androida i modelach telefonów. Kompletne demo można znaleźć tutaj ze szczególnym zainteresowaniem w DownloadFile.java

Przedstawiam poniższe jako przykład koncepcji

public class SimpleAsync extends AsyncTask<String, Integer, String> {
    private static ProgressDialog mProgressDialog = null;
    private final Context mContext;

    public SimpleAsync(Context context) {
        mContext = context;
        if ( mProgressDialog != null ) {
            onPreExecute();
        }
    }

    @Override
    protected void onPreExecute() {
        mProgressDialog = new ProgressDialog( mContext );
        mProgressDialog.show();
    }

    @Override
    protected void onPostExecute(String result) {
        if ( mProgressDialog != null ) {
            mProgressDialog.dismiss();
            mProgressDialog = null;
        }
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        mProgressDialog.setProgress( progress[0] );
    }

    @Override
    protected String doInBackground(String... sUrl) {
        // Do some work here
        publishProgress(1);
        return null;
    }

    public void dismiss() {
        if ( mProgressDialog != null ) {
            mProgressDialog.dismiss();
        }
    }
}

Użycie w działaniu na Androida jest proste

public class MainActivity extends Activity {
    DemoServiceClient mClient = null;
    DownloadFile mDownloadFile = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.main );
        mDownloadFile = new DownloadFile( this );

        Button downloadButton = (Button) findViewById( R.id.download_file_button );
        downloadButton.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mDownloadFile.execute( "http://www.textfiles.com/food/bakebred.txt");
            }
        });
    }

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

0

Po zmianie orientacji Android zabija tę aktywność i tworzy nową aktywność. Sugeruję użyć modernizacji z Rx Java. który uchwyt ulega awarii automatycznie.

Skorzystaj z tej metody przy ponownym wywołaniu.

.subscribeOn (Schedulers.io ()) .observeOn (AndroidSchedulers.mainThread ())

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.