Fragmen Android. Mempertahankan AsyncTask selama rotasi layar atau perubahan konfigurasi

86

Saya sedang mengerjakan aplikasi Smartphone / Tablet, hanya menggunakan satu APK, dan memuat sumber daya yang diperlukan tergantung pada ukuran layar, pilihan desain terbaik tampaknya menggunakan Fragmen melalui ACL.

Aplikasi ini telah berfungsi dengan baik sampai sekarang hanya berdasarkan aktivitas. Ini adalah kelas tiruan tentang cara saya menangani AsyncTasks dan ProgressDialogs dalam Aktivitas agar dapat berfungsi bahkan saat layar diputar atau perubahan konfigurasi terjadi di tengah komunikasi.

Saya tidak akan mengubah manifes untuk menghindari rekreasi Aktivitas, ada banyak alasan mengapa saya tidak ingin melakukannya, tetapi terutama karena dokumen resmi mengatakan itu tidak direkomendasikan dan saya sudah berhasil tanpanya sejauh ini, jadi tolong jangan rekomendasikan itu rute.

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

Kode ini berfungsi dengan baik, saya memiliki sekitar 10.000 pengguna tanpa keluhan, jadi sepertinya logis untuk hanya menyalin logika ini ke Desain Berbasis Fragmen yang baru, tetapi, tentu saja, ini tidak berfungsi.

Berikut adalah LoginFragment:

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

Saya tidak dapat menggunakan onRetainNonConfigurationInstance()karena harus dipanggil dari Aktivitas dan bukan Fragmen, begitu juga dengan getLastNonConfigurationInstance(). Saya telah membaca beberapa pertanyaan serupa di sini tanpa jawaban.

Saya mengerti bahwa mungkin diperlukan beberapa pekerjaan untuk mengatur barang-barang ini dengan benar dalam fragmen, yang dikatakan, saya ingin mempertahankan logika desain dasar yang sama.

Apa cara yang tepat untuk mempertahankan AsyncTask selama perubahan konfigurasi, dan jika masih berjalan, tunjukkan progressDialog, dengan mempertimbangkan bahwa AsyncTask adalah kelas dalam ke Fragmen dan Fragmen itu sendiri yang memanggil AsyncTask.execute ()?

penutup mata
sumber
1
Mungkin utas tentang cara menangani perubahan konfigurasi dengan AsyncTask dapat membantu
rds
mengaitkan AsyncTask dengan siklus hidup aplikasi .. sehingga dapat dilanjutkan saat aktivitas dibuat ulang
Fred Grott
Lihat posting saya tentang topik ini: Menangani Perubahan Konfigurasi dengan Fragments
Alex Lockwood

Jawaban:

75

Fragmen sebenarnya dapat membuat ini jauh lebih mudah. Cukup gunakan metode Fragment.setRetainInstance (boolean) agar instance fragmen Anda dipertahankan selama perubahan konfigurasi. Perhatikan bahwa ini adalah pengganti yang disarankan untuk Activity.onRetainnonConfigurationInstance () di dokumen.

Jika karena alasan tertentu Anda benar-benar tidak ingin menggunakan fragmen yang dipertahankan, ada pendekatan lain yang dapat Anda lakukan. Perhatikan bahwa setiap fragmen memiliki pengenal unik yang dikembalikan oleh Fragment.getId () . Anda juga bisa mengetahui apakah sebuah fragmen sedang dihancurkan untuk perubahan konfigurasi melalui Fragment.getActivity (). IsChangingConfigurations () . Jadi, pada titik di mana Anda akan memutuskan untuk menghentikan AsyncTask Anda (di onStop () atau onDestroy () kemungkinan besar), Anda dapat misalnya memeriksa apakah konfigurasi berubah dan jika demikian tetap di SparseArray statis di bawah pengenal fragmen, lalu di onCreate () atau onStart () Anda lihat apakah Anda memiliki AsyncTask dalam larik jarang yang tersedia.

