Como processar ciclos de vida com componentes que os reconhecem (Views)

Conceitos e implementação do Jetpack Compose

Os componentes com reconhecimento de ciclo de vida executam ações em resposta a uma mudança no status do ciclo de vida de outro componente, como atividades e fragmentos. Esses componentes ajudam você a produzir códigos mais organizados e, com frequência, mais leves e mais fáceis de manter.

Um padrão comum é implementar as ações dos componentes dependentes nos métodos de ciclo de vida de atividades e fragmentos. No entanto, esse padrão leva a um código desorganizado e à proliferação de erros. Usando componentes com reconhecimento de ciclo de vida, é possível mover o código de componentes dependentes dos métodos de ciclo de vida para os próprios componentes.

O pacote androidx.lifecycle fornece classes e interfaces que permitem criar componentes compatíveis com o ciclo de vida, que podem ajustar automaticamente o comportamento com base no estado atual do ciclo de vida de uma atividade ou um fragmento.

A maioria dos componentes de app definidos no Android Framework tem ciclos de vida anexados a eles. Os ciclos de vida são gerenciados pelo sistema operacional ou pelo código da estrutura em execução no processo. Eles são fundamentais para o modo como o Android funciona e seu aplicativo precisa respeitá-los. Não fazer isso pode provocar vazamentos de memória ou até mesmo falhas no aplicativo.

Imagine que temos uma atividade que mostra o local do dispositivo na tela. Uma implementação comum pode ser semelhante a esta:

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val callback: (Location) -> Unit
) {

    fun start() {
        // connect to system location service
    }

    fun stop() {
        // disconnect from system location service
    }
}

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        myLocationListener.start()
        // manage other components that need to respond
        // to the activity lifecycle
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Java

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Mesmo que esse exemplo pareça bom, em um app real, você acabaria tendo muitas chamadas que gerenciam a IU e outros componentes em resposta ao estado atual do ciclo de vida. O gerenciamento de vários componentes coloca uma quantidade considerável de código em métodos de ciclo de vida, como onStart() e onStop, o que dificulta a manutenção.

Além disso, não há qualquer garantia de que o componente será iniciado antes da atividade ou que o fragmento será interrompido. Isso é especialmente verdadeiro se precisarmos realizar uma operação de longa duração, como uma verificação de configuração em onStart. Isso pode causar uma disputa em que o método onStop() termina antes do onStart, mantendo o componente ativo por mais tempo que o necessário.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        Util.checkUserStatus { result ->
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start()
            }
        }
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

O pacote androidx.lifecycle fornece classes e interfaces que ajudam a lidar com esses problemas de forma resiliente e isolada.

Ciclo de vida

Lifecycle é uma classe que contém as informações sobre o estado do ciclo de vida de um componente (como uma atividade ou um fragmento) e permite que outros objetos observem esse estado.

O Lifecycle usa duas enumerações principais para rastrear o status do ciclo de vida do componente associado:

Evento

Os eventos de ciclo de vida que são enviados pelo framework e pela classe Lifecycle. Esses eventos são mapeados para os eventos de callback em atividades e fragmentos.

Estado

O estado atual do componente rastreado pelo objeto Lifecycle.

Figura 1. Estados e eventos do ciclo de vida da atividade do Android.

Pense nos estados como nós de um gráfico e nos eventos como as arestas entre esses nós.

Uma classe pode monitorar o status do ciclo de vida do componente implementando a DefaultLifecycleObserver e substituindo métodos correspondentes, como onCreate, onStart etc. Depois, você pode adicionar um observador chamando o método addObserver() da classe Lifecycle e transmitindo uma instância do seu observador, conforme mostrado no exemplo a seguir:

Kotlin

class MyObserver : DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        connect()
    }

    override fun onPause(owner: LifecycleOwner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(MyObserver())

Java

public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }

    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

No exemplo acima, o objeto myLifecycleOwner implementa a interface LifecycleOwner, que é explicada na seção a seguir.

LifecycleOwner

LifecycleOwner é uma interface de método único que indica que a classe tem um Lifecycle. Ela tem um método, getLifecycle, que precisa ser implementado pela classe. Se você estiver tentando gerenciar o ciclo de vida de um processo de aplicativo inteiro, consulte ProcessLifecycleOwner.

