Change type of ViewFileState.lines from ImmutableList<String> to AsyncData<List<String>> to properly handle loading and error states.
This commit is contained in:
parent
bed9fcfae1
commit
4601e2acd3
8 changed files with 102 additions and 60 deletions
|
|
@ -47,4 +47,5 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,21 +24,16 @@ import java.io.File
|
|||
import javax.inject.Inject
|
||||
|
||||
interface FileContentReader {
|
||||
suspend fun getLines(path: String): List<String>
|
||||
suspend fun getLines(path: String): Result<List<String>>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultFileContentReader @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : FileContentReader {
|
||||
override suspend fun getLines(path: String): List<String> = withContext(dispatchers.io) {
|
||||
try {
|
||||
override suspend fun getLines(path: String): Result<List<String>> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
File(path).readLines()
|
||||
} catch (exception: Exception) {
|
||||
buildList {
|
||||
add("Error reading file $path")
|
||||
add(exception.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -57,13 +57,16 @@ class ViewFilePresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
var lines by remember { mutableStateOf(emptyList<String>()) }
|
||||
var lines: AsyncData<List<String>> by remember { mutableStateOf(AsyncData.Loading()) }
|
||||
LaunchedEffect(Unit) {
|
||||
lines = fileContentReader.getLines(path)
|
||||
lines = fileContentReader.getLines(path).fold(
|
||||
onSuccess = { AsyncData.Success(it) },
|
||||
onFailure = { AsyncData.Failure(it) }
|
||||
)
|
||||
}
|
||||
return ViewFileState(
|
||||
name = name,
|
||||
lines = lines.toImmutableList(),
|
||||
lines = lines,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
package io.element.android.features.viewfolder.impl.file
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class ViewFileState(
|
||||
val name: String,
|
||||
val lines: ImmutableList<String>,
|
||||
val lines: AsyncData<List<String>>,
|
||||
val eventSink: (ViewFileEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,24 +17,29 @@
|
|||
package io.element.android.features.viewfolder.impl.file
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class ViewFileStateProvider : PreviewParameterProvider<ViewFileState> {
|
||||
override val values: Sequence<ViewFileState>
|
||||
get() = sequenceOf(
|
||||
aViewFileState(),
|
||||
aViewFileState(lines = AsyncData.Loading()),
|
||||
aViewFileState(lines = AsyncData.Failure(Exception("A failure"))),
|
||||
aViewFileState(lines = AsyncData.Success(emptyList())),
|
||||
aViewFileState(
|
||||
lines = listOf(
|
||||
"Line 1",
|
||||
"Line 2",
|
||||
"Line 3 lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor" +
|
||||
" incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,",
|
||||
"01-23 13:14:50.740 25818 25818 V verbose",
|
||||
"01-23 13:14:50.740 25818 25818 D debug",
|
||||
"01-23 13:14:50.740 25818 25818 I info",
|
||||
"01-23 13:14:50.740 25818 25818 W warning",
|
||||
"01-23 13:14:50.740 25818 25818 E error",
|
||||
"01-23 13:14:50.740 25818 25818 A assertion",
|
||||
lines = AsyncData.Success(
|
||||
listOf(
|
||||
"Line 1",
|
||||
"Line 2",
|
||||
"Line 3 lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor" +
|
||||
" incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,",
|
||||
"01-23 13:14:50.740 25818 25818 V verbose",
|
||||
"01-23 13:14:50.740 25818 25818 D debug",
|
||||
"01-23 13:14:50.740 25818 25818 I info",
|
||||
"01-23 13:14:50.740 25818 25818 W warning",
|
||||
"01-23 13:14:50.740 25818 25818 E error",
|
||||
"01-23 13:14:50.740 25818 25818 A assertion",
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -42,9 +47,9 @@ open class ViewFileStateProvider : PreviewParameterProvider<ViewFileState> {
|
|||
|
||||
fun aViewFileState(
|
||||
name: String = "aName",
|
||||
lines: List<String> = emptyList(),
|
||||
lines: AsyncData<List<String>> = AsyncData.Uninitialized,
|
||||
) = ViewFileState(
|
||||
name = name,
|
||||
lines = lines.toImmutableList(),
|
||||
lines = lines,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -103,35 +106,51 @@ fun ViewFileView(
|
|||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (state.lines.isEmpty()) {
|
||||
item {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
Text(
|
||||
text = "Empty file",
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items = state.lines,
|
||||
) { index, line ->
|
||||
LineRow(
|
||||
lineNumber = index + 1,
|
||||
line = line,
|
||||
)
|
||||
}
|
||||
}
|
||||
when (state.lines) {
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading -> AsyncLoading()
|
||||
is AsyncData.Success -> FileContent(
|
||||
modifier = modifier.weight(1f),
|
||||
lines = state.lines.data,
|
||||
)
|
||||
is AsyncData.Failure -> AsyncFailure(throwable = state.lines.error, onRetry = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileContent(
|
||||
lines: List<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
) {
|
||||
if (lines.isEmpty()) {
|
||||
item {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
Text(
|
||||
text = "Empty file",
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items = lines,
|
||||
) { index, line ->
|
||||
LineRow(
|
||||
lineNumber = index + 1,
|
||||
line = line,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LineRow(
|
||||
lineNumber: Int,
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ package io.element.android.features.viewfolder.test.file
|
|||
import io.element.android.features.viewfolder.impl.file.FileContentReader
|
||||
|
||||
class FakeFileContentReader : FileContentReader {
|
||||
private var result: List<String> = emptyList()
|
||||
private var result: Result<List<String>> = Result.success(emptyList())
|
||||
|
||||
fun givenResult(result: List<String>) {
|
||||
fun givenResult(result: Result<List<String>>) {
|
||||
this.result = result
|
||||
}
|
||||
|
||||
override suspend fun getLines(path: String): List<String> = result
|
||||
override suspend fun getLines(path: String): Result<List<String>> = result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import io.element.android.features.viewfolder.impl.file.FileSave
|
|||
import io.element.android.features.viewfolder.impl.file.FileShare
|
||||
import io.element.android.features.viewfolder.impl.file.ViewFileEvents
|
||||
import io.element.android.features.viewfolder.impl.file.ViewFilePresenter
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -37,24 +39,26 @@ class ViewFilePresenterTest {
|
|||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val fileContentReader = FakeFileContentReader().apply {
|
||||
givenResult(listOf("aLine"))
|
||||
givenResult(Result.success(listOf("aLine")))
|
||||
}
|
||||
val presenter = createPresenter(fileContentReader = fileContentReader)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.name).isEqualTo("aName")
|
||||
assertThat(initialState.lines.size).isEqualTo(1)
|
||||
assertThat(initialState.lines.first()).isEqualTo("aLine")
|
||||
assertThat(initialState.lines).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val loadedState = awaitItem()
|
||||
val lines = (loadedState.lines as AsyncData.Success).data
|
||||
assertThat(lines.size).isEqualTo(1)
|
||||
assertThat(lines.first()).isEqualTo("aLine")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - share should not have any side effect`() = runTest {
|
||||
val fileContentReader = FakeFileContentReader().apply {
|
||||
givenResult(listOf("aLine"))
|
||||
givenResult(Result.success(listOf("aLine")))
|
||||
}
|
||||
val fileShare = FakeFileShare()
|
||||
val fileSave = FakeFileSave()
|
||||
|
|
@ -70,10 +74,25 @@ class ViewFilePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - with error loading file`() = runTest {
|
||||
val fileContentReader = FakeFileContentReader().apply {
|
||||
givenResult(Result.failure(AN_EXCEPTION))
|
||||
}
|
||||
val presenter = createPresenter(fileContentReader = fileContentReader)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.lines).isInstanceOf(AsyncData.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save should not have any side effect`() = runTest {
|
||||
val fileContentReader = FakeFileContentReader().apply {
|
||||
givenResult(listOf("aLine"))
|
||||
givenResult(Result.success(listOf("aLine")))
|
||||
}
|
||||
val fileShare = FakeFileShare()
|
||||
val fileSave = FakeFileSave()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue