Как справиться с изменением ориентации экрана при активном диалоге прогресса и фоновом потоке?


524

Моя программа выполняет некоторую сетевую активность в фоновом потоке. Перед началом работы появляется диалоговое окно прогресса. Диалог закрывается на обработчике. Это все работает нормально, за исключением случаев, когда ориентация экрана изменяется, когда диалоговое окно открыто (и фоновая нить идет). В этот момент приложение либо вылетает, либо блокируется, либо попадает в странную стадию, когда приложение вообще не работает, пока все потоки не будут уничтожены.

Как я могу изящно справиться с изменением ориентации экрана?

Пример кода ниже примерно соответствует тому, что делает моя настоящая программа:

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

стек:

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)

Я пытался закрыть диалог прогресса в onSaveInstanceState, но это только предотвращает немедленный сбой. Фоновый поток все еще продолжается, а пользовательский интерфейс находится в частично отрисованном состоянии. Необходимо убить все приложение, прежде чем оно снова начнет работать.


1
Принимая во внимание ответы, которые вы получили, вы должны изменить принятый ответ в пользу лучшего, не так ли?
Rds

Смотрите также бывший вопрос stackoverflow.com/questions/456211/...
Rds

3
Все, получили действительно отличное объяснение и возможные решения этой проблемы. Пройдите по ссылке http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/ Лемм, если это помогло.
arcamax

2
В этом посте есть довольно полное объяснение того, как сохранить асинхронные фоновые задачи для разных ориентаций экрана . Проверьте это!
Адриан Монк

Просто установите для android: configChanges = "direction | screenSize" значение Activity в манифесте. Он остановит android для воссоздания своей активности
Zar E Ahmer

Ответы:


155

Когда вы меняете ориентацию, Android создаст новый вид. Вы, вероятно, получаете сбои, потому что ваш фоновый поток пытается изменить состояние старого. (Это также может быть проблема, потому что ваш фоновый поток не в потоке пользовательского интерфейса)

Я бы предложил сделать этот mHandler энергозависимым и обновлять его при изменении ориентации.


14
Возможно, вы точно определили причину аварии. Я избавился от аварии, но до сих пор не понял, как восстановить пользовательский интерфейс до того состояния, в котором он находился до надежного изменения ориентации. Но твой ответ продвинул меня вперед, поэтому я наградил его ответом.
Хейкки Тойвонен

4
Вы должны получить старт в своей деятельности, когда ориентация меняется. По сути, вы должны перенастроить представление, используя старые данные. Поэтому я бы предложил запросить числовые обновления состояния из индикатора выполнения и перестроить новое представление, когда вы получите это новое «onStart». Я не могу вспомнить, если вы также получаете новое действие, но некоторая охота по документации должна помочь.
haseman

6
Поиграв с этим недавно, я могу передать, что вы получаете новую активность, когда ваше приложение меняет ориентацию. (Вы также получаете новое представление) Если вы попытаетесь обновить старое представление, вы получите исключение, потому что старое представление имеет недопустимый контекст приложения (ваше старое действие). Вы можете обойти это, передав myActivity.getApplicationContext (). вместо указателя на само действие.
haseman

1
Может ли кто-нибудь объяснить использование / выгоду volatile в этом контексте
Zar E Ahmer,

2
@Nepster Да, мне тоже было интересно. Было бы здорово, если бы кто-то объяснил про изменчивость.
RestInPeace

261

Изменить: инженеры Google не рекомендуют этот подход, как описано Дайан Хэкборн (aka hackbod ) в этом сообщении StackOverflow . Проверьте это сообщение в блоге для получения дополнительной информации.


Вы должны добавить это к объявлению активности в манифесте:

android:configChanges="orientation|screenSize"

так выглядит

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

Дело в том, что система уничтожает активность при изменении конфигурации. Смотрите ConfigurationChanges .

Таким образом, включение этого в файл конфигурации позволяет избежать разрушения вашей системы. Вместо этого он вызывает onConfigurationChanged(Configuration)метод.


21
Это определенно лучшее решение; поскольку это просто вращает расположение (поведение, которое Вы ожидаете во-первых). Только не забудьте поставить android: configChanges = "direction | keyboardHidden" (из-за телефонов, которые имеют альбомную клавиатуру)
nikib3ro 13.10.10

