
Gambar tersebut menunjukkan pikiran pertama dari pembaca yang bertanya-tanya apa yang dapat ditulis tentang tugas sederhana seperti menampilkan dialog. Manajer berpikir dengan cara yang sama: "Tidak ada yang rumit di sini, Vasya kami akan lakukan dalam 5 menit." Tentu saja, saya melebih-lebihkan, tetapi pada kenyataannya semuanya tidak sesederhana seperti yang terlihat pada pandangan pertama. Apalagi jika kita berbicara tentang Android.
Jadi, 2019 ada di halaman, dan kami masih tidak tahu cara menampilkan dialog dengan benar .
Mari kita lakukan secara berurutan dan mulai dengan pernyataan masalah:
Diperlukan untuk menampilkan dialog sederhana dengan teks untuk mengonfirmasi tindakan dan tombol "konfirmasi / batal". Dengan mengklik tombol “konfirmasi” - lakukan tindakan, dengan tombol “batal” - tutup dialog.
Solusi Dahi
Saya akan menyebut metode ini junior, karena ini bukan pertama kalinya saya menemukan kesalahpahaman mengapa Anda tidak bisa menggunakan AlertDialog, seperti yang ditunjukkan di bawah ini:
AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { dialog, which ->
Cara yang cukup umum untuk pengembang pemula, sangat jelas dan intuitif. Tetapi, seperti dalam banyak kasus ketika bekerja dengan Android, metode ini sepenuhnya salah. Tiba-tiba, kami mendapatkan kebocoran memori, cukup hidupkan perangkat, dan Anda akan melihat kesalahan berikut dalam log:
stacktrace E/WindowManager: android.view.WindowLeaked: Activity com.example.testdialog.MainActivity has leaked window DecorView@71b5789[MainActivity] that was originally added here at android.view.ViewRootImpl.<init>(ViewRootImpl.java:511) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at android.app.Dialog.show(Dialog.java:329) at com.example.testdialog.MainActivity.onCreate(MainActivity.kt:27) at android.app.Activity.performCreate(Activity.java:7144) at android.app.Activity.performCreate(Activity.java:7135) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2931) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6718) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Di Stackoverflow, pertanyaan tentang masalah ini adalah salah satu yang paling populer. Singkatnya, masalahnya adalah kita menunjukkan dialog atau tidak menutup dialog setelah aktivasi selesai.
Anda dapat, tentu saja, mengabaikan panggilan pada dialog di aktivitas onPause atau onDestroy, seperti yang disarankan dalam jawaban dengan referensi . Tapi ini bukan yang kita butuhkan. Kami ingin dialog pulih setelah memutar perangkat.
Cara yang ketinggalan jaman
Sebelum fragmen muncul di Android, dialog seharusnya ditampilkan melalui panggilan ke metode aktivasi showDialog . Dalam hal ini, aktivitas mengelola siklus hidup dialog dengan benar dan mengembalikannya setelah berbelok. Penciptaan dialog itu sendiri harus diimplementasikan dalam panggilan balik onCreateDialog:
public class MainActivity extends Activity { private static final int CONFIRMATION_DIALOG_ID = 1;
Sangat tidak nyaman bahwa Anda harus memulai pengidentifikasi dialog dan meneruskan parameter melalui Bundle. Dan kita masih bisa mendapatkan masalah "jendela bocor" jika kita mencoba menampilkan dialog setelah memanggil onDestroy pada aktivitas. Ini dimungkinkan, misalnya, ketika mencoba menampilkan kesalahan setelah operasi asinkron.
Secara umum, masalah ini tipikal untuk Android, ketika Anda perlu melakukan sesuatu setelah operasi asinkron, dan aktivitas atau fragmen sudah hancur pada saat itu. Ini mungkin mengapa pola MV * lebih populer di komunitas Android daripada di antara pengembang iOS.
Metode dari dokumentasi
Fragmen muncul di Android Honeycomb , dan metode yang dijelaskan di atas sudah tidak digunakan lagi, dan metode showDialog dari aktivitas ditandai sebagai sudah usang. Tidak, AlertDialog tidak ketinggalan zaman, karena banyak yang salah. Baru saja ada DialogFragment , yang membungkus objek dialog dan mengontrol siklus hidupnya.
Cuplikan asli juga tidak digunakan lagi sejak API 28. Sekarang Anda harus menggunakan hanya implementasi dari Perpustakaan Dukungan (AndroidX).
Mari selesaikan tugas kita, sebagaimana ditentukan oleh dokumentasi resmi :
- Pertama, Anda perlu mewarisi dari DialogFragment dan mengimplementasikan pembuatan dialog dalam metode onCreateDialog.
- Jelaskan antarmuka acara dialog dan instantiate pendengar dalam metode onAttach.
- Menerapkan antarmuka acara dialog dalam suatu kegiatan atau fragmen.
Jika pembaca tidak begitu jelas mengapa pendengar tidak dapat melewati konstruktor, maka dia dapat membaca lebih lanjut tentang ini di sini
Kode fragmen dialog:
class ConfirmationDialogFragment : DialogFragment() { interface ConfirmationListener { fun confirmButtonClicked() fun cancelButtonClicked() } private lateinit var listener: ConfirmationListener override fun onAttach(context: Context?) { super.onAttach(context) try {
Kode Aktivasi:
class MainActivity : AppCompatActivity(), ConfirmationListener { private fun showConfirmationDialog() { ConfirmationDialogFragment() .show(supportFragmentManager, "ConfirmationDialogFragmentTag") } override fun confirmButtonClicked() {
Kode yang cukup ternyata, kan?
Sebagai aturan, ada beberapa jenis MVP dalam proyek ini, tetapi saya memutuskan bahwa panggilan presenter dapat dihilangkan dalam kasus ini. Dalam contoh di atas, ada baiknya menambahkan metode statis untuk membuat dialogInstance baru dan meneruskan parameter ke argumen fragmen, semua seperti yang diharapkan.
Dan ini semua agar dialog bersembunyi tepat waktu dan dikembalikan dengan benar. Tidak mengherankan bahwa pertanyaan seperti itu muncul di Stackoverflow: satu dan dua .
Menemukan solusi sempurna
Keadaan saat ini tidak sesuai dengan kami, dan kami mulai mencari cara untuk membuat bekerja dengan dialog lebih nyaman. Ada perasaan bahwa Anda dapat membuatnya lebih mudah, hampir seperti pada metode pertama.
Berikut ini adalah pertimbangan yang memandu kami:
- Apakah saya perlu menyimpan dan memulihkan dialog setelah mematikan proses aplikasi?
Dalam kebanyakan kasus, ini tidak diperlukan, seperti dalam contoh kami, ketika Anda perlu menunjukkan pesan sederhana atau bertanya sesuatu. Dialog semacam itu relevan sampai perhatian pengguna hilang. Jika Anda mengembalikannya setelah lama tidak ada dalam aplikasi, pengguna akan kehilangan konteks dengan tindakan yang direncanakan. Karena itu, Anda hanya perlu mendukung belokan perangkat dan menangani siklus hidup dialog dengan benar. Jika tidak, dari pergerakan perangkat yang canggung, pengguna dapat kehilangan pesan yang baru saja dibuka tanpa membacanya. - Saat menggunakan DialogFragment, terlalu banyak kode boilerplate muncul, kesederhanaan hilang. Oleh karena itu, alangkah baiknya untuk menghilangkan fragmen sebagai pembungkus dan menggunakan Dialog secara langsung . Untuk melakukan ini, Anda harus menyimpan keadaan dialog untuk menampilkannya lagi setelah membuat kembali Lihat dan menyembunyikannya ketika Tampilan mati.
- Semua orang terbiasa memahami tampilan dialog sebagai sebuah tim, terutama jika Anda hanya bekerja dengan MVP. Tugas pemulihan negara selanjutnya dipikul oleh Manajer Fragment. Tetapi Anda dapat melihat situasi ini secara berbeda dan mulai menganggap dialog sebagai suatu keadaan . Ini jauh lebih nyaman ketika bekerja dengan pola PM atau MVVM.
- Mengingat sebagian besar aplikasi sekarang menggunakan pendekatan reaktif, ada kebutuhan untuk dialog menjadi reaktif . Tugas utama bukanlah untuk memutus rantai yang mengawali tampilan dialog, dan untuk melampirkan aliran peristiwa yang reaktif untuk mendapatkan hasil darinya. Ini sangat nyaman di sisi PresentationModel / ViewModel ketika Anda memanipulasi beberapa aliran data.
Kami memperhitungkan semua persyaratan di atas dan menemukan cara untuk menampilkan dialog secara reaktif, yang berhasil kami implementasikan di perpustakaan RxPM kami (ada artikel terpisah tentang hal itu).
Solusinya sendiri tidak memerlukan perpustakaan dan dapat dilakukan secara terpisah. Dipandu oleh ide "dialog as state", Anda dapat mencoba membangun solusi berdasarkan ViewModel dan LiveData yang trendi. Tetapi saya akan menyerahkan hak ini kepada pembaca, dan kemudian kita akan berbicara tentang solusi yang sudah jadi dari perpustakaan.
Metode reaktif
Saya akan menunjukkan bagaimana tugas awal diselesaikan dalam RxPM, tetapi pertama-tama beberapa kata tentang konsep-konsep kunci dari perpustakaan:
- PresentationModel - menyimpan keadaan reaksi, berisi UI-logika, selamat dari belokan.
- Negara adalah keadaan reaktif. Anda dapat menganggapnya sebagai pembungkus BehaviorRelay.
- Action - pembungkus di atas PublishRelay, berfungsi untuk mentransfer acara dari View ke PresentationModel.
- Negara dan Tindakan telah diamati dan konsumen.
Kelas DialogControl bertanggung jawab atas keadaan dialog. Ini memiliki dua parameter: yang pertama untuk tipe data yang harus ditampilkan dalam dialog, yang kedua untuk tipe hasil. Dalam contoh kami, tipe data akan menjadi Unit, tetapi bisa berupa pesan kepada pengguna atau tipe lainnya.
DialogControl memiliki metode berikut:
show(data: T)
- hanya memberikan perintah untuk ditampilkan.showForResult(data: T): Maybe<R>
- menampilkan dialog dan membuka aliran untuk mendapatkan hasilnya.sendResult(result: R)
- mengirim hasilnya, dipanggil dari sisi View.dismiss()
- hanya menyembunyikan dialog.
DialogControl menyimpan status - apakah ada dialog di layar atau tidak (Ditampilkan / Tidak Ada). Ini adalah tampilannya di kode kelas:
class DialogControl<T, R> internal constructor(pm: PresentationModel) { val displayed = pm.State<Display>(Absent) private val result = pm.Action<R>() sealed class Display { data class Displayed<T>(val data: T) : Display() object Absent : Display() }
Buat PresentationModel sederhana:
class SamplePresentationModel : PresentationModel() { enum class ConfirmationDialogResult { CONFIRMED, CANCELED }
Harap perhatikan bahwa pemrosesan klik, konfirmasi konfirmasi, dan pemrosesan tindakan diimplementasikan dalam rantai yang sama. Ini memungkinkan Anda untuk membuat kode fokus dan tidak menyebarkan logika di beberapa panggilan balik.
Selanjutnya, kita cukup mengikat DialogControl ke View menggunakan ekstensi bindTo.
Kami mengumpulkan AlertDialog yang biasa, dan mengirim hasilnya melalui sendResult:
class SampleActivity : PmSupportActivity<SamplePresentationModel>() { override fun providePresentationModel() = SamplePresentationModel()
Dalam skenario tipikal, hal seperti ini terjadi di bawah tenda:
- Kami klik pada tombol, acara melalui Aksi "buttonClicks" masuk ke PresentationModel.
- Untuk acara ini, kami meluncurkan tampilan dialog melalui panggilan ke showForResult.
- Akibatnya, keadaan di DialogControl berubah dari Absen ke Ditampilkan.
- Ketika acara yang ditampilkan diterima, lambda yang kami lewati dalam ikatan bindTo disebut. Objek dialog dibuat di dalamnya, yang kemudian ditampilkan.
- Pengguna menekan tombol Konfirmasi, pendengar menyala, dan hasil klik dikirim ke DialogControl dengan memanggil sendResult.
- Selanjutnya, hasilnya jatuh ke dalam "Aksi hasil" internal, dan status dari Perubahan yang ditampilkan ke Absen.
- Ketika acara Absen diterima, dialog saat ini ditutup.
- Acara dari "hasil" Tindakan jatuh ke aliran yang dibuka oleh panggilan untuk showForResult dan diproses oleh rantai di PresentationModel.
Perlu dicatat bahwa dialog ditutup bahkan ketika View dilepaskan dari PresentationModel. Dalam hal ini, statusnya tetap Ditampilkan. Itu akan diterima pada ikatan berikutnya dan dialog akan dipulihkan.
Seperti yang Anda lihat, kebutuhan untuk DialogFragment hilang. Dialog ditampilkan ketika tampilan dilampirkan ke PresentationModel dan disembunyikan ketika tampilan dilepaskan. Karena kenyataan bahwa negara disimpan dalam DialogControl, yang pada gilirannya disimpan dalam PresentationModel, dialog dikembalikan setelah perangkat diputar.
Tulis dialog dengan benar
Kami telah memeriksa beberapa cara untuk menampilkan dialog. Jika Anda masih menunjukkan cara pertama, maka saya mohon, jangan lakukan lagi. Bagi pecinta MVP tidak ada yang tersisa selain menggunakan metode standar, yang dijelaskan dalam dokumentasi resmi. Sayangnya, kecenderungan imperatifitas dari pola ini tidak memungkinkan untuk melakukan sebaliknya. Yah, saya sarankan penggemar RxJava melihat lebih dekat pada metode reaktif dan perpustakaan RxPM kami.