Comment gérer le changement d'orientation de l'écran lorsque la boîte de dialogue de progression et le thread d'arrière-plan sont actifs?


524

Mon programme effectue une activité réseau dans un fil d'arrière-plan. Avant de commencer, une boîte de dialogue de progression apparaît. La boîte de dialogue est fermée sur le gestionnaire. Tout cela fonctionne très bien, sauf lorsque l'orientation de l'écran change pendant que la boîte de dialogue est ouverte (et que le fil d'arrière-plan est en cours). À ce stade, l'application se bloque ou se bloque, ou entre dans une étape étrange où l'application ne fonctionne pas du tout tant que tous les threads n'ont pas été supprimés.

Comment gérer avec élégance le changement d'orientation de l'écran?

L'exemple de code ci-dessous correspond à peu près à ce que fait mon vrai programme:

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

Empiler:

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)

J'ai essayé de fermer la boîte de dialogue de progression dans onSaveInstanceState, mais cela empêche simplement un plantage immédiat. Le thread d'arrière-plan est toujours actif et l'interface utilisateur est partiellement dessinée. Besoin de tuer l'application entière avant qu'elle ne recommence à fonctionner.


1
Compte tenu des réponses que vous avez reçues, vous devriez changer la réponse acceptée en faveur des meilleurs, n'est-ce pas?
rds

Voir aussi une ancienne question stackoverflow.com/questions/456211/…
rds

3
Tous, j'ai une très bonne explication et des solutions possibles à ce problème. Passez par http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/ Lemme sais si cela a aidé.
arcamax

2
Il y a une explication assez complète sur la façon de conserver les tâches d'arrière-plan asynchrones dans les orientations d'écran dans ce billet de blog . Vérifiez-le!
Adrian Monk

Définissez simplement android: configChanges = "orientation | screenSize" sur Activity dans manifest. Cela empêchera Android de recréer son activité
Zar E Ahmer

Réponses:


155