24
Кажется, это поведение, которое я ожидаю. Однако в документации указывается, что действие уничтожено, «потому что любой ресурс приложения, включая файлы макетов, может изменяться в зависимости от любого значения конфигурации. Таким образом, единственный безопасный способ обработки изменения конфигурации - это повторное получение всех ресурсов». И кроме того orientation, есть много других причин для изменения конфигурации: keyboardHidden(я уже отредактировал ответ в вики), uiMode(например, вход в или из режима автомобиля; смена ночного режима) и т. Д. Теперь я задаюсь вопросом, действительно ли это хороший ответ.
Rds

116
Это не приемлемое решение. Это просто маскирует истинную проблему.
rf43

18
Работает, но не рекомендуется Google.
Эд Бернетт

21
Пожалуйста, не следуйте этому подходу здесь. DDosAttack совершенно прав. Представьте, что вы создаете диалог прогресса для загрузки или чего-то еще, что занимает много времени. Как пользователь, вы не будете оставаться на этом мероприятии и смотреть на него. Вы бы переключились на домашний экран или на другое приложение, такое как игра, телефонный звонок или другой ресурс, который в итоге разрушит вашу деятельность. А что тогда? Вы сталкиваетесь с той же старой проблемой, которая НЕ решается с помощью этой аккуратной маленькой хитрости. Деятельность будет воссоздана заново, когда пользователь вернется.
Тигучи

68

Я придумал надежное решение для этих проблем, которое соответствует «Android Way» вещей. У меня есть все мои длительные операции с использованием шаблона IntentService.

То есть моя деятельность передает намерения, IntentService выполняет работу, сохраняет данные в БД и затем передает нежелательные намерения. Важная часть важна, так что даже если действие было приостановлено в течение времени после того, как пользователь начал работу и пропускает трансляцию в реальном времени от IntentService, мы все равно можем ответить и получить данные из вызывающего действия. ProgressDialogС этим шаблоном можно очень хорошо работать onSaveInstanceState().

По сути, вам нужно сохранить флаг, что у вас есть запущенный диалог в сохраненном экземпляре пакета. Не сохраняйте объект диалога прогресса, поскольку это приведет к утечке всей активности. Чтобы иметь постоянный дескриптор диалога прогресса, я храню его как слабую ссылку в объекте приложения. В случае изменения ориентации или чего-либо еще, что приводит к приостановке действия (телефонный звонок, пользователь попадает домой и т. Д.), А затем возобновляется, я закрываю старый диалог и заново создаю новый диалог во вновь созданном действии.

Для неопределенных диалогов прогресса это легко. Для стиля индикатора выполнения вы должны поместить последний известный прогресс в пакет и любую информацию, которую вы используете локально в упражнении, чтобы отслеживать прогресс. При восстановлении прогресса вы будете использовать эту информацию, чтобы повторно вызывать индикатор выполнения в том же состоянии, что и раньше, а затем обновлять его в зависимости от текущего состояния.

Итак, подведем итог: размещение долгосрочных задач в IntentService в сочетании с разумным использованием onSaveInstanceState()позволяет эффективно отслеживать диалоги и затем восстанавливать их в событиях жизненного цикла Activity. Соответствующие биты кода активности приведены ниже. Вам также понадобится логика в BroadcastReceiver для надлежащей обработки намерений Sticky, но это выходит за рамки этого.

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

кажется хорошим решением
Дерекий

«У меня есть все мои длительные операции с использованием шаблона IntentService». Это не идеальное решение, потому что это все равно, что стрелять из пушек в воробьев и много шаблонного кода, более подробно вы можете посмотреть youtube.com/watch?v=NJsq0TU0qeg
Камиль Неканович

28

Я встретил ту же проблему. Моя деятельность должна анализировать некоторые данные из URL, и это медленно. Поэтому я создаю поток для этого, а затем показываю диалог прогресса. Я позволил ветке опубликовать сообщение обратно в ветку пользовательского интерфейса, Handlerкогда она будет закончена. В Handler.handleMessage, я получаю объект данных (готово сейчас) из потока и заполняю его в пользовательский интерфейс. Так что это очень похоже на ваш пример.

После долгих проб и ошибок похоже, что я нашел решение. По крайней мере, теперь я могу поворачивать экран в любой момент, до или после завершения потока. Во всех тестах диалоговое окно правильно закрывается, и все поведение соответствует ожидаемому.