Essa interface abstrai a propriedade de um Lifecycle de classes individuais, como Fragment e AppCompatActivity, e permite escrever componentes que funcionam com elas. Qualquer classe de aplicativo personalizada pode implementar a interface LifecycleOwner.

Os componentes que implementam DefaultLifecycleObserver funcionam perfeitamente com os que implementam LifecycleOwner, porque um proprietário pode fornecer um ciclo de vida, que um observador pode registrar para monitorar.

No exemplo de rastreamento de local, podemos fazer com que a classe MyLocationListener implemente DefaultLifecycleObserver e, em seguida, inicialize-a com o Lifecycle da atividade no método onCreate(). Isso permite que a classe MyLocationListener seja autossuficiente, ou seja, a lógica para reagir a mudanças no status do ciclo de vida é declarada em MyLocationListener, e não na atividade. Armazenar os componentes individuais na sua própria lógica torna a lógica de atividades e fragmentos mais fácil de gerenciar.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this, lifecycle) { location ->
            // update UI
        }
        Util.checkUserStatus { result ->
            if (result) {
                myLocationListener.enable()
            }
        }
    }
}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

Um caso de uso comum é evitar invocar determinados callbacks se o Lifecycle não estiver em um bom estado no momento. Por exemplo, se o callback executar uma transação de fragmento depois que o estado da atividade for salvo, isso vai causar uma falha, então não queremos invocar esse callback.

Para facilitar esse caso de uso, a classe Lifecycle permite que outros objetos consultem o estado atual.

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val lifecycle: Lifecycle,
        private val callback: (Location) -> Unit
): DefaultLifecycleObserver {

    private var enabled = false

    override fun onStart(owner: LifecycleOwner) {
        if (enabled) {
            // connect
        }
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        // disconnect if connected
    }
}

Java

class MyLocationListener implements DefaultLifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       ...
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        // disconnect if connected
    }
}

Com essa implementação, a classe LocationListener tem reconhecimento total do ciclo de vida. Se for necessário usar o LocationListener de outra atividade ou fragmento, basta inicializá-lo. Todas as operações de configuração e desmontagem são gerenciadas pela própria classe.

Se uma biblioteca fornece classes que precisam trabalhar com o ciclo de vida do Android, recomendamos que você use componentes com reconhecimento de ciclo de vida. Seus clientes de biblioteca podem integrar facilmente esses componentes sem o gerenciamento manual do ciclo de vida no lado do cliente.

Implementar um LifecycleOwner personalizado

Fragmentos e atividades na Biblioteca de Suporte 26.1.0 e versões mais recentes já implementam a interface LifecycleOwner.

Se você tiver uma classe personalizada que queira transformar em um LifecycleOwner, use a classe LifecycleRegistry, mas será necessário encaminhar eventos para essa classe, conforme mostrado no exemplo de código a seguir:

Kotlin

class MyActivity : Activity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

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

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

Práticas recomendadas para componentes com reconhecimento de ciclo de vida

  • Mantenha seus controladores de IU (atividades e fragmentos) o mais simples possível. Eles não podem tentar adquirir os próprios dados. Em vez disso, use um ViewModel para fazer isso e observe um objeto LiveData para refletir as mudanças nas visualizações.
  • Tente escrever interfaces baseadas em dados, em que a responsabilidade do controlador de IU é atualizar as visualizações conforme os dados mudam ou notificar as ações do usuário de volta ao ViewModel.
  • Coloque a lógica de dados na classe ViewModel. ViewModel servirá como o conector entre o controlador de IU e o restante do app. Entretanto, tenha cuidado, porque não é responsabilidade do ViewModel buscar dados (por exemplo, de uma rede). Em vez disso, ViewModel precisa chamar o componente adequado para buscar os dados e, em seguida, fornecer o resultado para o controlador de IU.
  • Use a vinculação de dados para manter uma interface limpa entre as visualizações e o controlador de IU. Isso permite que você torne suas visualizações mais declarativas e minimize o código de atualização que precisa ser escrito nas atividades e nos fragmentos. Se você preferir fazer isso na linguagem de programação Java, use uma biblioteca como a Butter Knife para evitar código boilerplate e ter uma abstração melhor.
  • Se a interface for complexa, crie uma classe presenter para processar as modificações. Essa pode ser uma tarefa trabalhosa, mas pode facilitar o teste dos componentes da interface.
  • Evite referenciar um contexto View ou Activity no seu ViewModel. Se o ViewModel durar mais que a atividade (em caso de mudanças de configuração), sua atividade vai vazar e não será descartada corretamente pelo coletor de lixo.
  • Use corrotinas do Kotlin para gerenciar tarefas de longa duração e outras operações que podem ser executadas de forma assíncrona.

Casos de uso para componentes com reconhecimento de ciclo de vida

Componentes com reconhecimento de ciclo de vida podem facilitar muito o gerenciamento de ciclos de vida em diversos casos. Vejas alguns exemplos:

  • Alternar entre atualizações de localização aproximada ou detalhada. Use componentes com reconhecimento de ciclo de vida para ativar atualizações de localização detalhadas enquanto seu app de local estiver visível e mude para atualizações aproximadas quando o app estiver em segundo plano. O LiveData, um componente com reconhecimento de ciclo de vida, permite que o app atualize a IU automaticamente quando o usuário muda de local.
  • Parar e iniciar o armazenamento de vídeos em buffer. Use componentes com reconhecimento de ciclo de vida para iniciar o armazenamento em buffer de vídeo o mais rápido possível, mas adie a reprodução até que o app esteja totalmente iniciado. Também é possível usar componentes com reconhecimento de ciclo de vida para encerrar o armazenamento em buffer quando o app é destruído.
  • Iniciar e interromper a conectividade de rede. Use componentes com reconhecimento de ciclo de vida para ativar a atualização em tempo real (streaming) de dados de rede enquanto um app estiver em primeiro plano e também para pausá-la automaticamente quando ele estiver em segundo plano.
  • Pausar e retomar drawables animados. Use componentes com reconhecimento de ciclo de vida para pausar drawables animados quando o app estiver em segundo plano e retomar os drawables depois que ele voltar para o primeiro plano.

Gerenciar eventos "on stop"

Quando um Lifecycle pertence a um AppCompatActivity ou Fragment, o estado do Lifecycle muda para CREATED e o evento ON_STOP é enviado quando o onSaveInstanceState() do AppCompatActivity ou Fragment é chamado.

Quando o estado de um Fragment ou AppCompatActivity é salvo usando onSaveInstanceState, a interface é considerada imutável até que ON_START seja chamado. Tentar modificar a IU depois que o estado é salvo pode causar inconsistências no estado de navegação do aplicativo. Por isso, o FragmentManager gera uma exceção se o app executar um FragmentTransaction depois que o estado for salvo. Consulte commit() para mais detalhes.

LiveData evita esse caso extremo ao não chamar o observador se o Lifecycle associado a ele não estiver pelo menos em STARTED. Nos bastidores, ele chama isAtLeast() antes de decidir invocar o observador.

Infelizmente, o método onStop() da AppCompatActivity é chamado depois de onSaveInstanceState. Isso gera uma lacuna em que as mudanças de estado da IU não são permitidas, mas o Lifecycle ainda não foi passado para o estado CREATED.

Para evitar esse problema, a classe do Lifecycle na versão beta2 e anteriores marca o estado como CREATED sem despachar o evento para que qualquer código que verificar o estado atual receba o valor real, mesmo que o evento não seja despachado até onStop() ser chamado pelo sistema.

Essa solução tem dois grandes problemas:

  • No nível de API 23 ou anterior, o sistema Android realmente salva o estado de uma atividade, mesmo que ele seja parcialmente coberto por outra atividade. Em outras palavras, o sistema Android chama onSaveInstanceState(), mas não necessariamente onStop. Isso cria um intervalo potencialmente longo em que o observador ainda acha que o ciclo de vida está ativo, mesmo que o estado da IU não possa ser modificado.
  • Qualquer classe que queira expor um comportamento semelhante à classe LiveData precisa implementar a solução alternativa fornecida pela versão beta 2 e anteriores do Lifecycle.

Outros recursos

Para saber mais sobre como processar ciclos de vida com componentes que os reconhecem, consulte os recursos adicionais a seguir.

Codelabs

Blogs