Lorsque vous changez d'orientation, Android crée une nouvelle vue. Vous obtenez probablement des plantages car votre thread d'arrière-plan essaie de changer l'état de l'ancien. (Il peut également avoir des problèmes car votre thread d'arrière-plan n'est pas sur le thread d'interface utilisateur)

Je suggère de rendre ce mHandler volatile et de le mettre à jour lorsque l'orientation change.


14
Vous avez peut-être identifié la raison de l'accident. Je me suis débarrassé de l'accident, mais je n'ai toujours pas compris comment restaurer l'interface utilisateur dans son état avant le changement d'orientation de manière fiable. Mais votre réponse m'a fait avancer, alors l'attribuer comme réponse.
Heikki Toivonen

4
Vous devriez obtenir un onStart dans votre activité lorsque l'orientation change. Essentiellement, vous devez reconfigurer la vue à l'aide des anciennes données. Je suggère donc de demander une mise à jour du statut numérique dans la barre de progression et de reconstruire une nouvelle vue lorsque vous obtenez ce nouveau `` onStart ''. Je ne me souviens pas si vous obtenez également une nouvelle activité, mais une recherche dans la documentation devrait vous aider.
haseman

6
Ayant joué avec elle récemment, je peux vous dire que vous obtenez une nouvelle activité lorsque votre application change d'orientation. (Vous obtenez également une nouvelle vue) Si vous essayez de mettre à jour l'ancienne vue, vous obtiendrez une exception car l'ancienne vue a un contexte d'application non valide (votre ancienne activité) Vous pouvez en quelque sorte contourner cela en passant myActivity.getApplicationContext () au lieu d'un pointeur vers l'activité elle-même.
haseman

1
Quelqu'un peut-il expliquer l'utilisation / les avantages des produits volatils dans ce contexte
Zar E Ahmer

2
@Nepster Oui, je me posais également des questions à ce sujet. Ce serait formidable si quelqu'un expliquait la volatilité.
RestInPeace

261

Edit: les ingénieurs de Google ne recommandent pas cette approche, comme décrit par Dianne Hackborn (alias hackbod ) dans ce post StackOverflow . Consultez cet article de blog pour plus d'informations.


Vous devez l'ajouter à la déclaration d'activité dans le manifeste:

android:configChanges="orientation|screenSize"

donc ça ressemble

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

Le problème est que le système détruit l'activité lorsqu'un changement de configuration se produit. Voir ConfigurationChanges .

Donc, mettre cela dans le fichier de configuration évite au système de détruire votre activité. Au lieu de cela, il appelle la onConfigurationChanged(Configuration)méthode.


21
C'est certainement la meilleure solution; car il fait simplement pivoter la mise en page (le comportement que vous attendez en premier lieu). Assurez-vous simplement de mettre android: configChanges = "orientation | keyboardHidden" (à cause des téléphones qui ont un clavier paysage)
nikib3ro

24
Cela semble être le comportement que j'attends. Cependant, la documentation indique que l'activité est détruite "car toute ressource d'application, y compris les fichiers de mise en page, peut changer en fonction de n'importe quelle valeur de configuration. Ainsi, le seul moyen sûr de gérer un changement de configuration est de récupérer toutes les ressources". Et en plus orientation, il y a beaucoup plus de raisons pour que la configuration change: keyboardHidden(j'ai déjà édité la réponse wiki), uiMode(par exemple, entrer ou sortir du mode voiture; changer le mode nuit), etc. Je me demande maintenant si c'est réellement une bonne réponse.
rds

116
Ce n'est pas une solution acceptable. Cela masque simplement le vrai problème.
rf43

18
Fonctionne, mais n'est pas recommandé par Google.
Ed Burnette

21
Veuillez ne pas suivre cette approche ici. DDosAttack a parfaitement raison. Imaginez que vous créez une boîte de dialogue de progression pour un téléchargement ou autre chose qui prend beaucoup de temps. En tant qu'utilisateur, vous ne resterez pas sur cette activité et ne la regarderez pas. Vous passeriez à l'écran d'accueil ou à une autre application comme un jeu ou un appel téléphonique pourrait arriver ou quelque chose d'autre de faim qui finira par détruire votre activité. Et quoi encore? Vous êtes confronté au même vieux problème qui n'est PAS résolu avec ce petit truc soigné. L'activité sera recréée à nouveau lorsque l'utilisateur reviendra.
tiguchi

68

J'ai trouvé une solution solide pour ces problèmes qui est conforme à la «manière Android» des choses. J'ai toutes mes opérations de longue durée en utilisant le modèle IntentService.

Autrement dit, mes activités diffusent des intentions, IntentService fait le travail, enregistre les données dans la base de données, puis diffuse des intentions collantes . La partie collante est importante, de sorte que même si l'activité a été interrompue pendant le temps après que l'utilisateur a commencé le travail et manque la diffusion en temps réel de IntentService, nous pouvons toujours répondre et récupérer les données de l'activité appelante. ProgressDialogs peut très bien fonctionner avec ce modèle aveconSaveInstanceState() .

Fondamentalement, vous devez enregistrer un indicateur indiquant qu'une boîte de dialogue de progression est exécutée dans le groupe d'instances enregistré. N'enregistrez pas l'objet de boîte de dialogue de progression car cela entraînerait une fuite de l'ensemble de l'activité. Pour avoir une poignée persistante dans la boîte de dialogue de progression, je la stocke en tant que référence faible dans l'objet d'application. Lors d'un changement d'orientation ou de toute autre chose qui provoque une pause de l'activité (appel téléphonique, l'utilisateur revient à la maison, etc.), puis je reprends, j'écarte l'ancienne boîte de dialogue et recrée une nouvelle boîte de dialogue dans l'activité nouvellement créée.

Pour les dialogues de progression indéfinie, c'est facile. Pour le style de barre de progression, vous devez mettre la dernière progression connue dans le bundle et toutes les informations que vous utilisez localement dans l'activité pour suivre la progression. Lors de la restauration de la progression, vous utiliserez ces informations pour relancer la barre de progression dans le même état qu'avant, puis la mettre à jour en fonction de l'état actuel des choses.

Donc, pour résumer, mettre des tâches de longue durée dans un IntentService couplé à une utilisation judicieuse de onSaveInstanceState()vous permet de garder efficacement la trace des dialogues et de les restaurer à travers les événements du cycle de vie de l'activité. Les bits pertinents du code d'activité sont ci-dessous. Vous aurez également besoin de logique dans votre BroadcastReceiver pour gérer les intentions collantes de manière appropriée, mais cela dépasse le cadre de cela.

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

semble une bonne solution
Derekyy

"J'ai toutes mes opérations de longue durée en utilisant le modèle IntentService." Ce n'est pas une solution parfaite car c'est comme tirer du canon sur des moineaux et beaucoup de code passe-partout, pour plus vous pouvez regarder youtube.com/watch?v=NJsq0TU0qeg
Kamil Nekanowicz

28

J'ai rencontré le même problème. Mon activité doit analyser certaines données d'une URL et c'est lent. Je crée donc un fil pour le faire, puis affiche une boîte de dialogue de progression. Je laisse le fil publier un message sur le fil de l'interface utilisateur via Handlerquand il est terminé. Dans Handler.handleMessage, j'obtiens l'objet de données (prêt maintenant) du thread et le remplis dans l'interface utilisateur. C'est donc très similaire à votre exemple.

Après beaucoup d'essais et d'erreurs, il semble que j'ai trouvé une solution. Au moins maintenant, je peux faire pivoter l'écran à tout moment, avant ou après le fil. Dans tous les tests, la boîte de dialogue est correctement fermée et tous les comportements sont conformes aux attentes.

Ce que j'ai fait est illustré ci-dessous. L'objectif est de remplir mon modèle de données ( mDataObject), puis de le remplir dans l'interface utilisateur. Devrait permettre la rotation de l'écran à tout moment sans surprise.

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

Voilà ce qui fonctionne pour moi. Je ne sais pas si c'est la méthode "correcte" telle que conçue par Android - ils prétendent que cette "activité de destruction / recréation pendant la rotation de l'écran" rend les choses plus faciles, donc je suppose que cela ne devrait pas être trop compliqué.

Faites-moi savoir si vous voyez un problème dans mon code. Comme dit ci-dessus, je ne sais pas vraiment s'il y a un effet secondaire.


1
Merci beaucoup! L'astuce onRetainNonConfigurationInstance()et getLastNonConfigurationInstance()m'a aidé à résoudre mon problème. Pouces vers le haut!
sven

15

Le problème initialement perçu était que le code ne survivrait pas à un changement d'orientation de l'écran. Apparemment, cela a été "résolu" en demandant au programme de gérer lui-même le changement d'orientation de l'écran, au lieu de laisser le cadre de l'interface utilisateur le faire (en appelant onDestroy)).

Je dirais que si le problème sous-jacent est que le programme ne survivra pas à onDestroy (), alors la solution acceptée n'est qu'une solution de contournement qui laisse le programme avec d'autres problèmes et vulnérabilités graves. N'oubliez pas que le cadre Android indique spécifiquement que votre activité risque d'être détruite presque à tout moment en raison de circonstances indépendantes de votre volonté. Par conséquent, votre activité doit être en mesure de survivre à onDestroy () et à onCreate () pour une raison quelconque, et pas seulement un changement d'orientation de l'écran.

Si vous acceptez de gérer vous-même les changements d'orientation de l'écran pour résoudre le problème de l'OP, vous devez vérifier que les autres causes de onDestroy () n'entraînent pas la même erreur. Pouvez-vous faire cela? Sinon, je me demande si la réponse "acceptée" est vraiment très bonne.


14

Ma solution a été d'étendre la ProgressDialogclasse pour obtenir la mienne MyProgressDialog.
J'ai redéfini show()et les dismiss()méthodes pour verrouiller l'orientation avant d'afficher Dialoget de la déverrouiller en cas de Dialogrejet. Ainsi, lorsque Dialogs'affiche et que l'orientation de l'appareil change, l'orientation de l'écran reste jusqu'à ce que vous dismiss()appeliez, puis l'orientation de l'écran change en fonction des valeurs du capteur / de l'orientation de l'appareil.

Voici mon code:

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

J'ai rencontré ce même problème, et j'ai trouvé une solution qui n'impliquait pas l'utilisation de ProgressDialog et j'obtiens des résultats plus rapides.

Ce que j'ai fait, c'est créer une mise en page contenant une barre de progression.

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

Ensuite, dans la méthode onCreate, procédez comme suit

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

Ensuite, effectuez la longue tâche dans un thread et, une fois terminé, un Runnable définit la vue de contenu sur la mise en page réelle que vous souhaitez utiliser pour cette activité.

Par exemple:

mHandler.post(new Runnable(){

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

C'est ce que j'ai fait, et j'ai constaté qu'il fonctionne plus rapidement que d'afficher le ProgressDialog et qu'il est moins intrusif et a une meilleure apparence à mon avis.

Cependant, si vous souhaitez utiliser ProgressDialog, cette réponse n'est pas pour vous.


Cette solution est élégante dans un cas d'utilisation simple, mais présente des inconvénients. Vous devez reconstruire la vue du contenu complet. setContentView(R.layout.my_layout);c'est insuffisant; vous devez configurer tous les écouteurs, réinitialiser les données, etc.
rds

@rds vous avez raison. Ce n'est vraiment qu'une solution à un cas simple, ou si vous avez besoin de soulever des objets lourds dans votre méthode onCreate avant d'afficher votre vue.
Pzanno

Je ne comprends pas très bien. Au lieu de définir des écouteurs dans onCreate (), comme nous le ferions généralement, nous pourrions les configurer dans run (). Est-ce que j'ai râté quelque chose?
Code Poet,

7

J'ai découvert une solution à cela que je n'ai pas encore vue ailleurs. Vous pouvez utiliser un objet d'application personnalisé qui sait si vous avez des tâches en arrière-plan au lieu d'essayer de le faire dans l'activité qui est détruite et recréée lors d'un changement d'orientation. J'ai blogué à ce sujet ici .


1
La création d'une personnalisation Applicationest normalement utilisée pour maintenir un état d'application global. Je ne dis pas que cela ne fonctionne pas, mais cela semble trop compliqué. Extrait du document "Il n'est normalement pas nécessaire de sous-classer Application.". Je préfère largement la réponse de sonxurxo.
rds

7

Je vais apporter mon approche pour gérer ce problème de rotation. Cela peut ne pas être pertinent pour OP car il n'utilise pas AsyncTask, mais peut-être que d'autres le trouveront utile. C'est assez simple mais il semble faire le travail pour moi:

J'ai une activité de connexion avec une AsyncTaskclasse imbriquée appelée BackgroundLoginTask.

Dans mon BackgroundLoginTaskje ne fais rien d'extraordinaire sauf pour ajouter un chèque nul lors de l'appel ProgressDialogde licenciement:

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

Il s'agit de gérer le cas où la tâche d'arrière-plan se termine alors que le Activityn'est pas visible et, par conséquent, la boîte de dialogue de progression a déjà été fermée par la onPause()méthode.

Ensuite, dans ma Activityclasse parent , je crée des descripteurs statiques globaux pour ma AsyncTaskclasse et mon ProgressDialog(le AsyncTask, étant imbriqué, peut accéder à ces variables):

private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

Cela sert à deux fins: Premièrement, cela me permet Activityd'accéder toujours à l' AsyncTaskobjet même à partir d'une nouvelle activité post-rotation. Deuxièmement, cela me permet BackgroundLoginTaskd'accéder et de supprimer le ProgressDialogmême après une rotation.

Ensuite, j'ajoute ceci à onPause(), provoquant la disparition de la boîte de dialogue de progression lorsque notre Activityquitte le premier plan (empêchant ce vilain crash "force close"):

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

Enfin, j'ai les éléments suivants dans ma onResume()méthode:

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

Cela permet Dialogde réapparaître après la Activityrecréation du.

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

    }
}

Je ne suis en aucun cas un développeur Android chevronné, alors n'hésitez pas à commenter.


1
Intéressant! Surtout pour ceux d'entre nous qui utilisent AsyncTask. Je viens d'essayer votre solution et elle semble fonctionner principalement. Il y a un problème: le ProgressDialog semble se terminer un peu tôt après une rotation PENDANT que le ProgressDialog est toujours actif. Je vais jouer pour voir exactement ce qui se passe et comment y remédier. Mais je ne reçois plus ces plantages!
Scott Biggs

1
Trouvé un correctif. Il semble que le problème ici soit le ProgressDialog statique. Lorsque les rotations interrompent le ProgressDialog, il obtient parfois sa méthode .dismiss () appelée après son redémarrage dans la nouvelle activité. En créant le ProgressDialog créé avec chaque activité, nous nous assurons que ce nouveau ProgressDialog n'est pas tué avec l'ancienne activité. J'ai également veillé à ce que ProgressDialog soit défini sur null chaque fois qu'il est rejeté (pour faciliter la collecte des ordures). Nous avons donc une solution ici! Bravo à ceux qui utilisent AsyncTask!
Scott Biggs

4

Déplacez la tâche longue vers une classe séparée. Implémentez-le comme un modèle sujet-observateur. Chaque fois que l'activité est créée, enregistrez-vous et fermez la session en vous désinscrivant avec la classe de tâches. La classe de tâches peut utiliser AsyncTask.


1
Je ne vois pas comment cela pourrait aider. Pourriez-vous expliquer plus en détail comment cela empêche les problèmes que je vois.
Heikki Toivonen

1
Comme Haseman l'a dit, cela empêche le backend d'accéder aux éléments de l'interface utilisateur et nous pouvons séparer l'interface du backend, le backend s'exécute dans un thread séparé et continue de s'exécuter même après que l'écran est réorienté et s'inscrire-désinscrire avec la tâche backend pour les mises à jour d'état . Le vrai exemple que j'ai résolu en utilisant ceci est que j'ai une tâche de téléchargement, je l'ai déplacée vers un thread séparé, chaque fois que le thread est créé, je m'enregistre-désenregistre-le.
Vinay

Ok, je revisite cette question et je ne pense pas que je comprends toujours bien cette réponse. Supposons que l'activité principale démarre une tâche AsyncTask pour effectuer une opération réseau de longue durée que nous ne voulons pas interrompre pendant le changement d'orientation de l'écran. Je ne vois pas comment la nouvelle activité peut envoyer un message à la AsyncTask démarrée par l'ancienne activité. Pouvez-vous donner un exemple de code?
Heikki Toivonen

@Heikki, mon implémentation est-elle inférieure à ce que vous voulez dire?
beetstra

4

L'astuce consiste à afficher / fermer la boîte de dialogue dans AsyncTask pendant onPreExecute / onPostExecute comme d'habitude, bien qu'en cas de changement d'orientation, créez / affichez une nouvelle instance de la boîte de dialogue dans l'activité et transmettez sa référence à la tâche.

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

Je l'ai fait comme ça:

    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 {

                }
            }
        }

    }

