Koncepcje i implementacja Jetpack Compose
Współprogramy Kotlin udostępniają interfejs API, który umożliwia pisanie kodu asynchronicznego. Dzięki współprogramom Kotlin możesz zdefiniować CoroutineScope, który
pomaga zarządzać czasem działania współprogramów. Każda operacja asynchroniczna jest wykonywana w określonym zakresie.
Komponenty uwzględniające cykl życia zapewniają najwyższej jakości obsługę współprogramów w przypadku
zakresów logicznych w aplikacji oraz warstwę interoperacyjności z
LiveData. Z tego artykułu dowiesz się, jak efektywnie korzystać ze współprogramów z komponentami uwzględniającymi cykl życia.
Dodawanie zależności KTX
Wbudowane zakresy współprogramów opisane w tym artykule znajdują się w rozszerzeniach KTX dla każdego odpowiedniego komponentu. Pamiętaj, aby podczas korzystania z tych zakresów dodać odpowiednie zależności.
- W przypadku
ViewModelScopeużyjandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0lub nowszej. - W przypadku
LifecycleScopeużyjandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0lub nowszej. - W przypadku
liveDataużyjandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0lub nowszej.
Zakresy współprogramów uwzględniające cykl życia
Komponenty uwzględniające cykl życia definiują te wbudowane zakresy, których możesz używać w aplikacji.
ViewModelScope
Dla każdego elementu ViewModel w aplikacji jest zdefiniowany ViewModelScope. Każdy
współprogram uruchomiony w tym zakresie jest automatycznie anulowany, jeśli element ViewModel zostanie
wyczyszczony. Współprogramy są przydatne, gdy masz do wykonania zadanie, które musi zostać wykonane tylko wtedy, gdy element ViewModel jest aktywny. Jeśli na przykład obliczasz dane dla układu, powinieneś ograniczyć zakres pracy do elementu ViewModel, aby w przypadku wyczyszczenia elementu ViewModel praca została automatycznie anulowana, co pozwoli uniknąć zużywania zasobów.
Dostęp do CoroutineScope elementu ViewModel możesz uzyskać za pomocą właściwości viewModelScope elementu ViewModel, jak pokazano w tym przykładzie:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
Dla każdego obiektu Lifecycle jest zdefiniowany LifecycleScope. Każdy współprogram uruchomiony w tym zakresie jest anulowany, gdy obiekt Lifecycle zostanie zniszczony. Dostęp do CoroutineScope elementu Lifecycle możesz uzyskać za pomocą właściwości lifecycle.coroutineScope lub lifecycleOwner.lifecycleScope.
W tym przykładzie pokazujemy, jak używać lifecycleOwner.lifecycleScope do asynchronicznego tworzenia wstępnie obliczonego tekstu:
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)
}
}
}
Współprogramy uwzględniające cykl życia, które można ponownie uruchomić
Chociaż lifecycleScope zapewnia odpowiedni sposób automatycznego anulowania długotrwałych operacji, gdy obiekt Lifecycle jest DESTROYED, mogą wystąpić inne przypadki, w których chcesz rozpocząć wykonywanie bloku kodu, gdy obiekt Lifecycle jest w określonym stanie, i anulować je, gdy jest w innym stanie. Możesz na przykład chcieć zbierać dane z przepływu, gdy obiekt Lifecycle jest STARTED, i anulować zbieranie, gdy jest STOPPED. Dzięki temu podejście przetwarza emisje przepływu tylko wtedy, gdy interfejs jest widoczny na ekranie, co pozwala oszczędzać zasoby i potencjalnie uniknąć awarii aplikacji.
W takich przypadkach Lifecycle i LifecycleOwner udostępniają zawieszający
repeatOnLifecycle interfejs API, który robi dokładnie to. Ten przykład zawiera blok kodu, który jest uruchamiany za każdym razem, gdy powiązany obiekt Lifecycle jest co najmniej w stanie STARTED, i jest anulowany, gdy obiekt Lifecycle jest 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 danych z przepływu uwzględniające cykl życia
Jeśli musisz wykonać zbieranie danych z pojedynczego przepływu uwzględniające cykl życia, możesz
użyć metody Flow.flowWithLifecycle(), aby uprościć kod:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
Jeśli jednak musisz równolegle zbierać dane z wielu przepływów uwzględniające cykl życia, musisz zbierać dane z każdego przepływu w różnych współprogramach. W takim przypadku bardziej efektywne jest bezpośrednie użycie 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ółprogramów uwzględniających cykl życia
Chociaż CoroutineScope zapewnia odpowiedni sposób automatycznego anulowania długotrwałych operacji, mogą wystąpić inne przypadki, w których chcesz zawiesić wykonywanie bloku kodu, chyba że obiekt Lifecycle jest w określonym stanie. Aby na przykład uruchomić FragmentTransaction, musisz poczekać, aż obiekt Lifecycle będzie co najmniej STARTED. W takich przypadkach obiekt Lifecycle udostępnia dodatkowe metody:
lifecycle.whenCreated, lifecycle.whenStarted i lifecycle.whenResumed. Każdy współprogram uruchomiony w tych blokach jest zawieszany, jeśli obiekt Lifecycle nie jest co najmniej w minimalnym pożądanym stanie.
Ten przykład zawiera blok kodu, który jest uruchamiany tylko wtedy, gdy powiązany obiekt Lifecycle jest co najmniej w stanie 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 obiekt Lifecycle zostanie zniszczony, gdy współprogram jest aktywny za pomocą jednej z metod when, współprogram zostanie automatycznie anulowany. W tym przykładzie blok finally jest uruchamiany, gdy stan obiektu Lifecycle to 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
Podczas korzystania z LiveData może być konieczne asynchroniczne obliczanie wartości.
Możesz na przykład chcieć pobrać preferencje użytkownika i wyświetlić je w interfejsie. W takich przypadkach możesz użyć funkcji konstruktora liveData, aby wywołać funkcję suspend, która zwraca wynik jako obiekt LiveData.
W tym przykładzie loadUser() jest funkcją zawieszającą zadeklarowaną w innym miejscu. Użyj funkcji konstruktora liveData, aby asynchronicznie wywołać loadUser(), a następnie użyj emit(), aby wyemitować wynik:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
Element składowy liveData służy jako prymityw współbieżności strukturalnej między współprogramami a LiveData. Blok kodu zaczyna się wykonywać, gdy LiveData staje się aktywny, i jest automatycznie anulowany po konfigurowalnym czasie oczekiwania, gdy LiveData staje się nieaktywny. Jeśli zostanie anulowany przed zakończeniem, zostanie ponownie uruchomiony, gdy LiveData ponownie stanie się aktywny. Jeśli w poprzednim uruchomieniu zakończył się pomyślnie, nie zostanie ponownie uruchomiony. Pamiętaj, że zostanie ponownie uruchomiony tylko wtedy, gdy zostanie automatycznie anulowany. Jeśli blok zostanie anulowany z innego powodu (np. z powodu zgłoszenia CancellationException), nie zostanie ponownie uruchomiony.
Możesz też emitować wiele wartości z bloku. Każde wywołanie emit() zawiesza wykonywanie bloku, dopóki wartość LiveData nie zostanie ustawiona 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ć 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 emitować wiele wartości z LiveData, wywołując funkcję emitSource() za każdym razem, gdy chcesz wyemitować nową wartość. Pamiętaj, że każde wywołanie emit() lub emitSource() usuwa wcześniej dodane źródło.
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 współprogramach znajdziesz pod tymi linkami:
- Zwiększanie wydajności aplikacji za pomocą współprogramów Kotlin
- Omówienie współprogramów
- 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.
Przykłady
Blogi
- Współprogramy na Androidzie: wzorce aplikacji
- Łatwe współprogramy na Androidzie: viewModelScope
- Testowanie 2 kolejnych emisji LiveData we współprogramach