Фрагменти Android. Збереження AsyncTask під час обертання екрана або зміни конфігурації


86

Я працюю над додатком для смартфонів / планшетів, використовуючи лише один файл .apk, і завантажуючи ресурси, залежно від розміру екрану, найкращим вибором дизайну було використання фрагментів через ACL.

Ця програма працювала нормально дотепер, будучи лише заснованою на діяльності. Це фіктивний клас того, як я обробляю AsyncTasks та ProgressDialogs у Activity, щоб змусити їх працювати, навіть коли екран обертається або конфігурація змінюється в середині зв'язку.

Я не буду міняти маніфест, щоб уникнути відтворення Діяльності, є багато причин, чому я не хочу це робити, але головним чином тому, що в офіційних документах сказано, що це не рекомендується, і я поки не впорався з цим, тому, будь ласка, не рекомендуйте це маршруту.

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

Цей код працює нормально, у мене близько 10 000 користувачів без скарг, тому здавалося логічним просто скопіювати цю логіку в новий дизайн на основі фрагментів, але, звичайно, він не працює.

Ось фрагмент Login:

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

Я не можу використовувати, onRetainNonConfigurationInstance()оскільки його потрібно викликати з Activity, а не з Fragment, те саме стосується getLastNonConfigurationInstance(). Я прочитав тут кілька подібних запитань без відповіді.

Я розумію, що може знадобитися певна робота, щоб правильно організувати ці речі у фрагментах, і, як уже говорилося, я хотів би підтримувати ту саму основну логіку проектування.

Яким буде правильний спосіб зберегти AsyncTask під час зміни конфігурації, і якщо він все ще працює, покажіть progressDialog, беручи до уваги, що AsyncTask є внутрішнім класом для Fragment, і саме Fragment викликає AsyncTask.execute ()?



пов’язати AsyncTask із життєвим циклом програми .. таким чином можна відновити, коли активність відтворюється
Фред Гротт

Відповіді:


75

Фрагменти насправді можуть це зробити набагато простіше. Просто використовуйте метод Fragment.setRetainInstance (boolean), щоб ваш екземпляр фрагмента зберігався під час змін конфігурації. Зауважте, що це рекомендована заміна Activity.onRetainnonConfigurationInstance () у документах.

Якщо з якихось причин ви дійсно не хочете використовувати збережений фрагмент, можна застосувати інші підходи. Зверніть увагу, що кожен фрагмент має унікальний ідентифікатор, який повертає Fragment.getId () . Ви також можете дізнатись, чи зривається фрагмент для зміни конфігурації за допомогою Fragment.getActivity (). IsChangingConfigurations () . Отже, в той момент, коли ви вирішите зупинити свій AsyncTask (у onStop () або onDestroy (), швидше за все), ви можете, наприклад, перевірити, чи змінюється конфігурація, і якщо так, вставте її в статичний SparseArray під ідентифікатором фрагмента, а потім у вашому onCreate () або onStart () перевірте, чи є у вас доступний AsyncTask у розрідженому масиві.


Зверніть увагу, що setRetainInstance є лише в тому випадку, якщо ви не використовуєте зворотний стек.
Ніл

4
Чи не можливо, що AsyncTask надсилає свій результат назад до того, як запуститься onCreateView збереженого Фрагменту?
jakk

6
@jakk Методи життєвого циклу для Діяльності, Фрагментів тощо викликаються послідовно чергою повідомлень основного потоку графічного інтерфейсу, тому навіть якщо завдання одночасно закінчується у фоновому режимі до того, як ці методи життєвого циклу будуть завершені (або навіть викликані), onPostExecuteметод все одно потребуватиме зачекайте, перш ніж нарешті буде оброблена чергою повідомлень основного потоку.
Алекс Локвуд,

Цей підхід (RetainInstance = true) не спрацює, якщо ви хочете завантажити різні файли макета для кожної орієнтації.
Джастін

Запуск асинктаску в методі onCreate MainActivity працює, здається, працює лише в тому випадку, якщо асинктаск - усередині фрагмента "робітник" - запущений явними діями користувача. Оскільки основний потік та користувальницький інтерфейс доступні. Запуск asynctask відразу після запуску програми, однак - без дії користувача, як натискання кнопки - дає виняток. У цьому випадку асинктаск можна викликати в методі onStart на MainActivity, а не в методі onCreate.
ʕ ᵔᴥᵔ ʔ

66

Думаю, вам сподобається мій надзвичайно вичерпний та робочий приклад, детально описаний нижче.

  1. Обертання працює, і діалог виживає.
  2. Ви можете скасувати завдання та діалогове вікно, натиснувши кнопку "Назад" (якщо ви хочете зробити це).
  3. Тут використовуються фрагменти.
  4. Макет фрагмента під активністю змінюється належним чином, коли пристрій обертається.
  5. Існує повне завантаження вихідного коду та попередньо скомпільований файл .apk, щоб ви могли побачити, чи поведінка відповідає вашим вимогам.

Редагувати

На прохання Бреда Ларсона, я відтворив більшість пов'язаних рішень нижче. Також відтоді, як я його розмістив, на мене вказали AsyncTaskLoader. Я не впевнений, що він цілком застосовний до тих самих проблем, але ви все одно повинні це перевірити.

Використання AsyncTaskз діалогами прогресу та обертання пристрою.

Робоче рішення!

Нарешті я все встиг працювати. Мій код має такі функції:

  1. A Fragment, макет якого змінюється з орієнтацією.
  2. В AsyncTaskякому ви можете зробити якусь роботу.
  3. A, DialogFragmentякий показує хід виконання завдання на індикаторі прогресу (а не просто невизначеному блешні).
  4. Обертання працює, не перериваючи завдання та не відкидаючи діалогове вікно.
  5. Кнопка "Назад" закриває діалогове вікно та скасовує завдання (хоча ви можете досить легко змінити цю поведінку).

Я не думаю, що поєднання працездатності можна зустріти деінде.

Основна ідея така. Існує MainActivityклас, який містить один фрагмент - MainFragment. MainFragmentмає різні макети для горизонтальної та вертикальної орієнтації, і setRetainInstance()є помилковим, щоб макет міг змінюватися. Це означає , що при зміні орієнтації пристрою змінюється, так MainActivityі MainFragmentповністю знищені , і відтворені.

Окремо ми маємо MyTask(розширено з AsyncTask), яке виконує всю роботу. Ми не можемо зберігати його, MainFragmentтому що це буде знищено, і Google припинив використання будь-чого подібного setRetainNonInstanceConfiguration(). Це не завжди доступно, і в кращому випадку це потворний хак. Натомість ми збережемо MyTaskв іншому фрагменті, DialogFragmentвикликаному TaskFragment. Цей фрагмент буде вже setRetainInstance()встановлено вірно, так як пристрій обертається цей фрагмент не руйнується і MyTaskзберігається.

Нарешті, нам потрібно сказати TaskFragmentкому повідомити, коли це закінчено, і ми робимо це, використовуючи, setTargetFragment(<the MainFragment>)коли ми створюємо його. Коли пристрій обертається, а MainFragmentфайл знищується і створюється новий екземпляр, ми використовуємо, FragmentManagerщоб знайти діалогове вікно (на основі його тегу) і виконати setTargetFragment(<the new MainFragment>). Це майже все.

Мені потрібно було зробити ще дві речі: спочатку скасувати завдання, коли діалогове вікно закрито, а друге встановити нульове повідомлення про звільнення, інакше діалогове вікно дивним чином відхиляється при обертанні пристрою.

Кодекс

Я не буду перераховувати макети, вони досить очевидні, і ви можете знайти їх у завантаженні проекту нижче.

MainActivity

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

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment

Це довго, але того варто!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

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

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

Фрагмент завдання

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

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

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

MyTask

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

Завантажте приклад проекту

Ось вихідний код та APK . На жаль, ADT наполягав на додаванні бібліотеки підтримки, перш ніж вона дозволить мені створити проект. Я впевнений, ви можете його видалити


4
Я б уникнув збереження індикатора прогресу DialogFragment, оскільки в ньому є елементи інтерфейсу, які містять посилання на старий контекст. Натомість я зберігав би AsyncTaskв іншому порожньому фрагменті і встановлював DialogFragmentйого цільовим.
SD

Чи не буде видалено ці посилання, коли пристрій повернеться і onCreateView()буде викликаний знову? Старе mProgressBarбуде замінено на принаймні нове.
Timmmm

Не прямо, але я в цьому майже впевнений. Ви можете додати mProgressBar = null;в , onDestroyView()якщо ви хочете бути дуже впевнений. Метод Сингулярності може бути непоганою ідеєю, але він ще більше збільшить складність коду!
Тімммм

1
Посилання, яке ви тримаєте на асинктаск, - це фрагмент діалогового прогресу, так? отже, 2 питання: 1- що, якщо я хочу змінити реальний фрагмент, який викликає діалог прогресу; 2- що, якщо я хочу передати параметри в асинктаск? З повагою,
Maxrunner

1
@Maxrunner, Для передачі параметрів найпростіше, мабуть, перейти mTask.execute()до MainFragment.onClick(). Крім того, ви можете дозволити передачу параметрів setTask()або навіть зберегти їх у MyTaskсобі. Я не зовсім впевнений, що означає ваше перше запитання, але, можливо, ви хочете використати TaskFragment.getTargetFragment()? Я майже впевнений, що це буде працювати за допомогою ViewPager. Але ViewPagersвони не дуже добре зрозумілі чи задокументовані, тому удачі! Пам'ятайте, що ваш фрагмент не створюється, доки його не буде видно вперше.
Timmmm

16

Нещодавно я опублікував статтю, в якій описується, як обробляти зміни конфігурації за допомогою збережених Fragments. Це добре вирішує проблему збереження AsyncTaskзміни повороту поперек.

TL; DR - використовувати хост у вашому AsyncTaskінтерфейсі Fragment, зателефонувати setRetainInstance(true)на нього Fragmentта повідомити про AsyncTaskхід / результати про '' Activity(або це ціль Fragment, якщо ви вирішите використовувати підхід, описаний @Timmmm) через збережений файл Fragment.


5
Як би ви розглянули вкладені фрагменти? Подібно до того, як AsyncTask починається з RetainFragment всередині іншого фрагмента (вкладки).
Рекін

Якщо фрагмент вже збережено, то чому б просто не виконати завдання асинхронізації з цього збереженого фрагмента? Якщо воно вже збережене, асинхронне завдання зможе повідомити про нього, навіть якщо відбудеться зміна конфігурації.
Алекс Локвуд,

@AlexLockwood Дякую за блог. Замість того, щоб обробляти onAttachі onDetach, чи буде краще, якщо всередині TaskFragment, ми просто дзвонимо, getActivityколи нам потрібно здійснити зворотний виклик. (Перевіряючи факт TaskCallbacks)
Чеок Ян Ченг

1
Ви можете зробити це в будь-який спосіб. Я просто зробив це, onAttach()і onDetach()тому я міг уникати постійної передачі активності TaskCallbacksкожного разу, коли я захотів її використовувати.
Алекс Локвуд,

1
@AlexLockwood Якщо моя програма виконує одну діяльність - дизайн декількох фрагментів, чи повинен я мати окремий фрагмент завдання для кожного з моїх фрагментів інтерфейсу користувача? Отже, в основному життєвим циклом кожного фрагмента завдання керуватиметься його цільовим фрагментом, і не буде зв’язку з діяльністю.
Манас Баджадж

