¿Cómo manejar el cambio de orientación de la pantalla cuando el diálogo de progreso y el hilo de fondo están activos?


524

Mi programa realiza alguna actividad de red en un hilo de fondo. Antes de comenzar, aparece un cuadro de diálogo de progreso. El diálogo se descarta en el controlador. Todo esto funciona bien, excepto cuando la orientación de la pantalla cambia mientras el cuadro de diálogo está activo (y el hilo de fondo continúa). En este punto, la aplicación se bloquea o se interrumpe, o entra en una etapa extraña en la que la aplicación no funciona en absoluto hasta que se hayan eliminado todos los hilos.

¿Cómo puedo manejar el cambio de orientación de la pantalla con gracia?

El siguiente código de muestra coincide aproximadamente con lo que hace mi programa real:

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

Apilar:

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)

Intenté descartar el cuadro de diálogo de progreso en onSaveInstanceState, pero eso solo evita un bloqueo inmediato. El subproceso de fondo aún continúa y la interfaz de usuario está en estado parcialmente dibujado. Necesita matar toda la aplicación antes de que comience a funcionar nuevamente.


1
Teniendo en cuenta las respuestas que ha recibido, debe cambiar la respuesta aceptada en favor de la mejor, ¿no?
rds

Ver también una pregunta anterior stackoverflow.com/questions/456211/…
rds

3
Todos, obtuve una gran explicación y posibles soluciones a este problema. Visite http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/ Déjeme saber si esto ayudó.
arcamax

2
Hay una explicación bastante completa sobre cómo retener tareas de fondo asincrónicas en las orientaciones de pantalla en esta publicación de blog . ¡Echale un vistazo!
Adrian Monk

Simplemente configure android: configChanges = "oriente | screenSize" en Actividad en manifiesto. Detendrá a Android para recrear su actividad
Zar E Ahmer

Respuestas:


155

Cuando cambie de orientación, Android creará una nueva vista. Probablemente te estés bloqueando porque tu hilo de fondo está tratando de cambiar el estado del anterior. (También puede estar teniendo problemas porque su hilo de fondo no está en el hilo de la interfaz de usuario)

Sugeriría hacer que mHandler sea volátil y actualizarlo cuando cambie la orientación.


14
Es posible que haya identificado la razón del accidente. Me deshice del bloqueo, pero todavía no he descubierto cómo restaurar la interfaz de usuario al estado en que estaba antes del cambio de orientación de manera confiable. Pero tu respuesta me hizo avanzar, por lo que la adjudiqué como respuesta.
Heikki Toivonen

44
Debería obtener un onStart en su actividad cuando cambie la orientación. Esencialmente, debe reconfigurar la vista utilizando los datos antiguos. Por lo tanto, sugeriría solicitar udpates de estado numérico de la barra de progreso y reconstruir una nueva vista cuando obtenga ese nuevo 'onStart' que no recuerdo fácilmente si obtiene una nueva actividad también, pero algo de búsqueda en la documentación debería ayudar.
haseman

66
Después de haber jugado con él recientemente, puedo transmitir que obtienes una nueva actividad cuando tu aplicación cambia de orientación. (También obtiene una nueva vista) Si intenta actualizar la vista anterior, obtendrá una excepción porque la vista anterior tiene un contexto de aplicación no válido (su actividad anterior) Puede sortear esto pasando myActivity.getApplicationContext () en lugar de un puntero a la actividad misma.
haseman el

1
¿Alguien puede explicar el uso / beneficio de la volatilidad en este contexto
Zar E Ahmer

2
@Nepster Sí, yo también me preguntaba sobre eso. Sería genial si alguien explicara lo volátil.
RestInPeace

261

Editar: los ingenieros de Google no recomiendan este enfoque, como lo describe Dianne Hackborn (también conocido como hackbod ) en esta publicación de StackOverflow . Mira esta publicación de blog para más información.


Debe agregar esto a la declaración de actividad en el manifiesto:

android:configChanges="orientation|screenSize"

entonces parece

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

El problema es que el sistema destruye la actividad cuando ocurre un cambio en la configuración. Ver cambios de configuración .

Poner eso en el archivo de configuración evita que el sistema destruya su actividad. En su lugar, invoca el onConfigurationChanged(Configuration)método.


21
Esta es definitivamente la mejor solución; ya que simplemente gira el diseño (el comportamiento que espera en primer lugar). Solo asegúrese de poner android: configChanges = "orientación | keyboardHidden" (debido a los teléfonos que tienen teclado horizontal)
nikib3ro

24
Este parece ser el comportamiento que espero. Sin embargo, la documentación indica que la actividad se destruye "porque cualquier recurso de la aplicación, incluidos los archivos de diseño, puede cambiar en función de cualquier valor de configuración. Por lo tanto, la única forma segura de manejar un cambio de configuración es recuperar todos los recursos". Y además orientation, hay muchas más razones para que cambie la configuración: keyboardHidden(ya he editado la respuesta wiki), uiMode(Por ejemplo, entrar o salir del modo de automóvil; cambio de modo nocturno), etc. Ahora me pregunto si esto es realmente Una buena respuesta.
rds

116
Esta no es una solución aceptable. Simplemente enmascara el verdadero problema.
rf43

18
Funciona, pero no lo recomienda Google.
Ed Burnette

21
Por favor, no siga este enfoque aquí. DDosAttack tiene toda la razón. Imagine que está creando un cuadro de diálogo de progreso para una descarga o algo más que lleva mucho tiempo. Como usuario, no te quedarás en esa actividad y la mirarás. Cambiaría a la pantalla de inicio o a otra aplicación, como un juego o una llamada telefónica, o cualquier otra fuente de recursos que eventualmente destruirá su actividad. ¿Y luego que? Estás enfrentando el mismo problema que NO se resuelve con ese pequeño truco. La actividad se volverá a crear de nuevo cuando el usuario regrese.
tiguchi 01 de

68

Se me ocurrió una solución sólida para estos problemas que se ajusta a la 'forma Android' de las cosas. Tengo todas mis operaciones de larga duración utilizando el patrón IntentService.

Es decir, mis intentos actividades de difusión, el IntentService hace el trabajo, guarda los datos en la base de datos y luego difunde pegajosas intenciones. La parte adhesiva es importante, de modo que incluso si la Actividad se detuvo durante el tiempo después de que el usuario inició el trabajo y pierde la transmisión en tiempo real desde IntentService, aún podemos responder y recoger los datos de la Actividad que realiza la llamada. ProgressDialogs puede funcionar muy bien con este patrón onSaveInstanceState().

Básicamente, debe guardar un indicador de que tiene un cuadro de diálogo de progreso ejecutándose en el paquete de instancias guardadas. No guarde el objeto de diálogo de progreso porque esto filtrará toda la Actividad. Para tener un control persistente del diálogo de progreso, lo almaceno como una referencia débil en el objeto de la aplicación. En el cambio de orientación o cualquier otra cosa que haga que la Actividad se detenga (llamada telefónica, usuario llegue a casa, etc.) y luego se reanude, descarto el diálogo anterior y vuelvo a crear un diálogo nuevo en la Actividad recién creada.

Para diálogos de progreso indefinidos, esto es fácil. Para el estilo de barra de progreso, debe colocar el último progreso conocido en el paquete y cualquier información que esté utilizando localmente en la actividad para realizar un seguimiento del progreso. Al restaurar el progreso, utilizará esta información para volver a generar la barra de progreso en el mismo estado que antes y luego actualizar según el estado actual de las cosas.

Para resumir, colocar tareas de larga duración en un IntentService junto con un uso juicioso de le onSaveInstanceState()permite realizar un seguimiento eficiente de los diálogos y restaurarlos a lo largo de los eventos del ciclo de vida de la Actividad. Los bits relevantes del código de actividad están a continuación. También necesitará lógica en su BroadcastReceiver para manejar adecuadamente las intenciones de Sticky, pero eso está fuera del alcance de esto.

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

parece una buena solución
Derekyy

"Tengo todas mis operaciones de larga duración utilizando el patrón IntentService". Esta no es una solución perfecta porque es como disparar desde el cañón a gorriones y mucho código repetitivo, para más puedes ver youtube.com/watch?v=NJsq0TU0qeg
Kamil Nekanowicz

28

Me encontré con el mismo problema. Mi actividad necesita analizar algunos datos de una URL y es lenta. Entonces creo un hilo para hacerlo, luego muestro un diálogo de progreso. Dejo que el hilo publique un mensaje en el hilo de la interfaz de usuario Handlercuando esté terminado. En Handler.handleMessage, obtengo el objeto de datos (listo ahora) del hilo y lo relleno en la interfaz de usuario. Entonces es muy similar a tu ejemplo.

Después de muchas pruebas y errores, parece que encontré una solución. Al menos ahora puedo rotar la pantalla en cualquier momento, antes o después de que el hilo esté terminado. En todas las pruebas, el cuadro de diálogo se cierra correctamente y todos los comportamientos son los esperados.

Lo que hice se muestra a continuación. El objetivo es llenar mi modelo de datos ( mDataObject) y luego completarlo en la interfaz de usuario. Debe permitir la rotación de la pantalla en cualquier momento sin 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.
    }
}

Eso es lo que me funciona. No sé si este es el método "correcto" diseñado por Android: afirman que esta "actividad de destruir / recrear durante la rotación de la pantalla" en realidad hace las cosas más fáciles, así que supongo que no debería ser demasiado complicado.

Avíseme si ve un problema en mi código. Como se dijo anteriormente, realmente no sé si hay algún efecto secundario.


1
¡Muchas gracias! La pista onRetainNonConfigurationInstance()y getLastNonConfigurationInstance()me ayudó a resolver mi problema. ¡Pulgares hacia arriba!
sven

15

El problema original percibido era que el código no sobreviviría a un cambio de orientación de la pantalla. Aparentemente, esto se "resolvió" haciendo que el programa manejara el cambio de orientación de la pantalla, en lugar de dejar que el marco de la interfaz de usuario lo hiciera (llamando a onDestroy).

Diría que si el problema subyacente es que el programa no sobrevivirá en Destroy (), entonces la solución aceptada es solo una solución que deja al programa con otros problemas y vulnerabilidades graves. Recuerde que el marco de Android establece específicamente que su actividad está en riesgo de ser destruida casi en cualquier momento debido a circunstancias fuera de su control. Por lo tanto, su actividad debe poder sobrevivir en onDestroy () y en onCreate () posterior por cualquier motivo, no solo un cambio de orientación de la pantalla.

Si va a aceptar manejar los cambios de orientación de la pantalla usted mismo para resolver el problema del OP, debe verificar que otras causas de onDestroy () no generen el mismo error. ¿Eres capaz de hacer esto? Si no, me preguntaría si la respuesta "aceptada" es realmente muy buena.


14

Mi solución fue extender la ProgressDialogclase para obtener la mía MyProgressDialog.
Redefiní show()y dismiss()métodos para bloquear la orientación antes de mostrarla Dialogy desbloquearla nuevamente cuando Dialogse descarta. Entonces, cuando Dialogse muestra y cambia la orientación del dispositivo, la orientación de la pantalla permanece hasta que dismiss()se llama, luego la orientación de la pantalla cambia de acuerdo con los valores del sensor / orientación del dispositivo.

Aquí está mi código:

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

Me enfrenté a este mismo problema y se me ocurrió una solución que no se invocaba con ProgressDialog y obtengo resultados más rápidos.

Lo que hice fue crear un diseño que tiene una barra de progreso.

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

Luego, en el método onCreate, haga lo siguiente

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

Luego haga la tarea larga en un hilo, y cuando termine, haga que Runnable configure la vista de contenido en el diseño real que desea usar para esta actividad.

Por ejemplo:

