Come gestire la modifica dell'orientamento dello schermo quando la finestra di dialogo di avanzamento e il thread in background sono attivi?


524

Il mio programma esegue alcune attività di rete in un thread in background. Prima di iniziare, viene visualizzata una finestra di dialogo di avanzamento. La finestra di dialogo viene chiusa sul gestore. Tutto funziona bene, tranne quando l'orientamento dello schermo cambia mentre la finestra di dialogo è attiva (e il thread di sfondo è attivo). A questo punto l'app si arresta in modo anomalo o si blocca o entra in una strana fase in cui l'app non funziona affatto fino a quando tutti i thread non sono stati eliminati.

Come posso gestire il cambiamento di orientamento dello schermo con garbo?

Il seguente codice di esempio corrisponde approssimativamente a ciò che fa il mio vero programma:

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

Pila:

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)

Ho provato a chiudere la finestra di dialogo di avanzamento in onSaveInstanceState, ma ciò impedisce solo un arresto immediato. Il thread in background continua a funzionare e l'interfaccia utente è in uno stato parzialmente disegnato. È necessario uccidere l'intera app prima che riprenda a funzionare.


1
Considerando le risposte che hai ricevuto, dovresti cambiare la risposta accettata in favore del meglio, vero?
RDS

Vedi anche una precedente domanda stackoverflow.com/questions/456211/…
rds

3
Tutti, ho un'ottima spiegazione e possibili soluzioni a questo problema. Vai su http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/ Lemme sa se questo ha aiutato.
arcamax,

2
C'è una spiegazione piuttosto completa su come conservare le attività in background asincrone attraverso gli orientamenti dello schermo in questo post del blog . Controlla!
Adrian Monk,

Basta impostare android: configChanges = "orientamento | screenSize" su Activity in manifest. Arresterà Android per ricreare la sua attività
Zar E Ahmer,

Risposte:


155