Vous pouvez également essayer de me faire savoir que cela fonctionne pour vous ou non


Le code onDestroy peut ne pas être exécuté du tout, à partir de la page du développeur : "Notez la colonne" Killable "dans le tableau ci-dessus - pour les méthodes qui sont marquées comme pouvant être tuées, après que cette méthode retourne le processus hébergeant l'activité peut être tué par le système à tout moment sans qu'une autre ligne de son code ne soit exécutée "
ilomambo

Je crois fermement que "onRetainNonConfigurationInstance ()" est la méthode à utiliser pour de tels cas ... nyc work
Nitin Bansal

Je suis confronté à un problème similaire, car Sachin Gurnani utilise-t-il une déclaration statique pour résoudre mon problème. stackoverflow.com/questions/12058774/…
Steven Du

2

Si vous créez un arrière Service- plan qui fait tout le gros du travail (requêtes / réponses tcp, dégroupage), le Viewet Activitypeut être détruit et recréé sans fuite de fenêtre ou perte de données. Cela permet au comportement recommandé par Android, qui est de détruire une activité à chaque changement de configuration (par exemple pour chaque changement d'orientation).

C'est un peu plus complexe, mais c'est le meilleur moyen d'invoquer la demande du serveur, le pré / post-traitement des données, etc.

Vous pouvez même utiliser votre Service pour mettre chaque demande en file d'attente sur un serveur, ce qui rend la gestion de ces choses simple et efficace.

Le guide de développement contient un chapitreServices complet .


A Serviceest plus de travail qu'un, AsyncTaskmais peut être une meilleure approche dans certaines situations. Ce n'est pas nécessairement mieux, n'est-ce pas? Cela étant dit, je ne comprends pas comment cela résout le problème de la ProgressDialogfuite de la principale Activity. Où instanciez-vous le ProgressDialog? Où la rejetez-vous?
rds

2

J'ai une implémentation qui permet à l'activité d'être détruite lors d'un changement d'orientation d'écran, mais détruit toujours la boîte de dialogue dans l'activité recréée avec succès. j'utilise...NonConfigurationInstance pour attacher la tâche d'arrière-plan à l'activité recréée. Le cadre Android normal gère la recréation de la boîte de dialogue elle-même, rien n'y change.

J'ai sous-classé AsyncTask en ajoutant un champ pour l'activité «propriétaire» et une méthode pour mettre à jour ce propriétaire.

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

  ...
}