То, что я сделал, показано ниже. Цель состоит в том, чтобы заполнить мою модель данных ( mDataObject) и затем заполнить ее для пользовательского интерфейса. Должен позволить поворот экрана в любой момент без сюрпризов.

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

Это то, что работает для меня. Я не знаю, является ли это «правильным» методом, разработанным Android - они утверждают, что эта операция «уничтожить / воссоздать во время поворота экрана» на самом деле облегчает ситуацию, поэтому я не думаю, что она не должна быть слишком сложной.

Дайте мне знать, если вы видите проблему в моем коде. Как сказано выше, я не знаю, есть ли побочный эффект.


1
Большое спасибо! Подсказка onRetainNonConfigurationInstance()и getLastNonConfigurationInstance()помогла мне решить мою проблему. Недурно!
Sven

15

Первоначальная проблема заключалась в том, что код не выдерживал изменения ориентации экрана. По-видимому, это было «решено», когда программа сама обрабатывала изменение ориентации экрана, а не позволяла каркасу пользовательского интерфейса (через вызов onDestroy)).

Я бы сказал, что если основная проблема заключается в том, что программа не выживет в Destroy (), то принятое решение - это просто обходной путь, который оставляет программу с другими серьезными проблемами и уязвимостями. Помните, что платформа Android специально заявляет, что ваша деятельность может быть уничтожена практически в любое время из-за обстоятельств, не зависящих от вас. Следовательно, ваша деятельность должна быть в состоянии пережить onDestroy () и последующую onCreate () по любой причине, а не только с изменением ориентации экрана.

Если вы собираетесь самостоятельно обрабатывать изменения ориентации экрана, чтобы решить проблему OP, вам необходимо убедиться, что другие причины onDestroy () не приводят к той же ошибке. Вы можете сделать это? Если нет, я бы спросил, действительно ли «принятый» ответ очень хороший.


14

Моим решением было расширить ProgressDialogкласс, чтобы получить свой собственный MyProgressDialog.
Я переопределил show()и dismiss()методы, чтобы заблокировать ориентацию, прежде чем показывать Dialogи разблокировать его обратно, когда Dialogуволен. Таким образом, когда Dialogотображается и ориентация устройства изменяется, ориентация экрана сохраняется до тех пор, пока не dismiss()будет вызван, тогда ориентация экрана изменяется в соответствии со значениями датчика / ориентации устройства.

Вот мой код:

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

Я столкнулся с той же проблемой, и я нашел решение, которое не требует использования ProgressDialog, и я получаю более быстрые результаты.

Что я сделал, так это создал макет, в котором есть ProgressBar.

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

Затем в методе onCreate сделайте следующее

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

Затем выполните длинную задачу в потоке, и когда она будет завершена, запустите Runnable, чтобы установить представление контента в реальный макет, который вы хотите использовать для этого действия.

Например:

mHandler.post(new Runnable(){

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

Это то, что я сделал, и я обнаружил, что он работает быстрее, чем отображение ProgressDialog, и он менее навязчив и выглядит лучше, на мой взгляд.

Однако, если вы хотите использовать ProgressDialog, тогда этот ответ не для вас.


Это решение элегантно в простом случае использования, но имеет недостатки. Вам необходимо восстановить полный просмотр содержимого. setContentView(R.layout.my_layout);недостаточно; вам нужно установить всех слушателей, сбросить данные и т. д.
rds

@ Ты прав. На самом деле это только решение простого случая, или если вам нужно выполнить некоторые тяжелые действия в методе onCreate перед отображением вашего представления.
Пзанно

Я не совсем понимаю. Вместо прослушивателей настроек в onCreate (), как мы обычно это делаем, мы можем настроить их в run (). Я что-то здесь упускаю?
Кодовый поэт

7

Я нашел решение для этого, которого я еще не видел в другом месте. Вы можете использовать пользовательский объект приложения, который знает, выполняются ли фоновые задачи, вместо того, чтобы пытаться делать это в действии, которое уничтожается и воссоздается при изменении ориентации. Я писал об этом здесь .


1
Создание пользовательских Applicationобычно используется для поддержания глобального состояния приложения. Я не говорю, что это не работает, но это кажется слишком сложным. Из документа «Обычно нет необходимости создавать подкласс приложения». Я в основном предпочитаю ответ Sonxurxo.
Rds

7

Я собираюсь внести свой подход к решению этой проблемы ротации. Это может не относиться к OP, так как он не используетAsyncTask , но, возможно, другие найдут его полезным. Это довольно просто, но, кажется, делает эту работу за меня:

У меня есть вход в систему с вложенным AsyncTaskклассом под названием BackgroundLoginTask.

По моему, BackgroundLoginTaskя не делаю ничего необычного, кроме добавления нулевой проверки при вызове ProgressDialogdismiss:

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

Это Activityсделано для того, чтобы справиться со случаем, когда фоновая задача завершается, когда она не видна, и, следовательно, диалоговое окно прогресса уже закрыто onPause()методом.

Затем в своем родительском Activityклассе я создаю глобальные статические дескрипторы для своего AsyncTaskкласса и мой ProgressDialog( AsyncTaskбудучи вложенным, может обращаться к этим переменным):

private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

Это служит двум целям: во-первых, это позволяет мне Activityвсегда получать доступ к AsyncTaskобъекту даже после нового, пост-ротационного действия. Во-вторых, это позволяет мне BackgroundLoginTaskполучить доступ и отклонитьProgressDialog даже после поворота.

Затем я добавляю это к onPause(), вызывая исчезновение диалогового окна прогресса, когда мы Activityпокидаем передний план (предотвращая этот ужасный сбой принудительного закрытия):

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

Наконец, у меня есть следующее в моем onResume()методе:

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

Это позволяет Dialogвновь появиться после Activityвоссоздания.

Вот весь класс:

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

    }
}