Quando cambi orientamento, Android creerà una nuova vista. Probabilmente stai ricevendo arresti anomali perché il thread in background sta cercando di cambiare lo stato su quello precedente. (Potrebbe anche avere problemi perché il thread in background non si trova sul thread dell'interfaccia utente)

Suggerirei di rendere volatile quel mHandler e di aggiornarlo quando cambia l'orientamento.


14
Potresti aver individuato il motivo dell'incidente. Mi sono sbarazzato del crash, ma non ho ancora capito come ripristinare l'interfaccia utente allo stato in cui si trovava prima che l'orientamento cambiasse in modo affidabile. Ma la tua risposta mi ha fatto avanzare, premiandola come risposta.
Heikki Toivonen,

4
Dovresti ottenere un avvio nella tua attività quando cambia l'orientamento. In sostanza, è necessario riconfigurare la vista utilizzando i vecchi dati. Quindi suggerirei di richiedere udpates di stato numerico dalla barra di avanzamento e di ricostruire una nuova vista quando ottieni quel nuovo 'onStart' che non ricordo di persona se ottieni anche una nuova attività, ma un po 'di caccia attraverso la documentazione dovrebbe aiutare.
Haseman,

6
Avendolo giocato di recente, posso passare che ricevi una nuova attività quando l'app cambia orientamento. (Ottieni anche una nuova vista) Se provi ad aggiornare la vecchia vista otterrai un'eccezione perché la vecchia vista ha un contesto di applicazione non valido (la tua vecchia attività) Puoi aggirare il problema passando myActivity.getApplicationContext () invece di un puntatore all'attività stessa.
Haseman,

1
Qualcuno può spiegare l'uso / beneficio di volatile in questo contesto
Zar E Ahmer,

2
@Nepster Sì, mi stavo chiedendo anche questo. Sarebbe bello se qualcuno spiegasse del volatile.
RestInPeace,

261

Modifica: gli ingegneri di Google non raccomandano questo approccio, come descritto da Dianne Hackborn (aka hackbod ) in questo post StackOverflow . Dai un'occhiata a questo post sul blog per ulteriori informazioni.


Devi aggiungere questo alla dichiarazione di attività nel manifest:

android:configChanges="orientation|screenSize"

così sembra

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

La questione è che il sistema distrugge l'attività quando si verifica un cambiamento nella configurazione. Vedi ConfigurationChanges .

Quindi inserendolo nel file di configurazione evita che il sistema distrugga la tua attività. Invece invoca il onConfigurationChanged(Configuration)metodo.


21
Questa è sicuramente la soluzione migliore; poiché ruota semplicemente il layout (il comportamento che ti aspetti in primo luogo). Assicurati di mettere android: configChanges = "orientamento | keyboardHidden" (a causa di telefoni con tastiera orizzontale)
nikib3ro

24
Questo sembra essere il comportamento che mi aspetto. Tuttavia, la documentazione indica che l'attività viene distrutta "perché qualsiasi risorsa dell'applicazione, inclusi i file di layout, può cambiare in base a qualsiasi valore di configurazione. Pertanto, l'unico modo sicuro per gestire una modifica della configurazione è recuperare nuovamente tutte le risorse". Inoltre orientation, ci sono molte altre ragioni per cambiare la configurazione: keyboardHidden(Ho già modificato la risposta della wiki), uiMode(Ad esempio, entrare o uscire dalla modalità auto; cambiare la modalità notturna), ecc. Ora mi chiedo se questo è in realtà una buona risposta.
RDS

116
Questa non è una soluzione accettabile. Maschera solo il vero problema.
rf43,

18
Funziona, ma non raccomandato da Google.
Ed Burnette,

21
Si prega di non seguire questo approccio qui. DDosAttack ha perfettamente ragione. Immagina di creare una finestra di dialogo di avanzamento per un download o qualcos'altro che richiede molto tempo. Come utente non starai su quell'attività e non la fisserai. Passeresti alla schermata principale o ad un'altra app come potrebbe entrare un gioco o una telefonata o qualcos'altro che ha fame di risorse che alla fine distruggerà la tua attività. E allora? Stai affrontando lo stesso vecchio problema che NON viene risolto con quel piccolo trucco. L'attività verrà ricreata nuovamente quando l'utente torna.
Tiguchi,

68

Ho trovato una soluzione solida per questi problemi, conforme al "Android Way" delle cose. Ho tutte le mie operazioni di lunga durata utilizzando il modello IntentService.

Cioè, le mie attività trasmettono intenti, IntentService fa il lavoro, salva i dati nel DB e quindi trasmette intenti appiccicosi . La parte adesiva è importante, in modo tale che anche se l'attività è stata messa in pausa durante il periodo in cui l'utente ha iniziato il lavoro e ha perso la trasmissione in tempo reale da IntentService, possiamo ancora rispondere e raccogliere i dati dall'attività chiamante. ProgressDialogs può funzionare abbastanza bene con questo modelloonSaveInstanceState() .

Fondamentalmente, è necessario salvare un flag che ha una finestra di dialogo di avanzamento in esecuzione nel pacchetto di istanze salvato. Non salvare l'oggetto della finestra di dialogo di avanzamento poiché ciò perderà l'intera attività. Per avere un handle persistente nella finestra di dialogo di avanzamento, lo memorizzo come riferimento debole nell'oggetto applicazione. In caso di modifica dell'orientamento o qualsiasi altra cosa che provochi la pausa dell'attività (telefonata, l'utente arriva a casa, ecc.) E quindi riprendo, chiudo la vecchia finestra di dialogo e ricrea una nuova finestra di dialogo nella nuova attività creata.

Per dialoghi di progresso indefiniti questo è facile. Per lo stile della barra di avanzamento, è necessario inserire gli ultimi progressi noti nel pacchetto e qualsiasi informazione si stia utilizzando localmente nell'attività per tenere traccia dell'avanzamento. Quando ripristini l'avanzamento, utilizzerai queste informazioni per rigenerare la barra di avanzamento nello stesso stato di prima e quindi aggiornarla in base allo stato attuale delle cose.

Quindi, per riassumere, l'inserimento di attività di lunga durata in un IntentService abbinato a un uso oculato di onSaveInstanceState()consente di tenere traccia delle finestre di dialogo e ripristinarle in modo efficiente attraverso gli eventi del ciclo di vita delle attività. I bit rilevanti del codice attività sono riportati di seguito. Avrai anche bisogno di logica nel tuo BroadcastReceiver per gestire gli intenti Sticky in modo appropriato, ma questo va oltre lo scopo.

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

sembra una buona soluzione
Derekyy,

"Ho tutte le mie operazioni di lunga durata utilizzando il modello IntentService." Questa non è una soluzione perfetta perché è come sparare dal cannone in passeri e molto codice di targa, per di più puoi guardare youtube.com/watch?v=NJsq0TU0qeg
Kamil Nekanowicz,

28

Ho riscontrato lo stesso problema. La mia attività deve analizzare alcuni dati da un URL ed è lenta. Quindi creo un thread per farlo, quindi faccio vedere una finestra di avanzamento. Lascio che il thread riporti un messaggio al thread dell'interfaccia utente tramite Handlerquando è finito. In Handler.handleMessage, ottengo l'oggetto dati (pronto ora) dal thread e lo popolo nell'interfaccia utente. Quindi è molto simile al tuo esempio.

