Kotlin library for reactive data loading
- separated persister and fetcher
- contains fetch policy
- supports RxJava2
- persister returns Flowable
- simple to extend
- suitable for Unit Testing
- 100% Kotlin code
Add the dependency in your build.gradle:
dependencies {
implementation "com.github.popalay:hoard:$hoardVersion"
}
Create Hoard instance and observe changes
GithubUserHoard(githubUserService, githubUserDao)
.get(GithubUserHoard.Key.All, dataIsEmpty = { it.isEmpty() })
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(adapter::submitList, ::handleError)
Hoard implementation
class GithubUserHoard(
service: GithubUserService,
dao: GithubUserDao
) : Hoard<List<GithubUser>, GithubUserHoard.Key>(
fetcher = GithubUserFetcher(service),
persister = GithubUserPersister(dao),
fetchPolicy = FetchPolicyFactory.timeFetchPolicy(TimeFetchPolicy.MEDIUM_DELAY)
) {
sealed class Key : com.github.popalay.hoard.Key {
object All : Key()
}
}
Persister implementation
private class GithubUserPersister(
private val dao: GithubUserDao
) : Persister<List<GithubUser>, GithubUserHoard.Key> {
override fun read(key: GithubUserHoard.Key): Flowable<List<GithubUser>> = with(key) {
when (this) {
GithubUserHoard.Key.All -> dao.findAll()
}
}
override fun write(data: List<GithubUser>, key: GithubUserHoard.Key): Completable = Completable.fromAction {
dao.deleteAndInsert(data)
}
override fun isNotEmpty(key: GithubUserHoard.Key): Single<Boolean> = with(key) {
when (this) {
GithubUserHoard.Key.All -> dao.isNotEmpty().toSingle(false)
}
}
}
Fetcher implementation
private class GithubUserFetcher(
private val service: GithubUserService
) : Fetcher<List<GithubUser>, GithubUserHoard.Key> {
override fun fetch(key: GithubUserHoard.Key): Flowable<List<GithubUser>> = with(key) {
when (this) {
GithubUserHoard.Key.All -> service.fetchUsers()
}
}
}
Custom mapper
sealed class Result<out T> {
data class Loading<out T>(val content: T) : Result<T>()
data class Success<out T>(val content: T) : Result<T>()
object Empty : Result<Nothing>()
object Idle : Result<Nothing>()
data class Outdated<out T>(val throwable: Throwable) : Result<T>()
data class Error(val throwable: Throwable) : Result<Nothing>()
fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Loading -> Loading(transform(content))
is Success -> Success(transform(content))
is Outdated -> Outdated(throwable)
is Error -> this
Empty -> Empty
Idle -> Idle
}
}
internal class ResultMapper<in KEY, IN>(
private val fetchPolicy: FetchPolicy<IN>,
private val dataIsEmpty: (data: IN) -> Boolean
) : Mapper<KEY, IN, Result<IN>> {
override fun mapNext(key: KEY, data: IN) = when {
fetchPolicy.shouldFetch(data) -> Result.Loading(data)
!fetchPolicy.shouldFetch(data) && !dataIsEmpty(key, data) -> Result.Success(data)
else -> Result.Empty
}
override fun mapError(throwable: Throwable): Flowable<Result<IN>> = when {
throwable is EmptyResultSetException || throwable is NoSuchElementException -> Flowable.just(Result.Empty)
throwable.isConnectivityExceptions() -> Flowable.just(Result.Error(throwable))
else -> Flowable.error(throwable)
}
override fun mapErrorWithData(key: KEY, data: IN, throwable: Throwable): Flowable<Result<IN>> =
if (dataIsEmpty(key, data)) {
Flowable.just(Result.Error(throwable))
} else {
Flowable.just(Result.Outdated(throwable))
}
private fun dataIsEmpty(key: KEY, data: IN): Boolean {
val dataIsEmpty = dataIsEmpty(data)
return dataIsEmpty && (key as? PageKey)?.offset ?: 0 == 0
}
}
internal fun <KEY : Key, RAW> Hoard<RAW, KEY>.getWithResult(
key: KEY,
dataIsEmpty: (data: RAW) -> Boolean = { false }
): Flowable<Result<RAW>> = flow(
key,
ignoreConnectivity = false,
mapper = ResultMapper(fetchPolicy, dataIsEmpty)
).startWith(Result.Idle)
Copyright (c) 2018 Denys Nykyforov (@popalay)
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.