From 93c013c33cfa8c500a08577abef68fa4b5d681ab Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 27 Mar 2024 14:46:58 +0800 Subject: [PATCH] Add observe extensions for Flow to reduce nesting --- app/build.gradle | 1 + .../tusky/AccountsInListFragment.kt | 19 +- .../tusky/EditProfileActivity.kt | 170 +++++++++--------- .../com/keylesspalace/tusky/ListsActivity.kt | 19 +- .../com/keylesspalace/tusky/MainActivity.kt | 67 ++++--- .../tusky/appstore/CacheUpdater.kt | 40 ++--- .../keylesspalace/tusky/appstore/EventsHub.kt | 9 +- .../components/account/AccountActivity.kt | 61 +++---- .../components/account/AccountViewModel.kt | 9 +- .../account/list/ListSelectionFragment.kt | 88 +++++---- .../account/media/AccountMediaFragment.kt | 10 +- .../announcements/AnnouncementsActivity.kt | 65 ++++--- .../components/compose/ComposeActivity.kt | 117 +++++------- .../components/compose/ComposeViewModel.kt | 61 ++++--- .../conversation/ConversationsFragment.kt | 17 +- .../domainblocks/DomainBlocksFragment.kt | 17 +- .../tusky/components/drafts/DraftsActivity.kt | 8 +- .../components/filters/EditFilterActivity.kt | 37 ++-- .../components/filters/FiltersActivity.kt | 75 ++++---- .../followedtags/FollowedTagsActivity.kt | 8 +- .../components/login/LoginWebViewActivity.kt | 25 ++- .../tusky/components/report/ReportActivity.kt | 33 ++-- .../report/fragments/ReportDoneFragment.kt | 65 ++++--- .../report/fragments/ReportNoteFragment.kt | 17 +- .../fragments/ReportStatusesFragment.kt | 10 +- .../scheduled/ScheduledStatusActivity.kt | 19 +- .../search/fragments/SearchFragment.kt | 10 +- .../components/timeline/TimelineFragment.kt | 28 ++- .../timeline/viewmodel/TimelineViewModel.kt | 6 +- .../trending/TrendingTagsFragment.kt | 10 +- .../viewmodel/TrendingTagsViewModel.kt | 13 +- .../viewthread/ViewThreadFragment.kt | 141 +++++++-------- .../viewthread/ViewThreadViewModel.kt | 18 +- .../viewthread/edits/ViewEditsFragment.kt | 117 ++++++------ .../com/keylesspalace/tusky/db/DraftsAlert.kt | 49 +++-- .../tusky/util/FlowExtensions.kt | 52 ++++++ 36 files changed, 712 insertions(+), 799 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f801f16085..aae47806da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,7 @@ android { kotlinOptions { freeCompilerArgs = [ + "-Xcontext-receivers", "-Xno-param-assertions", "-Xno-call-assertions", "-Xno-receiver-assertions" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 0e88e0c7e9..16d7c4eba1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -24,7 +24,6 @@ import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager @@ -40,13 +39,13 @@ import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.State import javax.inject.Inject -import kotlinx.coroutines.launch private typealias AccountInfo = Pair @@ -104,17 +103,15 @@ class AccountsInListFragment : DialogFragment(), Injectable { binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) binding.accountsSearchRecycler.adapter = searchAdapter - viewLifecycleOwner.lifecycleScope.launch { - viewModel.state.collect { state -> - adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) + viewModel.state.observe(viewLifecycleOwner) { state -> + adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) - when (state.accounts) { - is Either.Right -> binding.messageView.hide() - is Either.Left -> handleError(state.accounts.value) - } - - setupSearchView(state) + when (state.accounts) { + is Either.Right -> binding.messageView.hide() + is Either.Left -> handleError(state.accounts.value) } + + setupSearchView(state) } binding.searchView.isSubmitButtonEnabled = true diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index e0f6a3bc80..03e457fad7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -49,6 +49,7 @@ import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.await +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewmodel.EditProfileViewModel @@ -152,85 +153,79 @@ class EditProfileActivity : BaseActivity(), Injectable { viewModel.obtainProfile() - lifecycleScope.launch { - viewModel.profileData.collect { profileRes -> - if (profileRes == null) return@collect - when (profileRes) { - is Success -> { - val me = profileRes.data - if (me != null) { - binding.displayNameEditText.setText(me.displayName) - binding.noteEditText.setText(me.source?.note) - binding.lockedCheckBox.isChecked = me.locked - - accountFieldEditAdapter.setFields(me.source?.fields.orEmpty()) - binding.addFieldButton.isVisible = - (me.source?.fields?.size ?: 0) < maxAccountFields - - if (viewModel.avatarData.value == null) { - Glide.with(this@EditProfileActivity) - .load(me.avatar) - .placeholder(R.drawable.avatar_default) - .transform( - FitCenter(), - RoundedCorners( - resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp) - ) + viewModel.profileData.observe { profileRes -> + if (profileRes == null) return@observe + when (profileRes) { + is Success -> { + val me = profileRes.data + if (me != null) { + binding.displayNameEditText.setText(me.displayName) + binding.noteEditText.setText(me.source?.note) + binding.lockedCheckBox.isChecked = me.locked + + accountFieldEditAdapter.setFields(me.source?.fields.orEmpty()) + binding.addFieldButton.isVisible = + (me.source?.fields?.size ?: 0) < maxAccountFields + + if (viewModel.avatarData.value == null) { + Glide.with(this@EditProfileActivity) + .load(me.avatar) + .placeholder(R.drawable.avatar_default) + .transform( + FitCenter(), + RoundedCorners( + resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp) ) - .into(binding.avatarPreview) - } - - if (viewModel.headerData.value == null) { - Glide.with(this@EditProfileActivity) - .load(me.header) - .into(binding.headerPreview) - } + ) + .into(binding.avatarPreview) + } + + if (viewModel.headerData.value == null) { + Glide.with(this@EditProfileActivity) + .load(me.header) + .into(binding.headerPreview) } } - is Error -> { - Snackbar.make( - binding.avatarButton, - R.string.error_generic, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_retry) { - viewModel.obtainProfile() - } - .show() - } - is Loading -> { } } + is Error -> { + Snackbar.make( + binding.avatarButton, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.obtainProfile() + } + .show() + } + is Loading -> { } } } - lifecycleScope.launch { - viewModel.instanceData.collect { instanceInfo -> - maxAccountFields = instanceInfo.maxFields - accountFieldEditAdapter.setFieldLimits( - instanceInfo.maxFieldNameLength, - instanceInfo.maxFieldValueLength - ) - binding.addFieldButton.isVisible = - accountFieldEditAdapter.itemCount < maxAccountFields - } + viewModel.instanceData.observe { instanceInfo -> + maxAccountFields = instanceInfo.maxFields + accountFieldEditAdapter.setFieldLimits( + instanceInfo.maxFieldNameLength, + instanceInfo.maxFieldValueLength + ) + binding.addFieldButton.isVisible = + accountFieldEditAdapter.itemCount < maxAccountFields } observeImage(viewModel.avatarData, binding.avatarPreview, true) observeImage(viewModel.headerData, binding.headerPreview, false) - lifecycleScope.launch { - viewModel.saveData.collect { - if (it == null) return@collect - when (it) { - is Success -> { - finish() - } - is Loading -> { - binding.saveProgressBar.visibility = View.VISIBLE - } - is Error -> { - onSaveFailure(it.errorMessage) - } + viewModel.saveData.observe { + if (it == null) return@observe + when (it) { + is Success -> { + finish() + } + is Loading -> { + binding.saveProgressBar.visibility = View.VISIBLE + } + is Error -> { + onSaveFailure(it.errorMessage) } } } @@ -258,10 +253,8 @@ class EditProfileActivity : BaseActivity(), Injectable { } onBackPressedDispatcher.addCallback(this, onBackCallback) - lifecycleScope.launch { - viewModel.isChanged.collect { dataWasChanged -> - onBackCallback.isEnabled = dataWasChanged - } + viewModel.isChanged.observe { dataWasChanged -> + onBackCallback.isEnabled = dataWasChanged } } @@ -277,26 +270,23 @@ class EditProfileActivity : BaseActivity(), Injectable { imageView: ImageView, roundedCorners: Boolean ) { - lifecycleScope.launch { - flow.collect { imageUri -> - - // skipping all caches so we can always reuse the same uri - val glide = Glide.with(imageView) - .load(imageUri) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - - if (roundedCorners) { - glide.transform( - FitCenter(), - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) - ).into(imageView) - } else { - glide.into(imageView) - } - - imageView.show() + flow.observe { imageUri -> + // skipping all caches so we can always reuse the same uri + val glide = Glide.with(imageView) + .load(imageUri) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + + if (roundedCorners) { + glide.transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ).into(imageView) + } else { + glide.into(imageView) } + + imageView.show() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index f8a2e94659..80ed3309b8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -28,7 +28,6 @@ import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.widget.doOnTextChanged -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -42,6 +41,7 @@ import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding @@ -56,7 +56,6 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -import kotlinx.coroutines.launch // TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?) @@ -95,9 +94,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { binding.swipeRefreshLayout.setOnRefreshListener { viewModel.retryLoading() } binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - lifecycleScope.launch { - viewModel.state.collect(this@ListsActivity::update) - } + viewModel.state.observe(this@ListsActivity::update) viewModel.retryLoading() @@ -105,13 +102,11 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { showlistNameDialog(null) } - lifecycleScope.launch { - viewModel.events.collect { event -> - when (event) { - Event.CREATE_ERROR -> showMessage(R.string.error_create_list) - Event.UPDATE_ERROR -> showMessage(R.string.error_rename_list) - Event.DELETE_ERROR -> showMessage(R.string.error_delete_list) - } + viewModel.events.observe { event -> + when (event) { + Event.CREATE_ERROR -> showMessage(R.string.error_create_list) + Event.UPDATE_ERROR -> showMessage(R.string.error_rename_list) + Event.DELETE_ERROR -> showMessage(R.string.error_delete_list) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 499d080f75..aad904c059 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -108,6 +108,7 @@ import com.keylesspalace.tusky.util.deleteStaleCachedMedia import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getDimension import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.reduceSwipeSensitivity import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation @@ -349,46 +350,44 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje setupTabs(showNotificationTab) - lifecycleScope.launch { - eventHub.events.collect { event -> - when (event) { - is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) - is MainTabsChangedEvent -> { - refreshMainDrawerItems( - addSearchButton = hideTopToolbar, - addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS), - addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES) - ) + eventHub.events.observe { event -> + when (event) { + is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) + is MainTabsChangedEvent -> { + refreshMainDrawerItems( + addSearchButton = hideTopToolbar, + addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS), + addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES) + ) - setupTabs(false) - } - is AnnouncementReadEvent -> { - unreadAnnouncementsCount-- - updateAnnouncementsBadge() - } - is NewNotificationsEvent -> { - directMessageTab?.let { - if (event.accountId == activeAccount.accountId) { - val hasDirectMessageNotification = - event.notifications.any { - it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT - } - - if (hasDirectMessageNotification) { - showDirectMessageBadge(true) + setupTabs(false) + } + is AnnouncementReadEvent -> { + unreadAnnouncementsCount-- + updateAnnouncementsBadge() + } + is NewNotificationsEvent -> { + directMessageTab?.let { + if (event.accountId == activeAccount.accountId) { + val hasDirectMessageNotification = + event.notifications.any { + it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT } + + if (hasDirectMessageNotification) { + showDirectMessageBadge(true) } } } - is NotificationsLoadingEvent -> { - if (event.accountId == activeAccount.accountId) { - showDirectMessageBadge(false) - } + } + is NotificationsLoadingEvent -> { + if (event.accountId == activeAccount.accountId) { + showDirectMessageBadge(false) } - is ConversationsLoadingEvent -> { - if (event.accountId == activeAccount.accountId) { - showDirectMessageBadge(false) - } + } + is ConversationsLoadingEvent -> { + if (event.accountId == activeAccount.accountId) { + showDirectMessageBadge(false) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index c6ef00e813..c7a80e1fe1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -3,12 +3,12 @@ package com.keylesspalace.tusky.appstore import com.google.gson.Gson import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.util.observe import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch class CacheUpdater @Inject constructor( eventHub: EventHub, @@ -22,26 +22,24 @@ class CacheUpdater @Inject constructor( init { val timelineDao = appDatabase.timelineDao() - scope.launch { - eventHub.events.collect { event -> - val accountId = accountManager.activeAccount?.id ?: return@collect - when (event) { - is StatusChangedEvent -> { - val status = event.status - timelineDao.update( - accountId = accountId, - status = status, - gson = gson - ) - } - is UnfollowEvent -> - timelineDao.removeAllByUser(accountId, event.accountId) - is StatusDeletedEvent -> - timelineDao.delete(accountId, event.statusId) - is PollVoteEvent -> { - val pollString = gson.toJson(event.poll) - timelineDao.setVoted(accountId, event.statusId, pollString) - } + eventHub.events.observe(scope) { event -> + val accountId = accountManager.activeAccount?.id ?: return@observe + when (event) { + is StatusChangedEvent -> { + val status = event.status + timelineDao.update( + accountId = accountId, + status = status, + gson = gson + ) + } + is UnfollowEvent -> + timelineDao.removeAllByUser(accountId, event.accountId) + is StatusDeletedEvent -> + timelineDao.delete(accountId, event.statusId) + is PollVoteEvent -> { + val pollString = gson.toJson(event.poll) + timelineDao.setVoted(accountId, event.statusId, pollString) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt index 05bd4542c5..1d75472918 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt @@ -1,14 +1,13 @@ package com.keylesspalace.tusky.appstore import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.util.observe import java.util.function.Consumer import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch interface Event @@ -24,10 +23,8 @@ class EventHub @Inject constructor() { // TODO remove as soon as NotificationsFragment is Kotlin fun subscribe(lifecycleOwner: LifecycleOwner, consumer: Consumer) { - lifecycleOwner.lifecycleScope.launch { - events.collect { event -> - consumer.accept(event) - } + events.observe(lifecycleOwner) { event -> + consumer.accept(event) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index 35ec65c7ae..a4aa3c79d5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -48,7 +48,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.updatePadding import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer @@ -89,6 +88,7 @@ import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.reduceSwipeSensitivity import com.keylesspalace.tusky.util.setClickableText @@ -110,7 +110,6 @@ import java.text.SimpleDateFormat import java.util.Locale import javax.inject.Inject import kotlin.math.abs -import kotlinx.coroutines.launch class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, HasAndroidInjector, LinkListener { @@ -431,32 +430,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide * Subscribe to data loaded at the view model */ private fun subscribeObservables() { - lifecycleScope.launch { - viewModel.accountData.collect { - if (it == null) return@collect - when (it) { - is Success -> onAccountChanged(it.data) - is Error -> { - Snackbar.make( - binding.accountCoordinatorLayout, - R.string.error_generic, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_retry) { viewModel.refresh() } - .show() - } - is Loading -> { } - } - } - } - lifecycleScope.launch { - viewModel.relationshipData.collect { - val relation = it?.data - if (relation != null) { - onRelationshipChanged(relation) - } - - if (it is Error) { + viewModel.accountData.observe { + if (it == null) return@observe + when (it) { + is Success -> onAccountChanged(it.data) + is Error -> { Snackbar.make( binding.accountCoordinatorLayout, R.string.error_generic, @@ -465,13 +443,28 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide .setAction(R.string.action_retry) { viewModel.refresh() } .show() } + is Loading -> { } } } - lifecycleScope.launch { - viewModel.noteSaved.collect { - binding.saveNoteInfo.visible(it, View.INVISIBLE) + viewModel.relationshipData.observe { + val relation = it?.data + if (relation != null) { + onRelationshipChanged(relation) + } + + if (it is Error) { + Snackbar.make( + binding.accountCoordinatorLayout, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() } } + viewModel.noteSaved.observe { + binding.saveNoteInfo.visible(it, View.INVISIBLE) + } // "Post failed" dialog should display in this activity draftsAlert.observeInContext(this, true) @@ -487,10 +480,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide */ private fun setupRefreshLayout() { binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() } - lifecycleScope.launch { - viewModel.isRefreshing.collect { isRefreshing -> - binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true - } + viewModel.isRefreshing.observe { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true } binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt index c21e679d2b..72cedbf642 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt @@ -19,6 +19,7 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.getDomain +import com.keylesspalace.tusky.util.observe import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -61,11 +62,9 @@ class AccountViewModel @Inject constructor( private val activeAccount = accountManager.activeAccount!! init { - viewModelScope.launch { - eventHub.events.collect { event -> - if (event is ProfileEditedEvent && event.newProfileData.id == _accountData.value?.data?.id) { - _accountData.value = Success(event.newProfileData) - } + eventHub.events.observe(viewModelScope) { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == _accountData.value?.data?.id) { + _accountData.value = Success(event.newProfileData) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt index 585c7e3111..e64bf7de48 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt @@ -40,13 +40,13 @@ import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.visible import javax.inject.Inject import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class ListSelectionFragment : DialogFragment(), Injectable { @@ -102,58 +102,52 @@ class ListSelectionFragment : DialogFragment(), Injectable { showProgressBarJob.start() // TODO change this to a (single) LoadState like elsewhere? - lifecycleScope.launch { - viewModel.states.collectLatest { states -> - binding.progressBar.hide() - showProgressBarJob.cancel() - if (states.isEmpty()) { - binding.messageView.show() - binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) - } else { - binding.listsView.show() - adapter.submitList(states) - } + viewModel.states.observeLatest { states -> + binding.progressBar.hide() + showProgressBarJob.cancel() + if (states.isEmpty()) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) + } else { + binding.listsView.show() + adapter.submitList(states) } } - lifecycleScope.launch { - viewModel.loadError.collectLatest { error -> - Log.e(TAG, "failed to load lists", error) - binding.progressBar.hide() - showProgressBarJob.cancel() - binding.listsView.hide() - binding.messageView.apply { - show() - setup(error) { load() } - } + viewModel.loadError.observeLatest { error -> + Log.e(TAG, "failed to load lists", error) + binding.progressBar.hide() + showProgressBarJob.cancel() + binding.listsView.hide() + binding.messageView.apply { + show() + setup(error) { load() } } } - lifecycleScope.launch { - viewModel.actionError.collectLatest { error -> - when (error.type) { - ActionError.Type.ADD -> { - Snackbar.make( - binding.root, - R.string.failed_to_add_to_list, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_retry) { - viewModel.addAccountToList(accountId!!, error.listId) - } - .show() - } - ActionError.Type.REMOVE -> { - Snackbar.make( - binding.root, - R.string.failed_to_remove_from_list, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_retry) { - viewModel.removeAccountFromList(accountId!!, error.listId) - } - .show() - } + viewModel.actionError.observeLatest { error -> + when (error.type) { + ActionError.Type.ADD -> { + Snackbar.make( + binding.root, + R.string.failed_to_add_to_list, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.addAccountToList(accountId!!, error.listId) + } + .show() + } + ActionError.Type.REMOVE -> { + Snackbar.make( + binding.root, + R.string.failed_to_remove_from_list, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.removeAccountFromList(accountId!!, error.listId) + } + .show() } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt index 49f19390ad..829c69f282 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt @@ -26,7 +26,6 @@ import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager @@ -41,6 +40,7 @@ import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding @@ -50,8 +50,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch /** * Fragment with multiple columns of media previews for the specified account. @@ -109,10 +107,8 @@ class AccountMediaFragment : binding.statusView.visibility = View.GONE - viewLifecycleOwner.lifecycleScope.launch { - viewModel.media.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.media.observeLatest(viewLifecycleOwner) { pagingData -> + adapter.submitData(pagingData) } adapter.addLoadStateListener { loadState -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index 1ed3b37793..5a660b2ec9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -26,7 +26,6 @@ import android.view.View import android.widget.PopupWindow import androidx.activity.viewModels import androidx.core.view.MenuProvider -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -44,6 +43,7 @@ import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.unsafeLazy @@ -54,7 +54,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject -import kotlinx.coroutines.launch class AnnouncementsActivity : BottomSheetActivity(), @@ -113,46 +112,42 @@ class AnnouncementsActivity : binding.announcementsList.adapter = adapter - lifecycleScope.launch { - viewModel.announcements.collect { - if (it == null) return@collect - when (it) { - is Success -> { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - if (it.data.isNullOrEmpty()) { - binding.errorMessageView.setup( - R.drawable.elephant_friend_empty, - R.string.no_announcements - ) - binding.errorMessageView.show() - } else { - binding.errorMessageView.hide() - } - adapter.updateList(it.data ?: listOf()) - } - is Loading -> { - binding.errorMessageView.hide() - } - is Error -> { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false + viewModel.announcements.observe { + if (it == null) return@observe + when (it) { + is Success -> { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + if (it.data.isNullOrEmpty()) { binding.errorMessageView.setup( - R.drawable.errorphant_error, - R.string.error_generic - ) { - refreshAnnouncements() - } + R.drawable.elephant_friend_empty, + R.string.no_announcements + ) binding.errorMessageView.show() + } else { + binding.errorMessageView.hide() } + adapter.updateList(it.data ?: listOf()) + } + is Loading -> { + binding.errorMessageView.hide() + } + is Error -> { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup( + R.drawable.errorphant_error, + R.string.error_generic + ) { + refreshAnnouncements() + } + binding.errorMessageView.show() } } } - lifecycleScope.launch { - viewModel.emoji.collect { - picker.adapter = EmojiAdapter(it, this@AnnouncementsActivity, animateEmojis) - } + viewModel.emoji.observe { + picker.adapter = EmojiAdapter(it, this@AnnouncementsActivity, animateEmojis) } viewModel.load() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index ba13556a5f..e84652226e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -107,6 +107,7 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.highlightSpans import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.modernLanguageCode +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.setDrawableTint import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.unsafeLazy @@ -123,7 +124,6 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.max import kotlin.math.min -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -490,91 +490,70 @@ class ComposeActivity : } private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { - lifecycleScope.launch { - viewModel.instanceInfo.collect { instanceData -> - maximumTootCharacters = instanceData.maxChars - charactersReservedPerUrl = instanceData.charactersReservedPerUrl - maxUploadMediaNumber = instanceData.maxMediaAttachments - updateVisibleCharactersLeft() - } - } - - lifecycleScope.launch { - viewModel.emoji.collect(::setEmojiList) + viewModel.instanceInfo.observe { instanceData -> + maximumTootCharacters = instanceData.maxChars + charactersReservedPerUrl = instanceData.charactersReservedPerUrl + maxUploadMediaNumber = instanceData.maxMediaAttachments + updateVisibleCharactersLeft() } - lifecycleScope.launch { - viewModel.showContentWarning.combine( - viewModel.markMediaAsSensitive - ) { showContentWarning, markSensitive -> - updateSensitiveMediaToggle(markSensitive, showContentWarning) - showContentWarning(showContentWarning) - }.collect() - } + viewModel.emoji.observe(::setEmojiList) - lifecycleScope.launch { - viewModel.statusVisibility.collect(::setStatusVisibility) - } + viewModel.showContentWarning.combine( + viewModel.markMediaAsSensitive + ) { showContentWarning, markSensitive -> + updateSensitiveMediaToggle(markSensitive, showContentWarning) + showContentWarning(showContentWarning) + }.observe() + viewModel.statusVisibility.observe(::setStatusVisibility) - lifecycleScope.launch { - viewModel.media.collect { media -> - mediaAdapter.submitList(media) + viewModel.media.observe { media -> + mediaAdapter.submitList(media) - binding.composeMediaPreviewBar.visible(media.isNotEmpty()) - updateSensitiveMediaToggle( - viewModel.markMediaAsSensitive.value, - viewModel.showContentWarning.value - ) - } + binding.composeMediaPreviewBar.visible(media.isNotEmpty()) + updateSensitiveMediaToggle( + viewModel.markMediaAsSensitive.value, + viewModel.showContentWarning.value + ) } - lifecycleScope.launch { - viewModel.poll.collect { poll -> - binding.pollPreview.visible(poll != null) - poll?.let(binding.pollPreview::setPoll) - } + viewModel.poll.observe { poll -> + binding.pollPreview.visible(poll != null) + poll?.let(binding.pollPreview::setPoll) } - lifecycleScope.launch { - viewModel.scheduledAt.collect { scheduledAt -> - if (scheduledAt == null) { - binding.composeScheduleView.resetSchedule() - } else { - binding.composeScheduleView.setDateTime(scheduledAt) - } - updateScheduleButton() + viewModel.scheduledAt.observe { scheduledAt -> + if (scheduledAt == null) { + binding.composeScheduleView.resetSchedule() + } else { + binding.composeScheduleView.setDateTime(scheduledAt) } + updateScheduleButton() } - lifecycleScope.launch { - viewModel.media.combine(viewModel.poll) { media, poll -> - val active = poll == null && - media.size < maxUploadMediaNumber && - (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) - enableButton(binding.composeAddMediaButton, active, active) - enablePollButton(media.isEmpty()) - }.collect() - } + viewModel.media.combine(viewModel.poll) { media, poll -> + val active = poll == null && + media.size < maxUploadMediaNumber && + (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) + enableButton(binding.composeAddMediaButton, active, active) + enablePollButton(media.isEmpty()) + }.observe() - lifecycleScope.launch { - viewModel.uploadError.collect { throwable -> - if (throwable is UploadServerError) { - displayTransientMessage(throwable.errorMessage) - } else { - displayTransientMessage( - getString( - R.string.error_media_upload_sending_fmt, - throwable.message - ) + viewModel.uploadError.observe { throwable -> + if (throwable is UploadServerError) { + displayTransientMessage(throwable.errorMessage) + } else { + displayTransientMessage( + getString( + R.string.error_media_upload_sending_fmt, + throwable.message ) - } + ) } } - lifecycleScope.launch { - viewModel.closeConfirmation.collect { - updateOnBackPressedCallbackState() - } + viewModel.closeConfirmation.observe { + updateOnBackPressedCallbackState() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 639779eaa2..a5f6fbfbd4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -37,6 +37,7 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.MediaToSend import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.StatusToSend +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.randomAlphanumericString import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -169,42 +170,40 @@ class ComposeViewModel @Inject constructor( } val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that - viewModelScope.launch { - mediaUploader - .uploadMedia(mediaItem, instanceInfo.first()) - .collect { event -> - val item = media.value.find { it.localId == mediaItem.localId } - ?: return@collect - val newMediaItem = when (event) { - is UploadEvent.ProgressEvent -> - item.copy(uploadPercent = event.percentage) - is UploadEvent.FinishedEvent -> - item.copy( - id = event.mediaId, - uploadPercent = -1, - state = if (event.processed) { - QueuedMedia.State.PROCESSED - } else { - QueuedMedia.State.UNPROCESSED - } - ) - is UploadEvent.ErrorEvent -> { - media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } - uploadError.emit(event.error) - return@collect - } - } - media.update { mediaList -> - mediaList.map { mediaItem -> - if (mediaItem.localId == newMediaItem.localId) { - newMediaItem + mediaUploader + .uploadMedia(mediaItem, instanceInfo.first()) + .observe(viewModelScope) { event -> + val item = media.value.find { it.localId == mediaItem.localId } + ?: return@observe + val newMediaItem = when (event) { + is UploadEvent.ProgressEvent -> + item.copy(uploadPercent = event.percentage) + is UploadEvent.FinishedEvent -> + item.copy( + id = event.mediaId, + uploadPercent = -1, + state = if (event.processed) { + QueuedMedia.State.PROCESSED } else { - mediaItem + QueuedMedia.State.UNPROCESSED } + ) + is UploadEvent.ErrorEvent -> { + media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } + uploadError.emit(event.error) + return@observe + } + } + media.update { mediaList -> + mediaList.map { mediaItem -> + if (mediaItem.localId == newMediaItem.localId) { + newMediaItem + } else { + mediaItem } } } - } + } updateCloseConfirmation() return mediaItem } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index caa264d8f9..fc37c1d50c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -56,6 +56,8 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.isAnyLoading +import com.keylesspalace.tusky.util.observe +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData @@ -67,7 +69,6 @@ import javax.inject.Inject import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class ConversationsFragment : @@ -202,10 +203,8 @@ class ConversationsFragment : } }) - viewLifecycleOwner.lifecycleScope.launch { - viewModel.conversationFlow.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.conversationFlow.observeLatest(viewLifecycleOwner) { pagingData -> + adapter.submitData(pagingData) } viewLifecycleOwner.lifecycleScope.launch { @@ -222,11 +221,9 @@ class ConversationsFragment : } } - lifecycleScope.launch { - eventHub.events.collect { event -> - if (event is PreferenceChangedEvent) { - onPreferenceChanged(event.preferenceKey) - } + eventHub.events.observe { event -> + if (event is PreferenceChangedEvent) { + onPreferenceChanged(event.preferenceKey) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt index 1adfb911ca..3626779c9c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt @@ -5,7 +5,6 @@ import android.util.Log import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -15,12 +14,12 @@ import com.keylesspalace.tusky.databinding.FragmentDomainBlocksBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable { @@ -41,16 +40,12 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab binding.recyclerView.adapter = adapter binding.recyclerView.layoutManager = LinearLayoutManager(view.context) - viewLifecycleOwner.lifecycleScope.launch { - viewModel.uiEvents.collect { event -> - showSnackbar(event) - } + viewModel.uiEvents.observe(viewLifecycleOwner) { event -> + showSnackbar(event) } - lifecycleScope.launch { - viewModel.domainPager.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.domainPager.observeLatest { pagingData -> + adapter.submitData(pagingData) } adapter.addLoadStateListener { loadState -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index 1db7982c6d..f509b89428 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -36,10 +36,10 @@ import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.db.DraftsAlert import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.visible import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class DraftsActivity : BaseActivity(), DraftActionListener { @@ -80,10 +80,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener { bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) - lifecycleScope.launch { - viewModel.drafts.collectLatest { draftData -> - adapter.submitData(draftData) - } + viewModel.drafts.observeLatest { draftData -> + adapter.submitData(draftData) } adapter.addLoadStateListener { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt index 709e2c5f76..1afd8702f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt @@ -27,6 +27,7 @@ import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.FilterKeyword import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import java.util.Date @@ -145,33 +146,25 @@ class EditFilterActivity : BaseActivity() { } private fun observeModel() { - lifecycleScope.launch { - viewModel.title.collect { title -> - if (title != binding.filterTitle.text.toString()) { - // We also get this callback when typing in the field, - // which messes with the cursor focus - binding.filterTitle.setText(title) - } + viewModel.title.observe { title -> + if (title != binding.filterTitle.text.toString()) { + // We also get this callback when typing in the field, + // which messes with the cursor focus + binding.filterTitle.setText(title) } } - lifecycleScope.launch { - viewModel.keywords.collect { keywords -> - updateKeywords(keywords) - } + viewModel.keywords.observe { keywords -> + updateKeywords(keywords) } - lifecycleScope.launch { - viewModel.contexts.collect { contexts -> - for (entry in contextSwitches) { - entry.key.isChecked = contexts.contains(entry.value) - } + viewModel.contexts.observe { contexts -> + for (entry in contextSwitches) { + entry.key.isChecked = contexts.contains(entry.value) } } - lifecycleScope.launch { - viewModel.action.collect { action -> - when (action) { - Filter.Action.HIDE -> binding.filterActionHide.isChecked = true - else -> binding.filterActionWarn.isChecked = true - } + viewModel.action.observe { action -> + when (action) { + Filter.Action.HIDE -> binding.filterActionHide.isChecked = true + else -> binding.filterActionWarn.isChecked = true } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt index 0acc043aed..6ca6b1900b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt @@ -11,6 +11,7 @@ import com.keylesspalace.tusky.databinding.ActivityFiltersBinding import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding @@ -53,48 +54,46 @@ class FiltersActivity : BaseActivity(), FiltersListener { } private fun observeViewModel() { - lifecycleScope.launch { - viewModel.state.collect { state -> - binding.progressBar.visible( - state.loadingState == FiltersViewModel.LoadingState.LOADING - ) - binding.swipeRefreshLayout.isRefreshing = state.loadingState == FiltersViewModel.LoadingState.LOADING - binding.addFilterButton.visible( - state.loadingState == FiltersViewModel.LoadingState.LOADED - ) + viewModel.state.observe { state -> + binding.progressBar.visible( + state.loadingState == FiltersViewModel.LoadingState.LOADING + ) + binding.swipeRefreshLayout.isRefreshing = state.loadingState == FiltersViewModel.LoadingState.LOADING + binding.addFilterButton.visible( + state.loadingState == FiltersViewModel.LoadingState.LOADED + ) - when (state.loadingState) { - FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide() - FiltersViewModel.LoadingState.ERROR_NETWORK -> { - binding.messageView.setup( - R.drawable.errorphant_offline, - R.string.error_network - ) { - loadFilters() - } - binding.messageView.show() + when (state.loadingState) { + FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide() + FiltersViewModel.LoadingState.ERROR_NETWORK -> { + binding.messageView.setup( + R.drawable.errorphant_offline, + R.string.error_network + ) { + loadFilters() } - FiltersViewModel.LoadingState.ERROR_OTHER -> { + binding.messageView.show() + } + FiltersViewModel.LoadingState.ERROR_OTHER -> { + binding.messageView.setup( + R.drawable.errorphant_error, + R.string.error_generic + ) { + loadFilters() + } + binding.messageView.show() + } + FiltersViewModel.LoadingState.LOADED -> { + binding.filtersList.adapter = FiltersAdapter(this@FiltersActivity, state.filters) + if (state.filters.isEmpty()) { binding.messageView.setup( - R.drawable.errorphant_error, - R.string.error_generic - ) { - loadFilters() - } + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) binding.messageView.show() - } - FiltersViewModel.LoadingState.LOADED -> { - binding.filtersList.adapter = FiltersAdapter(this@FiltersActivity, state.filters) - if (state.filters.isEmpty()) { - binding.messageView.setup( - R.drawable.elephant_friend_empty, - R.string.message_empty, - null - ) - binding.messageView.show() - } else { - binding.messageView.hide() - } + } else { + binding.messageView.hide() } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt index 82a17265d5..73807a990e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt @@ -26,11 +26,11 @@ import com.keylesspalace.tusky.interfaces.HashtagActionListener import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class FollowedTagsActivity : @@ -69,10 +69,8 @@ class FollowedTagsActivity : setupAdapter().let { adapter -> setupRecyclerView(adapter) - lifecycleScope.launch { - viewModel.pager.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.pager.observeLatest { pagingData -> + adapter.submitData(pagingData) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt index b8c113b900..56d5b47439 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -35,7 +35,6 @@ import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.core.content.IntentCompat import androidx.core.net.toUri -import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R @@ -43,10 +42,10 @@ import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import javax.inject.Inject -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize /** Contract for starting [LoginWebViewActivity]. */ @@ -197,18 +196,16 @@ class LoginWebViewActivity : BaseActivity(), Injectable { viewModel.init(data.domain) - lifecycleScope.launch { - viewModel.instanceRules.collect { instanceRules -> - binding.loginRules.visible(instanceRules.isNotEmpty()) - binding.loginRules.setOnClickListener { - AlertDialog.Builder(this@LoginWebViewActivity) - .setTitle(getString(R.string.instance_rule_title, data.domain)) - .setMessage( - instanceRules.joinToString(separator = "\n\n") { "• $it" } - ) - .setPositiveButton(android.R.string.ok, null) - .show() - } + viewModel.instanceRules.observe { instanceRules -> + binding.loginRules.visible(instanceRules.isNotEmpty()) + binding.loginRules.setOnClickListener { + AlertDialog.Builder(this@LoginWebViewActivity) + .setTitle(getString(R.string.instance_rule_title, data.domain)) + .setMessage( + instanceRules.joinToString(separator = "\n\n") { "• $it" } + ) + .setPositiveButton(android.R.string.ok, null) + .show() } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 72ebbd2030..09a6d07332 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -19,17 +19,16 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels -import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter import com.keylesspalace.tusky.databinding.ActivityReportBinding import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -import kotlinx.coroutines.launch class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @@ -84,26 +83,22 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun subscribeObservables() { - lifecycleScope.launch { - viewModel.navigation.collect { screen -> - if (screen == null) return@collect - viewModel.navigated() - when (screen) { - Screen.Statuses -> showStatusesPage() - Screen.Note -> showNotesPage() - Screen.Done -> showDonePage() - Screen.Back -> showPreviousScreen() - Screen.Finish -> closeScreen() - } + viewModel.navigation.observe { screen -> + if (screen == null) return@observe + viewModel.navigated() + when (screen) { + Screen.Statuses -> showStatusesPage() + Screen.Note -> showNotesPage() + Screen.Done -> showDonePage() + Screen.Back -> showPreviousScreen() + Screen.Finish -> closeScreen() } } - lifecycleScope.launch { - viewModel.checkUrl.collect { - if (!it.isNullOrBlank()) { - viewModel.urlChecked() - viewUrl(it) - } + viewModel.checkUrl.observe { + if (!it.isNullOrBlank()) { + viewModel.urlChecked() + viewUrl(it) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 2b5ed2628f..460fc7f3af 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -19,7 +19,6 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen @@ -28,10 +27,10 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import javax.inject.Inject -import kotlinx.coroutines.launch class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { @@ -49,43 +48,39 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { } private fun subscribeObservables() { - viewLifecycleOwner.lifecycleScope.launch { - viewModel.muteState.collect { - if (it == null) return@collect - if (it !is Loading) { - binding.buttonMute.show() - binding.progressMute.show() - } else { - binding.buttonMute.hide() - binding.progressMute.hide() - } - - binding.buttonMute.setText( - when (it.data) { - true -> R.string.action_unmute - else -> R.string.action_mute - } - ) + viewModel.muteState.observe(viewLifecycleOwner) { + if (it == null) return@observe + if (it !is Loading) { + binding.buttonMute.show() + binding.progressMute.show() + } else { + binding.buttonMute.hide() + binding.progressMute.hide() } - } - viewLifecycleOwner.lifecycleScope.launch { - viewModel.blockState.collect { - if (it == null) return@collect - if (it !is Loading) { - binding.buttonBlock.show() - binding.progressBlock.show() - } else { - binding.buttonBlock.hide() - binding.progressBlock.hide() + binding.buttonMute.setText( + when (it.data) { + true -> R.string.action_unmute + else -> R.string.action_mute } - binding.buttonBlock.setText( - when (it.data) { - true -> R.string.action_unblock - else -> R.string.action_block - } - ) + ) + } + + viewModel.blockState.observe(viewLifecycleOwner) { + if (it == null) return@observe + if (it !is Loading) { + binding.buttonBlock.show() + binding.progressBlock.show() + } else { + binding.buttonBlock.hide() + binding.progressBlock.hide() } + binding.buttonBlock.setText( + when (it.data) { + true -> R.string.action_unblock + else -> R.string.action_block + } + ) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index 215414ff9f..5246aa12fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -20,7 +20,6 @@ import android.view.View import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel @@ -32,11 +31,11 @@ import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import java.io.IOException import javax.inject.Inject -import kotlinx.coroutines.launch class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { @@ -81,14 +80,12 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun subscribeObservables() { - viewLifecycleOwner.lifecycleScope.launch { - viewModel.reportingState.collect { - if (it == null) return@collect - when (it) { - is Success -> viewModel.navigateTo(Screen.Done) - is Loading -> showLoading() - is Error -> showError(it.cause) - } + viewModel.reportingState.observe(viewLifecycleOwner) { + if (it == null) return@observe + when (it) { + is Success -> viewModel.navigateTo(Screen.Done) + is Loading -> showLoading() + is Error -> showError(it.cause) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 9f20265f14..65eb91535e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -26,7 +26,6 @@ import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration @@ -52,6 +51,7 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData @@ -60,8 +60,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), @@ -175,10 +173,8 @@ class ReportStatusesFragment : binding.recyclerView.adapter = adapter (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - lifecycleScope.launch { - viewModel.statusesFlow.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.statusesFlow.observeLatest { pagingData -> + adapter.submitData(pagingData) } adapter.addLoadStateListener { loadState -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt index 89da2e12d0..3e4f814b53 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt @@ -24,7 +24,6 @@ import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.core.view.MenuProvider -import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -39,6 +38,8 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.mikepenz.iconics.IconicsDrawable @@ -46,8 +47,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch class ScheduledStatusActivity : BaseActivity(), @@ -89,10 +88,8 @@ class ScheduledStatusActivity : binding.scheduledTootList.addItemDecoration(divider) binding.scheduledTootList.adapter = adapter - lifecycleScope.launch { - viewModel.data.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.data.observeLatest { pagingData -> + adapter.submitData(pagingData) } adapter.addLoadStateListener { loadState -> @@ -120,11 +117,9 @@ class ScheduledStatusActivity : } } - lifecycleScope.launch { - eventHub.events.collect { event -> - if (event is StatusScheduledEvent) { - adapter.refresh() - } + eventHub.events.observe { event -> + if (event is StatusScheduledEvent) { + adapter.refresh() } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 1b0a92466e..39b9c0f78b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -9,7 +9,6 @@ import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.PagingDataAdapter @@ -28,6 +27,7 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible @@ -37,8 +37,6 @@ import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch abstract class SearchFragment : Fragment(R.layout.fragment_search), @@ -79,10 +77,8 @@ abstract class SearchFragment : } private fun subscribeObservables() { - viewLifecycleOwner.lifecycleScope.launch { - data.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + data.observeLatest { pagingData -> + adapter.submitData(pagingData) } adapter.addLoadStateListener { loadState -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index 4eb95f2bb6..6375ae13aa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -65,6 +65,8 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.unsafeLazy @@ -78,7 +80,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class TimelineFragment : @@ -278,10 +279,8 @@ class TimelineFragment : } }) - viewLifecycleOwner.lifecycleScope.launch { - viewModel.statuses.collectLatest { pagingData -> - adapter.submitData(pagingData) - } + viewModel.statuses.observeLatest(viewLifecycleOwner) { pagingData -> + adapter.submitData(pagingData) } if (actionButtonPresent()) { @@ -305,21 +304,18 @@ class TimelineFragment : }) } - viewLifecycleOwner.lifecycleScope.launch { - eventHub.events.collect { event -> - when (event) { - is PreferenceChangedEvent -> { - onPreferenceChanged(event.preferenceKey) - } + eventHub.events.observe(viewLifecycleOwner) { event -> + when (event) { + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } - is StatusComposedEvent -> { - val status = event.status - handleStatusComposeEvent(status) - } + is StatusComposedEvent -> { + val status = event.status + handleStatusComposeEvent(status) } } } - updateRelativeTimePeriodically { adapter.notifyItemRangeChanged( 0, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 3d02b90dc6..d4bb8b5b68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -47,6 +47,7 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -97,10 +98,7 @@ abstract class TimelineViewModel( this.alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia this.alwaysOpenSpoilers = accountManager.activeAccount!!.alwaysOpenSpoiler - viewModelScope.launch { - eventHub.events - .collect { event -> handleEvent(event) } - } + eventHub.events.observe(viewModelScope) { event -> handleEvent(event) } reloadFilters() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt index 6b0a62af9d..4dff14b047 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt @@ -23,7 +23,6 @@ import android.view.accessibility.AccessibilityManager import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.RecyclerView @@ -40,13 +39,12 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observeLatest import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.TrendingViewData import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch class TrendingTagsFragment : Fragment(R.layout.fragment_trending_tags), @@ -90,10 +88,8 @@ class TrendingTagsFragment : } }) - viewLifecycleOwner.lifecycleScope.launch { - viewModel.uiState.collectLatest { trendingState -> - processViewState(trendingState) - } + viewModel.uiState.observeLatest(viewLifecycleOwner) { trendingState -> + processViewState(trendingState) } if (activity is ActionButtonActivity) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt index 5a7ad9232e..56b4976fb2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt @@ -25,6 +25,7 @@ import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.end import com.keylesspalace.tusky.entity.start import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.TrendingViewData import java.io.IOException @@ -62,13 +63,11 @@ class TrendingTagsViewModel @Inject constructor( // Collect PreferenceChangedEvent, FiltersActivity creates them when a filter is created // or deleted. Unfortunately, there's nothing in the event to determine if it's a filter // that was modified, so refresh on every preference change. - viewModelScope.launch { - eventHub.events - .filterIsInstance() - .collect { - invalidate() - } - } + eventHub.events + .filterIsInstance() + .observe(viewModelScope) { + invalidate() + } } /** diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt index e38d7f3644..8b6a6e4916 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -51,6 +51,7 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation @@ -156,103 +157,99 @@ class ViewThreadFragment : var initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500) var threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500) - viewLifecycleOwner.lifecycleScope.launch { - viewModel.uiState.collect { uiState -> - when (uiState) { - is ThreadUiState.Loading -> { - revealButtonState = RevealButtonState.NO_BUTTON + viewModel.uiState.observe(viewLifecycleOwner) { uiState -> + when (uiState) { + is ThreadUiState.Loading -> { + revealButtonState = RevealButtonState.NO_BUTTON - binding.recyclerView.hide() - binding.statusView.hide() + binding.recyclerView.hide() + binding.statusView.hide() - initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500) - initialProgressBar.start() + initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500) + initialProgressBar.start() + } + + is ThreadUiState.LoadingThread -> { + if (uiState.statusViewDatum == null) { + // no detailed statuses available, e.g. because author is blocked + activity?.finish() + return@observe } - is ThreadUiState.LoadingThread -> { - if (uiState.statusViewDatum == null) { - // no detailed statuses available, e.g. because author is blocked - activity?.finish() - return@collect - } + initialProgressBar.cancel() + threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500) + threadProgressBar.start() - initialProgressBar.cancel() - threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500) - threadProgressBar.start() + if (viewModel.isInitialLoad) { + adapter.submitList(listOf(uiState.statusViewDatum)) - if (viewModel.isInitialLoad) { - adapter.submitList(listOf(uiState.statusViewDatum)) + // else this "submit one and then all on success below" will always center on the one + } - // else this "submit one and then all on success below" will always center on the one - } + revealButtonState = uiState.revealButton + binding.swipeRefreshLayout.isRefreshing = false - revealButtonState = uiState.revealButton - binding.swipeRefreshLayout.isRefreshing = false + binding.recyclerView.show() + binding.statusView.hide() + } - binding.recyclerView.show() - binding.statusView.hide() - } + is ThreadUiState.Error -> { + Log.w(TAG, "failed to load status", uiState.throwable) + initialProgressBar.cancel() + threadProgressBar.cancel() - is ThreadUiState.Error -> { - Log.w(TAG, "failed to load status", uiState.throwable) - initialProgressBar.cancel() - threadProgressBar.cancel() + revealButtonState = RevealButtonState.NO_BUTTON + binding.swipeRefreshLayout.isRefreshing = false - revealButtonState = RevealButtonState.NO_BUTTON - binding.swipeRefreshLayout.isRefreshing = false + binding.recyclerView.hide() + binding.statusView.show() - binding.recyclerView.hide() - binding.statusView.show() + binding.statusView.setup( + uiState.throwable + ) { viewModel.retry(thisThreadsStatusId) } + } - binding.statusView.setup( - uiState.throwable - ) { viewModel.retry(thisThreadsStatusId) } + is ThreadUiState.Success -> { + if (uiState.statusViewData.none { viewData -> viewData.isDetailed }) { + // no detailed statuses available, e.g. because author is blocked + activity?.finish() + return@observe } - is ThreadUiState.Success -> { - if (uiState.statusViewData.none { viewData -> viewData.isDetailed }) { - // no detailed statuses available, e.g. because author is blocked - activity?.finish() - return@collect - } - - threadProgressBar.cancel() + threadProgressBar.cancel() - adapter.submitList(uiState.statusViewData) { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && viewModel.isInitialLoad) { - viewModel.isInitialLoad = false + adapter.submitList(uiState.statusViewData) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && viewModel.isInitialLoad) { + viewModel.isInitialLoad = false - // Ensure the top of the status is visible - (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - uiState.detailedStatusPosition, - 0 - ) - } + // Ensure the top of the status is visible + (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + uiState.detailedStatusPosition, + 0 + ) } + } - revealButtonState = uiState.revealButton - binding.swipeRefreshLayout.isRefreshing = false + revealButtonState = uiState.revealButton + binding.swipeRefreshLayout.isRefreshing = false - binding.recyclerView.show() - binding.statusView.hide() - } + binding.recyclerView.show() + binding.statusView.hide() + } - is ThreadUiState.Refreshing -> { - threadProgressBar.cancel() - } + is ThreadUiState.Refreshing -> { + threadProgressBar.cancel() } } } - lifecycleScope.launch { - viewModel.errors.collect { throwable -> - Log.w(TAG, "failed to load status context", throwable) - Snackbar.make(binding.root, R.string.error_generic, Snackbar.LENGTH_SHORT) - .setAction(R.string.action_retry) { - viewModel.retry(thisThreadsStatusId) - } - .show() - } + viewModel.errors.observe { throwable -> + Log.w(TAG, "failed to load status context", throwable) + Snackbar.make(binding.root, R.string.error_generic, Snackbar.LENGTH_SHORT) + .setAction(R.string.action_retry) { + viewModel.retry(thisThreadsStatusId) + } + .show() } viewModel.loadThread(thisThreadsStatusId) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt index b864b37208..fd426b1281 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -41,6 +41,7 @@ import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.TranslationViewData @@ -87,16 +88,13 @@ class ViewThreadViewModel @Inject constructor( alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false - viewModelScope.launch { - eventHub.events - .collect { event -> - when (event) { - is StatusChangedEvent -> handleStatusChangedEvent(event.status) - is BlockEvent -> removeAllByAccountId(event.accountId) - is StatusComposedEvent -> handleStatusComposedEvent(event) - is StatusDeletedEvent -> handleStatusDeletedEvent(event) - } - } + eventHub.events.observe(viewModelScope) { event -> + when (event) { + is StatusChangedEvent -> handleStatusChangedEvent(event.status) + is BlockEvent -> removeAllByAccountId(event.accountId) + is StatusComposedEvent -> handleStatusComposedEvent(event) + is StatusDeletedEvent -> handleStatusDeletedEvent(event) + } } loadFilters() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt index d03ed9e8d8..8ad939249f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt @@ -26,7 +26,6 @@ import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -45,6 +44,7 @@ import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.observe import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.unicodeWrap @@ -54,7 +54,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import javax.inject.Inject -import kotlinx.coroutines.launch class ViewEditsFragment : Fragment(R.layout.fragment_view_edits), @@ -95,67 +94,65 @@ class ViewEditsFragment : R.dimen.avatar_radius_48dp ) - viewLifecycleOwner.lifecycleScope.launch { - viewModel.uiState.collect { uiState -> - when (uiState) { - EditsUiState.Initial -> {} - EditsUiState.Loading -> { - binding.recyclerView.hide() - binding.statusView.hide() - binding.initialProgressBar.show() - } - EditsUiState.Refreshing -> {} - is EditsUiState.Error -> { - Log.w(TAG, "failed to load edits", uiState.throwable) - - binding.swipeRefreshLayout.isRefreshing = false - binding.recyclerView.hide() - binding.statusView.show() - binding.initialProgressBar.hide() - - when (uiState.throwable) { - is ViewEditsViewModel.MissingEditsException -> { - binding.statusView.setup( - R.drawable.elephant_friend_empty, - R.string.error_missing_edits - ) - } - else -> { - binding.statusView.setup(uiState.throwable) { - viewModel.loadEdits(statusId, force = true) - } + viewModel.uiState.observe(viewLifecycleOwner) { uiState -> + when (uiState) { + EditsUiState.Initial -> {} + EditsUiState.Loading -> { + binding.recyclerView.hide() + binding.statusView.hide() + binding.initialProgressBar.show() + } + EditsUiState.Refreshing -> {} + is EditsUiState.Error -> { + Log.w(TAG, "failed to load edits", uiState.throwable) + + binding.swipeRefreshLayout.isRefreshing = false + binding.recyclerView.hide() + binding.statusView.show() + binding.initialProgressBar.hide() + + when (uiState.throwable) { + is ViewEditsViewModel.MissingEditsException -> { + binding.statusView.setup( + R.drawable.elephant_friend_empty, + R.string.error_missing_edits + ) + } + else -> { + binding.statusView.setup(uiState.throwable) { + viewModel.loadEdits(statusId, force = true) } } } - is EditsUiState.Success -> { - binding.swipeRefreshLayout.isRefreshing = false - binding.recyclerView.show() - binding.statusView.hide() - binding.initialProgressBar.hide() - - binding.recyclerView.adapter = ViewEditsAdapter( - edits = uiState.edits, - animateEmojis = animateEmojis, - useBlurhash = useBlurhash, - listener = this@ViewEditsFragment - ) - - // Focus on the most recent version - (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPosition( - 0 - ) - - val account = uiState.edits.first().account - loadAvatar( - account.avatar, - binding.statusAvatar, - avatarRadius, - animateAvatars - ) - - binding.statusDisplayName.text = account.name.unicodeWrap().emojify(account.emojis, binding.statusDisplayName, animateEmojis) - binding.statusUsername.text = account.username - } + } + is EditsUiState.Success -> { + binding.swipeRefreshLayout.isRefreshing = false + binding.recyclerView.show() + binding.statusView.hide() + binding.initialProgressBar.hide() + + binding.recyclerView.adapter = ViewEditsAdapter( + edits = uiState.edits, + animateEmojis = animateEmojis, + useBlurhash = useBlurhash, + listener = this@ViewEditsFragment + ) + + // Focus on the most recent version + (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPosition( + 0 + ) + + val account = uiState.edits.first().account + loadAvatar( + account.avatar, + binding.statusAvatar, + avatarRadius, + animateAvatars + ) + + binding.statusDisplayName.text = account.name.unicodeWrap().emojify(account.emojis, binding.statusDisplayName, animateEmojis) + binding.statusUsername.text = account.username } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt index 5b2993f689..84b312f7b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.drafts.DraftsActivity +import com.keylesspalace.tusky.util.observe import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.launch @@ -55,34 +56,32 @@ class DraftsAlert @Inject constructor(db: AppDatabase) { // observe ensures that this gets called at the most appropriate moment wrt the context lifecycle— // at init, at next onResume, or immediately if the context is resumed already. - coroutineScope.launch { - if (showAlert) { - draftsNeedUserAlert.collect { count -> - Log.d(TAG, "User id $activeAccountId changed: Notification-worthy draft count $count") - if (count > 0) { - AlertDialog.Builder(context) - .setTitle(R.string.action_post_failed) - .setMessage( - context.resources.getQuantityString(R.plurals.action_post_failed_detail, count) - ) - .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int -> - clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts + if (showAlert) { + draftsNeedUserAlert.observe(coroutineScope) { count -> + Log.d(TAG, "User id $activeAccountId changed: Notification-worthy draft count $count") + if (count > 0) { + AlertDialog.Builder(context) + .setTitle(R.string.action_post_failed) + .setMessage( + context.resources.getQuantityString(R.plurals.action_post_failed_detail, count) + ) + .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int -> + clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts - val intent = DraftsActivity.newIntent(context) - context.startActivity(intent) - } - .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int -> - clearDraftsAlert(coroutineScope, activeAccountId) // User doesn't care - } - .show() - } - } - } else { - draftsNeedUserAlert.collect { - Log.d(TAG, "User id $activeAccountId: Clean out notification-worthy drafts") - clearDraftsAlert(coroutineScope, activeAccountId) + val intent = DraftsActivity.newIntent(context) + context.startActivity(intent) + } + .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int -> + clearDraftsAlert(coroutineScope, activeAccountId) // User doesn't care + } + .show() } } + } else { + draftsNeedUserAlert.observe(coroutineScope) { + Log.d(TAG, "User id $activeAccountId: Clean out notification-worthy drafts") + clearDraftsAlert(coroutineScope, activeAccountId) + } } } ?: run { Log.w(TAG, "Attempted to observe drafts, but there is no active account") diff --git a/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt index 54eaa8a23d..a553183e53 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt @@ -17,11 +17,21 @@ package com.keylesspalace.tusky.util +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import kotlin.time.Duration import kotlin.time.TimeMark import kotlin.time.TimeSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch /** * Returns a flow that mirrors the original flow, but filters out values that occur within @@ -63,3 +73,45 @@ fun Flow.throttleFirst(timeout: Duration, timeSource: TimeSource = TimeSo } } } + +fun Flow.observe( + scope: CoroutineScope, + collector: FlowCollector +): Job = scope.launch { + collect(collector) +} + +fun Flow.observeLatest( + scope: CoroutineScope, + action: suspend (value: T) -> Unit +): Job = scope.launch { + collectLatest(action) +} + +fun Flow.observe( + lifecycleOwner: LifecycleOwner, + collector: FlowCollector? = null +): Job = lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + if (collector == null) { + collect() + } else { + collect(collector) + } + } +} + +fun Flow.observeLatest( + lifecycleOwner: LifecycleOwner, + action: suspend (value: T) -> Unit +): Job = observeLatest(lifecycleOwner.lifecycleScope, action) + +context(LifecycleOwner) +fun Flow.observe( + collector: FlowCollector? = null +): Job = observe(this@LifecycleOwner, collector) + +context(LifecycleOwner) +fun Flow.observeLatest( + action: suspend (value: T) -> Unit +): Job = observeLatest(this@LifecycleOwner, action)