Dopo un sacco di tentativi ed errori sembra che ho trovato una soluzione. Almeno ora posso ruotare lo schermo in qualsiasi momento, prima o dopo aver terminato il thread. In tutti i test, la finestra di dialogo è chiusa correttamente e tutti i comportamenti sono come previsto.

Quello che ho fatto è mostrato di seguito. L'obiettivo è riempire il mio modello di dati ( mDataObject) e quindi inserirlo nell'interfaccia utente. Dovrebbe consentire la rotazione dello schermo in qualsiasi momento senza sorpresa.

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

Questo è ciò che funziona per me. Non so se questo sia il metodo "corretto" come progettato da Android - sostengono che questa attività "distruggi / ricrea durante la rotazione dello schermo" in realtà semplifichi le cose, quindi immagino che non dovrebbe essere troppo complicato.

Fammi sapere se vedi un problema nel mio codice. Come detto sopra, non so davvero se ci siano effetti collaterali.


1
Molte grazie! Il suggerimento onRetainNonConfigurationInstance()e getLastNonConfigurationInstance()mi ha aiutato a risolvere il mio problema. Pollice su!
sabato

15

Il problema originale percepito era che il codice non sarebbe sopravvissuto a un cambiamento di orientamento dello schermo. Apparentemente, questo è stato "risolto" facendo in modo che il programma gestisse il cambiamento di orientamento dello schermo stesso, invece di lasciare che il framework dell'interfaccia utente lo facesse (chiamando onDestroy)).

Sottolineerei che se il problema di fondo è che il programma non sopravviverà suDestroy (), la soluzione accettata è solo una soluzione alternativa che lascia il programma con seri altri problemi e vulnerabilità. Ricorda che il framework Android afferma specificamente che la tua attività è a rischio di essere distrutta quasi in qualsiasi momento a causa di circostanze al di fuori del tuo controllo. Pertanto, la tua attività deve essere in grado di sopravvivere a onDestroy () e successiva onCreate () per qualsiasi motivo, non solo un cambiamento di orientamento dello schermo.

Se accetti di gestire autonomamente le modifiche all'orientamento dello schermo per risolvere il problema del PO, devi verificare che altre cause di onDestroy () non provochino lo stesso errore. Sei in grado di farlo? Altrimenti, mi chiederei se la risposta "accettata" sia davvero ottima.


14

La mia soluzione era quella di estendere la ProgressDialogclasse per ottenere la mia MyProgressDialog.
Ho ridefinito show()e dismiss()metodi per bloccare l'orientamento prima di mostrare Dialoge sbloccarlo quando Dialogviene respinto. Pertanto, quando Dialogviene visualizzato e l'orientamento del dispositivo cambia, l'orientamento dello schermo rimane finché non dismiss()viene chiamato, quindi l'orientamento dello schermo cambia in base ai valori del sensore / orientamento del dispositivo.

Ecco il mio codice:

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

Ho affrontato lo stesso problema e ho trovato una soluzione che non ha invaso utilizzando ProgressDialog e ho ottenuto risultati più rapidi.

Quello che ho fatto è stato creare un layout che contenesse un ProgressBar.

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

Quindi nel metodo onCreate eseguire le seguenti operazioni

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

Quindi esegui la lunga attività in un thread e, al termine, esegui Runnable per impostare la visualizzazione del contenuto sul layout reale che desideri utilizzare per questa attività.

Per esempio:

mHandler.post(new Runnable(){

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

Questo è quello che ho fatto e ho scoperto che funziona più velocemente rispetto a ProgressDialog ed è meno invadente e secondo me ha un aspetto migliore.

Tuttavia, se desideri utilizzare ProgressDialog, questa risposta non fa per te.


Questa soluzione è elegante in un semplice caso d'uso, ma presenta degli svantaggi. Devi ricostruire l'intera visualizzazione del contenuto. setContentView(R.layout.my_layout);non è sufficiente; è necessario impostare tutti gli ascoltatori, i dati di ripristino, ecc
RDS

@rds hai ragione. Questa è davvero solo una soluzione per un caso semplice, o se è necessario eseguire un pesante sollevamento nel metodo onCreate prima di visualizzare la vista.
Pzanno

Non capisco bene. Invece di listener di impostazioni in onCreate (), come faremmo normalmente, potremmo configurarli in run (). Mi sto perdendo qualcosa qui?
Codice Poeta

7

Ho scoperto una soluzione a questo che non ho ancora visto altrove. È possibile utilizzare un oggetto applicazione personalizzato che sappia se sono in corso attività in background, invece di provare a farlo nell'attività che viene distrutta e ricreata al cambio di orientamento. Ho scritto un blog su questo qui .


1
La creazione di un'abitudine Applicationè normalmente utilizzata per mantenere uno stato globale dell'applicazione. Non dico che non funziona, ma sembra troppo complicato. Dal documento "Normalmente non è necessario sottoclassare l'Applicazione". Preferisco in gran parte la risposta di sonxurxo.
RDS

7

Ho intenzione di contribuire con il mio approccio alla gestione di questo problema di rotazione. Questo potrebbe non essere rilevante per OP poiché non lo sta utilizzando AsyncTask, ma forse altri lo troveranno utile. È abbastanza semplice ma sembra fare il lavoro per me:

Ho un'attività di accesso con una AsyncTaskclasse nidificata chiamata BackgroundLoginTask.

Nel mio BackgroundLoginTasknon faccio nulla fuori dall'ordinario se non quello di aggiungere un controllo nullo al momento ProgressDialogdel licenziamento:

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

Questo per gestire il caso in cui l'attività in background termina mentre Activitynon è visibile e, pertanto, la finestra di dialogo di avanzamento è già stata ignorata dal onPause()metodo.

Successivamente, nella mia Activityclasse genitore , creo handle statici globali per la mia AsyncTaskclasse e il mio ProgressDialog( AsyncTaskessendo, nidificato, può accedere a queste variabili):

private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

Questo ha due scopi: in primo luogo, mi consente Activitydi accedere sempre AsyncTaskall'oggetto anche da una nuova attività post-ruotata. In secondo luogo, mi consente BackgroundLoginTaskdi accedere e chiudere ProgressDialoganche dopo una rotazione.

Successivamente, aggiungo questo a onPause(), facendo scomparire la finestra di dialogo di avanzamento quando il nostro Activitysta lasciando il primo piano (impedendo quel brutto "arresto forzato"):

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

Infine, ho il seguente nel mio onResume()metodo:

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

Ciò consente Dialogdi riapparire dopo che Activityè stato ricreato.

Ecco l'intera classe:

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

    }
}

Non sono affatto uno sviluppatore Android esperto, quindi sentiti libero di commentare.


1
Interessante! Soprattutto per quelli di noi che usano AsyncTask. Ho appena provato la tua soluzione e sembra funzionare principalmente. C'è un problema: ProgressDialog sembra terminare un po 'presto dopo una rotazione, mentre ProgressDialog è ancora attivo. Giocherò per vedere esattamente cosa sta succedendo e come rimediare. Ma non ho più questi incidenti!
Scott Biggs,

1
Ho trovato una soluzione. Sembra che il problema qui sia ProgressDialog statico. Quando le rotazioni interrompono ProgressDialog, a volte viene richiamato il metodo .dismiss () dopo il riavvio nella nuova attività. Creando ProgressDialog creato con ogni attività, ci assicuriamo che questo nuovo ProgressDialog non venga ucciso insieme alla vecchia attività. Mi sono anche assicurato che ProgressDialog fosse impostato su null ogni volta che veniva respinto (per aiutare la garbage collection). Quindi abbiamo una soluzione qui! Saluti a coloro che usano AsyncTask!
Scott Biggs

4

Sposta l'attività lunga in una classe separata. Implementalo come modello soggetto-osservatore. Ogni volta che l'attività viene creata registrati e durante la chiusura annulla la registrazione con la classe di attività. La classe di attività può utilizzare AsyncTask.


1
Non vedo come ciò possa aiutare. Potresti spiegare in modo più dettagliato come ciò previene i problemi che sto vedendo.
Heikki Toivonen,

1
Come ha detto Haseman, impedisce al back-end di accedere agli elementi dell'interfaccia utente e possiamo separare l'interfaccia utente dal back-end, il back-end viene eseguito in thread separato e continua a essere eseguito anche dopo il riorientamento dello schermo e Register-UnRegister con l'attività Backend per gli aggiornamenti di stato . Il vero esempio che ho risolto usando questo è che ho un'attività di download, l'ho spostata in un thread separato, ogni volta che il thread viene creato mi registro-annulla la registrazione con esso.
Vinay,

Ok, sto rivisitando questo problema e non credo di capire ancora pienamente questa risposta. Supponiamo che l'attività principale avvii un AsyncTask per eseguire un'operazione di rete di lunga durata che non vogliamo interrompere durante il cambio di orientamento dello schermo. Non vedo come la nuova attività possa inviare un messaggio all'AsyncTask avviato dalla vecchia attività. Puoi fare un esempio di codice?
Heikki Toivonen

@Heikki, la mia implementazione è al di sotto di ciò che intendi?
Beetstra,

4

Il trucco è mostrare / chiudere la finestra di dialogo all'interno di AsyncTask durante onPreExecute / onPostExecute come al solito, anche se in caso di cambio di orientamento creare / mostrare una nuova istanza della finestra di dialogo nell'attività e passare il suo riferimento all'attività.

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