Я ни в коем случае не опытный разработчик Android, поэтому не стесняйтесь комментировать.


1
Интересно! Специально для тех из нас, кто использует AsyncTask. Только что опробовал ваше решение, и оно, похоже, в основном работает. Есть одна проблема: кажется, что ProgressDialog завершает работу немного раньше, чем ротация, пока ProgressDialog все еще активен. Я собираюсь поиграть, чтобы увидеть, что именно происходит и как это исправить. Но я больше не получаю эти сбои!
Скотт Биггс

1
Нашел исправление. Кажется, что проблема здесь в статическом ProgressDialog. Когда вращения прерывают ProgressDialog, он иногда вызывает метод .dismiss () после его перезапуска в новой операции. Создавая ProgressDialog, созданный с каждым действием, мы гарантируем, что этот новый ProgressDialog не будет уничтожен вместе со старым действием. Я также позаботился о том, чтобы ProgressDialog был установлен равным нулю всякий раз, когда он был отклонен (для облегчения сбора мусора). Таким образом, у нас есть решение здесь! Приветствую тех, кто использует AsyncTask!
Скотт Биггс

4

Переместите длинное задание в отдельный класс. Реализуйте это как образец субъекта-наблюдателя. Всякий раз, когда создается действие, регистрируйтесь, а при закрытии отменяйте регистрацию с помощью класса задач. Класс задачи может использовать AsyncTask.


1
Я не понимаю, как это могло бы помочь. Не могли бы вы объяснить более подробно, как это предотвращает проблемы, которые я вижу.
Хейкки Тойвонен

1
Как сказал Хасеман, это предотвращает доступ бэкэнда к элементам пользовательского интерфейса, и мы можем отделить пользовательский интерфейс от бэкэнда, бэкэнд работает в отдельном потоке и продолжает работать даже после переориентации экрана и Register-UnRegister с заданием Backend для обновлений состояния. , Реальный пример, который я решил, используя это, заключается в том, что у меня есть задача загрузки, я перемещаю ее в отдельный поток, и всякий раз, когда создается поток, я регистрируюсь и отменяю регистрацию в нем.
Vinay

Хорошо, я возвращаюсь к этой проблеме, и я не думаю, что все еще полностью понимаю этот ответ. Предположим, у нас есть основное действие - запустить AsyncTask для выполнения длительной сетевой операции, которую мы не хотим прерывать во время изменения ориентации экрана. Я не вижу, как новое действие может отправить сообщение AsyncTask, запущенному старым действием. Можете ли вы привести пример кода?
Хейкки Тойвонен

@ Heikki, моя реализация ниже того, что вы имеете в виду?
Beetstra

4

Хитрость заключается в том, чтобы показать / закрыть диалоговое окно в AsyncTask во время onPreExecute / onPostExecute как обычно, хотя в случае изменения ориентации создайте / покажите новый экземпляр диалога в действии и передайте его ссылку на задачу.

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

Я сделал это так:

    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 {

                }
            }
        }

    }

