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