Browse Source
* 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
25 changed files with 286 additions and 75 deletions
@ -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…
Reference in new issue