Dans ma classe d'activité, j'ai ajouté un champ backgroundTaskfaisant référence à la tâche d'arrière-plan «détenue», et je mets à jour ce champ à l'aide de onRetainNonConfigurationInstanceet 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;
  }
  ...
}

Suggestions d'améliorations supplémentaires:

  • Effacez la backgroundTaskréférence dans l'activité une fois la tâche terminée pour libérer la mémoire ou les autres ressources qui lui sont associées.
  • Effacer le ownerActivity référence dans la tâche d'arrière-plan avant de détruire l'activité au cas où elle ne serait pas recréée immédiatement.
  • Créez une BackgroundTaskinterface et / ou une collection pour permettre à différents types de tâches de s'exécuter à partir de la même activité propriétaire.

2

Si vous conservez deux dispositions, tous les threads de l'interface utilisateur doivent être terminés.

Si vous utilisez AsynTask, vous pouvez facilement appeler la .cancel()méthode à l'intérieur de la onDestroy()méthode de l'activité actuelle.

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

Pour AsyncTask, lisez plus dans la section "Annulation d'une tâche" ici .

Mise à jour: condition supplémentaire pour vérifier l'état, car elle ne peut être annulée que si elle est en cours d'exécution. Notez également que la tâche AsyncTask ne peut être exécutée qu'une seule fois.