Вы также можете попробовать сообщить мне, это работает для вас или нет


Код onDestroy может вообще не выполняться со страницы разработчика : «Обратите внимание на столбец« Killable »в приведенной выше таблице - для тех методов, которые помечены как подлежащие уничтожению, после того, как этот метод вернет процесс, в котором действие может быть уничтожено система в любое время без выполнения другой строки ее кода »
ilomambo

Я твердо верю, что «onRetainNonConfigurationInstance ()» - это метод, который будет использоваться для таких случаев ... нью-йоркская работа
Nitin Bansal

Я сталкиваюсь с подобной проблемой, так как Sachin Gurnani использует статическое объявление для решения моей проблемы. stackoverflow.com/questions/12058774/…
Стивен Дю

2

Если вы создаете фон, Serviceкоторый выполняет всю тяжелую работу (запросы / ответы tcp, unmarshalling), Viewи Activityмогут быть уничтожены и воссозданы без утечки окна или потери данных. Это позволяет поведение, рекомендуемое Android, которое заключается в уничтожении активности при каждом изменении конфигурации. (например, при каждом изменении ориентации).

Это немного сложнее, но это лучший способ для вызова запроса к серверу, предварительной / последующей обработки данных и т. Д.

Вы можете даже использовать свой Service команду для постановки каждого запроса на сервер, чтобы вам было легко и эффективно справляться с этими задачами.

У руководства разработчика есть полная главаServices .


А Serviceэто больше работа, чем, AsyncTaskно может быть лучшим подходом в некоторых ситуациях. Это не обязательно лучше, не так ли? При этом я не понимаю, как это решает проблему ProgressDialogутечки из основного Activity. Где вы воплощаете ProgressDialog? Где вы это отклоняете?
Rds

2

У меня есть реализация, которая позволяет уничтожить действие при изменении ориентации экрана, но все еще успешно уничтожает диалог в воссозданном действии. я использую...NonConfigurationInstance чтобы прикрепить фоновое задание к воссозданному действию. Нормальный фреймворк Android обрабатывает воссоздание самого диалога, там ничего не меняется.

Я вложил в подкласс AsyncTask добавление поля для действия «владелец» и метода для обновления этого владельца.

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

  ...
}

В своем классе деятельности я добавил поле, backgroundTaskотносящееся к «собственной» фоновой задаче, и я обновляю это поле, используя onRetainNonConfigurationInstanceи 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;
  }
  ...
}

Предложения по дальнейшему улучшению:

  • Очистить backgroundTask ссылку в действии после завершения задачи, чтобы освободить любую память или другие ресурсы, связанные с ней.
  • Очистить ownerActivity ссылку в фоновой задаче перед уничтожением действия, если оно не будет немедленно воссоздано.
  • Создайте BackgroundTaskинтерфейс и / или коллекцию, позволяющую запускать различные типы задач из одного и того же владельца.

2

Если вы поддерживаете два макета, весь поток пользовательского интерфейса должен быть прерван.

Если вы используете AsynTask, то вы можете легко вызвать .cancel()метод внутри onDestroy()метода текущей активности.

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

Для AsyncTask читайте больше в разделе «Отмена задачи» здесь .

Обновление: добавлено условие для проверки статуса, так как его можно отменить, только если он находится в рабочем состоянии. Также обратите внимание, что AsyncTask может быть выполнен только один раз.


2

Попытался внедрить решение jfelectron , потому что это « надежное решение этих проблем, которое соответствует« Android Way »вещей », но потребовалось некоторое время, чтобы найти и собрать все упомянутые элементы. Закончилось этим немного по-другому, и я думаю, что более элегантное решение выложено здесь полностью.

Использует IntentService, запущенный из действия, для выполнения длительной задачи в отдельном потоке. Служба запускает липкие Broadcast Intents для действия, которое обновляет диалог. Упражнение использует showDialog (), onCreateDialog () и onPrepareDialog (), чтобы исключить необходимость передачи постоянных данных в объект приложения или в комплекте saveInstanceState. Это должно работать независимо от того, как ваше приложение прерывается.

Класс деятельности:

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

Класс 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);
    }
}}

Записи файла манифеста:

перед разделом приложения:

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

внутри раздела приложения

service android:name=".MyService"

2

