Używaj współprogramów Kotlin z komponentami dopasowanymi do cyklu życia

Kotlin kotlin udostępnia interfejs API, który umożliwia pisanie kodu asynchronicznego. Za pomocą współprogramów Kotlin możesz zdefiniować element CoroutineScope, który ułatwia zarządzanie czasem uruchamiania współprac. Każda operacja asynchroniczna działa w obrębie określonego zakresu.

Komponenty uwzględniające cykl życia zapewniają najwyższej klasy obsługę współprogramów dla zakresów logicznych w aplikacji, a także warstwę interoperacyjności z LiveData. W tym temacie dowiesz się, jak skutecznie korzystać z współprogramów za pomocą komponentów uwzględniających cykl życia.

Dodaj zależności KTX

Wbudowane zakresy współrzędne opisane w tym temacie znajdują się w rozszerzeniach KTX każdego komponentu. Pamiętaj, by podczas korzystania z tych zakresów dodać odpowiednie zależności.

  • W przypadku ViewModelScope użyj androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 lub nowszej.
  • W przypadku LifecycleScope użyj androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 lub nowszej.
  • W przypadku liveData użyj androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 lub nowszej.

Zakresy Coroutine rozpoznające cykl życia

Komponenty uwzględniające cykl życia definiują te wbudowane zakresy, których możesz używać w aplikacji.

ViewModelScope

ViewModelScope jest definiowana dla każdej ViewModel w aplikacji. Każda współpraca uruchomiona w tym zakresie zostanie automatycznie anulowana, jeśli ViewModel zostanie wyczyszczona. Korutyny przydają się, gdy masz zadania do wykonania tylko wtedy, gdy ViewModel jest aktywny. Jeśli np. przetwarzasz dane związane z układem, ogranicz pracę do ViewModel. Dzięki temu po wyczyszczeniu elementu ViewModel zadanie będzie anulowane automatycznie, co pozwoli uniknąć zużywania zasobów.

Dostęp do CoroutineScope obiektu ViewModel możesz uzyskać za pomocą właściwości viewModelScope modelu ViewModel, jak widać w tym przykładzie:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

Zakres cyklu życia

Każdy obiekt Lifecycle musi mieć przypisaną LifecycleScope. Każda współpraca uruchomiona w tym zakresie zostanie anulowana po zniszczeniu Lifecycle. Dostęp do CoroutineScope elementu Lifecycle możesz uzyskać za pomocą właściwości lifecycle.coroutineScope lub lifecycleOwner.lifecycleScope.

Poniższy przykład pokazuje, jak za pomocą pola lifecycleOwner.lifecycleScope asynchronicznie tworzyć wstępnie obliczony tekst:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

Ponownie uruchamiające współprogramy rozpoznające cykl życia

Mimo że lifecycleScope umożliwia właściwy sposób automatycznego anulowania długo trwających operacji, gdy Lifecycle ma wartość DESTROYED, w innych przypadkach może być konieczne rozpoczynanie wykonywania bloku kodu, gdy Lifecycle znajduje się w określonym stanie, i anulowanie go, gdy jest w innym stanie. Możesz na przykład skonfigurować przepływ, gdy Lifecycle ma wartość STARTED, i anulować zbieranie, gdy będzie ustawiona wartość STOPPED. Ta metoda przetwarza emisje przepływu tylko wtedy, gdy interfejs użytkownika jest widoczny na ekranie, co pozwala zaoszczędzić zasoby i uniknąć awarii aplikacji.

W takich przypadkach Lifecycle i LifecycleOwner udostępniają interfejs API zawieszania repeatOnLifecycle, który właśnie to robi. Ten przykład zawiera blok kodu, który jest uruchamiany za każdym razem, gdy powiązany element Lifecycle ma co najmniej stan STARTED, i anuluje się, gdy Lifecycle ma wartość STOPPED:

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

Zbieranie informacji na temat cyklu życia