mHandler.post(new Runnable(){

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

Esto es lo que hice, y descubrí que se ejecuta más rápido que mostrar el ProgressDialog y es menos intrusivo y tiene una mejor apariencia en mi opinión.

Sin embargo, si desea utilizar ProgressDialog, esta respuesta no es para usted.


Esta solución es elegante en un caso de uso simple, pero tiene inconvenientes. Necesita reconstruir la vista de contenido completo. setContentView(R.layout.my_layout);No es suficiente; es necesario configurar todos los oyentes, datos de reposición, etc.
RDS

@rds tienes razón. Esto es realmente solo una solución para un caso simple, o si necesita hacer un trabajo pesado en su método onCreate antes de mostrar su vista.
Pzanno

No lo entiendo del todo. En lugar de los oyentes de configuración en onCreate (), como lo haríamos normalmente, podríamos configurarlos en run (). ¿Me estoy perdiendo de algo?
Code Poet

7

Descubrí una solución a esto que aún no he visto en ningún otro lado. Puede usar un objeto de aplicación personalizado que sepa si tiene tareas en segundo plano en lugar de intentar hacer esto en la actividad que se destruye y se recrea en el cambio de orientación. Blogueé sobre esto aquí .


1
La creación de una costumbre Applicationse usa normalmente para mantener un estado de aplicación global. No digo que no funcione, pero parece demasiado complicado. Del documento "Normalmente no hay necesidad de subclase Aplicación". Prefiero en gran medida la respuesta de sonxurxo.
rds

7

Voy a aportar mi enfoque para manejar este problema de rotación. Esto puede no ser relevante para OP ya que no lo está usando AsyncTask, pero tal vez otros lo encuentren útil. Es bastante simple pero parece hacer el trabajo por mí:

Tengo una actividad de inicio de sesión con una AsyncTaskclase anidada llamada BackgroundLoginTask.

En mi caso BackgroundLoginTask, no hago nada fuera de lo común, excepto agregar un cheque nulo al ProgressDialogdescartar la llamada :

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

Esto es para manejar el caso donde la tarea en segundo plano finaliza mientras Activityno está visible y, por lo tanto, el diálogo de progreso ya ha sido descartado por el onPause()método.

A continuación, en mi Activityclase principal , creo identificadores estáticos globales para mi AsyncTaskclase y mi ProgressDialog(el AsyncTask, anidado, puede acceder a estas variables):

private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

Esto tiene dos propósitos: en primer lugar, me permite Activityacceder siempre al AsyncTaskobjeto, incluso desde una nueva actividad posterior a la rotación. En segundo lugar, me permite BackgroundLoginTaskacceder y descartar ProgressDialogincluso después de una rotación.

A continuación, agrego esto a onPause(), haciendo que el cuadro de diálogo de progreso desaparezca cuando nuestro Activityabandona el primer plano (evitando ese feo bloqueo de "cierre forzado"):

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

Finalmente, tengo lo siguiente en mi onResume()método:

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

Esto permite Dialogque reaparezca después de que se vuelve a Activitycrear.

Aquí está toda la clase:

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

    }
}

De ninguna manera soy un desarrollador experimentado de Android, así que siéntase libre de comentar.


1
¡Interesante! Especialmente para aquellos de nosotros que usamos AsyncTask. Acabo de probar su solución, y parece funcionar principalmente. Hay un problema: el ProgressDialog parece terminar un poco antes de una rotación MIENTRAS que el ProgressDialog todavía está activo. Voy a jugar para ver exactamente qué está pasando y cómo remediarlo. ¡Pero ya no tengo esos accidentes!
Scott Biggs

1
Encontró una solución. Parece que el problema aquí es el ProgressDialog estático. Cuando las rotaciones interrumpen el ProgressDialog, a veces recibe su método .dismiss () después de que se reinicia en la nueva Actividad. Al hacer el ProgressDialog creado con cada Actividad, nos aseguramos de que este nuevo ProgressDialog no se elimine junto con la Actividad anterior. También me aseguré de que ProgressDialog esté establecido en nulo cada vez que se descarta (para ayudar a la recolección de basura). ¡Entonces tenemos una solución aquí! ¡Saludos a quienes usan AsyncTask!
Scott Biggs

4

Mueva la tarea larga a una clase separada. Impleméntelo como un patrón sujeto-observador. Cada vez que se crea la actividad, regístrese y, al cerrar, anule el registro con la clase de tarea. La clase de tarea puede usar AsyncTask.


1
No veo cómo eso ayudaría. ¿Podría explicar con más detalle cómo esto previene los problemas que estoy viendo?
Heikki Toivonen

1
Como dijo Haseman, evita que el backend acceda a los elementos de la interfaz de usuario y podemos separar la interfaz de usuario del backend, el backend se ejecuta en un hilo separado y continúa ejecutándose incluso después de que la pantalla se reorienta y se registra y se registra con la tarea Backend para las actualizaciones de estado . El verdadero ejemplo que he resuelto usando esto es que tengo una Tarea de descarga, la he movido a un hilo separado, cada vez que se crea el hilo me registro y no me registro.
Vinay el

Ok, estoy revisando este problema y no creo que todavía entienda completamente esta respuesta. Supongamos que la actividad principal inicia una AsyncTask para realizar una operación de red de larga duración que no queremos interrumpir durante el cambio de orientación de la pantalla. No veo cómo la nueva actividad puede enviar un mensaje a AsyncTask iniciado por la actividad anterior. ¿Puedes dar un código de ejemplo?
Heikki Toivonen,

@Heikki, ¿mi implementación está por debajo de lo que quieres decir?
beetstra

4