hackbod
sumber
Perhatikan bahwa setRetainInstance hanya jika Anda tidak menggunakan back-stack.
Neil
4
Bukankah mungkin AsyncTask mengirimkan kembali hasilnya sebelum onCreateView dari Fragment yang dipertahankan akan berjalan?
jakk
6
@jakk Metode siklus hidup untuk Aktivitas, Fragmen, dll. dipanggil secara berurutan oleh antrian pesan utas GUI utama, jadi meskipun tugas selesai secara bersamaan di latar belakang sebelum metode siklus hidup ini selesai (atau bahkan dipanggil), onPostExecutemetode tersebut masih perlu tunggu sebelum akhirnya diproses oleh antrian pesan utas utama.
Alex Lockwood
Pendekatan ini (RetainInstance = true) tidak akan berfungsi jika Anda ingin memuat file tata letak yang berbeda untuk setiap orientasi.
Justin
Memulai asynctask dalam metode onCreate MainActivity sepertinya hanya berfungsi jika asynctask - di dalam fragmen "worker" - dimulai oleh tindakan pengguna yang eksplisit. Karena utas utama dan antarmuka pengguna tersedia. Namun, memulai asynctask tepat setelah meluncurkan aplikasi - tanpa tindakan pengguna seperti mengklik tombol - memberikan pengecualian. Dalam hal ini, asynctask bisa dipanggil dalam metode onStart pada MainActivity, bukan dalam metode onCreate.
ʕ ᵔᴥᵔ ʔ
66

Saya pikir Anda akan menikmati contoh saya yang sangat komprehensif dan berfungsi yang dijelaskan di bawah ini.

  1. Rotasi berfungsi, dan dialog bertahan.
  2. Anda dapat membatalkan tugas dan dialog dengan menekan tombol kembali (jika Anda menginginkan perilaku ini).
  3. Ini menggunakan fragmen.
  4. Tata letak fragmen di bawah aktivitas berubah dengan benar saat perangkat berputar.
  5. Ada unduhan kode sumber lengkap dan APK yang telah dikompilasi sehingga Anda dapat melihat apakah perilakunya sesuai dengan yang Anda inginkan.

Sunting

Seperti yang diminta oleh Brad Larson, saya telah mereproduksi sebagian besar solusi terkait di bawah ini. Juga sejak saya mempostingnya, saya telah ditunjuk AsyncTaskLoader. Saya tidak yakin ini benar-benar berlaku untuk masalah yang sama, tetapi Anda tetap harus memeriksanya.

Menggunakan AsyncTaskdengan dialog kemajuan dan rotasi perangkat.

Solusi yang berhasil!

Saya akhirnya mendapatkan segalanya untuk bekerja. Kode saya memiliki beberapa fitur berikut:

  1. A Fragmentyang tata letaknya berubah dengan orientasi.
  2. Di AsyncTaskmana Anda dapat melakukan beberapa pekerjaan.
  3. SEBUAH DialogFragment yang menunjukkan kemajuan tugas di bilah kemajuan (bukan hanya pemintal tak tentu).
  4. Rotasi bekerja tanpa mengganggu tugas atau menutup dialog.
  5. Tombol kembali menutup dialog dan membatalkan tugas (Anda dapat mengubah perilaku ini dengan cukup mudah).

Saya tidak berpikir kombinasi kerja dapat ditemukan di tempat lain.

Ide dasarnya adalah sebagai berikut. Ada MainActivitykelas yang berisi satu fragmen - MainFragment. MainFragmentmemiliki tata letak yang berbeda untuk orientasi horizontal dan vertikal, dan setRetainInstance()salah sehingga tata letak bisa berubah. Ini berarti bahwa ketika orientasi perangkat berubah, baik MainActivitydan MainFragmentbenar-benar hancur dan diciptakan.

Secara terpisah kami memiliki MyTask(diperpanjang dari AsyncTask) yang melakukan semua pekerjaan. Kami tidak dapat menyimpannya MainFragmentkarena itu akan dihancurkan, dan Google sudah tidak lagi menggunakan hal seperti itu setRetainNonInstanceConfiguration(). Itu tidak selalu tersedia dan merupakan peretasan yang buruk. Sebagai gantinya kita akan menyimpan MyTaskdi fragmen lain, yang DialogFragmentdisebut TaskFragment. Ini fragmen akan telah setRetainInstance()diatur ke benar, sehingga perangkat berputar fragmen ini tidak hancur, dan MyTaskdipertahankan.

Akhirnya kita perlu memberi tahu TaskFragmentsiapa yang harus diinformasikan ketika sudah selesai, dan kita melakukannya menggunakan setTargetFragment(<the MainFragment>)saat kita membuatnya. Ketika perangkat diputar dan MainFragmentdihancurkan dan sebuah instance baru dibuat, kita menggunakan FragmentManagerdialog untuk menemukan (berdasarkan tagnya) dan lakukan setTargetFragment(<the new MainFragment>). Cukup banyak.

