Browse Source

Async API improvements "v2" (#672)

* Async API improvements "v2"

**NB: This PR actually changes only 3 files in `libraries/architecture/`. All the other changes are automated refactors to fix the calling code.**

This is a proposal for improvements to our `Async` type as discussed in: https://github.com/vector-im/element-x-android/pull/598/files#r1230664392 and in other chats.

Please bear in mind it is just a proposal, I'd love to hear your feedback about it, especially when it comes to naming: I've tried to make parameter and function names use a terminology similar to what we find in the Kotlin stdlib and its `Result` type.

I'm inclined to like more the non-extension flavours of the new `run*` APIs, though I'd also like your feedback about what API shape you prefer.

### Summary of the changes:
#### Functional
- Adds `exceptionOrNull()` API to complement the existing `dataOrNull()` API.
- Adds `isFailure()`, `isLoading()`, `isSuccess()` and `isUninitialized()` courtesy APIs.
- Renames `executeResult()` to `runUpdatingState()`:
	- Becomes the base API to which all the other similarly named APIs call into.
	- Makes it inline.
	- Adds contract.
	- Passes over any `prevData` to newre Async states.
	- Passes through the `block`s return value.
	- Adds unit tests.
- Renames `execute` to `runCatchingUpdatingState()` and makes it just call into `runUpdatingState()`
- Adds extension function overloads to the `run*` functions to accept `MutableState` as receiver

#### Cosmetics
- Reorders classes and methods in alphabetic order.
- Reorder parameter names to mimic conventions in Kotlin stdlib.
- Adds docstrings where useful.

* Use `fold()`

* rename pop to popFirst

* Add docstrings

* Please Detekt

* Rename exception to error.

* Please detekt

* Update existing usages.
feature/julioromano/geocoding_api
Marco Romano 1 year ago committed by GitHub
parent
commit
316d57d1b6
  1. 4
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
  2. 2
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
  3. 4
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt
  4. 2
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt
  5. 6
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
  6. 2
      features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt
  7. 4
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt
  8. 4
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt
  9. 2
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt
  10. 2
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt
  11. 4
      features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt
  12. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
  13. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
  14. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
  15. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
  16. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt
  17. 6
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
  18. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt
  19. 4
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt
  20. 4
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
  21. 3
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt
  22. 4
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt
  23. 4
      libraries/architecture/build.gradle.kts
  24. 163
      libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt
  25. 113
      libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncKtTest.kt

4
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt

@ -30,7 +30,7 @@ import io.element.android.features.createroom.impl.CreateRoomConfig @@ -30,7 +30,7 @@ import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@ -131,7 +131,7 @@ class ConfigureRoomPresenter @Inject constructor( @@ -131,7 +131,7 @@ class ConfigureRoomPresenter @Inject constructor(
dataStore.clearCachedData()
analyticsService.capture(CreatedRoom(isDM = false))
}
}.execute(createRoomAction)
}.runCatchingUpdatingState(createRoomAction)
}
private suspend fun uploadAvatar(avatarUri: Uri): String {

2
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt

@ -84,7 +84,7 @@ fun ConfigureRoomView( @@ -84,7 +84,7 @@ fun ConfigureRoomView(
if (state.createRoomAction is Async.Success) {
LaunchedEffect(state.createRoomAction) {
onRoomCreated(state.createRoomAction.state)
onRoomCreated(state.createRoomAction.data)
}
}

4
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt

@ -28,7 +28,7 @@ import io.element.android.features.createroom.impl.userlist.UserListPresenter @@ -28,7 +28,7 @@ import io.element.android.features.createroom.impl.userlist.UserListPresenter
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@ -95,6 +95,6 @@ class CreateRoomRootPresenter @Inject constructor( @@ -95,6 +95,6 @@ class CreateRoomRootPresenter @Inject constructor(
suspend {
matrixClient.createDM(user.userId).getOrThrow()
.also { analyticsService.capture(CreatedRoom(isDM = true)) }
}.execute(startDmAction)
}.runCatchingUpdatingState(startDmAction)
}
}

2
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt

@ -69,7 +69,7 @@ fun CreateRoomRootView( @@ -69,7 +69,7 @@ fun CreateRoomRootView(
) {
if (state.startDmAction is Async.Success) {
LaunchedEffect(state.startDmAction) {
onOpenDM(state.startDmAction.state)
onOpenDM(state.startDmAction.data)
}
}

6
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt

@ -30,7 +30,7 @@ import io.element.android.features.invitelist.impl.model.InviteListInviteSummary @@ -30,7 +30,7 @@ import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
@ -135,7 +135,7 @@ class InviteListPresenter @Inject constructor( @@ -135,7 +135,7 @@ class InviteListPresenter @Inject constructor(
it.acceptInvitation().getOrThrow()
}
roomId
}.execute(acceptedAction)
}.runCatchingUpdatingState(acceptedAction)
}
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
@ -143,7 +143,7 @@ class InviteListPresenter @Inject constructor( @@ -143,7 +143,7 @@ class InviteListPresenter @Inject constructor(
client.getRoom(roomId)?.use {
it.rejectInvitation().getOrThrow()
} ?: Unit
}.execute(declinedAction)
}.runCatchingUpdatingState(declinedAction)
}
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {

2
features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt

@ -58,7 +58,7 @@ fun InviteListView( @@ -58,7 +58,7 @@ fun InviteListView(
) {
if (state.acceptedAction is Async.Success) {
LaunchedEffect(state.acceptedAction) {
onInviteAccepted(state.acceptedAction.state)
onInviteAccepted(state.acceptedAction.data)
}
}

4
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt

@ -26,7 +26,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat @@ -26,7 +26,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import kotlinx.coroutines.CoroutineScope
@ -71,6 +71,6 @@ class ChangeServerPresenter @Inject constructor( @@ -71,6 +71,6 @@ class ChangeServerPresenter @Inject constructor(
// Valid, remember user choice
accountProviderDataSource.userSelection(data)
}.getOrThrow()
}.execute(changeServerAction, errorMapping = ChangeServerError::from)
}.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from)
}
}

