Carregadores

O uso de carregadores foi descontinuado no Android 9 (nível 28 da API) e versões mais recentes. A opção recomendada para lidar com o carregamento de dados ao processar os ciclos de vida Activity e Fragment é usar uma combinação de objetos ViewModel e LiveData. Os modelos de visualização sobrevivem a mudanças de configuração, como carregadores, mas com menos código boilerplate. O LiveData fornece uma maneira de carregar dados que reconhece o ciclo de vida e que você pode reutilizar em vários modelos de visualização. Também é possível combinar LiveData usando MediatorLiveData. Todas as consultas observáveis, como as de um banco de dados do Room, podem ser usadas para observar mudanças nos dados.

ViewModel e LiveData também estão disponíveis em situações em que você não tem acesso ao LoaderManager, como em um Service. Usar os dois em conjunto oferece uma maneira fácil de acessar os dados de que o app precisa sem precisar lidar com o ciclo de vida da interface. Para saber mais sobre LiveData, consulte a visão geral de LiveData. Para saber mais sobre ViewModel, consulte a visão geral de ViewModel.

A API Loader permite carregar dados de um provedor de conteúdo ou outra fonte de dados para exibição em um FragmentActivity ou Fragment.

Sem os carregadores, alguns dos problemas que você pode encontrar incluem:

  • Se você buscar os dados diretamente na atividade ou no fragmento, os usuários sofrerão com a falta de capacidade de resposta devido à execução de consultas potencialmente lentas na linha de execução de IU.
  • Se você buscar os dados de outra linha de execução, talvez com AsyncTask, você vai ser responsável por gerenciar essa linha de execução e a de interface por meio de vários eventos de ciclo de vida de atividades ou fragmentos, como onDestroy() e mudanças de configuração.

Os carregadores resolvem esses problemas e incluem outros benefícios:

  • Os carregadores são executados em linhas de execução separadas para evitar uma interface lenta ou que não responde.
  • Os carregadores simplificam o gerenciamento de linhas de execução fornecendo métodos de callback quando os eventos ocorrem.
  • 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 mudanças na fonte de dados subjacente. Por exemplo, CursorLoader registra automaticamente um ContentObserver para acionar uma atualização quando os dados mudam.

Resumo da API Loader

Há várias classes e interfaces que podem estar envolvidas ao usar carregadores em um app. Elas estão resumidas na tabela a seguir:

Classe/interface Descrição
LoaderManager Uma classe abstrata associada a FragmentActivity ou Fragment para gerenciar uma ou mais instâncias de Loader. Há apenas um LoaderManager por atividade ou fragmento, mas um LoaderManager pode gerenciar vários carregadores.

Para receber um LoaderManager, chame getSupportLoaderManager() da atividade ou do fragmento.

Para começar a carregar dados de um carregador, chame initLoader() ou restartLoader(). O sistema determina automaticamente se já existe um carregador com o mesmo ID de número inteiro e cria um novo carregador ou reutiliza um que já existe.

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:
  • onCreateLoader(int, Bundle): chamado quando o sistema precisa criar um novo carregador. No código, crie um objeto Loader e retorne-o ao sistema.
  • onLoadFinished(Loader<D>, D): chamado quando um carregador termina de carregar dados. Normalmente, os dados são exibidos ao usuário no código.
  • onLoaderReset(Loader<D>): chamado quando um carregador criado anteriormente está sendo redefinido, quando você chama destroyLoader(int) ou quando a atividade ou o fragmento é destruído, tornando os dados indisponíveis. No código, remova todas as referências aos dados do carregador.
A atividade ou o fragmento normalmente implementa essa interface e é registrado quando você chama initLoader() ou restartLoader().
Loader Os carregadores executam o carregamento de dados. Essa classe é abstrata e serve como a classe base para todos os carregadores. É possível criar subclasses diretamente de Loader ou usar uma das subclasses integradas abaixo para simplificar a implementação:

As seções abaixo mostram como usar essas classes e interfaces em um aplicativo.

Usar carregadores em um aplicativo

Esta seção descreve como usar os carregadores em um aplicativo do Android. Um aplicativo que usa carregadores geralmente inclui o seguinte:

Iniciar um carregador

O LoaderManager gerencia uma ou mais instâncias de Loader em um FragmentActivity ou Fragment. Há apenas um LoaderManager por atividade ou fragmento.

Normalmente, um Loader é inicializado no método onCreate() da atividade ou no método onCreate() 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() usa os seguintes parâmetros:

  • Um código exclusivo que identifica o carregador. Neste exemplo, o ID é 0.
  • Argumentos opcionais a serem fornecidos ao carregador em construção (null neste exemplo).
  • Uma implementação de LoaderManager.LoaderCallbacks, que o LoaderManager chama para informar eventos do carregador. Neste exemplo, a classe local implementa a interface LoaderManager.LoaderCallbacks, transmitindo uma referência a si mesma, this.

A chamada initLoader() garante que um carregador seja inicializado e que esteja ativo. Ela tem dois possíveis resultados:

  • Se o carregador especificado pelo código já existir, o último carregador criado será reutilizado.
  • Se o carregador especificado pelo ID não existir, initLoader() acionará o onCreateLoader() do método LoaderManager.LoaderCallbacks. É aqui que você implementa o código para instanciar e retornar um novo carregador. Para saber mais, consulte a seção sobre onCreateLoader.