Ada dua hal lain yang perlu saya lakukan: pertama batalkan tugas saat dialog ditutup, dan kedua setel pesan tutup ke null, jika tidak, dialog akan ditutup secara aneh saat perangkat diputar.

Kode

Saya tidak akan mencantumkan tata letak, mereka cukup jelas dan Anda dapat menemukannya di unduhan proyek di bawah.

Aktifitas utama

Ini sangat mudah. Saya menambahkan panggilan balik ke dalam aktivitas ini sehingga tahu kapan tugas selesai, tetapi Anda mungkin tidak membutuhkannya. Terutama saya hanya ingin menunjukkan mekanisme callback aktivitas fragmen karena cukup rapi dan Anda mungkin belum pernah melihatnya sebelumnya.

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

Itu panjang tapi sepadan!

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

TaskFragment

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

Tugasku

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

Unduh proyek contoh

Berikut adalah kode sumber dan APK . Maaf, ADT bersikeras menambahkan pustaka dukungan sebelum mengizinkan saya membuat proyek. Saya yakin Anda bisa menghapusnya.

Timmmm
sumber
4
Saya akan menghindari mempertahankan bilah kemajuan DialogFragment, karena memiliki elemen UI yang menyimpan referensi ke konteks lama. Sebagai gantinya, saya akan menyimpan AsyncTaskdi fragmen kosong lainnya, dan menetapkan DialogFragmentsebagai targetnya.
SD
Tidakkah referensi tersebut akan dihapus ketika perangkat diputar dan onCreateView()dipanggil lagi? Yang lama mProgressBarsetidaknya akan ditimpa dengan yang baru.
Timmmm
Tidak secara eksplisit, tapi saya cukup yakin tentang itu. Anda dapat menambahkan mProgressBar = null;di onDestroyView()jika Anda ingin ekstra yakin. Metode Singularity mungkin merupakan ide yang bagus, tetapi metode ini akan semakin meningkatkan kompleksitas kode!
Timmmm
1
Referensi yang Anda pegang pada asynctask adalah fragmen dialog kemajuan, bukan? jadi 2 pertanyaan: 1- bagaimana jika saya ingin mengubah fragmen nyata yang memanggil dialog kemajuan; 2- Bagaimana jika saya ingin meneruskan params ke asynctask? Salam,
Maxrunner
1
@Maxrunner, Untuk meneruskan parameter, hal termudah mungkin adalah pindah mTask.execute()ke MainFragment.onClick(). Alternatifnya, Anda dapat mengizinkan parameter untuk diteruskan setTask()atau bahkan menyimpannya MyTasksendiri. Saya tidak begitu yakin apa arti pertanyaan pertama Anda, tetapi mungkin Anda ingin menggunakannya TaskFragment.getTargetFragment()? Saya cukup yakin ini akan berhasil menggunakan file ViewPager. Tapi ViewPagerstidak terlalu dipahami atau didokumentasikan, jadi semoga berhasil! Ingatlah bahwa fragmen Anda tidak dibuat sampai terlihat pertama kali.
Timmmm
16

Saya baru-baru ini memposting artikel yang menjelaskan cara menangani perubahan konfigurasi menggunakan retained Fragments. Ini memecahkan masalah mempertahankan fileAsyncTask perubahan rotasi yang melintasi dengan baik.

TL; DR adalah untuk penggunaan host Anda AsyncTaskdi dalam Fragment, panggilan setRetainInstance(true)pada Fragment, dan melaporkan AsyncTask's kemajuan / hasil kembali ke itu Activity(atau itu sasaran Fragment, jika Anda memilih untuk menggunakan pendekatan yang dijelaskan oleh @Timmmm) melalui dipertahankan Fragment.

Alex Lockwood
sumber
5
Bagaimana Anda akan melakukan Fragmen bersarang? Seperti AsyncTask yang dimulai dari RetainedFragment di dalam Fragment (Tab) lain.
Rekin
Jika fragmen sudah dipertahankan, mengapa tidak menjalankan tugas asinkron dari dalam fragmen yang dipertahankan itu? Jika sudah dipertahankan maka tugas asinkron akan dapat melaporkan kembali ke sana meskipun terjadi perubahan konfigurasi.
Alex Lockwood
@AlexLockwood Terima kasih untuk blognya. Daripada harus menangani onAttachdan onDetach, akan lebih baik jika di dalam TaskFragment, kita panggil saja getActivitykapan saja kita perlu untuk memanggil balik. (Dengan memeriksa instaceof TaskCallbacks)
Cheok Yan Cheng
1
Anda bisa melakukannya dengan cara apa pun. Aku hanya melakukannya di onAttach()dan onDetach()jadi saya bisa menghindari terus-menerus pengecoran aktivitas untuk TaskCallbackssetiap kali saya ingin menggunakannya.
Alex Lockwood
1
@AlexLockwood Jika aplikasi saya mengikuti satu aktivitas - desain beberapa fragmen, haruskah saya memiliki fragmen tugas terpisah untuk setiap fragmen UI saya? Jadi pada dasarnya setiap siklus proses fragmen tugas akan dikelola oleh fragmen targetnya, dan tidak akan ada komunikasi dengan aktivitas tersebut.
Manas Bajaj
13

Saran pertama saya adalah untuk menghindari AsyncTasks batin , Anda dapat membaca pertanyaan yang saya tanyakan tentang ini dan jawabannya: Android: Rekomendasi AsyncTask: kelas privat atau kelas publik?

Setelah itu saya mulai menggunakan non-inner dan ... sekarang saya melihat BANYAK manfaat.

Yang kedua adalah, simpan referensi AsyncTask Anda yang sedang berjalan di ApplicationKelas - http://developer.android.com/reference/android/app/Application.html

Setiap kali Anda memulai AsyncTask, setel pada Aplikasi dan setelah selesai setel ke null.

Ketika sebuah fragmen / aktivitas dimulai, Anda bisa memeriksa apakah AsyncTask sedang berjalan (dengan memeriksa apakah itu null atau tidak pada Aplikasi) dan kemudian menyetel referensi di dalamnya ke apa pun yang Anda inginkan (aktivitas, fragmen, dll sehingga Anda bisa melakukan callback).

Ini akan menyelesaikan masalah Anda: Jika Anda hanya menjalankan 1 AsyncTask pada waktu yang ditentukan, Anda dapat menambahkan referensi sederhana:

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

Lain, miliki di Aplication sebuah HashMap dengan referensi ke sana.

Dialog kemajuan dapat mengikuti prinsip yang sama persis.

neteinstein.dll
sumber
2
Saya setuju selama Anda mengikat siklus hidup AsyncTask ke Induknya (dengan mendefinisikan AsyncTask sebagai kelas dalam Aktivitas / Fragmen), cukup sulit untuk membuat AsyncTask Anda keluar dari rekreasi siklus hidup induknya, namun, saya tidak suka Anda solusi, itu terlihat sangat hacky.
yorkw
Pertanyaannya adalah .. apakah Anda punya solusi yang lebih baik?
neteinstein
1
Saya harus setuju dengan @yorkw di sini, solusi ini disajikan kepada saya beberapa waktu yang lalu ketika saya berurusan dengan masalah ini tanpa menggunakan fragmen (aplikasi berbasis aktivitas). Pertanyaan ini: stackoverflow.com/questions/2620917/… memiliki jawaban yang sama, dan saya setuju dengan salah satu komentar yang mengatakan "Instance aplikasi memiliki siklus hidupnya sendiri - dapat juga dimatikan oleh OS, jadi solusi ini dapat menyebabkan bug yang sulit direproduksi "
blindstuff
1
Masih saya tidak melihat cara lain yang kurang "hacky" seperti yang dikatakan @yorkw. Saya telah menggunakannya di beberapa aplikasi, dan dengan beberapa perhatian pada kemungkinan masalah semuanya berfungsi dengan baik.
neteinstein
Mungkin solusi @hackbod lebih cocok untuk Anda.
neteinstein
4

Saya datang dengan metode menggunakan AsyncTaskLoaders untuk ini. Ini cukup mudah digunakan dan membutuhkan lebih sedikit overhead IMO ..

Pada dasarnya Anda membuat AsyncTaskLoader seperti ini:

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

Kemudian dalam aktivitas Anda yang menggunakan AsyncTaskLoader di atas saat sebuah tombol diklik:

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

    }
}

Ini tampaknya menangani perubahan orientasi dengan baik dan tugas latar belakang Anda akan berlanjut selama rotasi.