El truco consiste en mostrar / descartar el diálogo dentro de AsyncTask durante onPreExecute / onPostExecute como de costumbre, aunque en caso de cambio de orientación cree / muestre una nueva instancia del diálogo en la actividad y pase su referencia a la tarea.

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

Lo he hecho así:

    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 {

                }
            }
        }

    }

También puedes intentar y hacerme saber que funciona para ti o no


Es posible que el código onDestroy no se ejecute en absoluto, desde la página del desarrollador : "Tenga en cuenta la columna" Eliminable "en la tabla anterior - para aquellos métodos que están marcados como eliminables, después de que ese método devuelve el proceso que aloja la actividad puede ser eliminado por el sistema en cualquier momento sin que se ejecute otra línea de su código "
ilomambo

Creo firmemente que "onRetainNonConfigurationInstance ()" es el método que se utilizará para tales casos ... trabajo de
Nueva

Estoy enfrentando un problema similar, ya que Sachin Gurnani lo hace usando una declaración estática para solucionar mi problema. stackoverflow.com/questions/12058774/…
Steven Du

2

Si crea un fondo Serviceque hace todo el trabajo pesado (solicitudes / respuestas de tcp, desorganización), el Viewy Activitypuede ser destruido y recreado sin filtrar la ventana o perder datos. Esto permite el comportamiento recomendado de Android, que es destruir una Actividad en cada cambio de configuración (por ejemplo, para cada cambio de orientación).

Es un poco más complejo, pero es la mejor manera de invocar la solicitud del servidor, el procesamiento previo / posterior de datos, etc.

Incluso puede usar su Servicepara poner en cola cada solicitud a un servidor, por lo que es fácil y eficiente manejar esas cosas.

La guía de desarrollo tiene un capítuloServices completo sobre .


A Servicees más trabajo que un AsyncTaskpero puede ser un mejor enfoque en algunas situaciones. No es necesariamente mejor, ¿verdad? Dicho esto, no entiendo cómo esto resuelve el problema de lo ProgressDialogque se filtra desde el principal Activity. ¿Dónde instancias el ProgressDialog? ¿Dónde lo descartas?
rds

2

Tengo una implementación que permite que la actividad se destruya en un cambio de orientación de la pantalla, pero aún así destruye el diálogo en la actividad recreada con éxito. Utilizo ...NonConfigurationInstancepara adjuntar la tarea en segundo plano a la actividad recreada. El marco normal de Android maneja la recreación del diálogo en sí, no se cambia nada allí.

Subclase AsyncTask agregando un campo para la actividad 'propietaria' y un método para actualizar este propietario.

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

  ...
}

En mi clase de actividad, agregué un campo que hace backgroundTaskreferencia a la tarea de fondo "propiedad" y actualizo este campo con onRetainNonConfigurationInstancey 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;
  }
  ...
}

Sugerencias para seguir mejorando:

  • Borre la backgroundTaskreferencia en la actividad después de que la tarea haya finalizado para liberar cualquier memoria u otros recursos asociados con ella.
  • Borre la ownerActivityreferencia en la tarea de fondo antes de que la actividad se destruya en caso de que no se vuelva a crear de inmediato.
  • Cree una BackgroundTaskinterfaz y / o colección para permitir que se ejecuten diferentes tipos de tareas desde la misma actividad propietaria.

2

Si mantiene dos diseños, todos los subprocesos de la interfaz de usuario deben terminarse.

Si usa AsynTask, puede llamar fácilmente al .cancel()método dentro del onDestroy()método de la actividad actual.

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

Para AsyncTask, lea más en la sección "Cancelar una tarea" aquí .

Actualización: condición agregada para verificar el estado, ya que solo se puede cancelar si está en estado de ejecución. También tenga en cuenta que AsyncTask solo se puede ejecutar una vez.


2

Intenté implementar la solución de jfelectron porque es una " solución sólida para estos problemas que se ajusta a la" forma de Android "de las cosas ", pero llevó un tiempo buscar y reunir todos los elementos mencionados. Terminé con esta solución ligeramente diferente, y creo que más elegante, publicada aquí en su totalidad.

Utiliza un IntentService disparado desde una actividad para realizar la tarea de ejecución larga en un hilo separado. El servicio devuelve Intenciones de difusión fijas a la actividad que actualiza el diálogo. La Actividad usa showDialog (), onCreateDialog () y onPrepareDialog () para eliminar la necesidad de que se pasen datos persistentes en el objeto de la aplicación o el paquete savedInstanceState. Esto debería funcionar sin importar cómo se interrumpa su aplicación.

