Pure Kotlin Library which adds one sealed class
named Container
and a couple of helper methods and classes for
Kotlin Flow and for converting async data loaders into reactive Flows.
Just add the following line to your build.gradle
file:
implementation "com.elveum:container:0.7"
Click here.
The library contains:
Container<T>
class - represents the status of async operation (Pending
,Error
,Success
)- A couple of helper extension methods for easier working (mapping, filtering, etc.) with
Kotlin Flows containing
Container<T>
orContainer<List<T>>
instances - Two subjects:
FlowSubject
- represents a simple finite flow which emissions can be controlled outsideLazyFlowSubject
- provides a convenient way of transforming suspend functions into Kotlin Flow with the possibility to update values, replace loaders, load multiple values, cache latest loaded value and with loading values only on demand
LazyCache
which can hold multiple instances ofLazyFlowSubject
StateFlow
extensions:stateMap
function which can convertStateFlow<T>
intoStateFlow<R>
(originmap
function convertsStateFlow<T>
into more genericFlow<R>
)combineStates
function which can merge 2 or moreStateFlow<*>
instances into oneStateFlow<R>
instance (origincombine
function merges flows into more genericFlow<R>
, but not intoStateFlow<R>
)
Container
is a simple class which represents the status of async operation:
Container.Pending
- data is loadingContainer.Success<T>(value)
- data has been loaded successfullyContainer.Error(exception)
- loading has been failed with error
There are a couple of methods which can simplify a code working with such statuses:
map
unwrap
,unwrapOrNull
getOrNull
,exceptionOrNull
Flow<Container<T>>.unwrapFirst()
returns the first non-pending T value from the flowFlow<Container<T>>.unwrapFirstOrElse()
andFlow<Container<T>>.unwrapFirstOrDefault()
returns the first non-pending T value from the flow or the default value otherwiseFlow<Container<T>>.containerMap()
converts the flow of typeContainer<T>
into a flow of typeContainer<R>
Flow<Container<T>>.containerFilter()
filters allContainer.Success<T>
values by a given predicate
Also, there are the following type aliases:
ListContainer<T>
=Container<List<T>>
ContainerFlow<T>
=Flow<Container<T>>
ListContainerFlow<T>
=Flow<Container<List<T>>>
Subjects are classes controlling flow emissions (name is taken from Reactive Streams)
FlowSubject
represents a finite Flow
which emission is controlled outside
(like StateFlow
and SharedFlow
). The FlowSubject
holds the latest value but
there are differences from StateFlow
:
FlowSubject
is a finite flow and it can be completed by usingonComplete
andonError
methodsFlowSubject
doesn't need a starting default valueFlowSubject
doesn't hold the latest value if it has been completed with error
Usage example:
val flowSubject = FlowSubject.create<String>()
flowSubject.onNext("first")
flowSubject.onNext("second")
flowSubject.onComplete()
flowSubject.listen().collect {
// ...
}
LazyFlowSubject<T>
provides a mechanism of converting a load
function into a Flow<Container<T>>
.
Features:
- The load function is usually executed only once (except #6 and #8)
- The load function is executed only when at least one subscriber starts collecting the flow
- The load function can emit more than one value
- The load function is cancelled when the last subscriber stops collecting the flow after timeout (default timeout = 1sec)
- The latest result is cached, so any new subscriber can receive the most actual loaded value without triggering the load function again and again
- There is a timeout (default value is 1sec) after the last subscriber stops collecting the flow. When timeout expires, the cached value is cleared so any further subscribers will execute the load function again
- The flow can be collected by using
listen()
method - You can replace the load function at any time by using the following methods:
newLoad
- assign a new load function which can emit more than one value. This method also returns a separate flow which differs from the flow returned bylisten()
: it emits only values emitted by a new load function and completes as soon as a new load function completesnewAsyncLoad
- the same asnewLoad
but it returns Unit immediatelynewSimpleLoad
- assign a new load function which can emit only one value. This is a suspend function and it waits until a new load function completes and returns its result (or throws an exception)newSimpleAsyncLoad
- the same asnewSimpleLoad
but it doesn't wait for load results and returns immediately
- Also you can use
updateWith
method in order to cancel any active loader and place your own value immediately to the subject. The previous loader function will be used again if you callreload()
or if cache has been expired.
Usage example:
class ProductRepository(
private val productsLocalDataSource: ProductsLocalDataSource,
private val productsRemoteDataSource: ProductsRemoteDataSource,
) {
private val productsSubject = LazyFlowSubject.create {
val localProducts = productsLocalDataSource.getProducts()
if (localProducts != null) emit(localProducts)
val remoteProducts = productsRemoteDataSource.getProducts()
productsLocalDataSource.saveProducts(remoteProducts)
emit(remoteProducts)
}
// ListContainerFlow<T> is an alias to Flow<Container<List<T>>>
fun listenProducts(): ListContainerFlow<Product> {
return productsSubject.listen()
}
fun reload() {
productsSubject.reloadAsync()
}
}
You can access an additional field named loadTrigger
within the
LazyFlowSubject.create { ... }
block. Depending on its value, you can change the load
logic. For example, you can skip loading data from the local cache if the loading process has
been initiated by reload
call:
private val productsSubject = LazyFlowSubject.create {
if (loadTrigger != LoadTrigger.Reload) {
val localProducts = productsLocalDataSource.getProducts()
if (localProducts != null) emit(localProducts)
}
val remoteProducts = productsRemoteDataSource.getProducts()
productsLocalDataSource.saveProducts(localProducts)
emit(remoteProducts)
}
Optionally you can assign a SourceType
to any Container.Success
value
just to let them know about an actual source where data arrived from.
// in loader function:
val subject = LazyFlowSubject.create {
val remoteProducts = productsRemoteDataSource.getProducts()
emit(remoteProducts, RemoteSourceType)
}
// in `Container.Success()` directly:
subject.updateWith(Container.Success("hello", FakeSourceType))
// in `Container.updateIfSuccess`:
subject.updateIfSuccess(ImmediateSourceType) { oldValue ->
oldValue.copy(isFavorite = true)
}
Source types can be accessed via Container.Success
instance:
subject.listen()
.filterIsInstance<Container.Success<String>>()
.collectLatest { successContainer ->
val value = successContainer.value
val sourceType = successContainer.source
println("$value, isRemote = ${sourceType == RemoteSourceType}")
}
LazyCache
is a store of multiple LazyFlowSubject
instances. It allows you
defining listeners and loader functions with additional arguments.
val lazyCache = LazyCache.create<Long, User> { id ->
val localUser = localUsersDataSource.getUserById(id)
if (localUser != null) {
emit(localUser)
}
val remoteUser = remoteUsersDataSource.getUserById(id)
localUsersDataSource.save(remoteUser)
emit(remoteUser)
}
fun getUserById(id: Long): Flow<Container<User>> {
return lazyCache.listen(id)
}