Beberapa hal yang perlu diperhatikan:

  1. Jika di onCreate Anda memasang kembali ke asynctaskloader, Anda akan dipanggil kembali di onLoadFinished () dengan hasil sebelumnya (meskipun Anda telah diberi tahu bahwa permintaan telah selesai). Ini sebenarnya adalah perilaku yang baik di sebagian besar waktu, tetapi terkadang sulit untuk ditangani. Meskipun saya membayangkan ada banyak cara untuk menangani ini, yang saya lakukan adalah memanggil loader.abandon () di onLoadFinished. Kemudian saya menambahkan check in onCreate untuk hanya memasang kembali ke loader jika belum ditinggalkan. Jika Anda membutuhkan data yang dihasilkan lagi, Anda tidak akan ingin melakukannya. Dalam banyak kasus, Anda menginginkan datanya.

Saya memiliki detail lebih lanjut tentang penggunaan ini untuk panggilan http di sini

Matt Wolfe
sumber
Yakin itu getSupportLoaderManager().getLoader(0);tidak akan mengembalikan null (karena loader dengan id itu, 0, belum ada)?
EmmanuelMess
1
Ya itu akan menjadi nol kecuali jika perubahan konfigurasi menyebabkan aktivitas untuk restart sementara loader sedang berlangsung .. Itulah mengapa saya memeriksa null.
Matt Wolfe
3

Saya membuat perpustakaan tugas latar belakang sumber terbuka yang sangat kecil yang sangat didasarkan pada Marshmallow AsyncTasktetapi dengan fungsionalitas tambahan seperti:

  1. Secara otomatis mempertahankan tugas di seluruh perubahan konfigurasi;
  2. Callback UI (pendengar);
  3. Tidak memulai ulang atau membatalkan tugas saat perangkat berputar (seperti yang dilakukan Loader);

Pustaka secara internal menggunakan Fragmenttanpa antarmuka pengguna apa pun, yang dipertahankan selama perubahan konfigurasi (setRetainInstance(true) ).

Anda dapat menemukannya di GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Contoh paling dasar (versi 0.2.0):

Contoh ini sepenuhnya mempertahankan tugas, menggunakan jumlah kode yang sangat terbatas.

Tugas:

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

Aktivitas:

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();
    }
}
Rolf ツ
sumber
1

Pendekatan saya adalah dengan menggunakan pola desain delegasi, secara umum, kita dapat mengisolasi logika bisnis yang sebenarnya (membaca data dari internet atau database atau apa pun) dari AsyncTask (delegator) ke BusinessDAO (delegasi), dalam metode AysncTask.doInBackground () Anda , delegasikan tugas aktual ke BusinessDAO, lalu terapkan mekanisme proses tunggal di BusinessDAO, sehingga beberapa panggilan ke BusinessDAO.doSomething () hanya akan memicu satu tugas aktual yang berjalan setiap waktu dan menunggu hasil tugas. Idenya adalah mempertahankan delegasi (yaitu BusinessDAO) selama perubahan konfigurasi, bukan delegator (yaitu AsyncTask).

  1. Buat / Implementasikan Aplikasi kita sendiri, tujuannya adalah untuk membuat / menginisialisasi BusinessDAO di sini, sehingga siklus hidup BusinessDAO kita mencakup aplikasi, bukan cakupan aktivitas, perhatikan bahwa Anda perlu mengubah AndroidManifest.xml untuk menggunakan 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 / Fragement kita sebagian besar tidak berubah, masih mengimplementasikan AsyncTask sebagai kelas dalam dan melibatkan AsyncTask.execute () dari Activity / Fragement, bedanya sekarang adalah AsyncTask akan mendelegasikan tugas sebenarnya ke BusinessDAO, jadi selama perubahan konfigurasi, AsyncTask kedua akan diinisialisasi dan dijalankan, dan memanggil BusinessDAO.doSomething () kedua kalinya, namun, panggilan kedua ke BusinessDAO.doSomething () tidak akan memicu tugas baru yang sedang berjalan, sebagai gantinya, menunggu tugas yang sedang berjalan selesai:

    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. Di dalam BusinessDAO, terapkan mekanisme proses tunggal, misalnya:

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

Saya tidak 100% yakin apakah ini akan berhasil, terlebih lagi, potongan kode sampel harus dianggap sebagai pseudocode. Saya hanya mencoba memberi Anda petunjuk dari tingkat desain. Setiap umpan balik atau saran dipersilakan dan dihargai.

yorkw
sumber
Sepertinya solusi yang sangat bagus. Sejak Anda menjawab ini sekitar 2 setengah tahun yang lalu, apakah Anda mengujinya sejak itu ?! Anda mengatakan saya tidak yakin itu berhasil, masalahnya juga bukan saya !! Saya mengunci solusi yang telah teruji untuk masalah ini. Apakah Anda punya saran?
Alireza A. Ahmadi
1