Это мое предлагаемое решение:

  • Переместите AsyncTask или Thread в оставшийся фрагмент, как описано здесь . Я считаю хорошей практикой переводить все сетевые вызовы в фрагменты. Если вы уже используете фрагменты, один из них может быть привлечен к ответственности за звонки. В противном случае вы можете создать фрагмент только для выполнения запроса, как предлагает связанная статья.
  • Фрагмент будет использовать интерфейс слушателя, чтобы сигнализировать о завершении / сбое задачи. Вам не нужно беспокоиться об изменении ориентации там. Фрагмент всегда будет иметь правильную ссылку на текущее действие, и диалог прогресса можно будет безопасно возобновить.
  • Сделайте ваш диалог прогресса членом вашего класса. На самом деле вы должны сделать это для всех диалогов. В методе onPause вы должны отклонить их, в противном случае у вас появится окно с изменением конфигурации. Состояние занятости должно сохраняться фрагментом. Когда фрагмент прикреплен к действию, вы можете снова вызвать диалоговое окно хода выполнения, если вызов все еще выполняется. Для void showProgressDialog()этой цели в интерфейс прослушивателя фрагмента-активности можно добавить метод.

Идеальное решение, но не понимаю, почему этот ответ омрачен другими !!!
blackkara

2

Я столкнулся с такой же ситуацией. Я получил только один экземпляр для моего диалога прогресса во всем приложении.

Сначала я создал класс DialogSingleton, чтобы получить только один экземпляр (шаблон 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();
        }
    }
}

Как я показываю в этом классе, у меня есть диалог прогресса как атрибут. Каждый раз, когда мне нужно показать диалог прогресса, я получаю уникальный экземпляр и создаю новый ProgressDialog.

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

Когда я закончу с фоновой задачей, я снова вызываю уникальный экземпляр и закрываю его диалог.

DialogSingleton.GetInstance().DialogDismiss(this);

Я сохраняю статус фоновой задачи в моих общих настройках. Когда я поворачиваю экран, я спрашиваю, запущено ли у меня задание для этого действия: (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).

Когда я запускаю фоновую задачу:

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

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

Когда я закончу запуск фоновой задачи:

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

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

Я надеюсь, что это помогает.


2

Это очень старый вопрос, который почему-то возник на боковой панели.

Если фоновой задаче необходимо выжить, пока активность находится на переднем плане, «новым» решением является размещение фонового потока (или, предпочтительно, AsyncTask) в сохраненном фрагменте , как описано в этом руководстве разработчика и многочисленных вопросах и ответах .

Сохраненный фрагмент выживает, если действие уничтожается для изменения конфигурации, но не тогда, когда действие уничтожается в фоновом или заднем стеке. Следовательно, фоновое задание все равно должно быть прервано, если значение isChangingConfigurations()равно false onPause().


2

Я более свежий в Android, и я попробовал это, и это сработало.

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

Я пробовал ВСЕ. Провел дни экспериментов. Я не хотел блокировать вращение активности. Мой сценарий был:

  1. Диалог прогресса, показывающий динамическую информацию для пользователя. Например: «Подключение к серверу ...», «Загрузка данных ...» и т. Д.
  2. Поток, выполняющий тяжелые вещи и обновляющий диалог
  3. Обновление интерфейса с результатами в конце.

Проблема заключалась в том, что при повороте экрана каждое решение в книге не удавалось. Даже с классом AsyncTask, который является правильным способом Android в таких ситуациях. При повороте экрана текущий контекст, с которым работает начальный поток, исчезает, и это портится в отображаемом диалоговом окне. Проблема всегда заключалась в диалоге, независимо от того, сколько трюков я добавил в код (передача нового контекста запущенным потокам, сохранение состояний потоков посредством вращений и т. Д.). Сложность кода в конце всегда была огромной, и всегда было что-то, что могло пойти не так.

Единственное решение, которое сработало для меня, это трюк «Активность / Диалог». Это просто и гениально, и все это доказательство вращения:

  1. Вместо того, чтобы создавать диалог и просить показать его, создайте действие, которое было установлено в манифесте с android: theme = "@ android: style / Theme.Dialog". Итак, это просто выглядит как диалог.

  2. Замените showDialog (DIALOG_ID) на startActivityForResult (yourActivityDialog, yourCode);

  3. Используйте onActivityResult в вызывающем Activity, чтобы получить результаты из выполняющегося потока (даже ошибки) и обновить пользовательский интерфейс.

  4. На вашем ActivityDialog используйте потоки или AsyncTask для выполнения длинных задач и onRetainNonConfigurationInstance для сохранения состояния «диалога» при повороте экрана.