Clase de actividad:

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

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

Entradas de archivo de manifiesto:

Antes de la sección de aplicación:

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

sección de aplicación interior

service android:name=".MyService"

2

Esta es mi solución propuesta:

  • Mueva AsyncTask o Thread a un Fragmento retenido, como se explica aquí . Creo que es una buena práctica mover todas las llamadas de red a fragmentos. Si ya está utilizando fragmentos, uno de ellos podría hacerse responsable de las llamadas. De lo contrario, puede crear un fragmento solo para realizar la solicitud, como propone el artículo vinculado.
  • El fragmento utilizará una interfaz de escucha para indicar la finalización / falla de la tarea. No tiene que preocuparse por los cambios de orientación allí. El fragmento siempre tendrá el enlace correcto a la actividad actual y el diálogo de progreso se puede reanudar de forma segura.
  • Convierta su diálogo de progreso en un miembro de su clase. De hecho, debe hacer eso para todos los cuadros de diálogo. En el método onPause, debe descartarlos; de lo contrario, filtrará una ventana sobre el cambio de configuración. El estado ocupado debe mantenerse por el fragmento. Cuando el fragmento se adjunta a la actividad, puede volver a abrir el cuadro de diálogo de progreso, si la llamada aún se está ejecutando. Se void showProgressDialog()puede agregar un método a la interfaz de escucha de actividad de fragmentos para este propósito.

Solución perfecta, pero no entiendo por qué esta respuesta se ve ensombrecida por los demás.
blackkara

2

Me enfrenté a la misma situación. Lo que hice fue obtener solo una instancia para mi diálogo de progreso en toda la aplicación.

Primero, creé una clase DialogSingleton para obtener solo una instancia (patrón 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();
        }
    }
}

Como muestro en esta clase, tengo el diálogo de progreso como atributo. Cada vez que necesito mostrar un diálogo de progreso, obtengo una instancia única y creo un nuevo ProgressDialog.

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

Cuando termino con la tarea en segundo plano, vuelvo a llamar a la instancia única y descarto su diálogo.

DialogSingleton.GetInstance().DialogDismiss(this);

Guardo el estado de la tarea en segundo plano en mis preferencias compartidas. Cuando giro la pantalla, pregunto si tengo una tarea ejecutándose para esta actividad: (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).

Cuando empiezo a ejecutar una tarea en segundo plano:

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

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

Cuando termino de ejecutar una tarea en segundo plano:

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

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

Espero que ayude.


2

Esta es una pregunta muy antigua que apareció en la barra lateral por alguna razón.

Si la tarea en segundo plano solo necesita sobrevivir mientras la actividad está en primer plano, la "nueva" solución es alojar el subproceso en segundo plano (o, preferiblemente, AsyncTask) en un fragmento retenido , como se describe en esta guía del desarrollador y numerosas preguntas y respuestas .

Un fragmento retenido sobrevive si la actividad se destruye por un cambio de configuración, pero no cuando la actividad se destruye en segundo plano o en la pila. Por lo tanto, la tarea en segundo plano aún debe interrumpirse si isChangingConfigurations()es falsa onPause().


2

Estoy más fresco en Android e intenté esto y funcionó.

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

Lo he intentado TODO. Pasé días experimentando. No quería bloquear la rotación de la actividad. Mi escenario fue:

  1. Un diálogo de progreso que muestra información dinámica al usuario. Por ejemplo: "Conectando al servidor ...", "Descargando datos ...", etc.
  2. Un hilo haciendo cosas pesadas y actualizando el diálogo
  3. Actualización de la interfaz de usuario con los resultados al final.

El problema era que, al girar la pantalla, todas las soluciones del libro fallaban. Incluso con la clase AsyncTask, que es la forma correcta de Android de lidiar con estas situaciones. Al girar la pantalla, el contexto actual con el que está trabajando el hilo inicial desaparece y eso se confunde con el diálogo que se muestra. El problema siempre fue el Diálogo, sin importar cuántos trucos agregué al código (pasar nuevos contextos a hilos en ejecución, retener estados de hilo a través de rotaciones, etc.). La complejidad del código al final siempre fue enorme y siempre había algo que podía salir mal.