2

J'ai essayé d'implémenter la solution de jfelectron car c'est une « solution solide à ces problèmes qui est conforme à la« manière Android »des choses », mais il a fallu un certain temps pour rechercher et rassembler tous les éléments mentionnés. Je me suis retrouvé avec cette solution légèrement différente, et je pense plus élégante, publiée ici dans son intégralité.

Utilise un IntentService déclenché à partir d'une activité pour effectuer la tâche de longue durée sur un thread distinct. Le service renvoie des intentions de diffusion collantes à l'activité qui met à jour la boîte de dialogue. L'activité utilise showDialog (), onCreateDialog () et onPrepareDialog () pour éliminer la nécessité de transmettre des données persistantes dans l'objet d'application ou le bundle savedInstanceState. Cela devrait fonctionner quelle que soit la façon dont votre application est interrompue.

Classe d'activité:

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

Entrées du fichier manifeste:

avant la section application:

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

à l'intérieur de la section d'application

service android:name=".MyService"

2

Voici ma solution proposée:

  • Déplacez la tâche asynchrone ou le thread vers un fragment conservé, comme expliqué ici . Je crois que c'est une bonne pratique de déplacer tous les appels réseau vers des fragments. Si vous utilisez déjà des fragments, l'un d'eux pourrait être rendu responsable des appels. Sinon, vous pouvez créer un fragment juste pour faire la demande, comme le propose l'article lié.
  • Le fragment utilisera une interface d'écoute pour signaler la fin / l'échec de la tâche. Vous n'avez pas à vous soucier des changements d'orientation là-bas. Le fragment aura toujours le lien correct vers l'activité en cours et la boîte de dialogue de progression peut être reprise en toute sécurité.
  • Faites de votre dialogue de progression un membre de votre classe. En fait, vous devriez le faire pour toutes les boîtes de dialogue. Dans la méthode onPause, vous devez les ignorer, sinon vous perdrez une fenêtre sur le changement de configuration. L'état occupé doit être conservé par le fragment. Lorsque le fragment est attaché à l'activité, vous pouvez à nouveau afficher la boîte de dialogue de progression, si l'appel est toujours en cours. Une void showProgressDialog()méthode peut être ajoutée à l'interface d'écoute fragment-activité à cet effet.

