Skip to content

Commit e2f99ba

Browse files
authored
feat: ✨ handle error state handy (#23)
Create a new property on QueryViewModel error to describe an error on UI Create a new QueryViewState error to model the state To not duplicate code on data sources, errors are handled on ComicRepository Model error using Either<ComicError, List<T>> Empty results on ComicLocalDataSource produce EmptyResultsError Any network error produce NetworkError Suggestions will display error as a single suggestion A search will display error as text on the screen, hiding result list Error suggestions do not propagate search results closes #16
1 parent e8d9ad8 commit e2f99ba

File tree

17 files changed

+198
-40
lines changed

17 files changed

+198
-40
lines changed
Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,55 @@
11
package es.ffgiraldez.comicsearch.comics.data
22

3+
import arrow.core.Either
34
import arrow.core.None
5+
import arrow.core.Option
46
import arrow.core.Some
7+
import es.ffgiraldez.comicsearch.comics.domain.ComicError
8+
import es.ffgiraldez.comicsearch.comics.domain.ComicError.EmptyResultsError
9+
import es.ffgiraldez.comicsearch.comics.domain.ComicError.NetworkError
10+
import es.ffgiraldez.comicsearch.comics.domain.Query
11+
import es.ffgiraldez.comicsearch.platform.left
12+
import es.ffgiraldez.comicsearch.platform.right
513
import io.reactivex.Flowable
614

715
abstract class ComicRepository<T>(
816
private val local: ComicLocalDataSource<T>,
917
private val remote: ComicRemoteDataSource<T>
1018
) {
11-
fun findByTerm(term: String): Flowable<List<T>> =
19+
fun findByTerm(term: String): Flowable<Either<ComicError, List<T>>> =
1220
local.findQueryByTerm(term)
13-
.flatMap {
14-
when (it) {
15-
is None -> remote.findByTerm(term)
16-
.flatMapPublisher { local.insert(term, it).toFlowable<List<T>>() }
17-
is Some -> local.findByQuery(it.t)
21+
.flatMap { findSuggestions(it, term) }
22+
23+
private fun findSuggestions(
24+
query: Option<Query>,
25+
term: String
26+
): Flowable<out Either<ComicError, List<T>>> = when (query) {
27+
is None -> searchSuggestions(term)
28+
is Some -> fetchSuggestions(query)
29+
}
30+
31+
private fun searchSuggestions(term: String): Flowable<Either<ComicError, List<T>>> =
32+
remote.findByTerm(term)
33+
.map { right<ComicError, List<T>>(it) }
34+
.onErrorReturn { left<ComicError, List<T>>(NetworkError) }
35+
.flatMapPublisher { saveSuggestions(it, term) }
36+
37+
private fun saveSuggestions(
38+
results: Either<ComicError, List<T>>,
39+
term: String
40+
): Flowable<Either<ComicError, List<T>>> =
41+
results.fold({ _ ->
42+
Flowable.just(results)
43+
}, {
44+
local.insert(term, it).toFlowable<Either<ComicError, List<T>>>()
45+
})
46+
47+
private fun fetchSuggestions(it: Some<Query>): Flowable<Either<EmptyResultsError, List<T>>> =
48+
local.findByQuery(it.t)
49+
.map {
50+
when (it.isEmpty()) {
51+
true -> Either.left(EmptyResultsError)
52+
false -> Either.right(it)
1853
}
1954
}
2055
}

app/src/main/java/es/ffgiraldez/comicsearch/comics/di/comicModule.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import retrofit2.Retrofit
1111
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
1212
import retrofit2.converter.gson.GsonConverterFactory
1313

14-
const val ACTIVITY_PARAM: String = "activity"
1514
const val CONTEXT_PARAM: String = "context"
1615

1716
val comicModule = applicationContext {

app/src/main/java/es/ffgiraldez/comicsearch/comics/domain/Entities.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ data class Volume(
99
data class Query(
1010
val identifier: Long,
1111
val searchTerm: String
12-
)
12+
)
13+
14+
sealed class ComicError {
15+
object NetworkError : ComicError()
16+
object EmptyResultsError : ComicError()
17+
}

app/src/main/java/es/ffgiraldez/comicsearch/navigation/di/androidModule.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import es.ffgiraldez.comicsearch.navigation.Navigator
44
import org.koin.dsl.module.applicationContext
55

66
const val ACTIVITY_PARAM: String = "activity"
7-
const val CONTEXT_PARAM: String = "context"
87

98
val navigationModule = applicationContext {
109
factory { params -> Navigator(params[ACTIVITY_PARAM]) }
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package es.ffgiraldez.comicsearch.platform
2+
3+
import arrow.core.Either
4+
import arrow.core.left
5+
import arrow.core.right
6+
7+
fun <A, B, C> safe(first: A?, second: B?, block: (A, B) -> C): C? {
8+
return if (first != null && second != null) {
9+
block(first, second)
10+
} else {
11+
null
12+
}
13+
}
14+
15+
fun <A, B> left(a: A): Either<A, B> = a.left()
16+
fun <A, B> right(b: B): Either<A, B> = b.right()

app/src/main/java/es/ffgiraldez/comicsearch/query/base/presentation/QueryViewModel.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package es.ffgiraldez.comicsearch.query.base.presentation
22

33
import android.arch.lifecycle.MutableLiveData
44
import android.arch.lifecycle.ViewModel
5+
import arrow.core.Option
6+
import arrow.core.none
7+
import arrow.core.some
8+
import es.ffgiraldez.comicsearch.comics.domain.ComicError
59
import es.ffgiraldez.comicsearch.platform.toFlowable
610
import io.reactivex.Flowable
711
import org.reactivestreams.Publisher
@@ -11,6 +15,7 @@ open class QueryViewModel<T>(
1115
) : ViewModel() {
1216

1317
val query: MutableLiveData<String> = MutableLiveData()
18+
val error: MutableLiveData<Option<ComicError>> = MutableLiveData()
1419
val loading: MutableLiveData<Boolean> = MutableLiveData()
1520
val results: MutableLiveData<List<T>> = MutableLiveData()
1621

@@ -19,17 +24,17 @@ open class QueryViewModel<T>(
1924
.compose { transformer(it) }
2025
.subscribe {
2126
when (it) {
22-
QueryViewState.Idle -> applyState(false, emptyList())
23-
is QueryViewState.Loading -> applyState(true, emptyList())
24-
is QueryViewState.Result -> applyState(false, it.results)
27+
is QueryViewState.Loading -> applyState(isLoading = true)
28+
is QueryViewState.Idle -> applyState(isLoading = false)
29+
is QueryViewState.Error -> applyState(isLoading = false, error = it.error.some())
30+
is QueryViewState.Result -> applyState(isLoading = false, results = it.results)
2531
}
2632
}
27-
28-
2933
}
3034

31-
private fun applyState(isLoading: Boolean, results: List<T>) {
35+
private fun applyState(isLoading: Boolean, results: List<T> = emptyList(), error: Option<ComicError> = none()) {
3236
this.loading.postValue(isLoading)
3337
this.results.postValue(results)
38+
this.error.postValue(error)
3439
}
3540
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package es.ffgiraldez.comicsearch.query.base.presentation
22

3+
import es.ffgiraldez.comicsearch.comics.domain.ComicError
4+
35
sealed class QueryViewState<out T> {
46

57
companion object {
68
fun <T> result(volumeList: List<T>): QueryViewState<T> = Result(volumeList)
79
fun <T> idle(): QueryViewState<T> = Idle
810
fun <T> loading(): QueryViewState<T> = Loading
11+
fun <T> error(error: ComicError): QueryViewState<T> = Error(error)
912
}
1013

1114
object Idle : QueryViewState<Nothing>()
1215
object Loading : QueryViewState<Nothing>()
1316
data class Result<out T>(val results: List<T>) : QueryViewState<T>()
17+
data class Error(val error: ComicError) : QueryViewState<Nothing>()
1418

1519
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package es.ffgiraldez.comicsearch.query.base.ui
2+
3+
import es.ffgiraldez.comicsearch.comics.domain.ComicError
4+
5+
fun ComicError.toHumanResponse(): String = when (this) {
6+
ComicError.NetworkError -> "no internet connection"
7+
ComicError.EmptyResultsError -> "search without suggestion"
8+
}

app/src/main/java/es/ffgiraldez/comicsearch/query/base/ui/QueryActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import android.databinding.DataBindingUtil
44
import android.os.Bundle
55
import android.support.v7.app.AppCompatActivity
66
import es.ffgiraldez.comicsearch.R
7+
import es.ffgiraldez.comicsearch.comics.di.CONTEXT_PARAM
78
import es.ffgiraldez.comicsearch.databinding.QueryActivityBinding
8-
import es.ffgiraldez.comicsearch.navigation.di.ACTIVITY_PARAM
9-
import es.ffgiraldez.comicsearch.navigation.di.CONTEXT_PARAM
109
import es.ffgiraldez.comicsearch.navigation.Navigator
10+
import es.ffgiraldez.comicsearch.navigation.di.ACTIVITY_PARAM
1111
import es.ffgiraldez.comicsearch.query.search.presentation.SearchViewModel
1212
import es.ffgiraldez.comicsearch.query.sugestion.presentation.SuggestionViewModel
1313
import org.koin.android.architecture.ext.viewModel

app/src/main/java/es/ffgiraldez/comicsearch/query/base/ui/QuerySearchSuggestion.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ package es.ffgiraldez.comicsearch.query.base.ui
33
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
44
import kotlinx.android.parcel.Parcelize
55

6-
@Parcelize
7-
data class QuerySearchSuggestion(
8-
private val volume: String
6+
sealed class QuerySearchSuggestion(
7+
private val suggestion: String
98
) : SearchSuggestion {
10-
override fun getBody(): String = volume
9+
override fun getBody(): String = suggestion
10+
11+
@Parcelize
12+
data class ResultSuggestion(val volume: String) : QuerySearchSuggestion(volume)
13+
14+
@Parcelize
15+
data class ErrorSuggestion(val volume: String) : QuerySearchSuggestion(volume)
16+
1117
}

0 commit comments

Comments
 (0)