L'ho fatto in questo modo:

    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 {

                }
            }
        }

    }

Puoi anche provare a farmi sapere che funziona per te o no


Il codice onDestroy potrebbe non essere eseguito affatto, dalla pagina dello sviluppatore : "Nota la colonna" Killable "nella tabella sopra - per quei metodi che sono contrassegnati come killable, dopo che quel metodo restituisce il processo che ospita l'attività può essere ucciso dal sistema in qualsiasi momento senza che venga eseguita un'altra riga del suo codice "
ilomambo,

Sono fermamente convinto che "onRetainNonConfigurationInstance ()" sia il metodo da utilizzare per tali casi ... nyc work
Nitin Bansal,

Sto affrontando un problema simile, poiché Sachin Gurnani utilizza la dichiarazione statica per risolvere il mio problema. stackoverflow.com/questions/12058774/…
Steven Du

2

Se si crea uno sfondo Serviceche fa tutto il lavoro pesante (richieste / risposta tcp, smascheramento), Viewe Activitypuò essere distrutto e ricreato senza perdite di finestra o perdita di dati. Ciò consente il comportamento consigliato da Android, ovvero distruggere un'attività su ogni modifica della configurazione (ad es. Per ciascuna modifica dell'orientamento).

È un po 'più complesso, ma è il modo migliore per invocare la richiesta del server, la pre / post-elaborazione dei dati, ecc.

Puoi persino usare il tuo Service per mettere in coda ogni richiesta a un server, in modo che sia facile ed efficiente gestire queste cose.

La guida agli sviluppatori ha un capitoloServices completo su .


A Serviceè più un lavoro che un AsyncTaskma può essere un approccio migliore in alcune situazioni. Non è necessariamente migliore, vero? Detto questo, non capisco come questo risolva il problema ProgressDialogtrapelato dalla rete principale Activity. Dove installi il ProgressDialog? Dove lo licenzi?
RDS

2

Ho un'implementazione che consente di distruggere l'attività su un cambiamento di orientamento dello schermo, ma distrugge comunque con successo la finestra di dialogo nell'attività ricreata. Uso ...NonConfigurationInstanceper associare l'attività in background all'attività ricreata. Il normale framework Android gestisce la ricostruzione della finestra di dialogo stessa, nulla è cambiato lì.

Ho eseguito la sottoclasse di AsyncTask aggiungendo un campo per l'attività "proprietaria" e un metodo per aggiornare questo proprietario.

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

  ...
}

Nella mia classe di attività ho aggiunto un campo che si backgroundTaskriferisce al backgroundtask "posseduto" e aggiorno questo campo usando onRetainNonConfigurationInstancee 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;
  }
  ...
}

Suggerimenti per un ulteriore miglioramento:

  • Cancella il backgroundTaskriferimento nell'attività al termine dell'attività per liberare memoria o altre risorse ad essa associate.
  • Cancella il ownerActivity riferimento nel task di sfondo prima che l'attività venga distrutta nel caso in cui non venga ricreata immediatamente.
  • Creare BackgroundTaskun'interfaccia e / o una raccolta per consentire l'esecuzione di diversi tipi di attività dalla stessa attività proprietaria.

2

Se si mantengono due layout, è necessario terminare tutto il thread dell'interfaccia utente.

Se usi AsynTask, puoi facilmente chiamare il .cancel()metodo all'interno del onDestroy()metodo dell'attività corrente.

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

Per AsyncTask, leggi di più nella sezione "Annullamento di un'attività" qui .

Aggiornamento: aggiunta la condizione per verificare lo stato, in quanto può essere annullata solo se è in esecuzione. Si noti inoltre che AsyncTask può essere eseguito solo una volta.


2

Ho cercato di implementare la soluzione di jfelectron perché si tratta di una " soluzione solida per questi problemi che è conforme alla" Via Android "delle cose " ma ci è voluto del tempo per cercare e mettere insieme tutti gli elementi citati. Finì con questa soluzione leggermente diversa, e penso più elegante, pubblicata qui nella sua interezza.

Utilizza un IntentService generato da un'attività per eseguire l'attività a esecuzione prolungata su un thread separato. Il servizio restituisce gli Intenti di trasmissione appiccicosi all'attività che aggiorna la finestra di dialogo. L'attività utilizza showDialog (), onCreateDialog () e onPrepareDialog () per eliminare la necessità di far passare i dati persistenti nell'oggetto applicazione o nel bundle saveInstanceState. Questo dovrebbe funzionare indipendentemente dal modo in cui l'applicazione viene interrotta.

Classe di attività:

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

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

Voci del file manifest:

prima della sezione dell'applicazione:

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

all'interno della sezione dell'applicazione

service android:name=".MyService"

2

Questa è la mia soluzione proposta:

  • Sposta l'AsyncTask o il Thread su un frammento mantenuto, come spiegato qui . Credo che sia una buona pratica spostare tutte le chiamate di rete in frammenti. Se stai già utilizzando frammenti, uno di essi potrebbe essere ritenuto responsabile delle chiamate. Altrimenti, puoi creare un frammento solo per fare la richiesta, come propone l'articolo collegato.
  • Il frammento utilizzerà un'interfaccia listener per segnalare il completamento / errore dell'attività. Non devi preoccuparti per i cambiamenti di orientamento lì. Il frammento avrà sempre il collegamento corretto all'attività corrente e la finestra di dialogo di avanzamento può essere ripresa in modo sicuro.
  • Rendi la finestra di dialogo dei progressi un membro della tua classe. In effetti, dovresti farlo per tutte le finestre di dialogo. Nel metodo onPause dovresti eliminarli, altrimenti perderai una finestra sulla modifica della configurazione. Lo stato di occupato dovrebbe essere mantenuto dal frammento. Quando il frammento è collegato all'attività, è possibile visualizzare nuovamente la finestra di dialogo di avanzamento, se la chiamata è ancora in esecuzione. A void showProgressDialog()tale scopo è possibile aggiungere un metodo all'interfaccia del listener di attività frammentate.

Soluzione perfetta, ma non capisco perché questa risposta sia oscurata dagli altri !!!
Blackkara,

2

Ho affrontato la stessa situazione. Quello che ho fatto è stato ottenere solo un'istanza per la mia finestra di dialogo di avanzamento nell'intera applicazione.

Innanzitutto, ho creato una classe DialogSingleton per ottenere solo un'istanza (modello 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();
        }
    }
}

Come mostro in questa classe, ho la finestra di dialogo di avanzamento come attributo. Ogni volta che devo mostrare una finestra di avanzamento, ottengo l'istanza univoca e creo un nuovo ProgressDialog.

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

Quando ho finito con l'attività in background, chiamo di nuovo l'istanza univoca e chiudo la sua finestra di dialogo.

DialogSingleton.GetInstance().DialogDismiss(this);

Salvo lo stato dell'attività in background nelle mie preferenze condivise. Quando ruoto lo schermo, chiedo se ho un'attività in esecuzione per questa attività: (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).

Quando inizio a eseguire un'attività in background:

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

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

Al termine dell'esecuzione di un'attività in background:

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

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

Spero possa essere d'aiuto.


2

Questa è una domanda molto antica che è emersa sulla barra laterale per qualche motivo.

Se l'attività in background deve sopravvivere solo quando l'attività è in primo piano, la "nuova" soluzione è quella di ospitare il thread in background (o, preferibilmente, AsyncTask) in un frammento mantenuto , come descritto in questa guida per sviluppatori e numerose domande e risposte .

Un frammento mantenuto sopravvive se l'attività viene distrutta per una modifica della configurazione, ma non quando l'attività viene distrutta in background o nello stack posteriore. Pertanto, l'attività in background deve essere comunque interrotta se isChangingConfigurations()è false onPause().


2

Sono un dispositivo Android più fresco e ho provato questo ed ha funzionato.

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

Ho provato TUTTO. Ho passato giorni a sperimentare. Non volevo bloccare la rotazione dell'attività. Il mio scenario era:

  1. Una finestra di dialogo di avanzamento che mostra all'utente informazioni dinamiche. Ad esempio: "Connessione al server ...", "Download dei dati ...", ecc.
  2. Un thread che fa le cose pesanti e aggiorna la finestra di dialogo
  3. Aggiornamento dell'interfaccia utente con i risultati alla fine.

Il problema era che, ruotando lo schermo, ogni soluzione sul libro falliva. Anche con la classe AsyncTask, che è il modo Android corretto per affrontare queste situazioni. Quando si ruota lo schermo, l'attuale Contesto con cui sta lavorando il thread iniziale scompare e questo si confonde con la finestra di dialogo che sta mostrando. Il problema è sempre stato la finestra di dialogo, indipendentemente da quanti trucchi ho aggiunto al codice (passando nuovi contesti ai thread in esecuzione, mantenendo gli stati dei thread attraverso le rotazioni, ecc ...). La complessità del codice alla fine era sempre enorme e c'era sempre qualcosa che poteva andare storto.