Jeśli zbieranie danych z uwzględnieniem cyklu życia chcesz przeprowadzić tylko w ramach jednego procesu, możesz uprościć kod, stosując metodę Flow.flowWithLifecycle():

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

Jeśli jednak musisz przeprowadzać gromadzenie danych z uwzględnieniem cyklu życia w kilku równoległych przepływach, musisz zbierać dane za pomocą różnych współczesnych przepływów. W takim przypadku lepiej jest użyć bezpośrednio atrybutu repeatOnLifecycle():

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

Zawieszanie współużytkowania oprogramowania zależnego od cyklu życia

Chociaż CoroutineScope umożliwia właściwy sposób automatycznego anulowania długo trwających operacji, mogą się zdarzyć, że zechcesz zawiesić wykonanie bloku kodu, chyba że Lifecycle znajduje się w określonym stanie. Aby np. uruchomić FragmentTransaction, musisz poczekać, aż Lifecycle będzie mieć wartość co najmniej STARTED. W takich przypadkach Lifecycle udostępnia dodatkowe metody: lifecycle.whenCreated, lifecycle.whenStarted i lifecycle.whenResumed. Każde uruchomienie współużytkowania w tych blokach jest zawieszane, jeśli Lifecycle nie osiągnie przynajmniej oczekiwanego stanu minimalnego.

Przykład poniżej zawiera blok kodu, który działa tylko wtedy, gdy powiązany element Lifecycle ma co najmniej stan STARTED:

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

Jeśli Lifecycle zostanie zniszczony, gdy współprogram jest aktywna za pomocą jednej z metod when, współprogram zostanie automatycznie anulowany. W poniższym przykładzie blok finally jest uruchamiany, gdy stan Lifecycle wynosi DESTROYED:

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

Używanie współprogramów z LiveData

Jeśli używasz LiveData, może być konieczne asynchroniczne obliczanie wartości. Możliwe na przykład, że chcesz pobrać ustawienia użytkownika i udostępnić je w swoim interfejsie. W takich przypadkach możesz użyć funkcji kreatora liveData, aby wywołać funkcję suspend, która wyświetli wynik jako obiekt LiveData.

W przykładzie poniżej loadUser() to funkcja zawieszania zadeklarowana w innym miejscu. Użyj funkcji kreatora liveData, aby asynchronicznie wywołać metodę loadUser(), a potem użyj metody emit(), aby wygenerować wynik:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

Element liveData jest elementem podstawowym o strukturze równoczesności między współprogramami i LiveData. Blok kodu jest wykonywany, gdy LiveData stanie się aktywny, i zostaje automatycznie anulowany po konfigurowanym czasie oczekiwania, gdy LiveData stanie się nieaktywny. Jeśli zostanie anulowany przed zakończeniem, zostanie uruchomiony ponownie, gdy LiveData ponownie stanie się aktywny. Jeśli w poprzednim uruchomieniu udało się zakończyć proces, nie uruchomi się ponownie. Pamiętaj, że zostanie ona uruchomiona ponownie tylko w przypadku anulowania automatycznego. Jeśli blokada zostanie anulowana z jakiegokolwiek innego powodu (np. odrzucenia: CancellationException), nie zostanie uruchomiona ponownie.

Możesz też emitować wiele wartości z bloku. Każde wywołanie emit() zawiesza wykonanie bloku do momentu ustawienia wartości LiveData w wątku głównym.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

Możesz też połączyć dyrektywę liveData z Transformations, jak pokazano w tym przykładzie:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

Możesz wygenerować wiele wartości z funkcji LiveData, wywołując funkcję emitSource() za każdym razem, gdy chcesz wygenerować nową wartość. Pamiętaj, że każde wywołanie emit() lub emitSource() powoduje usunięcie wcześniej dodanego źródła.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

Więcej informacji o koordynacjach znajdziesz pod tymi linkami:

Dodatkowe materiały

Więcej informacji o używaniu współprogramów z komponentami uwzględniającymi cykl życia znajdziesz w tych dodatkowych materiałach.

Próbki

Blogi