Em ambos os casos, a implementação de LoaderManager.LoaderCallbacks fornecida é associada ao carregador e é chamada quando o estado dele muda. Se, no momento dessa chamada, o autor da chamada estiver no estado iniciado e o carregador solicitado já existir e tiver gerado os dados, o sistema chamará onLoadFinished() imediatamente, durante initLoader(). Você precisa estar preparado para isso. Para saber mais sobre esse callback, consulte a seção sobre onLoadFinished.

O método initLoader() retorna o Loader criado, mas você não precisa capturar uma referência a ele. O LoaderManager gerencia a vida do carregador automaticamente. O LoaderManager inicia e para de carregar quando necessário e mantém o estado do carregador e o conteúdo associado a ele.

Como isso implica, você raramente interage diretamente com os carregadores. Geralmente, os métodos LoaderManager.LoaderCallbacks são usados para intervir no processo de carregamento quando ocorrem eventos específicos. Para ver mais discussões sobre esse tópico, consulte a seção Como usar os callbacks de LoaderManager.

Reiniciar um carregador

Quando você usa initLoader(), como mostrado na seção anterior, ele utiliza um carregador existente com o ID especificado, se houver um. Se não houver, será criada uma. Mas, às vezes, você quer descartar seus dados antigos e começar de novo.

Para descartar seus dados antigos, use restartLoader(). Por exemplo, a seguinte implementação de SearchView.OnQueryTextListener reinicia o carregador quando a consulta do usuário muda. O carregador precisa ser reiniciado para que possa usar o filtro de pesquisa revisado e fazer 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;
}

Usar os callbacks de LoaderManager

LoaderManager.LoaderCallbacks é uma interface de callback que permite que um cliente interaja com o LoaderManager.

Os carregadores, em especial CursorLoader, precisam reter os dados após serem interrompidos. Isso permite que os aplicativos mantenham os dados nos métodos onStop() e onStart() da atividade ou do fragmento, para que, quando os usuários retornarem a um aplicativo, não precisem esperar a atualização dos dados.

Use os métodos LoaderManager.LoaderCallbacks para saber quando criar um novo carregador e informar ao aplicativo quando é o momento de parar de usar os dados dele.

LoaderManager.LoaderCallbacks inclui estes métodos:

  • onLoadFinished(): chamado quando um carregador criado anteriormente termina o carregamento.
  • onLoaderReset(): chamado quando um carregador criado anteriormente está sendo redefinido, tornando os dados indisponíveis.

Esses métodos são descritos com mais detalhes nas seções seguintes.

onCreateLoader

Quando você tenta acessar um carregador, por exemplo, por meio de initLoader(), ele verifica se o carregador especificado pelo ID existe. Caso contrário, ele vai acionar o método onCreateLoader() do LoaderManager.LoaderCallbacks. É aqui que você cria um novo carregador. Normalmente, esse é um CursorLoader, mas é possível implementar sua própria subclasse Loader.

No exemplo a seguir, o método de callback onCreateLoader() cria uma CursorLoader usando o método construtor, que requer o conjunto completo de informações necessárias para executar uma consulta ao ContentProvider. Especificamente, ele precisa do seguinte:

  • uri: o URI do conteúdo a ser recuperado.
  • projection: uma lista de quais colunas retornar. Transmitir null retorna todas as colunas, o que é ineficiente.
  • selection: um filtro que declara quais linhas retornar, formatado como uma cláusula SQL WHERE (excluindo WHERE). Transmitir null retorna todas as linhas do URI especificado.
  • selectionArgs: se você incluir ?s na seleção, eles serão substituídos pelos valores de selectionArgs na ordem em que aparecem na seleção. Os valores são vinculados como strings.
  • sortOrder: como ordenar as linhas, formatadas como uma cláusula SQL ORDER BY (excluindo o próprio ORDER BY). Transmitir null usa a ordem de classificação padrão, que pode ser desordenada.

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. Esse método será chamado antes da liberação dos últimos dados fornecidos por esse carregador. Neste ponto, remova todo o uso dos dados antigos, já que eles serão liberados. Mas não libere os dados por conta própria. O carregador é o proprietário deles e cuida disso.

O carregador libera os dados quando sabe que o aplicativo não os usa mais. Por exemplo, se os dados forem um cursor de um CursorLoader, não chame close() nele. Se o cursor estiver sendo colocado em um CursorAdapter, use o método swapCursor() para que o Cursor antigo não seja fechado, conforme mostrado no exemplo a seguir.

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 você possa remover sua referência a eles.

Esta 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

Por exemplo, esta é a implementação completa de um Fragment que exibe um ListView contendo os resultados de uma consulta no provedor de conteúdo de contatos. Ela usa um CursorLoader para gerenciar a consulta no provedor.

Como este exemplo é de um aplicativo para acessar os contatos de um usuário, o manifesto dele precisa 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 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);
    }
}

Mais exemplos

Os exemplos a seguir ilustram como usar os carregadores: