Tugas latar belakang, dialog progres, perubahan orientasi - adakah solusi 100% yang berfungsi?

235

Saya mengunduh beberapa data dari internet di utas latar (saya menggunakan AsyncTask) dan menampilkan dialog progres saat mengunduh. Perubahan orientasi, Aktivitas dimulai kembali dan kemudian AsyncTask saya selesai - Saya ingin mengabaikan dialog progess dan memulai Kegiatan baru. Tetapi memanggil dismissDialog terkadang mengeluarkan pengecualian (mungkin karena Aktivitas dihancurkan dan Aktivitas baru belum dimulai).

Apa cara terbaik untuk menangani masalah semacam ini (memperbarui UI dari utas latar belakang yang berfungsi bahkan jika pengguna mengubah orientasi)? Apakah seseorang dari Google memberikan beberapa "solusi resmi"?

fhucho
sumber
4
Posting blog saya tentang topik ini mungkin bisa membantu. Ini tentang mempertahankan tugas yang sudah berjalan lama di seluruh perubahan konfigurasi.
Alex Lockwood
1
Ini pertanyaan terkait juga.
Alex Lockwood
Hanya FTR ada misteri yang terkait di sini .. stackoverflow.com/q/23742412/294884
Fattie

Jawaban:

336

Langkah # 1: Membuat AsyncTasksebuah statickelas bersarang, atau kelas yang sama sekali terpisah, hanya bukan (non-static bersarang) kelas batin.

Langkah # 2: Memiliki AsyncTaskpegangan ke Activitymelalui anggota data, atur melalui konstruktor dan setter.

Langkah # 3: Saat membuat AsyncTask, suplai arus Activityke konstruktor.

Langkah # 4: Masuk onRetainNonConfigurationInstance(), kembalikan AsyncTask, setelah melepaskannya dari aktivitas asli, yang sekarang sudah tidak ada.

Langkah # 5: Di onCreate(), jika getLastNonConfigurationInstance()tidak null, serahkan ke AsyncTaskkelas Anda dan panggil setter Anda untuk mengaitkan aktivitas baru Anda dengan tugas tersebut.

Langkah # 6: Jangan merujuk ke data aktivitas anggota doInBackground().

Jika Anda mengikuti resep di atas, semuanya akan berhasil. onProgressUpdate()dan onPostExecute()ditangguhkan antara awal onRetainNonConfigurationInstance()dan akhir berikutnya onCreate().

Berikut adalah contoh proyek yang menunjukkan teknik ini.

Pendekatan lain adalah dengan membuang AsyncTaskdan memindahkan pekerjaan Anda ke sebuah IntentService. Ini sangat berguna jika pekerjaan yang harus dilakukan mungkin lama dan harus berlanjut terlepas dari apa yang dilakukan pengguna dalam hal kegiatan (misalnya, mengunduh file besar). Anda dapat menggunakan siaran yang dipesan Intentuntuk meminta aktivitas merespons pekerjaan yang sedang dilakukan (jika masih di latar depan) atau menaikkan a Notificationuntuk memberi tahu pengguna jika pekerjaan telah dilakukan. Berikut adalah posting blog dengan lebih banyak tentang pola ini.

CommonsWare
sumber
8
Terima kasih banyak atas jawaban Anda yang luar biasa untuk masalah umum ini! Hanya untuk menjadi menyeluruh, Anda dapat menambahkan ke langkah # 4 yang kami harus lepaskan (setel ke nol) aktivitas di AsyncTask. Ini diilustrasikan dengan baik dalam proyek sampel.
Kevin Gaudin
3
Tetapi bagaimana jika saya perlu memiliki akses ke anggota Activity?
Eugene
3
@Andrew: Membuat kelas dalam statis atau sesuatu yang memegang beberapa objek, dan mengembalikannya.
CommonsWare
11
onRetainNonConfigurationInstance()sudah usang dan alternatif yang disarankan adalah menggunakan setRetainInstance(), tetapi tidak mengembalikan objek. Apakah mungkin untuk menangani asyncTaskperubahan konfigurasi dengan setRetainInstance()?
Indrek Kõue
10
@ SilylRR: Tentu saja. Memiliki Fragmentpalka AsyncTask. Milikilah Fragmentpanggilan setRetainInstance(true)itu sendiri. Apakah AsyncTaskhanya berbicara dengan Fragment. Sekarang, pada perubahan konfigurasi, Fragmentitu tidak dihancurkan dan diciptakan kembali (meskipun aktivitasnya), dan dengan demikian AsyncTaskdipertahankan melalui perubahan konfigurasi.
CommonsWare
13

Jawaban yang diterima sangat membantu, tetapi tidak memiliki dialog kemajuan.

Untungnya bagi Anda, pembaca, saya telah membuat contoh AsyncTask yang sangat komprehensif dan berfungsi dengan dialog kemajuan !

  1. Rotasi berfungsi, dan dialog tetap 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 ketika perangkat berputar.
Timmmm
sumber
Jawaban yang diterima adalah tentang kelas statis (bukan anggota). Dan itu perlu untuk menghindari bahwa AsyncTask memiliki pointer (tersembunyi) ke instance kelas luar yang menjadi kebocoran memori saat menghancurkan aktivitas.
Bananeweizen
Ya tidak yakin mengapa saya mengatakan itu tentang anggota statis, karena saya sebenarnya juga menggunakan mereka ... aneh. Jawaban yang diedit.
Timmmm
Bisakah Anda memperbarui tautan Anda? Saya sangat membutuhkan ini.
Romain Pellerin
Maaf, belum sempat memulihkan situs web saya - saya akan segera melakukannya! Tetapi sementara itu pada dasarnya sama dengan kode dalam jawaban ini: stackoverflow.com/questions/8417885/…
Timmmm
1
Tautannya palsu; hanya mengarah ke indeks yang tidak berguna tanpa indikasi di mana kode tersebut.
FractalBob
9

Saya sudah bekerja keras selama seminggu untuk menemukan solusi untuk dilema ini tanpa harus mengedit file manifes. Asumsi untuk solusi ini adalah:

  1. Anda selalu perlu menggunakan dialog progres
  2. Hanya satu tugas yang dilakukan sekaligus
  3. Anda perlu tugas untuk bertahan ketika telepon diputar dan dialog progres untuk secara otomatis ditolak.

Penerapan

Anda perlu menyalin dua file yang ditemukan di bagian bawah posting ini ke ruang kerja Anda. Pastikan saja:

  1. Semua milik Anda Activityharus diperluasBaseActivity

  2. In onCreate(), super.onCreate()harus dipanggil setelah Anda menginisialisasi anggota yang perlu diakses oleh anggota Anda ASyncTask. Juga, ganti getContentViewId()untuk memberikan id tata letak formulir.

  3. Ganti onCreateDialog() seperti biasa untuk membuat dialog yang dikelola oleh aktivitas.

  4. Lihat kode di bawah ini untuk contoh kelas dalam statis untuk membuat AsyncTasks Anda. Anda dapat menyimpan hasil Anda di mResult untuk diakses nanti.


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

Dan akhirnya, untuk meluncurkan tugas baru Anda:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

Itu dia! Saya harap solusi yang kuat ini akan membantu seseorang.

BaseActivity.java (mengatur impor sendiri)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

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

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
Oleg Vaskevich
sumber
4

Apakah seseorang dari Google memberikan beberapa "solusi resmi"?

Iya.

Solusinya lebih dari proposal arsitektur aplikasi daripada hanya beberapa kode .

Mereka mengusulkan 3 pola desain yang memungkinkan aplikasi untuk bekerja dalam sinkronisasi dengan server, terlepas dari keadaan aplikasi (itu akan bekerja bahkan jika pengguna menyelesaikan aplikasi, layar perubahan pengguna, aplikasi akan dihentikan, setiap keadaan yang mungkin lainnya di mana operasi data latar belakang dapat diinterupsi, ini meliputi itu)

Proposal tersebut dijelaskan dalam pidato aplikasi klien Android REST selama Google I / O 2010 oleh Virgil Dobjanschi. Panjangnya 1 jam, tetapi sangat layak untuk ditonton.

Dasarnya adalah abstrak operasi jaringan ke Serviceyang bekerja secara independen ke Activitydalam aplikasi. Jika Anda bekerja dengan database, penggunaan ContentResolverdan Cursorakan memberi Anda pola Observer out-of-the-box yang nyaman untuk memperbarui UI tanpa logika tambahan, setelah Anda memperbarui database lokal Anda dengan data jarak jauh yang diambil. Kode setelah operasi lainnya akan dijalankan melalui callback yang diteruskan ke Service(saya menggunakan ResultReceiversubkelas untuk ini).

Pokoknya, penjelasan saya sebenarnya cukup kabur, Anda harus menonton pidato itu.

Christopher Francisco
sumber
2

Meskipun jawaban Mark (CommonsWare) memang berfungsi untuk perubahan orientasi, namun gagal jika Aktivitas dihancurkan secara langsung (seperti dalam kasus panggilan telepon).

Anda dapat menangani perubahan orientasi DAN Peristiwa aktivitas yang jarang dihancurkan dengan menggunakan objek Aplikasi untuk referensi ASyncTask Anda.

Ada penjelasan yang sangat baik tentang masalah dan solusinya di sini :

Penghargaan sepenuhnya diberikan kepada Ryan untuk mengetahui yang satu ini.

Scott Biggs
sumber
1

Setelah 4 tahun Google menyelesaikan masalah dengan memanggil setRetainInstance (true) di Activity onCreate. Ini akan menjaga instance aktivitas Anda selama rotasi perangkat. Saya juga punya solusi sederhana untuk Android yang lebih lama.

Singagirl
sumber
1
Pengamat masalah orang terjadi karena Android menghancurkan kelas aktivitas di rotasi, ekstensi keyboard dan acara lainnya, tetapi tugas async masih menyimpan referensi untuk instance yang dihancurkan dan mencoba menggunakannya untuk pembaruan UI. Anda dapat menginstruksikan Android untuk tidak menghancurkan aktivitas baik secara nyata maupun pragmatis. Dalam hal ini referensi tugas async tetap valid dan tidak ada masalah yang diamati. Karena rotasi dapat memerlukan beberapa pekerjaan tambahan sebagai memuat ulang tampilan dan sebagainya, Google tidak merekomendasikan untuk menjaga aktivitas. Jadi Anda yang memutuskan.
Singagirl
Terima kasih, saya tahu tentang situasinya tetapi tidak tentang setRetainInstance (). Yang tidak saya mengerti adalah klaim Anda bahwa Google menggunakan ini untuk menyelesaikan masalah yang ditanyakan dalam pertanyaan. Bisakah Anda menautkan sumber informasi? Terima kasih.
jj_
onRetainNonConfigurationInstance () Fungsi ini disebut murni sebagai optimisasi, dan Anda tidak boleh bergantung padanya untuk dipanggil. <Dari sumber yang sama: developer.android.com/reference/android/app/…
Dhananjay M
0

Anda harus memanggil semua tindakan aktivitas menggunakan penangan aktivitas. Jadi jika Anda berada di utas Anda harus membuat Runnable dan memposting menggunakan Activitie's Handler. Kalau tidak, aplikasi Anda terkadang macet dengan pengecualian fatal.

xpepermint
sumber
0

Ini adalah solusi saya: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Pada dasarnya langkah-langkahnya adalah:

  1. Saya gunakan onSaveInstanceStateuntuk menyimpan tugas jika masih diproses.
  2. Di onCreatesaya mendapatkan tugas jika sudah disimpan.
  3. Di onPausesaya membuang ProgressDialogjika ditampilkan.
  4. Di onResumesaya tunjukkan ProgressDialogkalau tugas masih diproses.
Kena kau
sumber