Это быстро и отлично работает. Я все еще использую диалоги для других задач и AsyncTask для чего-то, что не требует постоянного диалога на экране. Но в этом сценарии я всегда использую шаблон «Активность / Диалог».

И я не пробовал, но даже можно заблокировать вращение Activity / Dialog во время работы потока, ускорить процесс и позволить вращающейся вызывающей Activity.


Хорошо, за исключением того, что параметры должны быть переданы через Intent, что является более ограничительным, чем ObjectразрешеноAsyncTask
rds

@Rui Я тоже использовал этот метод в прошлом году. Теперь мне пришло в голову, что это тоже неправильно. Зачем Google вообще иметь диалог, если это был способ «исправить» эту проблему? Проблема, которую я вижу, заключается в том, что если вы откроете ActivityB (Theme.Dialog) из ActivityA, ActivityA перемещается вниз по стеку Activity, поэтому помечается ОС как готовая к уничтожению, если это необходимо. Поэтому, если у вас длительный процесс и вы видите какое-то «диалоговое окно» с искусственным прогрессом, и оно заняло слишком много времени и память закончилась ... ActivityA уничтожен, и после завершения процесса возвращать тоже нечего.
rf43

1

В наши дни есть гораздо более четкий способ решения подобных проблем. Типичный подход:

1. Убедитесь, что ваши данные правильно отделены от пользовательского интерфейса:

Все, что является фоновым процессом, должно быть сохранено Fragment(установите это с помощью Fragment.setRetainInstance(). Это станет вашим «постоянным хранилищем данных», где хранятся все данные на основе, которые вы хотели бы сохранить. После события изменения ориентации это Fragmentвсе равно будет доступно в исходном виде состояние через FragmentManager.findFragmentByTag()вызов (когда вы создаете его, вы должны дать ему тег, а не идентификатор, поскольку он не привязан к a View).

См. Разработанное руководство по обработке изменений времени выполнения для получения информации о том, как сделать это правильно и почему это лучший вариант.

2. Убедитесь, что вы правильно и безопасно взаимодействуете между фоновыми процессами и вашим пользовательским интерфейсом:

Вы должны полностью изменить процесс связывания. В настоящий момент ваш фоновый процесс присоединяется к a View- вместо этого вы Viewдолжны присоединиться к фоновому процессу. Это имеет больше смысла, верно? В Viewдействии «S зависит от фонового процесса, в то время как фоновый процесс не зависит от Viewсредств .Эты изменяющихся ссылку на стандартный Listenerинтерфейс. Произнесите процесс (независимо от класса это - является ли оно AsyncTask, Runnableили любым другим ) определяет OnProcessFinishedListener, когда процесс сделан он должен вызвать этот слушатель , если он существует.

Этот ответ - хорошее краткое описание того, как сделать пользовательских слушателей.

3. Свяжите свой пользовательский интерфейс с процессом обработки данных при каждом создании пользовательского интерфейса (включая изменения ориентации):

Теперь вы должны беспокоиться о сопряжении фоновой задачи с текущей Viewструктурой. Если вы обрабатываете изменения ориентации должным образом (не configChangesвсегда рекомендуют хакеры), то ваша система Dialogбудет воссоздана заново. Это важно, это означает, что при изменении ориентации все ваши Dialogжизненные циклы вспоминаются. Таким образом, в любом из этих методов ( onCreateDialogобычно это хорошее место) вы можете сделать вызов, подобный следующему:

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

См. Жизненный цикл фрагмента, чтобы решить, где настройка слушателя лучше всего подходит для вашей индивидуальной реализации.

Это общий подход к обеспечению надежного и полного решения общей проблемы, задаваемой в этом вопросе. Вероятно, в этом ответе пропущено несколько незначительных частей в зависимости от вашего индивидуального сценария, но, как правило, это наиболее правильный подход для правильной обработки событий изменения ориентации.


1

Я нашел и более простое решение для обработки потоков при изменении ориентации. Вы можете просто сохранить статическую ссылку на вашу деятельность / фрагмент и проверить, имеет ли оно значение null, прежде чем действовать на пользовательский интерфейс. Я предлагаю также использовать попытку catch:

 public class DashListFragment extends Fragment {
     private static DashListFragment ACTIVE_INSTANCE;

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

        ACTIVE_INSTANCE = this;

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


            }
        }, 1500l);

    }

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

        ACTIVE_INSTANCE = null;
    }


}