La única solución que funcionó para mí fue el truco Actividad / Diálogo. Es simple y genial y es a prueba de rotación:

  1. En lugar de crear un cuadro de diálogo y pedir que se muestre, cree una actividad que se haya establecido en el manifiesto con android: theme = "@ android: style / Theme.Dialog". Entonces, solo parece un diálogo.

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

  3. Use onActivityResult en la Actividad de llamada para obtener los resultados del subproceso en ejecución (incluso los errores) y actualizar la IU.

  4. En su 'ActivityDialog', use hilos o AsyncTask para ejecutar tareas largas y onRetainNonConfigurationInstance para guardar el estado del "diálogo" al girar la pantalla.

Esto es rápido y funciona bien. Todavía uso diálogos para otras tareas y AsyncTask para algo que no requiere un diálogo constante en la pantalla. Pero con este escenario, siempre voy por el patrón Actividad / Diálogo.

Y no lo intenté, pero incluso es posible bloquear la rotación de esa Actividad / Diálogo, cuando el hilo se está ejecutando, acelerando las cosas, mientras que permite que la Actividad de llamada gire.


Bien, excepto que los parámetros se debe pasar a través de una Intent, que es más restrictivo que el Objectpermitido porAsyncTask
RDS

@Rui Yo también usé este método durante el último año. Ahora se me ha ocurrido que esto también es incorrecto. ¿Por qué Google incluso tendría un diálogo si esta fuera la forma de "solucionar" este problema? El problema que veo es que si abre ActivityB (Theme.Dialog) desde ActivityA, entonces ActivityA se mueve hacia abajo en la pila de Actividad, por lo tanto, el sistema operativo lo marca como listo para matar si es necesario. Por lo tanto, si tiene un proceso de larga ejecución y muestra algún tipo de "diálogo" de progreso falso y se demoró demasiado y la memoria se agotó ... La actividad A se anula y no hay nada que regrese al finalizar el progreso.
rf43

1

En estos días hay una forma mucho más clara de manejar este tipo de problemas. El enfoque típico es:

1. Asegúrese de que sus datos estén separados adecuadamente de la interfaz de usuario:

Todo lo que sea un proceso en segundo plano debe estar retenido Fragment(configúrelo con Fragment.setRetainInstance(). Esto se convierte en su 'almacenamiento de datos persistente' donde se guarda todo lo basado en datos que le gustaría retener. Después del evento de cambio de orientación, Fragmentaún será accesible en su original estado a través de una FragmentManager.findFragmentByTag()llamada (cuando lo cree, debe darle una etiqueta, no una ID, ya que no está adjunto a a View).

Consulte la guía desarrollada Manejo de cambios en tiempo de ejecución para obtener información sobre cómo hacer esto correctamente y por qué es la mejor opción.

2. Asegúrese de interactuar de manera correcta y segura entre los procesos en segundo plano y su IU:

Debe revertir su proceso de vinculación. En este momento, su proceso en segundo plano se une a un View- en su lugar, Viewdebería estar adjuntándose al proceso en segundo plano. Tiene más sentido ¿verdad? La Viewacción de 's depende del proceso en segundo plano, mientras que el proceso en segundo plano no depende del View. Esto significa cambiar el enlace a una Listenerinterfaz estándar . Digamos que su proceso (sea cual sea la clase que es - si se trata de una AsyncTask, Runnableo lo que sea) define una OnProcessFinishedListener, cuando el proceso se realiza debe llamar a ese oyente si existe.

Esta respuesta es una buena descripción concisa de cómo hacer oyentes personalizados.

3. Enlace su interfaz de usuario al proceso de datos cada vez que se crea la interfaz de usuario (incluidos los cambios de orientación):

Ahora debe preocuparse por interactuar la tarea en segundo plano con cualquiera que sea su Viewestructura actual . Si está manejando sus cambios de orientación correctamente (no es el configChangestruco que la gente siempre recomienda), Dialogel sistema recreará su sistema. Esto es importante, significa que en el cambio de orientación, todos los Dialogmétodos de ciclo de vida se recuperan. Entonces, en cualquiera de estos métodos ( onCreateDialoggeneralmente es un buen lugar), puede hacer una llamada como la siguiente:

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

Vea el ciclo de vida de Fragment para decidir dónde configurar mejor el oyente en su implementación individual.

Este es un enfoque general para proporcionar una solución sólida y completa al problema genérico formulado en esta pregunta. Probablemente falten algunas piezas menores en esta respuesta dependiendo de su escenario individual, pero este es generalmente el enfoque más correcto para manejar adecuadamente los eventos de cambio de orientación.


1

He encontrado una solución más fácil para manejar hilos cuando cambia la orientación. Puede mantener una referencia estática a su actividad / fragmento y verificar si es nulo antes de actuar en la interfaz de usuario. Sugiero usar un try catch también:

 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

Si tiene problemas para detectar los eventos de cambio de orientación de un diálogo INDEPENDIENTE DE UNA REFERENCIA DE ACTIVIDAD , este método funciona de manera emocionante. Lo uso porque tengo mi propia clase de diálogo que se puede mostrar en varias actividades diferentes, por lo que no siempre sé en qué actividad se muestra. Con este método no es necesario cambiar el manifiesto de Android, preocuparse por las referencias de actividad, y no necesitas un diálogo personalizado (como yo tengo). Sin embargo, necesita una vista de contenido personalizada para poder detectar los cambios de orientación utilizando esa vista en particular. Aquí está mi ejemplo:

Preparar

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

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

        //DO SOMETHING HERE!! :D
    }
}