Anda dapat menjadikan AsyncTask sebagai bidang statis. Jika Anda membutuhkan konteks, Anda harus mengirimkan konteks aplikasi Anda. Ini akan menghindari kebocoran memori, jika tidak, Anda akan menyimpan referensi ke seluruh aktivitas Anda.

Boude
sumber
1

Jika ada yang menemukan jalan mereka ke utas ini, saya menemukan pendekatan yang bersih adalah dengan menjalankan tugas Async dari app.Service(dimulai dengan START_STICKY) dan kemudian membuat ulang iterasi atas layanan yang berjalan untuk mengetahui apakah layanan (dan karenanya tugas asinkron) masih berlari;

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

Jika ya, tambahkan kembali DialogFragment(atau apa pun) dan jika tidak pastikan dialog telah ditutup.

Ini terutama penting jika Anda menggunakan v4.support.*pustaka karena (pada saat penulisan) mereka mengetahui masalah dengan setRetainInstancemetode dan tampilan paging. Selain itu, dengan tidak menyimpan instance, Anda dapat membuat ulang aktivitas menggunakan kumpulan sumber daya yang berbeda (yaitu tata letak tampilan yang berbeda untuk orientasi baru)

BrantApps
sumber
Bukankah berlebihan menjalankan Layanan hanya untuk mempertahankan AsyncTask? Layanan berjalan dalam prosesnya sendiri, dan itu tidak datang tanpa biaya tambahan.
WeNeigh
Vinay yang menarik. Belum memperhatikan aplikasi menjadi lebih berat sumber daya (toh cukup ringan saat ini). Apa yang kamu temukan? Saya melihat layanan sebagai lingkungan yang dapat diprediksi untuk membiarkan sistem melanjutkan dengan beberapa pengangkatan berat atau I / O terlepas dari status UI. Berkomunikasi dengan layanan untuk melihat ketika sesuatu telah selesai tampak 'benar'. Layanan yang saya mulai jalankan sedikit pekerjaan berhenti pada penyelesaian tugas sehingga biasanya bertahan sekitar 10-30-an.
BrantApps
Jawaban Commonsware di sini sepertinya menunjukkan bahwa Layanan adalah ide yang buruk. Sekarang saya sedang mempertimbangkan AsyncTaskLoader tetapi tampaknya mereka memiliki masalah sendiri (tidak fleksibel, hanya untuk pemuatan data, dll.)
WeNeigh
1
Saya melihat. Terlihat, layanan yang Anda tautkan ini secara eksplisit disiapkan untuk dijalankan dalam prosesnya sendiri. Master tampaknya tidak suka pola ini sering digunakan. Saya belum secara eksplisit memberikan properti "berjalan dalam proses baru setiap kali" jadi mudah-mudahan saya terisolasi dari bagian kritik itu. Saya akan berusaha untuk mengukur efeknya. Layanan sebagai sebuah konsep tentu saja bukan 'ide buruk' dan sangat penting bagi setiap aplikasi yang melakukan sesuatu yang menarik dari jarak jauh, tidak ada permainan kata-kata. JDoc mereka memberikan lebih banyak panduan tentang penggunaannya jika Anda masih tidak yakin.
BrantApps
0

Saya menulis kode samepl untuk mengatasi masalah ini

Langkah pertama adalah membuat kelas Aplikasi:

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

Di 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">

Kode dalam aktivitas:

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

}

Ketika orientasi aktivitas berubah, variabel mTask diinisi dari konteks aplikasi. Ketika tugas selesai, variabel disetel ke nol dan dihapus dari memori.

Bagi saya itu cukup.

Kenumir
sumber
0

Lihat contoh di bawah ini, cara menggunakan retained fragment untuk mempertahankan tugas background:

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

    }
}
DeepakPanwar
sumber
-1

Lihat di sini .

Ada solusi berdasarkan Timmmm's .

Tapi saya memperbaikinya:

  • Sekarang solusinya dapat diperpanjang - Anda hanya perlu memperpanjang FragmentAbleToStartTask

  • Anda dapat terus menjalankan beberapa tugas secara bersamaan.

    Dan menurut saya itu semudah startActivityForResult dan menerima hasil

  • Anda juga dapat menghentikan tugas yang sedang berjalan dan memeriksa apakah tugas tertentu sedang berjalan

Maaf untuk bahasa Inggris saya

l3onid-clean-coder
sumber
link pertama rusak
Tejzeratul