1

Если вы боретесь с обнаружением событий изменения ориентации в диалоге НЕЗАВИСИМЫЙ ОТ ССЫЛКИ О ДЕЯТЕЛЬНОСТИ , этот метод работает потрясающе хорошо. Я использую это, потому что у меня есть свой собственный класс диалога, который может отображаться в нескольких различных действиях, поэтому я не всегда знаю, в каком действии он отображается. С этим методом вам не нужно изменять AndroidManifest, беспокойтесь о ссылках на действия, и вам не нужен пользовательский диалог (как у меня). Тем не менее, вам нужен пользовательский вид контента, чтобы вы могли обнаружить изменения ориентации, используя этот конкретный вид. Вот мой пример:

Настроить

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

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

        //DO SOMETHING HERE!! :D
    }
}

Реализация 1 - Диалог

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

Реализация 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();

Реализация 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

Это мое решение, когда я столкнулся с этим: ProgressDialogэто не Fragmentребенок, поэтому мой пользовательский класс " ProgressDialogFragment" может расширяться DialogFragmentвместо этого, чтобы сохранить диалоговое окно, отображаемое для изменений конфигурации.

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

Задача состояла в том, чтобы сохранить заголовок и сообщение диалогового окна во время поворота экрана, поскольку они возвращаются к пустой строке по умолчанию, хотя диалоговое окно по-прежнему отображается

Есть 2 подхода для решения этой проблемы:

Первый подход: выполните действие, которое использует диалог для сохранения состояния во время изменения конфигурации в файле манифеста:

android:configChanges="orientation|screenSize|keyboardHidden"

Этот подход не является предпочтительным для Google.

Второй подход: в onCreate()методе действия вам нужно сохранить DialogFragmentего, перестроив ProgressDialogFragmentзаново с заголовком и сообщением, как показано ниже, если значение savedInstanceStateне равно NULL:

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

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

0

Кажется слишком «быстрым и грязным», чтобы быть правдой, поэтому, пожалуйста, укажите на недостатки, но я обнаружил, что сработало ...

В методе onPostExecute моего AsyncTask я просто обернул '.dismiss' для диалогового окна прогресса в блоке try / catch (с пустым catch), а затем просто проигнорировал возникшее исключение. Кажется, что это неправильно, но похоже, что нет никаких вредных последствий (по крайней мере, для того, что я делаю впоследствии, чтобы начать другую операцию, передаваемую в результате моего длительного выполнения запроса как Дополнительный)


Вы говорите, что на самом деле утечка памяти отсутствует, когда платформы Android сообщают об утечке окна?
Rds

Мой диалог прогресса - это переменная-член класса активности, поэтому я предположил, что когда действие будет уничтожено и воссоздано, оно будет собирать мусор и утечки не будет. Я ошибаюсь?
Симон

Да, я думаю, что это неправильно. Как вы говорите, Activityссылка имеет отношение к Dialog. Когда конфигурация изменяется, первое Activityуничтожается, что означает, что все поля установлены в null. Но низкий уровень WindowManagerтакже имеет ссылку на Dialog(поскольку он еще не был отклонен). Новый Activityпытается создать новый Dialog(in preExecute()), и оконный менеджер вызывает фатальное исключение, мешающее вам сделать это. Действительно, если это произойдет, не будет никакого способа полностью уничтожить, Dialogследовательно, сохраняя ссылку на начальное Activity. Я прав?
Rds

0

Самое простое и гибкое решение - использовать AsyncTask со статической ссылкой на ProgressBar . Это обеспечивает инкапсулированное и, следовательно, многоразовое решение проблем изменения ориентации. Это решение хорошо мне помогло при решении различных асинхронных задач, включая загрузку через Интернет, обмен данными со службами и сканирование файловой системы. Решение было хорошо протестировано на нескольких версиях Android и моделях телефонов. Полная демонстрация может быть найдена здесь с особым интересом в DownloadFile.java

Я представляю следующее в качестве примера концепции

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

Использование в Android-деятельности просто

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

Когда вы меняете ориентацию, Android убивает это действие и создает новое действие. Я предлагаю использовать модификацию с Rx Java. который обрабатывает сбой автоматически.

Используйте этот метод при модификации вызова.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.