Solution parfaite, mais je ne comprends pas pourquoi cette réponse est éclipsée par les autres !!!
blackkara

2

J'ai fait face à la même situation. J'ai obtenu une seule instance pour ma boîte de dialogue de progression dans l'ensemble de l'application.

Tout d'abord, j'ai créé une classe DialogSingleton pour obtenir une seule instance (modèle 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();
        }
    }
}

Comme je le montre dans cette classe, j'ai la boîte de dialogue de progression comme attribut. Chaque fois que j'ai besoin d'afficher une boîte de dialogue de progression, j'obtiens l'instance unique et crée un nouveau ProgressDialog.

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

Une fois la tâche en arrière-plan terminée, j'appelle à nouveau l'instance unique et ferme sa boîte de dialogue.

DialogSingleton.GetInstance().DialogDismiss(this);

J'enregistre l'état de la tâche en arrière-plan dans mes préférences partagées. Lorsque je fais pivoter l'écran, je demande si une tâche est en cours d'exécution pour cette activité: (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).

Lorsque je commence à exécuter une tâche en arrière-plan:

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

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

Lorsque je termine d'exécuter une tâche en arrière-plan:

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

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

J'espère que ça aide.


2

C'est une très vieille question qui est apparue dans la barre latérale pour une raison quelconque.

Si la tâche d'arrière-plan ne doit survivre que lorsque l'activité est au premier plan, la "nouvelle" solution consiste à héberger le thread d'arrière-plan (ou, de préférence, AsyncTask) dans un fragment conservé , comme décrit dans ce guide du développeur et de nombreuses questions et réponses .

Un fragment conservé survit si l'activité est détruite pour un changement de configuration, mais pas lorsque l'activité est détruite en arrière-plan ou en pile arrière. Par conséquent, la tâche d'arrière-plan doit toujours être interrompue si elle isChangingConfigurations()est fausse dans onPause().


2

Je suis plus frais sur Android et j'ai essayé et ça a fonctionné.

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

J'ai tout essayé. Nous avons passé des jours à expérimenter. Je ne voulais pas empêcher l'activité de tourner. Mon scénario était:

  1. Une boîte de dialogue de progression affichant des informations dynamiques pour l'utilisateur. Par exemple: "Connexion au serveur ...", "Téléchargement de données ...", etc.
  2. Un fil faisant le gros du travail et mettant à jour la boîte de dialogue
  3. Mise à jour de l'interface utilisateur avec les résultats à la fin.

