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żyjandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
lub nowszej. - W przypadku
LifecycleScope
użyjandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0
lub nowszej. - W przypadku
liveData
użyjandroidx.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:
- Zwiększanie wydajności aplikacji przy użyciu współprogramów Kotlin
- Omówienie Kalendarza
- Wątki w CoroutineWorker
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
- Kortyny na Androidzie: wzorce aplikacji
- Łatwe korzystanie z współprogramów na Androidzie: viewModelScope
- Testowanie 2 kolejnych emisji LiveData w współprogramach
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Omówienie LiveData
- Obsługa cyklu życia za pomocą komponentów uwzględniających cykl życia
- Wczytywanie i wyświetlanie danych z podziałem na strony