L'unica soluzione che ha funzionato per me era il trucco Activity / Dialog. È semplice e geniale ed è tutto a prova di rotazione:

  1. Invece di creare una finestra di dialogo e chiedere di mostrarla, crea un'attività che è stata impostata nel manifest con android: theme = "@ android: style / Theme.Dialog". Quindi, sembra solo una finestra di dialogo.

  2. Sostituisci showDialog (DIALOG_ID) con startActivityForResult (yourActivityDialog, yourCode);

  3. Utilizzare onActivityResult nell'attività chiamante per ottenere i risultati dal thread in esecuzione (anche gli errori) e aggiornare l'interfaccia utente.

  4. Su "ActivityDialog", utilizzare thread o AsyncTask per eseguire attività lunghe e onRetainNonConfigurationInstance per salvare lo stato "finestra di dialogo" quando si ruota lo schermo.

Questo è veloce e funziona bene. Uso ancora le finestre di dialogo per altre attività e l'AsyncTask per qualcosa che non richiede una finestra di dialogo costante sullo schermo. Ma con questo scenario, scelgo sempre il modello Attività / Finestra di dialogo.

E non l'ho provato, ma è anche possibile bloccare la rotazione di tale attività / dialogo, quando il thread è in esecuzione, accelerando le cose, consentendo al contempo all'attività chiamante di ruotare.


Bello tranne che i parametri devono essere passati attraverso un Intent, che è più restrittiva di quella Objectconsentita dalAsyncTask
RDS

@Rui Anch'io ho usato questo metodo per l'ultimo anno. Ora mi è venuto in mente che anche questo è errato. Perché Google dovrebbe anche avere una finestra di dialogo se questo fosse il modo di "risolvere" questo problema? Il problema che vedo è che se apri ActivityB (Theme.Dialog) da ActivityA, ActivityA viene spostato verso il basso nello stack Activity, quindi viene contrassegnato come pronto per essere ucciso dal sistema operativo, se necessario. Pertanto, se hai un lungo processo in corso e stai mostrando una sorta di 'finestra di dialogo' sui progressi falsi e ci è voluto troppo tempo e la memoria si è esaurita ... L'attività A viene uccisa e non c'è nulla da ritornare al completamento dei progressi.
rf43,

1

Al giorno d'oggi esiste un modo molto più distinto di gestire questo tipo di problemi. L'approccio tipico è:

1. Assicurati che i tuoi dati siano adeguatamente separati dall'interfaccia utente:

Tutto ciò che è un processo in background dovrebbe essere conservato Fragment(impostalo con Fragment.setRetainInstance(). Questo diventa il tuo 'archivio dati persistente' dove vengono conservati tutti i dati che desideri conservare. Dopo l'evento di cambio orientamento, questo Fragmentsarà ancora accessibile nel suo originale indicare attraverso una FragmentManager.findFragmentByTag()chiamata (quando lo crei dovresti dargli un tag non un ID in quanto non è collegato a View).

Vedere la guida sviluppata su Handling Runtime Changes per informazioni su come farlo correttamente e perché è l'opzione migliore.

2. Assicurati di avere un'interfaccia corretta e sicura tra i processi in background e l'interfaccia utente:

È necessario invertire il processo di collegamento. Al momento il tuo processo in background si attacca a un View- invece Viewdovresti collegarti al processo in background. Ha più senso, giusto? L' Viewazione dipende dal processo in background, mentre il processo in background non dipende dal View. Ciò significa cambiare il collegamento a un'interfaccia standard Listener. Di 'che il tuo processo (qualunque sia la classe - che sia una AsyncTask, Runnableo qualunque altra cosa) definisce una OnProcessFinishedListener, quando il processo è fatto, dovrebbe chiamare quel listener se esiste.

Questa risposta è una bella descrizione concisa di come fare ascoltatori personalizzati.

3. Collega la tua interfaccia utente al processo dei dati ogni volta che viene creata l'interfaccia utente (compresi i cambiamenti di orientamento):

Ora devi preoccuparti di interfacciare l'attività in background con qualunque sia la tua Viewstruttura attuale . Se stai gestendo correttamente i cambiamenti di orientamento (non sempre gli configChangeshacker lo consigliano), il tuo sistema Dialogverrà ricreato. Questo è importante, significa che al cambio di orientamento Dialogvengono richiamati tutti i metodi del tuo ciclo di vita. Quindi in uno di questi metodi (di onCreateDialogsolito è un buon posto), potresti fare una chiamata come la seguente:

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

Consulta il ciclo di vita di Frammento per decidere dove impostare l'ascoltatore più adatto alla tua implementazione individuale.

Questo è un approccio generale per fornire una soluzione solida e completa al problema generico posto in questa domanda. Probabilmente ci sono alcuni pezzi minori mancanti in questa risposta a seconda del tuo scenario individuale, ma questo è generalmente l'approccio più corretto per gestire correttamente gli eventi di cambio di orientamento.


1

ho trovato una soluzione più semplice per gestire i thread quando cambia l'orientamento. Puoi semplicemente mantenere un riferimento statico alla tua attività / frammento e verificare se è nullo prima di agire sull'interfaccia utente. Suggerisco di usare anche un tentativo di cattura:

 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

Se hai difficoltà a rilevare gli eventi di cambiamento di orientamento di una finestra di dialogo INDIPENDENTE DA UN RIFERIMENTO DI ATTIVITÀ , questo metodo funziona in modo eccitante. Lo uso perché ho una mia classe di dialogo che può essere mostrata in più attività diverse, quindi non sempre so in quale attività viene mostrata. Con questo metodo non è necessario modificare AndroidManifest, preoccuparsi dei riferimenti alle attività, e non hai bisogno di una finestra di dialogo personalizzata (come ho fatto io). Tuttavia, è necessaria una vista del contenuto personalizzata in modo da poter rilevare le modifiche all'orientamento utilizzando quella vista specifica. Ecco il mio esempio:

Impostare

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

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

        //DO SOMETHING HERE!! :D
    }
}

Implementazione 1 - Finestra di dialogo

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

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

Implementazione 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

Questa è la mia soluzione quando l'ho affrontata: ProgressDialognon è un Fragmentbambino, quindi la mia classe personalizzata " ProgressDialogFragment" può estendersi DialogFragmentinvece per mantenere la finestra di dialogo mostrata per le modifiche alla configurazione.

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

La sfida consisteva nel mantenere il titolo e il messaggio della finestra di dialogo durante la rotazione dello schermo mentre si ripristinavano sulla stringa vuota predefinita, sebbene la finestra di dialogo fosse ancora visualizzata

Ci sono 2 approcci per risolvere questo:

Primo approccio: eseguire l'attività che utilizza la finestra di dialogo per conservare lo stato durante la modifica della configurazione nel file manifest:

android:configChanges="orientation|screenSize|keyboardHidden"

Questo approccio non è preferito da Google.

Secondo approccio: sul onCreate()metodo dell'attività , è necessario conservare il proprio DialogFragmentricostruendo ProgressDialogFragmentnuovamente con il titolo e il messaggio come segue se savedInstanceStatenon è null:

@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

Sembra troppo "veloce e sporco" per essere vero, quindi per favore fai notare i difetti ma quello che ho trovato funzionante è stato ...

All'interno del metodo onPostExecute del mio AsyncTask, ho semplicemente spostato il '.dismiss' per la finestra di dialogo di avanzamento in un blocco try / catch (con un catch vuoto) e quindi ho semplicemente ignorato l'eccezione sollevata. Sembra sbagliato, ma sembra che non ci siano effetti negativi (almeno per quello che sto facendo successivamente, ovvero iniziare un'altra attività passando il risultato della mia query di lunga durata come Extra)


Stai dicendo che in realtà non c'è perdita di memoria quando le piattaforme Android dicono che la finestra è trapelata?
RDS

La mia finestra di dialogo sullo stato di avanzamento è una variabile membro della classe di attività, quindi ho ipotizzato che quando l'attività viene distrutta e ricreata, sarà raccolta dei rifiuti e non vi saranno perdite. Ho sbagliato?
Simon,

Sì, penso che sia sbagliato. Come dici tu, Activityha un riferimento a Dialog. Quando la configurazione viene modificata, la prima Activityviene distrutta, il che significa che tutti i campi sono impostati su null. Ma il livello basso WindowManagerha anche un riferimento al Dialog(poiché non è stato ancora respinto). Il nuovo Activitytenta di creare un nuovo Dialog(in preExecute()) e il window manager solleva un'eccezione fatale che ti impedisce di farlo. In effetti, se lo facesse, non ci sarebbe modo di distruggere in modo pulito Dialogmantenendo quindi un riferimento all'iniziale Activity. Ho ragione?
RDS

0

La soluzione più semplice e flessibile è utilizzare un AsyncTask con un riferimento statico a ProgressBar . Ciò fornisce una soluzione incapsulata e quindi riutilizzabile ai problemi di cambio di orientamento. Questa soluzione mi è stata utile per varie attività asincrone, inclusi download da Internet, comunicazioni con i servizi e scansioni del filesystem. La soluzione è stata ben testata su più versioni Android e modelli di telefono. Una demo completa può essere trovata qui con specifico interesse per DownloadFile.java

Vi presento quanto segue come esempio di concetto

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

L'utilizzo in un'attività Android è semplice

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

Quando cambi orientamento, Android interrompe quell'attività e crea una nuova attività. Suggerisco di usare il retrofit con Rx Java. quale handle si blocca automaticamente.

Utilizzare questi metodi per il retrofit della chiamata.

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

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