13

Моя перша пропозиція - уникати внутрішніх завдань AsyncTasks. Ви можете прочитати запитання, яке я задав щодо цього, та відповіді: Android: Рекомендації AsyncTask: приватний клас чи публічний клас?

Після цього я почав використовувати не внутрішні і ... тепер я бачу БАГАТО переваг.

Друге - збережіть посилання на ваш запущений AsyncTask у Applicationкласі - http://developer.android.com/reference/android/app/Application.html

Кожного разу, коли ви запускаєте AsyncTask, встановлюйте його в програмі, а коли він закінчує, встановлюйте його в нуль.

Коли фрагмент / активність запускається, ви можете перевірити, чи запущений будь-який AsyncTask (перевіривши, чи є він нульовим чи ні в додатку), а потім встановити посилання всередині, що завгодно (активність, фрагмент тощо, щоб ви могли робити зворотні дзвінки).

Це вирішить вашу проблему: якщо у вас у будь-який визначений час працює лише 1 AsyncTask, ви можете додати просте посилання:

AsyncTask<?,?,?> asyncTask = null;

В іншому випадку, мати в додатку HashMap з посиланнями на них.

Діалогове вікно прогресу може слідувати точно тому ж принципу.


2
Я погодився, поки ви прив'язуєте життєвий цикл AsyncTask до його Parent (визначаючи AsyncTask як внутрішній клас Діяльності / Фрагменту), досить складно змусити AsyncTask врятуватися від відпочинку батьківського життєвого циклу, однак, мені не подобається ваш рішення, це виглядає просто так хакі.
yorkw

Питання в тому .. чи є у вас краще рішення?
neteinstein

1
Мені доведеться погодитися з @yorkw тут, це рішення aws було представлене мені деякий час тому, коли я мав справу з цією проблемою без використання фрагментів (додаток на основі активності). Це запитання: stackoverflow.com/questions/2620917/… має однакову відповідь, і я погоджуюсь з одним із коментарів, який говорить: "Екземпляр програми має свій власний життєвий цикл - його також може вбити ОС, тому це рішення може спричинити важко відтворюється помилка "
сліпий

1
Проте я не бачу жодного іншого шляху, який би був менш "хакі", як сказав @yorkw. Я використовую його в декількох додатках, і, приділяючи увагу можливим проблемам, все це чудово працює.
neteinstein

Можливо, рішення @hackbod вам більше підходить.
neteinstein

4

Я придумав метод використання AsyncTaskLoaders для цього. Він досить простий у використанні та вимагає менших накладних витрат на IMO.

В основному ви створюєте AsyncTaskLoader таким чином:

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

Тоді у вашій діяльності, яка використовує вищевказаний AsyncTaskLoader при натисканні кнопки:

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

Здається, це добре справляється зі змінами орієнтації, і ваше фонове завдання продовжиться під час обертання.

Зазначимо кілька речей:

  1. Якщо в onCreate ви повторно приєднаєтесь до asynctaskloader, вам буде повернено дзвінок у onLoadFinished () з попереднім результатом (навіть якщо вам вже сказали, що запит завершено). Це насправді хороша поведінка більшу частину часу, але іноді це може бути складно впоратись. Незважаючи на те, що існує безліч способів впоратися з цим, що я зробив, це те, що я назвав loader.abandon () в onLoadFinished. Потім я додав реєстрацію onCreate, щоб повторно приєднати до завантажувача, якщо він ще не був відмовлений. Якщо вам знову знадобляться отримані дані, ви не захочете цього робити. У більшості випадків потрібні дані.

Я маю більше подробиць щодо використання цього для викликів http тут


Звичайно, getSupportLoaderManager().getLoader(0);це не поверне null (оскільки завантажувач з цим ідентифікатором 0 ще не існує)?
EmmanuelMess