Le problème était que lors de la rotation de l'écran, chaque solution du livre avait échoué. Même avec la classe AsyncTask, qui est la bonne façon Android de gérer ces situations. Lors de la rotation de l'écran, le contexte actuel avec lequel le thread de départ travaille est parti et cela gâche la boîte de dialogue qui s'affiche. Le problème a toujours été la boîte de dialogue, quel que soit le nombre de trucs que j'ai ajoutés au code (passage de nouveaux contextes aux threads en cours d'exécution, conservation des états des threads lors des rotations, etc.). La complexité du code à la fin était toujours énorme et il y avait toujours quelque chose qui pouvait mal tourner.

La seule solution qui a fonctionné pour moi était l'astuce Activité / Dialogue. C'est simple et génial et tout est à l'épreuve de la rotation:

  1. Au lieu de créer une boîte de dialogue et de demander à l'afficher, créez une activité qui a été définie dans le manifeste avec android: theme = "@ android: style / Theme.Dialog". Donc, cela ressemble à une boîte de dialogue.

  2. Remplacez showDialog (DIALOG_ID) par startActivityForResult (yourActivityDialog, yourCode);

  3. Utilisez onActivityResult dans l'activité appelante pour obtenir les résultats du thread d'exécution (même les erreurs) et mettre à jour l'interface utilisateur.

  4. Sur votre 'ActivityDialog', utilisez des threads ou AsyncTask pour exécuter de longues tâches et onRetainNonConfigurationInstance pour enregistrer l'état de "dialogue" lors de la rotation de l'écran.

C'est rapide et fonctionne bien. J'utilise toujours des boîtes de dialogue pour d'autres tâches et la tâche AsyncTask pour quelque chose qui ne nécessite pas de boîte de dialogue constante à l'écran. Mais avec ce scénario, je choisis toujours le modèle Activité / Dialogue.

Et, je ne l'ai pas essayé, mais il est même possible d'empêcher cette activité / boîte de dialogue de tourner, lorsque le thread est en cours d'exécution, ce qui accélère les choses, tout en permettant à l'activité appelante de tourner.


Agréable sauf que les paramètres doivent être passés par un Intent, ce qui est plus restrictif que celui Objectautorisé parAsyncTask
rds

@Rui J'ai aussi utilisé cette méthode l'année dernière. Il m'est maintenant venu à l'esprit que c'est également incorrect. Pourquoi Google aurait-il même une boîte de dialogue si c'était le moyen de «résoudre» ce problème? Le problème que je vois est que si vous ouvrez ActivityB (Theme.Dialog) à partir d'ActivityA, ActivityA est déplacé vers le bas sur la pile d'activité et est donc marqué comme prêt à tuer par le système d'exploitation si nécessaire. Par conséquent, si vous avez un long processus en cours et que vous affichez une sorte de «dialogue» de faux progrès et que cela a pris trop de temps et que la mémoire est insuffisante ... ActivityA est tué et il n'y a rien à revenir trop une fois la progression terminée.
rf43

1

De nos jours, il existe une manière beaucoup plus distincte de gérer ces types de problèmes. L'approche typique est:

1. Assurez-vous que vos données sont correctement séparées de l'interface utilisateur:

Tout ce qui est un processus d'arrière-plan doit être conservé Fragment(définissez-le avec Fragment.setRetainInstance(). Cela devient votre «stockage de données persistant» où toutes les données que vous souhaitez conserver sont conservées. Après l'événement de changement d'orientation, il Fragmentsera toujours accessible dans son original état via un FragmentManager.findFragmentByTag()appel (lorsque vous le créez, vous devez lui attribuer une balise et non un ID car il n'est pas attaché à a View).

Consultez le guide développé sur la gestion des modifications de l'exécution pour savoir comment procéder correctement et pourquoi c'est la meilleure option.

2. Assurez-vous d'interfacer correctement et en toute sécurité entre les processus d'arrière-plan et votre interface utilisateur:

Vous devez inverser votre processus de liaison. Pour le moment, votre processus d'arrière-plan s'attache à un View- au lieu de cela, vous Viewdevriez vous attacher au processus d'arrière-plan. Cela a plus de sens, non? L' Viewaction de dépend du processus d'arrière-plan, tandis que le processus d'arrière-plan ne dépend pas du View. Cela signifie changer le lien vers une Listenerinterface standard . Dites votre processus (quelle que soit la classe - qu'il s'agisse d'unAsyncTask , Runnableou autre) définit un OnProcessFinishedListener, lorsque le processus est terminé, il devrait appeler cet écouteur s'il existe.

Cette réponse est une belle description concise de la façon de faire des écouteurs personnalisés.

3. Liez votre interface utilisateur au processus de données chaque fois que l'interface utilisateur est créée (y compris les changements d'orientation):

Vous devez maintenant vous soucier d'interfacer la tâche d'arrière-plan avec la Viewstructure actuelle . Si vous gérez correctement vos changements d'orientation (pas le configChangeshack que les gens recommandent toujours), alors votre Dialogsera recréé par le système. Ceci est important, cela signifie que lors du changement d'orientation, toutes les Dialogméthodes de votre cycle de vie sont rappelées. Donc, dans l'une de ces méthodes ( onCreateDialogc'est généralement un bon endroit), vous pouvez faire un appel comme celui-ci:

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

Reportez-vous au cycle de vie des fragments pour décider où le réglage de l'écouteur convient le mieux à votre implémentation individuelle.

Il s'agit d'une approche générale pour fournir une solution robuste et complète au problème générique posé dans cette question. Il y a probablement quelques éléments mineurs manquants dans cette réponse en fonction de votre scénario individuel, mais c'est généralement l'approche la plus correcte pour gérer correctement les événements de changement d'orientation.


