Loader tidak digunakan lagi mulai Android 9 (API level 28). Opsi yang direkomendasikan untuk
menangani pemuatan data saat menangani siklus proses Activity
dan Fragment
adalah dengan menggunakan
kombinasi objek ViewModel
dan LiveData
.
Model tampilan tetap bertahan saat terjadi perubahan konfigurasi, seperti loader, tetapi dengan
lebih sedikit kode boilerplate. LiveData
menyediakan cara yang mendukung siklus proses untuk memuat data yang dapat Anda gunakan kembali
multi-tampilan. Anda juga dapat menggabungkan LiveData
menggunakan
MediatorLiveData
.
Kueri apa pun yang dapat diamati, seperti kueri dari
Database Room, dapat digunakan untuk mengamati perubahan
pada data.
ViewModel
dan LiveData
juga tersedia dalam situasi ketika Anda tidak memiliki akses
ke LoaderManager
, seperti di file
Service
. Menggunakan keduanya dalam
tandem menyediakan cara mudah untuk mengakses data yang dibutuhkan aplikasi Anda tanpa harus berurusan dengan UI
siklus proses. Untuk mempelajari LiveData
lebih lanjut, lihat
Ringkasan LiveData
. Untuk mempelajari lebih lanjut tentang
ViewModel
, lihat ringkasan ViewModel
.
Loader API memungkinkan Anda memuat data dari
penyedia konten
atau sumber data lainnya untuk ditampilkan di FragmentActivity
atau Fragment
.
Tanpa loader, beberapa masalah yang mungkin Anda temui meliputi hal berikut:
- Jika Anda mengambil data secara langsung dalam aktivitas atau fragmen, pengguna Anda mengalami kurangnya responsivitas karena kinerja yang berpotensi lambat kueri dari thread UI.
- Jika Anda mengambil data dari thread lain, mungkin dengan
AsyncTask
, maka Anda bertanggung jawab untuk mengelola thread itu dan UI thread melalui berbagai peristiwa siklus proses aktivitas atau fragmen, sepertionDestroy()
dan perubahan konfigurasi.
Loader mengatasi masalah ini dan menyertakan manfaat lainnya:
- Loader berjalan di thread terpisah untuk mencegah UI yang lambat atau tidak responsif.
- Loader menyederhanakan pengelolaan thread dengan menyediakan metode callback saat peristiwa terjadi.
- Loader mempertahankan dan meng-cache hasil ketika terjadi perubahan konfigurasi untuk mencegah duplikasi kueri.
- Loader bisa mengimplementasikan pengamat untuk memantau perubahan di
sumber data. Misalnya,
CursorLoader
akan otomatis mendaftarkanContentObserver
untuk memicu pemuatan ulang saat data berubah.
Ringkasan Loader API
Ada beberapa class dan antarmuka yang mungkin dilibatkan saat menggunakan loader di dalam aplikasi. Keduanya dirangkum dalam tabel berikut:
Class/Antarmuka | Deskripsi |
---|---|
LoaderManager |
Class abstrak yang terkait dengan FragmentActivity atau
Fragment untuk mengelola satu atau beberapa
Loader instance. Hanya ada satu
LoaderManager per aktivitas atau fragmen, tetapi
LoaderManager dapat mengelola beberapa loader.
Untuk mendapatkan Untuk mulai memuat data dari loader, panggil salah satu
|
LoaderManager.LoaderCallbacks |
Antarmuka ini berisi metode callback yang dipanggil saat
terjadi peristiwa loader. Antarmuka menetapkan tiga metode callback:
initLoader() atau
restartLoader() .
|
Loader |
Loader melakukan pemuatan data. Class ini bersifat abstrak dan berfungsi
sebagai kelas dasar untuk semua loader. Anda dapat langsung membuat subclass
Loader atau gunakan salah satu fitur bawaan berikut
subclass untuk menyederhanakan implementasi:
|
Bagian berikut menunjukkan cara menggunakannya dan antarmuka dalam aplikasi.
Menggunakan loader dalam aplikasi
Bagian ini menjelaskan cara menggunakan loader dalam aplikasi Android. Channel aplikasi yang menggunakan loader biasanya berisi hal berikut:
FragmentActivity
atauFragment
.- Instance
LoaderManager
. CursorLoader
untuk memuat data yang didukung olehContentProvider
. Atau, Anda dapat menerapkan subclass Anda sendiri dariLoader
atauAsyncTaskLoader
hingga memuat data dari beberapa sumber lainnya.- Implementasi untuk
LoaderManager.LoaderCallbacks
. Di sinilah Anda membuat loader baru dan mengelola referensi ke loader yang ada oleh banyak loader. - Cara untuk menampilkan data loader, seperti
SimpleCursorAdapter
. - Sumber data, seperti
ContentProvider
, saat menggunakanCursorLoader
.
Memulai loader
LoaderManager
mengelola satu atau beberapa instance Loader
dalam FragmentActivity
atau
Fragment
. Hanya ada satu LoaderManager
per aktivitas atau fragmen.
Anda biasanya
lakukan inisialisasi Loader
dalam metode onCreate()
aktivitas atau dalam metode
Metode onCreate()
. Anda
lakukan seperti berikut:
Kotlin
supportLoaderManager.initLoader(0, null, this)
Java
// Prepare the loader. Either re-connect with an existing one, // or start a new one. getSupportLoaderManager().initLoader(0, null, this);
Metode initLoader()
mengambil
parameter berikut:
- ID unik yang mengidentifikasi loader. Dalam contoh ini, ID-nya adalah
0
. - Argumen opsional untuk dipasok ke loader di
konstruksi (
null
dalam contoh ini). - Implementasi
LoaderManager.LoaderCallbacks
, yang panggilanLoaderManager
untuk melaporkan peristiwa loader. Di sini misalnya, class lokal mengimplementasikan antarmukaLoaderManager.LoaderCallbacks
sehingga meneruskan referensi menjadi dirinya sendiri,this
.
Panggilan initLoader()
memastikan bahwa loader
diinisialisasi dan aktif. Ia memiliki dua kemungkinan hasil:
- Jika loader yang disebutkan oleh ID sudah ada, loader yang terakhir dibuat akan digunakan kembali.
- Jika loader yang ditentukan oleh ID tidak ada,
initLoader()
memicu MetodeLoaderManager.LoaderCallbacks
onCreateLoader()
. Di sinilah Anda mengimplementasikan kode untuk membuat instance dan mengembalikan loader baru. Untuk diskusi selengkapnya, lihat bagian tentangonCreateLoader
.
Dalam kedua kasus tersebut, LoaderManager.LoaderCallbacks
yang diberikan
dikaitkan dengan loader dan dipanggil ketika
perubahan status loader. Jika, pada saat panggilan ini, pemanggil berada dalam
keadaan dimulai dan loader yang diminta sudah ada serta telah membuat
data, sistem akan memanggil onLoadFinished()
selama initLoader()
. Anda harus siap untuk hal ini. Untuk diskusi selengkapnya tentang callback ini, lihat bagian tentang
onLoadFinished
.
Metode initLoader()
akan menampilkan Loader
yang dibuat,
tetapi Anda tidak perlu
menangkap referensi ke sana. LoaderManager
mengelola
masa pakai loader secara otomatis. LoaderManager
memulai dan menghentikan pemuatan bila perlu dan menjaga status loader
dan konten yang terkait.
Seperti yang tersirat di sini, Anda jarang berinteraksi dengan loader
secara langsung.
Anda paling sering menggunakan metode LoaderManager.LoaderCallbacks
untuk melakukan intervensi dalam pemuatan
proses saat peristiwa tertentu terjadi. Untuk diskusi selengkapnya mengenai topik ini, lihat bagian Menggunakan callback Interface.
Memulai ulang loader
Saat Anda menggunakan initLoader()
, sebagai
ditunjukkan di bagian sebelumnya, loader yang sudah ada menggunakan ID yang ditetapkan jika ada.
Jika tidak ada, Anda akan membuatnya. Namun terkadang Anda ingin
membuang data lama Anda
dan memulai dari awal.
Untuk menghapus data lama, gunakan restartLoader()
. Misalnya,
implementasi SearchView.OnQueryTextListener
mulai ulang
loader saat kueri pengguna berubah. Loader perlu dimulai ulang
bahwa filter tersebut dapat menggunakan filter
penelusuran yang telah direvisi untuk melakukan kueri baru.
Kotlin
fun onQueryTextChanged(newText: String?): Boolean { // Called when the action bar search text has changed. Update // the search filter and restart the loader to do a new query // with this filter. curFilter = if (newText?.isNotEmpty() == true) newText else null supportLoaderManager.restartLoader(0, null, this) return true }
Java
public boolean onQueryTextChanged(String newText) { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = !TextUtils.isEmpty(newText) ? newText : null; getSupportLoaderManager().restartLoader(0, null, this); return true; }
Menggunakan callback Interface
LoaderManager.LoaderCallbacks
adalah antarmuka callback
yang memungkinkan klien berinteraksi dengan LoaderManager
.
Loader, khususnya CursorLoader
, diharapkan
mempertahankan data mereka
setelah dihentikan. Hal ini memungkinkan aplikasi menjaga
data di seluruh metode onStop()
dan onStart()
aktivitas atau fragmen, sehingga
saat pengguna kembali ke aplikasi, mereka tidak perlu menunggu data
memuat ulang.
Anda menggunakan metode LoaderManager.LoaderCallbacks
untuk mengetahui waktu membuat loader baru dan memberi tahu aplikasi kapan loader perlu dibuat
waktu untuk berhenti menggunakan data loader.
LoaderManager.LoaderCallbacks
menyertakannya
metode:
onCreateLoader()
: membuat instance dan menampilkanLoader
baru untuk ID yang diberikan.
-
onLoadFinished()
: dipanggil bila loader yang dibuat sebelumnya telah menyelesaikan pemuatannya.
onLoaderReset()
: dipanggil bila loader yang dibuat sebelumnya sedang di-reset, sehingga membuat data tidak tersedia.
Metode ini dijelaskan lebih detail dalam bagian berikutnya.
onCreateLoader
Saat Anda mencoba mengakses loader, seperti melalui initLoader()
, loader ini akan memeriksa apakah
loader yang ditetapkan oleh ID ada. Jika tidak, metode LoaderManager.LoaderCallbacks
akan dipicu onCreateLoader()
. Ini
adalah tempat Anda membuat loader baru. Biasanya ini adalah CursorLoader
, tetapi Anda dapat mengimplementasikan subclass Loader
Anda sendiri.
Dalam contoh berikut, onCreateLoader()
membuat CursorLoader
menggunakan metode konstruktornya, yang
memerlukan set informasi lengkap yang diperlukan untuk melakukan kueri ke ContentProvider
. Khususnya, diperlukan hal berikut:
- uri: URI untuk konten yang akan diambil.
- projection: daftar kolom yang akan ditampilkan. Lulus
null
menampilkan semua kolom, sehingga tidak efisien. - selection: filter yang mendeklarasikan baris mana yang akan ditampilkan,
diformat sebagai klausa SQL WHERE (tidak termasuk WHERE itu sendiri). Lulus
null
menampilkan semua baris untuk URI yang ditentukan. - selectionArgs: jika Anda menyertakan ?s dalam pilihan, baris tersebut diganti dengan nilai dari selectionArgs sesuai dengan urutan kemunculannya yang dipilih. Nilai-nilai diikat sebagai string.
- sortOrder: cara mengurutkan baris, yang diformat sebagai SQL
Klausa ORDER BY (tidak termasuk ORDER BY itu sendiri). Melewati
null
menggunakan tata urutan default, yang mungkin tidak berurutan.
Kotlin
// If non-null, this is the current filter the user has provided. private var curFilter: String? = null ... override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. val baseUri: Uri = if (curFilter != null) { Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, Uri.encode(curFilter)) } else { ContactsContract.Contacts.CONTENT_URI } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. val select: String = "((${Contacts.DISPLAY_NAME} NOTNULL) AND (" + "${Contacts.HAS_PHONE_NUMBER}=1) AND (" + "${Contacts.DISPLAY_NAME} != ''))" return (activity as? Context)?.let { context -> CursorLoader( context, baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, "${Contacts.DISPLAY_NAME} COLLATE LOCALIZED ASC" ) } ?: throw Exception("Activity cannot be null") }
Java
// If non-null, this is the current filter the user has provided. String curFilter; ... public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (curFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(curFilter)); } else { baseUri = Contacts.CONTENT_URI; } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); }
onLoadFinished
Metode ini dipanggil bila loader yang dibuat sebelumnya menyelesaikan pemuatannya. Metode ini dijamin akan dipanggil sebelum perilisan data terakhir yang disediakan untuk loader ini. Pada tahap ini, hapus semua penggunaan data lama, karena data itu akan dirilis. Tapi jangan lepaskan data diri Anda sendiri—loader memilikinya dan menanganinya.
Loader melepaskan data setelah mengetahui bahwa aplikasi tidak lagi
menggunakannya. Misalnya, jika data adalah kursor dari CursorLoader
,
jangan memanggil close()
sendiri. Jika kursor
ditempatkan di CursorAdapter
, gunakan metode swapCursor()
sehingga
Cursor
lama tidak ditutup, seperti yang ditunjukkan dalam contoh berikut:
Kotlin
private lateinit var adapter: SimpleCursorAdapter ... override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) adapter.swapCursor(data) }
Java
// This is the Adapter being used to display the list's data. SimpleCursorAdapter adapter; ... public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) adapter.swapCursor(data); }
onLoaderReset
Metode ini dipanggil bila loader yang dibuat sebelumnya sedang direset, sehingga membuat datanya tidak tersedia. Callback ini memungkinkan Anda mengetahui kapan data tersebut akan dirilis sehingga Anda dapat menghapus referensinya.
Implementasi ini memanggil
swapCursor()
dengan nilai null
:
Kotlin
private lateinit var adapter: SimpleCursorAdapter ... override fun onLoaderReset(loader: Loader<Cursor>) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. adapter.swapCursor(null) }
Java
// This is the Adapter being used to display the list's data. SimpleCursorAdapter adapter; ... public void onLoaderReset(Loader<Cursor> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. adapter.swapCursor(null); }
Contoh
Sebagai contoh, berikut adalah implementasi lengkap dari Fragment
yang menampilkan ListView
yang berisi
hasil kueri terhadap penyedia konten kontak. Fungsi ini menggunakan CursorLoader
untuk mengelola kueri pada penyedia.
Karena contoh ini berasal dari aplikasi untuk mengakses kontak pengguna,
manifes harus menyertakan izin
READ_CONTACTS
.
Kotlin
private val CONTACTS_SUMMARY_PROJECTION: Array<String> = arrayOf( Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, Contacts.CONTACT_PRESENCE, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY ) class CursorLoaderListFragment : ListFragment(), SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> { // This is the Adapter being used to display the list's data. private lateinit var mAdapter: SimpleCursorAdapter // If non-null, this is the current filter the user has provided. private var curFilter: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Prepare the loader. Either re-connect with an existing one, // or start a new one. loaderManager.initLoader(0, null, this) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Give some text to display if there is no data. In a real // application, this would come from a resource. setEmptyText("No phone numbers") // We have a menu item to show in action bar. setHasOptionsMenu(true) // Create an empty adapter we will use to display the loaded data. mAdapter = SimpleCursorAdapter(activity, android.R.layout.simple_list_item_2, null, arrayOf(Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS), intArrayOf(android.R.id.text1, android.R.id.text2), 0 ) listAdapter = mAdapter } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { // Place an action bar item for searching. menu.add("Search").apply { setIcon(android.R.drawable.ic_menu_search) setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) actionView = SearchView(activity).apply { setOnQueryTextListener(this@CursorLoaderListFragment) } } } override fun onQueryTextChange(newText: String?): Boolean { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = if (newText?.isNotEmpty() == true) newText else null loaderManager.restartLoader(0, null, this) return true } override fun onQueryTextSubmit(query: String): Boolean { // Don't care about this. return true } override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) { // Insert desired behavior here. Log.i("FragmentComplexList", "Item clicked: $id") } override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. val baseUri: Uri = if (curFilter != null) { Uri.withAppendedPath(Contacts.CONTENT_URI, Uri.encode(curFilter)) } else { Contacts.CONTENT_URI } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. val select: String = "((${Contacts.DISPLAY_NAME} NOTNULL) AND (" + "${Contacts.HAS_PHONE_NUMBER}=1) AND (" + "${Contacts.DISPLAY_NAME} != ''))" return (activity as? Context)?.let { context -> CursorLoader( context, baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, "${Contacts.DISPLAY_NAME} COLLATE LOCALIZED ASC" ) } ?: throw Exception("Activity cannot be null") } override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data) } override fun onLoaderReset(loader: Loader<Cursor>) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. mAdapter.swapCursor(null) } }
Java
public static class CursorLoaderListFragment extends ListFragment implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> { // This is the Adapter being used to display the list's data. SimpleCursorAdapter mAdapter; // If non-null, this is the current filter the user has provided. String curFilter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Give some text to display if there is no data. In a real // application, this would come from a resource. setEmptyText("No phone numbers"); // We have a menu item to show in action bar. setHasOptionsMenu(true); // Create an empty adapter we will use to display the loaded data. mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_2, null, new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS }, new int[] { android.R.id.text1, android.R.id.text2 }, 0); setListAdapter(mAdapter); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // Place an action bar item for searching. MenuItem item = menu.add("Search"); item.setIcon(android.R.drawable.ic_menu_search); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); SearchView sv = new SearchView(getActivity()); sv.setOnQueryTextListener(this); item.setActionView(sv); } public boolean onQueryTextChange(String newText) { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = !TextUtils.isEmpty(newText) ? newText : null; getLoaderManager().restartLoader(0, null, this); return true; } @Override public boolean onQueryTextSubmit(String query) { // Don't care about this. return true; } @Override public void onListItemClick(ListView l, View v, int position, long id) { // Insert desired behavior here. Log.i("FragmentComplexList", "Item clicked: " + id); } // These are the Contacts rows that we will retrieve. static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, Contacts.CONTACT_PRESENCE, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY, }; public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (curFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(curFilter)); } else { baseUri = Contacts.CONTENT_URI; } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data); } public void onLoaderReset(Loader<Cursor> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. mAdapter.swapCursor(null); } }
Contoh lainnya
Contoh berikut ini mengilustrasikan cara menggunakan loader:
- LoaderCursor: versi lengkap dari cuplikan sebelumnya.
- Mengambil daftar kontak:
panduan yang menggunakan
CursorLoader
untuk mengambil data dari penyedia kontak. - LoaderThrottle: contoh cara menggunakan throttling untuk mengurangi jumlah kueri yang dilakukan penyedia konten ketika datanya berubah.