1
Так, це буде нуль, якщо зміна конфігурації не призведе до перезапуску активності під час роботи завантажувача .. Ось чому я перевірив наявність нуля.
Matt Wolfe

3

Я створив дуже крихітну фонову бібліотеку завдань з відкритим кодом, яка значною мірою базується на Зефірі, AsyncTaskале має додаткові функції, такі як:

  1. Автоматичне збереження завдань при зміні конфігурації;
  2. Зворотний виклик інтерфейсу користувача (слухачі);
  3. Не перезапускає та не скасовує завдання, коли пристрій обертається (як це робили б Loaders);

Бібліотека використовує внутрішній Fragmentінтерфейс користувача без будь-якого інтерфейсу, який зберігається внаслідок змін конфігурації ( setRetainInstance(true)).

Ви можете знайти його на GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Найпростіший приклад (версія 0.2.0):

Цей приклад повністю зберігає завдання, використовуючи дуже обмежену кількість коду.

Завдання:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

Діяльність:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}

1

Мій підхід полягає у використанні шаблону дизайну делегування, загалом, ми можемо виділити фактичну ділову логіку (читати дані з Інтернету чи бази даних або що завгодно) від AsyncTask (делегатор) до BusinessDAO (делегат) у вашому методі AysncTask.doInBackground () , делегуйте фактичне завдання BusinessDAO, а потім впровадьте односторонній механізм процесу в BusinessDAO, щоб багаторазовий виклик BusinessDAO.doSomething () просто запускав одне фактичне завдання, що запускається кожного разу, і чекає результату завдання. Ідея полягає в тому, щоб зберегти делегата (тобто BusinessDAO) під час зміни конфігурації, а не делегатора (тобто AsyncTask).

  1. Створіть / впровадьте наш власний додаток, метою є створення / ініціалізація BusinessDAO тут, щоб життєвий цикл нашого BusinessDAO становив масштаб програми, а не масштаб діяльності, зверніть увагу, що вам потрібно змінити AndroidManifest.xml, щоб використовувати MyApplication:

    public class MyApplication extends android.app.Application {
      private BusinessDAO businessDAO;
    
      @Override
      public void onCreate() {
        super.onCreate();
        businessDAO = new BusinessDAO();
      }
    
      pubilc BusinessDAO getBusinessDAO() {
        return businessDAO;
      }
    
    }
    
  2. Наші існуючі Activity / Fragment в основному незмінені, все ще реалізують AsyncTask як внутрішній клас і залучають AsyncTask.execute () від Activity / Fragment, різниця в тому, що AsyncTask делегує фактичне завдання BusinessDAO, тому під час зміни конфігурації буде другий AsyncTask буде ініціалізовано та виконано, і викличте BusinessDAO.doSomething () вдруге, однак другий виклик BusinessDAO.doSomething () не викличе нове запущене завдання, натомість, чекаючи завершення поточного запущеного завдання:

    public class LoginFragment extends Fragment {
      ... ...
    
      public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
        // get a reference of BusinessDAO from application scope.
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //Handle task result and update UI stuff.
        }
      }
    
      ... ...
    }
    
  3. Усередині BusinessDAO реалізуйте механізм одиночного процесу, наприклад:

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          // nothing running at the moment, submit a new callable task to run.
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        // Task already submitted and running, waiting for the running task to finish.
        myFutureTask.get();
      }
    
      // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          // do your job here.
          return this;
        }
      }
    
    }
    

Я не впевнений на 100%, чи це спрацює, крім того, зразок фрагмента коду слід розглядати як псевдокод. Я просто намагаюся дати вам підказку з рівня дизайну. Будь-які відгуки та пропозиції вітаються та вітаються.


Здається дуже гарним рішенням. Оскільки ви відповіли на це близько 2 з половиною років тому, чи перевіряли це з тих пір ?! Ви кажете, що я не впевнений, що це працює, річ також не в цьому !! Я шукаю добре перевірене рішення цієї проблеми. Чи є у вас пропозиції?
Аліреза А. Ахмаді

