Skip to content

Commit

Permalink
initializers with reduce (#15)
Browse files Browse the repository at this point in the history
* some documentation updates

* document some definitions

* update todo

* initializers get a reduce function too
  • Loading branch information
1gravity authored Oct 4, 2022
1 parent c7349e7 commit b9e8de6
Show file tree
Hide file tree
Showing 19 changed files with 1,547 additions and 590 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ Note, this readme offers a quick overview of the framework. For more in-depth in
```kotlin
dependencies {
// the core library
implementation("com.1gravity:bloc-core:0.9.0")
implementation("com.1gravity:bloc-core:0.9.1")

// add to use the framework together with Redux
implementation("com.1gravity:bloc-redux:0.9.0")
implementation("com.1gravity:bloc-redux:0.9.1")

// useful extensions for Android and Jetpack/JetBrains Compose
implementation("com.1gravity:bloc-compose:0.9.0")
implementation("com.1gravity:bloc-compose:0.9.1")
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal interface BlocExtension<State : Any, Action : Any, SideEffect : Any, Pr
* The Initializer is launched in a CoroutineScope managed by the bloc's lifecycle.
* Only one Initializer can be executed during the lifetime of a bloc.
*/
fun initialize(initialize: Initializer<State, Action>)
fun initialize(initialize: Initializer<State, Action, Proposal>)

/**
* Dispatch a reducer to the bloc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private const val QUEUE_INITIAL_SIZE = 10
internal class BlocImpl<State : Any, Action : Any, SideEffect : Any, Proposal : Any>(
blocContext: BlocContext,
private val blocState: BlocState<State, Proposal>,
initialize: Initializer<State, Action>? = null,
initialize: Initializer<State, Action, Proposal>? = null,
thunks: List<MatcherThunk<State, Action, Action, Proposal>> = emptyList(),
reducers: List<MatcherReducer<State, Action, Effect<Proposal, SideEffect>>>,
initDispatcher: CoroutineDispatcher = Dispatchers.Default,
Expand Down Expand Up @@ -73,24 +73,27 @@ internal class BlocImpl<State : Any, Action : Any, SideEffect : Any, Proposal :
reducers = reducers
)

// this reducer emits the proposal directly to the BlocState, no reduce functionality
private val reducer: (proposal: Proposal) -> Unit = { proposal ->
reduceProcessor.reduce { Effect(proposal, emptyList()) }
}

private val thunkProcessor = ThunkProcessor(
lifecycle = blocLifecycle,
state = blocState,
dispatcher = thunkDispatcher,
thunks = thunks,
dispatch = reduceProcessor::send,
reduce = { proposal -> reduceProcessor.reduce {
// this reducer emits the proposal directly to the BlocState, no reduce functionality
Effect(proposal, emptyList())
}}
reduce = reducer
)

private val initializeProcessor = InitializeProcessor(
lifecycle = blocLifecycle,
state = blocState,
dispatcher = initDispatcher,
initializer = initialize,
dispatch = { thunkProcessor.send(it) }
dispatch = { thunkProcessor.send(it) },
reduce = reducer
)

init {
Expand Down Expand Up @@ -176,7 +179,7 @@ internal class BlocImpl<State : Any, Action : Any, SideEffect : Any, Proposal :
* BlocExtension interface implementation:
* onCreate { } -> run an initializer MVVM+ style
*/
override fun initialize(initialize: Initializer<State, Action>) {
override fun initialize(initialize: Initializer<State, Action, Proposal>) {
initializeProcessor.initialize(initialize)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ internal class InitializeProcessor<State : Any, Action : Any, Proposal : Any>(
private val lifecycle: BlocLifecycle,
private val state: BlocState<State, Proposal>,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
private var initializer: Initializer<State, Action>? = null,
private val dispatch: (Action) -> Unit
private var initializer: Initializer<State, Action, Proposal>? = null,
private val dispatch: (Action) -> Unit,
private val reduce: (proposal: Proposal) -> Unit
) {

/**
Expand Down Expand Up @@ -57,20 +58,21 @@ internal class InitializeProcessor<State : Any, Action : Any, Proposal : Any>(
* BlocExtension interface implementation:
* onCreate { } -> run an initializer MVVM+ style
*/
internal fun initialize(initializer: Initializer<State, Action>) {
internal fun initialize(initializer: Initializer<State, Action, Proposal>) {
if (this.initializer == null) {
this.initializer = initializer
lifecycle.initializerStarting()
}
}

private fun runInitializer(initialize: Initializer<State, Action>) =
private fun runInitializer(initialize: Initializer<State, Action, Proposal>) =
coroutine.scope?.launch {
if (mutex.tryLock(this@InitializeProcessor)) {
coroutine.runner?.let { runner ->
val context = InitializerContext(
state = state.value,
dispatch = dispatch,
reduce = reduce,
runner = runner
)
context.initialize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import kotlin.jvm.JvmName
@Suppress("TooManyFunctions")
public class BlocBuilder<State : Any, Action : Any, SE : Any, Proposal : Any> {

private var _initialize: Initializer<State, Action>? = null
private var _initialize: Initializer<State, Action, Proposal>? = null
private val _thunks = ArrayList<MatcherThunk<State, Action, Action, Proposal>>()
private val _reducers = ArrayList<MatcherReducer<State, Action, Effect<Proposal, SE>>>()
private var _initDispatcher: CoroutineDispatcher = Dispatchers.Default
Expand All @@ -52,7 +52,7 @@ public class BlocBuilder<State : Any, Action : Any, SE : Any, Proposal : Any> {
* Create an initializer (onCreate { })
*/
@BlocDSL
public fun onCreate(initialize: Initializer<State, Action>) {
public fun onCreate(initialize: Initializer<State, Action, Proposal>) {
when (_initialize) {
null -> _initialize = initialize
else -> logger.w("Initializer already defined -> ignoring this one")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ package com.onegravity.bloc.utils
* }
* ```
*/
public data class InitializerContext<State, Action>(
public data class InitializerContext<State, Action, Proposal>(
val state: State,
val dispatch: Dispatcher<Action>,
val reduce: (proposal: Proposal) -> Unit,
internal val runner: CoroutineRunner
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public typealias Dispatcher<Action> = suspend (Action) -> Unit
* @see <a href="https://1gravity.github.io/Kotlin-Bloc/docs/architecture/bloc/initializer">
* Initializer</a>
*/
public typealias Initializer<State, Action> = suspend InitializerContext<State, Action>.() -> Unit
public typealias Initializer<State, Action, Proposal> =
suspend InitializerContext<State, Action, Proposal>.() -> Unit

/**
* Function that runs asynchronous code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package com.onegravity.bloc.utils
* without exposing the bloc's CoroutineScope.
*/
@BlocDSL
public fun <State, Action> InitializerContext<State, Action>.launch(block: CoroutineBlock) {
public fun <State, Action, Proposal> InitializerContext<State, Action, Proposal>.launch(
block: CoroutineBlock
) {
runner.run(null, block)
}

Expand All @@ -16,7 +18,7 @@ public fun <State, Action> InitializerContext<State, Action>.launch(block: Corou
* @param jobConfig @see [JobConfig]
*/
@BlocDSL
public fun <State, Action> InitializerContext<State, Action>.launch(
public fun <State, Action, Proposal> InitializerContext<State, Action, Proposal>.launch(
jobConfig: JobConfig,
block: CoroutineBlock
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,36 @@ class BlocInitializerExecutionTests : BaseTestClass() {
lifecycleRegistry.onDestroy()
}

@Test
fun testInitializerReduce() = runTests {
val lifecycleRegistry = LifecycleRegistry()
val context = BlocContextImpl(lifecycleRegistry)

val bloc = bloc<Int, Int, Unit>(context, 1) {
onCreate {
reduce(state + 2)
}
reduce { state + action }
}

// initializer executes and reduces the state
lifecycleRegistry.onCreate()
delay(50)
assertEquals(3, bloc.value)

// this action however will be ignored
bloc.send(3)
delay(50)
assertEquals(3, bloc.value)

// only after onStart are "regular" reducers being executed
lifecycleRegistry.onStart()
bloc.send(3)
delay(50)
assertEquals(6, bloc.value)

lifecycleRegistry.onStop()
lifecycleRegistry.onDestroy()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class PostsComponentImpl(context: BlocContext) : PostsComponent() {
private val blocState = getKoinInstance<BlocState<PostsRootState, PostsRootState>>()

// internal actions
private object PostsLoading : PostsAction()
private data class PostsLoaded(val result: Result<List<Post>, Throwable>) : PostsAction()
private data class PostLoaded(val result: Result<Post, Throwable>) : PostsAction()

Expand All @@ -35,7 +34,8 @@ class PostsComponentImpl(context: BlocContext) : PostsComponent() {
override val bloc by lazy {
bloc<PostsRootState, PostsAction>(context, blocState) {
onCreate {
dispatch(PostsLoading)
// example of "reducing" state from an initializer directly
reduce(state.copy(postsState = state.postsState.copy(loading = true)))

// we can access the db here because Dispatchers.Default is a Bloc's default
// dispatcher, also we use Ktor which offloads the networking to another thread
Expand All @@ -44,10 +44,6 @@ class PostsComponentImpl(context: BlocContext) : PostsComponent() {
dispatch(PostsLoaded(result))
}

reduce<PostsLoading> {
state.copy(postsState = state.postsState.copy(loading = true))
}

reduce<PostsLoaded> {
state.copy(
postsState = state.postsState.copy(loading = false, posts = action.result)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ kotlin.internal.mpp.hierarchicalStructureByDefault=true

# Maven
POM_GROUP=com.1gravity
POM_VERSION_NAME=0.9.0
POM_VERSION_NAME=0.9.1
# set PUBLISH_AS_SNAPSHOT to false to publish a release version
PUBLISH_AS_SNAPSHOT=true

Expand Down
6 changes: 3 additions & 3 deletions versions.properties
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ version.androidx.navigation=2.5.2
version.androidx.recyclerview=1.2.1

## unused
version.com.1gravity..bloc-compose=0.9.0
version.com.1gravity..bloc-compose=0.9.1

## unused
version.com.1gravity..bloc-redux=0.9.0
version.com.1gravity..bloc-redux=0.9.1

## unused
version.com.1gravity..bloc-core=0.9.0
version.com.1gravity..bloc-core=0.9.1

version.com.1gravity..redux-kotlin-threadsafe=0.5.10

Expand Down
17 changes: 15 additions & 2 deletions website/docs/architecture/bloc/initializer.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,28 @@ If more than one initializer is defined, the first one (according to their order

### Context

An initializer is called with a `InitializerContext` as receiver. The context is giving access to the current `State` and a `Dispatcher`:
An initializer is called with a `InitializerContext` as receiver. The context is giving access to the current `State`, a `Dispatcher` and a function to "reduce" state directly:


```kotlin
public data class InitializerContext<State, Action>(
val state: State,
val dispatch: Dispatcher<Action>
val dispatch: Dispatcher<Action>,
val reduce: (proposal: Proposal) -> Unit
)
```
### reduce()

Analogous to thunks, initializers have a `reduce()` function to eliminate boilerplate code:
```kotlin
onCreate {
reduce( state.copy(loading = true) )

val books = repository.load()

reduce( state.copy(loading = false, books = books) )
}
```

## Execution
Actions dispatched by the initializer are processed by thunks and reducers even before `onStart()` is called. Actions that are not dispatched by the initializer however are ignored before the bloc transitions to the `Started` state (see [Lifecycle](lifecycle)). This guarantees that the initializer runs and finishes before any thunks and reducers are executed. The only exception to that rule is if the initializer launches asynchronous code e.g. via [launch](coroutine_launcher) and would dispatch actions from there (so don't do this).
Expand Down
4 changes: 2 additions & 2 deletions website/docs/architecture/bloc/thunk.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ While a Redux thunk is a function, dispatched as an action to a Redux store and

### Context

A thunk is called with a `ThunkContext` as receiver. The context is giving access to the current `State`, the `Action` that triggered the thunk's execution a `Dispatcher` and a function to "reduce" state directly:
A thunk is called with a `ThunkContext` as receiver. The context is giving access to the current `State`, the `Action` that triggered the thunk's execution, a `Dispatcher` and a function to "reduce" state directly:


```kotlin
Expand Down Expand Up @@ -72,7 +72,7 @@ This doesn't require to call `dispatch(action)` explicitly since it only catches
:::tip
There are extension functions to launch `Coroutines` from a thunk (see [Coroutine Launcher](coroutine_launcher)).
:::
## Thunk reduce()
### reduce()

Thunks are meant to run asynchronous code and reducers are meant to reduce state. In many cases however the reducers are very simple functions. In the example above we need to add a dedicated action `Loading`, dispatch that action in the thunk in order for a reducer to reduce the current state to one that indicates loading. While that "separation of concerns" is useful in many cases, it adds a good amount of boilerplate code for simple cases like the one we have here. To simplify this we can use the `ThunkContext.reduce` function:
```kotlin
Expand Down
2 changes: 1 addition & 1 deletion website/docs/extensions/android/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ hide_title: true
To use the [Jetpack Compose](https://developer.android.com/jetpack/compose) extensions please add the `bloc-compose` artifact as a dependency in the Gradle build file:

```kotlin
implementation("com.1gravity:bloc-compose:0.9.0")
implementation("com.1gravity:bloc-compose:0.9.1")
```

## observeState
Expand Down
2 changes: 1 addition & 1 deletion website/docs/extensions/redux/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ hide_title: true
To use the [Redux](https://developer.android.com/jetpack/compose) extensions please add the `bloc-redux` artifact as a dependency in the Gradle build file:

```kotlin
implementation("com.1gravity:bloc-redux:0.9.0")
implementation("com.1gravity:bloc-redux:0.9.1")
```

## Libraries
Expand Down
6 changes: 3 additions & 3 deletions website/docs/getting_started/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ hide_title: true
```kotlin
dependencies {
// the core library
implementation("com.1gravity:bloc-core:0.9.0")
implementation("com.1gravity:bloc-core:0.9.1")

// add to use the framework together with Redux
implementation("com.1gravity:bloc-redux:0.9.0")
implementation("com.1gravity:bloc-redux:0.9.1")

// useful extensions for Android and Jetpack/JetBrains Compose
implementation("com.1gravity:bloc-compose:0.9.0")
implementation("com.1gravity:bloc-compose:0.9.1")
}
```
8 changes: 4 additions & 4 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "2.0.0-beta.21",
"@docusaurus/preset-classic": "2.0.0-beta.21",
"@docusaurus/theme-search-algolia": "^2.0.0-beta.21",
"@docusaurus/core": "^2.1.0",
"@docusaurus/preset-classic": "^2.1.0",
"@docusaurus/theme-search-algolia": "^2.1.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1",
"prism-react-renderer": "^1.3.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.21"
"@docusaurus/module-type-aliases": "^2.1.0"
},
"browserslist": {
"production": [
Expand Down
Loading

0 comments on commit b9e8de6

Please sign in to comment.