1

j'ai trouvé et une solution plus simple pour gérer les fils lorsque l'orientation change. Vous pouvez simplement conserver une référence statique à votre activité / fragment et vérifier si sa valeur est nulle avant d'agir sur l'interface utilisateur. Je suggère également d'utiliser une prise d'essai:

 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 vous avez du mal à détecter les événements de changement d'orientation d'une boîte de dialogue INDÉPENDANTE D'UNE RÉFÉRENCE D'ACTIVITÉ , cette méthode fonctionne très bien. J'utilise cela parce que j'ai ma propre classe de dialogue qui peut être affichée dans plusieurs activités différentes, donc je ne sais pas toujours dans quelle activité il est affiché. et vous n'avez pas besoin d'une boîte de dialogue personnalisée (comme je l'ai fait). Vous avez cependant besoin d'une vue de contenu personnalisée pour pouvoir détecter les changements d'orientation à l'aide de cette vue particulière. Voici mon exemple:

Installer

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

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

        //DO SOMETHING HERE!! :D
    }
}

Implémentation 1 - Dialogue

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

Implémentation 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();

Mise en œuvre 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

C'est ma solution quand je l'ai rencontrée: ProgressDialogn'est pas un Fragmentenfant, donc ma classe personnalisée " ProgressDialogFragment" peut s'étendre à la DialogFragmentplace afin de garder la boîte de dialogue affichée pour les changements de configuration.

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

Le défi était de conserver le titre et le message de la boîte de dialogue lors de la rotation de l'écran alors qu'ils étaient réinitialisés à la chaîne vide par défaut, bien que la boîte de dialogue soit toujours affichée.

Il existe 2 approches pour résoudre ce problème:

Première approche: effectuez l'activité qui utilise la boîte de dialogue pour conserver l'état lors du changement de configuration dans le fichier manifeste:

android:configChanges="orientation|screenSize|keyboardHidden"

Cette approche n'est pas préférée par Google.

Deuxième approche: sur la onCreate()méthode de l'activité , vous devez conserver votre DialogFragmenten reconstruisant le ProgressDialogFragmentavec le titre et le message comme suit si le savedInstanceStaten'est pas nul:

@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

Semble beaucoup trop «rapide et sale» pour être vrai, alors veuillez signaler les défauts, mais ce que j'ai trouvé fonctionnait était ...

Dans la méthode onPostExecute de ma tâche AsyncTask, j'ai simplement enveloppé le '.dismiss' pour la boîte de dialogue de progression dans un bloc try / catch (avec une capture vide), puis j'ai simplement ignoré l'exception qui a été déclenchée. Semble mal à faire mais semble qu'il n'y ait pas d'effets néfastes (au moins pour ce que je fais par la suite qui est de démarrer une autre activité en passant à la suite de ma longue requête en tant qu'extra)


Êtes-vous en train de dire qu'il n'y a en fait aucune fuite de mémoire lorsque les plates-formes Android indiquent que la fenêtre a fui?
rds

Ma boîte de dialogue de progression est une variable membre de la classe d'activité, j'ai donc supposé que lorsque l'activité est détruite et recréée, elle sera récupérée et il n'y a aucune fuite. Ai-je tort?
Simon

Oui, je pense que c'est faux. Comme vous le dites, le Activityfait référence au Dialog. Lorsque la configuration est modifiée, la première Activityest détruite, ce qui signifie que tous les champs sont définis sur null. Mais le bas niveau fait WindowManagerégalement référence au Dialog(puisqu'il n'est pas encore rejeté). Le nouveau Activityessaie de créer un nouveau Dialog(en preExecute()) et le gestionnaire de fenêtres déclenche une exception fatale vous empêchant de le faire. En effet, si tel était le cas, il n'y aurait aucun moyen de détruire proprement le Dialogconservant ainsi une référence à l'initiale Activity. Ai-je raison?
rds

0

La solution la plus simple et la plus flexible consiste à utiliser une AsyncTask avec une référence statique à ProgressBar . Cela fournit une solution encapsulée et donc réutilisable aux problèmes de changement d'orientation. Cette solution m'a bien servi pour diverses tâches asynchrones, y compris les téléchargements Internet, la communication avec les services et les analyses de système de fichiers. La solution a été bien testée sur plusieurs versions Android et modèles de téléphones. Une démo complète peut être trouvée ici avec un intérêt spécifique pour DownloadFile.java

Je présente ce qui suit comme un exemple de concept

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'utilisation dans une activité Android est 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

Lorsque vous changez d'orientation, Android supprime cette activité et crée une nouvelle activité. Je suggère d'utiliser le retrofit avec Rx java. qui gère les plantages automatiquement.

Utilisez ces méthodes lors d'un appel ultérieur.

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.