1

Ви можете зробити AsyncTask статичним полем. Якщо вам потрібен контекст, вам слід надіслати контекст вашої програми. Це дозволить уникнути витоків пам’яті, інакше ви збережете посилання на всю свою діяльність.


1

Якщо хтось знаходить свій шлях до цього потоку, то я знайшов чистий підхід - запустити завдання Async із app.Service(розпочатого з START_STICKY), а потім повторно створити ітерацію по запущеним службам, щоб з'ясувати, чи служба (а отже, і задача асинхронізації) все ще є біг;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

Якщо це так, повторно додайте DialogFragment(або що завгодно), і якщо це не гарантує, що діалогове вікно було відхилено.

Це особливо доречно, якщо ви використовуєте v4.support.*бібліотеки, оскільки (на момент написання статті) вони мали проблеми з setRetainInstanceметодом та переглядом сторінок. Крім того, не зберігаючи екземпляр, ви можете відтворити свою діяльність, використовуючи інший набір ресурсів (тобто інший макет подання для нової орієнтації)


Хіба не надмірно запускати Службу лише для того, щоб зберегти AsyncTask? Послуга працює самостійно, і це не відбувається без додаткових витрат.
WeNeigh

Цікавий Віней. Не помічав, що додаток є більш важким (на даний момент він досить легкий). Що ти знайшов? Я розглядаю послугу як передбачуване середовище для того, щоб дозволити системі працювати з важким навантаженням або введенням-виведенням незалежно від стану інтерфейсу. Зв'язок зі службою, щоб побачити, коли щось завершено, здавався "правильним". Послуги, які я починаю виконувати, трохи зупиняються на завершенні завдання, тому зазвичай виживають близько 10-30-х років.
BrantApps

Відповідь Commonsware тут, схоже, припускає, що послуги - це погана ідея. Зараз я розглядаю AsyncTaskLoaders, але, схоже, вони мають власні проблеми (негнучкість, лише для завантаження даних тощо)
WeNeigh

1
Розумію. Примітно, що ця служба, до якої ви зв’язали, явно налаштовувалась на запуск у власному процесі. Здавалося, майстру не подобається, що цей шаблон часто використовується. Я прямо не надав властивості "кожного разу запускати новий процес", тому, сподіваюся, я ізольований від цієї частини критики. Я намагатимусь кількісно оцінити ефекти. Послуги, як концепція, звичайно, не є "поганими ідеями" і є фундаментальними для кожного додатка, який робить що-небудь віддалено цікаве, без каламбуру. Їх JDoc надає більше вказівок щодо їх використання, якщо ви все ще не впевнені.
BrantApps

0

Я пишу samepl код, щоб вирішити цю проблему

Перший крок - це зробити клас Application:

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

@Override
public void onCreate() {
    super.onCreate();
    sTheApp = this;
}

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

В AndroidManifest.xml

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

Код в діяльності:

public class MainActivity extends Activity {

private Task1 mTask1;

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

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

Коли орієнтація діяльності змінюється, змінна mTask ініціюється з контексту програми. Коли завдання закінчено, змінна встановлюється як нуль і видаляється з пам'яті.

Для мене цього достатньо.


0

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

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

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

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

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

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

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

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mListener != null) {
                mListener.onRequestProgressUpdate(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}

-1

Подивіться тут .

Існує рішення на основі Timmmm .

Але я його вдосконалив:

  • Тепер рішення можна розширити - вам потрібно лише продовжити FragmentAbleToStartTask

  • Ви можете продовжувати виконувати кілька завдань одночасно.

    І на мій погляд, це так само просто, як startActivityForResult і отримати результат

  • Ви також можете зупинити запущене завдання та перевірити, чи виконується конкретне завдання

Перепрошую за мою англійську


перше посилання порушено
Тейзератул
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.