Implementación 1 - Diálogo

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

Implementación 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();

Implementación 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

Esta es mi solución cuando lo enfrenté: ProgressDialogno es un Fragmentniño, por lo que mi clase personalizada " ProgressDialogFragment" puede extenderse DialogFragmenten su lugar para mantener el cuadro de diálogo que se muestra para los cambios de configuración.

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

El desafío consistía en retener el título y el mensaje del diálogo durante la rotación de la pantalla mientras se restablecían a la cadena vacía predeterminada, aunque el diálogo aún se mostraba

Hay 2 enfoques para resolver esto:

Primer enfoque: realice la actividad que utiliza el diálogo para retener el estado durante el cambio de configuración en el archivo de manifiesto:

android:configChanges="orientation|screenSize|keyboardHidden"

Google no prefiere este enfoque.

Segundo enfoque: en el onCreate()método de la actividad , debe conservarlo DialogFragmentreconstruyendo ProgressDialogFragmentnuevamente con el título y el mensaje de la siguiente manera si savedInstanceStateno es nulo:

@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

Parece demasiado 'rápido y sucio' para ser verdad, así que por favor señale los defectos, pero lo que encontré funcionó fue ...

Dentro del método onPostExecute de mi AsyncTask, simplemente envolví el '.dismiss' para el diálogo de progreso en un bloque try / catch (con un catch vacío) y luego simplemente ignoré la excepción que se generó. Parece incorrecto pero parece que no hay efectos nocivos (al menos para lo que estoy haciendo posteriormente, que es comenzar otra actividad pasando el resultado de mi consulta de larga duración como Extra)


¿Estás diciendo que en realidad no hay pérdida de memoria cuando las plataformas de Android dicen que la ventana se filtró?
rds

Mi cuadro de diálogo de progreso es una variable miembro de la clase de actividad, por lo que supuse que cuando la actividad se destruye y se recrea, se recolectará basura y no habrá fugas. ¿Me equivoco?
Simon

Sí, creo que está mal. Como usted dice, el Activitytiene una referencia al Dialog. Cuando se cambia la configuración, Activityse destruye el primero , lo que significa que todos los campos están configurados en null. Pero el nivel bajo WindowManagertambién tiene una referencia al Dialog(ya que aún no se ha descartado). El nuevo Activityintenta crear un nuevo Dialog(in preExecute()) y el administrador de ventanas genera una excepción fatal que le impide hacerlo. De hecho, si lo hace, no habría forma de destruir limpiamente el Dialogpor lo tanto manteniendo una referencia a la inicial Activity. Estoy en lo cierto?
rds

0

La solución más simple y flexible es utilizar una AsyncTask con una referencia estática a ProgressBar . Esto proporciona una solución encapsulada y, por lo tanto, reutilizable para los problemas de cambio de orientación. Esta solución me ha sido útil para diversas tareas asíncronas, incluidas las descargas de Internet, la comunicación con los Servicios y los análisis del sistema de archivos. La solución ha sido probada en múltiples versiones de Android y modelos de teléfonos. Puede encontrar una demostración completa aquí con interés específico en DownloadFile.java

Presento lo siguiente como un ejemplo conceptual

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

El uso en una actividad de Android es simple

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

Cuando cambia las orientaciones, Android elimina esa actividad y crea una nueva actividad. Sugiero usar retrofit con Rx java. cuyo mango se bloquea automáticamente.

Utilice este método cuando reequipa la llamada.

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.