4
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt

@ -30,7 +30,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat @@ -30,7 +30,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import kotlinx.coroutines.CoroutineScope
@ -95,6 +95,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( @@ -95,6 +95,6 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
throw IllegalStateException("Unsupported login flow")
}
}.getOrThrow()
}.execute(loginFlowAction, errorMapping = ChangeServerError::from)
}.runCatchingUpdatingState(loginFlowAction, errorTransform = ChangeServerError::from)
}
}

2
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt

@ -131,7 +131,7 @@ fun ConfirmAccountProviderView( @@ -131,7 +131,7 @@ fun ConfirmAccountProviderView(
}
is Async.Loading -> Unit // The Continue button shows the loading state
is Async.Success -> {
when (val loginFlowState = state.loginFlow.state) {
when (val loginFlowState = state.loginFlow.data) {
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
}

2
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt

@ -171,7 +171,7 @@ fun SearchAccountProviderView( @@ -171,7 +171,7 @@ fun SearchAccountProviderView(
}
}
is Async.Success -> {
items(state.userInputResult.state) { homeserverData ->
items(state.userInputResult.data) { homeserverData ->
val item = homeserverData.toAccountProvider()
AccountProviderView(
item = item,

4
features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt

@ -26,7 +26,7 @@ import io.element.android.features.logout.api.LogoutPreferenceEvents @@ -26,7 +26,7 @@ import io.element.android.features.logout.api.LogoutPreferenceEvents
import io.element.android.features.logout.api.LogoutPreferencePresenter
import io.element.android.features.logout.api.LogoutPreferenceState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope
@ -59,6 +59,6 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli @@ -59,6 +59,6 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli
private fun CoroutineScope.logout(logoutAction: MutableState<Async<Unit>>) = launch {
suspend {
matrixClient.logout()
}.execute(logoutAction)
}.runCatchingUpdatingState(logoutAction)
}
}

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt

@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject @@ -27,7 +27,7 @@ import dagger.assisted.AssistedInject
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.executeResult
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.mediaupload.api.MediaSender
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -83,8 +83,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( @@ -83,8 +83,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaAttachment: Attachment.Media,
sendActionState: MutableState<Async<Unit>>,
) {
suspend {
sendActionState.runUpdatingState {
mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible)
}.executeResult(sendActionState)
}
}
}

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt

@ -24,14 +24,12 @@ import androidx.compose.runtime.derivedStateOf @@ -24,14 +24,12 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt

@ -117,7 +117,7 @@ class MediaViewerPresenter @AssistedInject constructor( @@ -117,7 +117,7 @@ class MediaViewerPresenter @AssistedInject constructor(
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
if (localMedia is Async.Success) {
localMediaActions.saveOnDisk(localMedia.state)
localMediaActions.saveOnDisk(localMedia.data)
.onSuccess {
val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android)
snackbarDispatcher.post(snackbarMessage)
@ -131,7 +131,7 @@ class MediaViewerPresenter @AssistedInject constructor( @@ -131,7 +131,7 @@ class MediaViewerPresenter @AssistedInject constructor(
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
if (localMedia is Async.Success) {
localMediaActions.share(localMedia.state)
localMediaActions.share(localMedia.data)
.onFailure {
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
snackbarDispatcher.post(snackbarMessage)
@ -141,7 +141,7 @@ class MediaViewerPresenter @AssistedInject constructor( @@ -141,7 +141,7 @@ class MediaViewerPresenter @AssistedInject constructor(
private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch {
if (localMedia is Async.Success) {
localMediaActions.open(localMedia.state)
localMediaActions.open(localMedia.data)
.onFailure {
val snackbarMessage = SnackbarMessage(mediaActionsError(it))
snackbarDispatcher.post(snackbarMessage)

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt

@ -57,7 +57,6 @@ import io.element.android.features.messages.impl.media.local.LocalMediaView @@ -57,7 +57,6 @@ import io.element.android.features.messages.impl.media.local.LocalMediaView
import io.element.android.features.messages.impl.media.local.MediaInfo
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt

@ -29,8 +29,7 @@ import dagger.assisted.AssistedFactory @@ -29,8 +29,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.executeResult
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.matrix.api.core.EventId
@ -87,12 +86,12 @@ class ReportMessagePresenter @AssistedInject constructor( @@ -87,12 +86,12 @@ class ReportMessagePresenter @AssistedInject constructor(
blockUser: Boolean,
result: MutableState<Async<Unit>>,
) = launch {
suspend {
result.runUpdatingState {
val userIdToBlock = userId.takeIf { blockUser }
room.reportContent(eventId, reason, userIdToBlock)
.onSuccess {
snackbarDispatcher.post(SnackbarMessage(StringR.string.common_report_submitted))
}
}.executeResult(result)
}
}
}

6
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt

@ -29,7 +29,7 @@ import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase @@ -29,7 +29,7 @@ import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -128,13 +128,13 @@ class DeveloperSettingsPresenter @Inject constructor( @@ -128,13 +128,13 @@ class DeveloperSettingsPresenter @Inject constructor(
private fun CoroutineScope.computeCacheSize(cacheSize: MutableState<Async<String>>) = launch {
suspend {
computeCacheSizeUseCase()
}.execute(cacheSize)
}.runCatchingUpdatingState(cacheSize)
}
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<Async<Unit>>) = launch {
suspend {
clearCacheUseCase()
}.execute(clearCacheAction)
}.runCatchingUpdatingState(clearCacheAction)
}
}

2
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt

@ -23,8 +23,6 @@ import androidx.compose.ui.Modifier @@ -23,8 +23,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceView

4
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt

@ -31,7 +31,7 @@ import androidx.compose.runtime.setValue @@ -31,7 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.StateEventType
@ -154,7 +154,7 @@ class RoomDetailsEditPresenter @Inject constructor( @@ -154,7 +154,7 @@ class RoomDetailsEditPresenter @Inject constructor(
})
}
if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow()
}.execute(action)
}.runCatchingUpdatingState(action)
}
private suspend fun updateAvatar(avatarUri: Uri?): Result<Unit> {

4
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt

@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue @@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.room.RoomMember
@ -147,7 +147,7 @@ class RoomInviteMembersPresenter @Inject constructor( @@ -147,7 +147,7 @@ class RoomInviteMembersPresenter @Inject constructor(
withContext(coroutineDispatchers.io) {
roomMemberListDataSource.search("").toImmutableList()
}
}.execute(roomMembers)
}.runCatchingUpdatingState(roomMembers)
}
}

3
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt

@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp @@ -44,7 +44,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.roomdetails.impl.R
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
@ -110,7 +109,7 @@ fun RoomMemberListView( @@ -110,7 +109,7 @@ fun RoomMemberListView(
if (!state.isSearchActive) {
if (state.roomMembers is Async.Success) {
RoomMemberList(
roomMembers = state.roomMembers.state,
roomMembers = state.roomMembers.data,
showMembersCount = true,
onUserSelected = ::onUserSelected
)

4
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt

@ -55,8 +55,8 @@ class RoomMemberListPresenterTests { @@ -55,8 +55,8 @@ class RoomMemberListPresenterTests {
val loadedState = awaitItem()
Truth.assertThat(loadedState.roomMembers).isInstanceOf(Async.Success::class.java)
Truth.assertThat((loadedState.roomMembers as Async.Success).state.invited).isEqualTo(listOf(aVictor(), aWalter()))
Truth.assertThat((loadedState.roomMembers as Async.Success).state.joined).isNotEmpty()
Truth.assertThat((loadedState.roomMembers as Async.Success).data.invited).isEqualTo(listOf(aVictor(), aWalter()))
Truth.assertThat((loadedState.roomMembers as Async.Success).data.joined).isNotEmpty()
}
}

4
libraries/architecture/build.gradle.kts

@ -26,4 +26,8 @@ dependencies { @@ -26,4 +26,8 @@ dependencies {
api(libs.dagger)
api(libs.appyx.core)
api(libs.androidx.lifecycle.runtime)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.test.truth)
}

163
libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt

@ -18,51 +18,152 @@ package io.element.android.libraries.architecture @@ -18,51 +18,152 @@ package io.element.android.libraries.architecture
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Sealed type that allows to model an asynchronous operation.
*/
@Stable
sealed interface Async<out T> {
/**
* Represents a failed operation.
*
* @param T the type of data returned by the operation.
* @property error the error that caused the operation to fail.
* @property prevData the data returned by a previous successful run of the operation if any.
*/
data class Failure<out T>(
val error: Throwable,
val prevData: T? = null,
) : Async<T>
/**
* Represents an operation that is currently ongoing.
*
* @param T the type of data returned by the operation.
* @property prevData the data returned by a previous successful run of the operation if any.
*/
data class Loading<out T>(
val prevData: T? = null,
) : Async<T>
/**
* Represents a successful operation.
*
* @param T the type of data returned by the operation.
* @property data the data returned by the operation.
*/
data class Success<out T>(
val data: T,
) : Async<T>
/**
* Represents an uninitialized operation (i.e. yet to be run).
*/
object Uninitialized : Async<Nothing>
data class Loading<out T>(val prevState: T? = null) : Async<T>
data class Failure<out T>(val error: Throwable, val prevState: T? = null) : Async<T>
data class Success<out T>(val state: T) : Async<T>
fun dataOrNull(): T? {
return when (this) {
is Failure -> prevState
is Loading -> prevState
is Success -> state
Uninitialized -> null
}
/**
* Returns the data returned by the operation, or null otherwise.
*
* Please note this method may return stale data if the operation is not [Success].
*/
fun dataOrNull(): T? = when (this) {
is Failure -> prevData
is Loading -> prevData
is Success -> data
Uninitialized -> null
}
}
suspend inline fun <T> (suspend () -> T).execute(
state: MutableState<Async<T>>,
errorMapping: ((Throwable) -> Throwable) = { it },
) {
try {
state.value = Async.Loading()
val result = this()
state.value = Async.Success(result)
} catch (error: Throwable) {
state.value = Async.Failure(errorMapping.invoke(error))
/**
* Returns the error that caused the operation to fail, or null otherwise.
*/
fun errorOrNull(): Throwable? = when (this) {
is Failure -> error
else -> null
}
fun isFailure(): Boolean = this is Failure<T>
fun isLoading(): Boolean = this is Loading<T>
fun isSuccess(): Boolean = this is Success<T>
fun isUninitialized(): Boolean = this == Uninitialized
}
suspend inline fun <T> (suspend () -> Result<T>).executeResult(state: MutableState<Async<T>>) {
if (state.value !is Async.Success) {
state.value = Async.Loading()
suspend inline fun <T> MutableState<Async<T>>.runCatchingUpdatingState(
errorTransform: (Throwable) -> Throwable = { it },
block: () -> T,
): Result<T> = runUpdatingState(
state = this,
errorTransform = errorTransform,
resultBlock = {
runCatching {
block()
}
},
)
suspend inline fun <T> (suspend () -> T).runCatchingUpdatingState(
state: MutableState<Async<T>>,
errorTransform: (Throwable) -> Throwable = { it },
): Result<T> = runUpdatingState(
state = state,
errorTransform = errorTransform,
resultBlock = {
runCatching {
this()
}
},
)
suspend inline fun <T> MutableState<Async<T>>.runUpdatingState(
errorTransform: (Throwable) -> Throwable = { it },
resultBlock: () -> Result<T>,
): Result<T> = runUpdatingState(
state = this,
errorTransform = errorTransform,
resultBlock = resultBlock,
)
/**
* Calls the specified [Result]-returning function [resultBlock]
* encapsulating its progress and return value into an [Async] while
* posting its updates to the MutableState [state].
*
* @param T the type of data returned by the operation.
* @param state the [MutableState] to post updates to.
* @param errorTransform a function to transform the error before posting it.
* @param resultBlock a suspending function that returns a [Result].
* @return the [Result] returned by [resultBlock].
*/
@OptIn(ExperimentalContracts::class)
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
suspend inline fun <T> runUpdatingState(
state: MutableState<Async<T>>,
errorTransform: (Throwable) -> Throwable = { it },
resultBlock: suspend () -> Result<T>,
): Result<T> {
contract {
callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE)
}
this().fold(
val prevData = state.value.dataOrNull()
state.value = Async.Loading(prevData = prevData)
return resultBlock().fold(
onSuccess = {
state.value = Async.Success(it)
Result.success(it)
},
onFailure = {
state.value = Async.Failure(it)
val error = errorTransform(it)
state.value = Async.Failure(
error = error,
prevData = prevData,
)
Result.failure(error)
}
)
}
fun <T> Async<T>.isLoading(): Boolean {
return this is Async.Loading<T>
}

113
libraries/architecture/src/test/kotlin/io/element/android/libraries/architecture/AsyncKtTest.kt

@ -0,0 +1,113 @@ @@ -0,0 +1,113 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.architecture
import androidx.compose.runtime.MutableState
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AsyncKtTest {
@Test
fun `updates state when block returns success`() = runTest {
val state = TestableMutableState<Async<Int>>(Async.Uninitialized)
val result = runUpdatingState(state) {
delay(1)
Result.success(1)
}
assertThat(result.isSuccess).isTrue()
assertThat(result.getOrNull()).isEqualTo(1)
assertThat(state.popFirst()).isEqualTo(Async.Uninitialized)
assertThat(state.popFirst()).isEqualTo(Async.Loading(null))
assertThat(state.popFirst()).isEqualTo(Async.Success(1))
state.assertNoMoreValues()
}
@Test
fun `updates state when block returns failure`() = runTest {
val state = TestableMutableState<Async<Int>>(Async.Uninitialized)
val result = runUpdatingState(state) {
delay(1)
Result.failure(MyThrowable("hello"))
}
assertThat(result.isFailure).isTrue()
assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello"))
assertThat(state.popFirst()).isEqualTo(Async.Uninitialized)
assertThat(state.popFirst()).isEqualTo(Async.Loading(null))
assertThat(state.popFirst()).isEqualTo(Async.Failure<Int>(MyThrowable("hello")))
state.assertNoMoreValues()
}
@Test
fun `updates state when block returns failure transforming the error`() = runTest {
val state = TestableMutableState<Async<Int>>(Async.Uninitialized)
val result = runUpdatingState(state, { MyThrowable(it.message + " world") }) {
delay(1)
Result.failure(MyThrowable("hello"))
}
assertThat(result.isFailure).isTrue()
assertThat(result.exceptionOrNull()).isEqualTo(MyThrowable("hello world"))
assertThat(state.popFirst()).isEqualTo(Async.Uninitialized)
assertThat(state.popFirst()).isEqualTo(Async.Loading(null))
assertThat(state.popFirst()).isEqualTo(Async.Failure<Int>(MyThrowable("hello world")))
state.assertNoMoreValues()
}
}
/**
* A fake [MutableState] that allows to record all the states that were set.
*/
private class TestableMutableState<T>(
value: T
) : MutableState<T> {
private val _deque = ArrayDeque<T>(listOf(value))
override var value: T
get() = _deque.last()
set(value) {
_deque.addLast(value)
}
/**
* Returns the states that were set in the order they were set.
*/
fun popFirst(): T = _deque.removeFirst()
fun assertNoMoreValues() {
assertThat(_deque).isEmpty()
}
override operator fun component1(): T = value
override operator fun component2(): (T) -> Unit = { value = it }
}
/**
* An exception that is also a data class so we can compare it using equals.
*/
private data class MyThrowable(val myMessage: String) : Throwable(myMessage)
Loading…
Cancel
Save