Os carregadores estão obsoletos desde o Android P (API de nível 28). A opção recomendada para lidar com o carregamento de dados ao gerenciar os ciclos de vida de Activity e Fragment é usar uma combinação de ViewModels
e LiveData
. Os ViewModels sobrevivem a alterações de configuração, como carregadores, mas com menos código clichê. O LiveData fornece uma maneira consciente de carregar dados que pode ser reutilizada em vários ViewModels. Além disso, você pode combinar o LiveData usando MediatorLiveData
e usar qualquer consulta observável, como as do banco de dados Room, para observar alterações nos dados. As opções ViewModels e LiveData também estão disponíveis quando você não tem acesso a LoaderManager
, como em Service
. Usar os dois juntos é uma maneira fácil de acessar os dados necessários ao seu aplicativo sem ter que lidar com o ciclo de vida da IU. Para saber mais sobre o LiveData, consulte o guia do LiveData. Já para ver mais informações sobre ViewModels, leia o guia do ViewModel.
A Loader API permite que você carregue dados de um provedor de conteúdo ou de outra fonte de dados para exibir em um FragmentActivity
ou Fragment
. Se você não entender por que precisa da Loader API para executar essa operação aparentemente simples, avalie alguns dos problemas que poderá encontrar sem usar os carregadores:
- Se você buscar os dados diretamente em uma atividade ou um fragmento, seus usuários perceberão problemas na capacidade de resposta devido à realização de consultas potencialmente lentas do thread de IU.
- Ao buscar os dados a partir de outro thread, talvez usando
AsyncTask
, você será responsável por gerenciar o thread e o thread de IU por meio de vários eventos de ciclo de vida de uma atividade ou um fragmento, comoonDestroy()
, bem como as alterações das configurações.
Os carregadores resolvem esses problemas e incluem outros benefícios. Por exemplo:
- Os carregadores são executados em threads separados para impedir interfaces do usuário instáveis ou que não respondem.
- Os carregadores simplificam o gerenciamento dos threads fornecendo métodos de callback quando há eventos.
- Os carregadores persistem e armazenam em cache os resultados nas alterações de configuração para evitar consultas duplicadas.
- Os carregadores podem implementar um observador para monitorar as alterações na fonte de dados subjacente. Por exemplo,
CursorLoader
registra automaticamente umContentObserver
para acionar uma atualização quando houver alteração de dados.
Resumo da Loader API
Há várias classes e interfaces que podem ser envolvidas no uso de carregadores em um aplicativo. Elas são resumidas nesta tabela:
Classe/interface | Descrição |
---|---|
LoaderManager |
É uma classe abstrata associada a FragmentActivity ou Fragment para gerenciar uma ou mais instâncias de Loader . Só há um LoaderManager por atividade ou fragmento, mas um LoaderManager pode gerenciar vários carregadores.
Para receber LoaderManager, chame Para começar o carregamento de dados a partir de um carregador, chame |
LoaderManager.LoaderCallbacks |
Essa interface contém métodos de callback que são chamados quando ocorrem eventos do carregador. A interface define três métodos de callback:
initLoader() ou restartLoader() .
|
Loader |
Os carregadores executam o carregamento de dados. Essa classe é abstrata e serve como a classe base para todos os carregadores. Você pode usar diretamente a subclasse Loader ou uma das seguintes subclasses integradas para simplificar a implementação:
|
As seguintes seções mostram como usar essas classes e interfaces em um aplicativo.
Uso de carregadores em um aplicativo
Esta seção descreve como usar os carregadores em um aplicativo do Android. Um aplicativo que usa os carregadores geralmente inclui o seguinte:
- Uma
FragmentActivity
ou umFragment
. - Uma instância de
LoaderManager
. - Um
CursorLoader
para carregar dados baseados em umContentProvider
. Como alternativa, é possível implementar a própria subclasse deLoader
ouAsyncTaskLoader
para carregar dados de outra origem. - Faça uma implementação para
LoaderManager.LoaderCallbacks
. Aqui é possível criar novos carregadores e gerenciar as referências a carregadores existentes. - Uma maneira de exibir os dados do carregador, como um
SimpleCursorAdapter
. - Uma fonte de dados, como um
ContentProvider
, ao usarCursorLoader
.
Início de um carregador
O LoaderManager
gerencia uma ou mais instâncias de Loader
dentro de uma FragmentActivity
ou um Fragment
. Há somente um LoaderManager
por atividade ou fragmento.
Em geral, um Loader
é inicializado dentro do método onCreate()
da atividade ou dentro do método onActivityCreated()
do fragmento. Faça isso da seguinte maneira:
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);
O método initLoader()
recebe os seguintes parâmetros:
- Um código exclusivo que identifica o carregador. Nesse exemplo, o código é 0.
- Argumentos opcionais para fornecer ao carregador em construção (
null
nesse exemplo). - Uma implementação de
LoaderManager.LoaderCallbacks
, queLoaderManager
chama para relatar eventos do carregador. Nesse exemplo, a classe local implementa a interface deLoaderManager.LoaderCallbacks
, para que ela passe uma referência para si,this
.
A chamada de initLoader()
garante que o carregador foi inicializado e que está ativo. Ela tem dois possíveis resultados:
- Se o carregador especificado pelo código já existir, o último carregador criado será usado novamente.
- Se o carregador especificado pelo código não existir,
initLoader()
acionará o métodoLoaderManager.LoaderCallbacks
onCreateLoader()
. É aqui que você implementa o código para instanciar e retornar um novo carregador. Para mais informações, veja a seção onCreateLoader.
Em qualquer um dos casos, a implementação de LoaderManager.LoaderCallbacks
fornecida é associada ao carregador e será chamada quando o estado do carregador mudar. Se, no momento dessa chamada, o autor dela estiver no estado inicializado e o carregador solicitado já existir e tiver gerado seus dados, o sistema chamará onLoadFinished()
imediatamente (durante initLoader()
), e você deverá estar preparado para tais situações. Consulte onLoadFinished para ver mais informações sobre esse callback.
O método initLoader()
retorna o Loader
que é criado, mas você não precisará capturar uma referência para ele. O LoaderManager
gerencia a vida do carregador automaticamente. O LoaderManager
inicia e interrompe o carregamento quando necessário, além de manter o estado do carregador e do conteúdo associado. À medida que isso ocorre, você raramente interage com os carregadores diretamente. Para ver um exemplo de métodos para aprimorar o comportamento de um carregador, consulte a amostra de LoaderThrottle. Em geral, os métodos LoaderManager.LoaderCallbacks
são usados para intervir no processo de carregamento quando determinados eventos ocorrem. Para ver mais informações sobre esse assunto, consulte Uso dos callbacks de LoaderManager.
Reinício de um carregador
Ao usar initLoader()
, como mostrado acima, ele usará um carregador existente com o código especificado, se houver. Caso contrário, um código será criado. No entanto, às vezes, você quer descartar os dados antigos e começar do início.
Para descartar os dados antigos, use restartLoader()
. Por exemplo, essa implementação de SearchView.OnQueryTextListener
reinicia o carregador quando a consulta do usuário é alterada. O carregador precisa ser reiniciado para que possa usar o filtro de busca revisado para realizar uma nova consulta:
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; }
Uso dos callbacks de LoaderManager
LoaderManager.LoaderCallbacks
é uma interface de callbacks que permite que um cliente interaja com o LoaderManager
.
Os carregadores, em determinado CursorLoader
, devem reter os dados após serem interrompidos. Isso permite que os aplicativos mantenham os dados dos métodos onStop()
e onStart()
do fragmento ou da atividade para que os usuários não tenham que esperar a atualização dos dados quando voltarem a um aplicativo. Você usa os métodos LoaderManager.LoaderCallbacks
para saber quando deve criar um novo carregador e para dizer ao aplicativo quando deve interromper o uso dos dados de um carregador.
LoaderManager.LoaderCallbacks
inclui estes métodos:
onCreateLoader()
instancia e retorna um novoLoader
para o código fornecido.
-
onLoadFinished()
é chamado quando um carregador criado anteriormente termina seu carregamento.
onLoaderReset()
é chamado quando um carregador criado anteriormente é redefinido, tornando os dados indisponíveis.
Esses métodos são descritos com mais detalhes nas seções seguintes.
onCreateLoader
Ao tentar acessar um carregador (por exemplo, por meio de initLoader()
), ele verifica se o carregador especificado pelo código existe. Caso contrário, é acionado o método LoaderManager.LoaderCallbacks
onCreateLoader()
. É aqui que você pode criar um novo carregador. Normalmente, o carregador será um CursorLoader
, mas você pode implementar sua própria subclasse Loader
.
Neste exemplo, o método de callback onCreateLoader()
cria um CursorLoader
. Você precisa compilar CursorLoader
usando o método construtor, que exige um conjunto completo de informações necessárias para realizar uma consulta ao ContentProvider
. Especificamente, ele precisa de:
- uri — a URI do conteúdo a ser recuperado.
- projection — uma lista de quais colunas devem ser retornadas. Passar
null
retornará todas as colunas, o que é ineficiente. - selection — um filtro que declara quais linhas devem retornar, formatado por uma cláusula SQL WHERE (excluindo WHERE). Passar
null
retornará todas as linhas do URI em questão. - selectionArgs — é possível incluir interrogações na seleção, que serão substituídas pelos valores de selectionArgs, na ordem em que aparecem na seleção. Os valores serão vinculados como Strings.
- sortOrder — como ordenar as linhas, formatadas em uma cláusula SQL ORDER BY (excluindo ORDER BY). Passar
null
usará a ordem de classificação padrão, que pode ser desordenada.
Por exemplo:
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
Esse método é chamado quando um carregador criado anteriormente termina seu carregamento. Ele certamente será chamado antes da liberação dos últimos dados que forem fornecidos por esse carregador. Nesse ponto, você deve remover todo o uso dos dados antigos (já que serão liberados em breve), mas não deve fazer a liberação dos dados, já que pertencem ao carregador e ele lidará com isso.
O carregador liberará os dados quando souber que o aplicativo não está mais os usando. Por exemplo: se os dados forem um cursor de um CursorLoader
, você não deve chamar close()
por conta própria. Se o cursor estiver sendo colocado em um CursorAdapter
, você deve usar o método swapCursor()
para que o antigo Cursor
não seja fechado. Por exemplo:
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
Esse método é chamado quando um carregador criado anteriormente é redefinido, tornando os dados indisponíveis. Esse callback permite que você descubra quando os dados estão prestes a serem liberados para que seja possível remover a referência a eles.
Essa implementação chama swapCursor()
com um valor de 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); }
Exemplo
Como exemplo, a seguir há uma implementação completa de um Fragment
que exibe uma ListView
contendo os resultados de uma consulta aos provedores de conteúdo de contatos. Ela usa um CursorLoader
para gerenciar a consulta no provedor.
Para um aplicativo acessar os contatos de um usuário, como neste exemplo, o manifesto deverá incluir a permissão 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 onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(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 // Prepare the loader. Either re-connect with an existing one, // or start a new one. loaderManager.initLoader(0, null, this) } 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 onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(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); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } @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); } }
Mais exemplos
Os exemplos a seguir ilustram como usar os carregadores:
- LoaderCursor — uma versão completa do snippet exibido acima.
- Retrieving a List of Contacts — instruções que usam um
CursorLoader
para recuperar dados de um provedor de contatos. - LoaderThrottle — um exemplo de como usar o regulador para reduzir o número de consultas que o provedor de conteúdo realiza quando os dados são alterados.
AsyncTaskLoader
— um exemplo que usa umAsyncTaskLoader
para carregar os aplicativos instalados a partir de um gerenciador de pacotes.