Skip to content

Commit

Permalink
Feature/sync initializer bloc lifecycle (#12)
Browse files Browse the repository at this point in the history
* some refactoring to prepare the actual functionality

* re-adding the state machine, not connected to the Bloc lifecycle yet

* use AtomicFu in the state machine to synchronize axcess to the state variable

* WIP

* WIP

* fix a compile error

* document internal lifecycle as svg

* a ton of changes...

* re-add the CoroutineRunner to reducers and fix some initializer related lifecycle issues

* update the documentation

* minor library updates

* make test more robust

* upload test results

* upload test results

* upload test results

* new version number

* upload test results

Co-authored-by: Emanuel Moecklin <emanuel.moecklin@gmail.com>
  • Loading branch information
1gravity and Emanuel Moecklin authored Sep 25, 2022
1 parent 612a207 commit 8ead156
Show file tree
Hide file tree
Showing 47 changed files with 1,270 additions and 364 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/lint_and_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ jobs:
- name: Run Lint
run: ./gradlew lint

- name: Test results Debug
uses: actions/upload-artifact@v2
with:
name: debug-unit-tests-results
path: Kotlin-Bloc/Kotlin-Bloc/bloc-core/build/reports/tests/testDebugUnitTest/

- name: Test results Release
uses: actions/upload-artifact@v2
with:
name: release-unit-tests-results
path: Kotlin-Bloc/Kotlin-Bloc/bloc-core/build/reports/tests/testReleaseUnitTest/

tests:
runs-on: ubuntu-latest

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- **predictable**: write reactive applications that behave consistently and are easy to debug and test
- **composable**: grows with the complexity of the app and the size of the team

<img alt="Bloc Architecture - Overview" src="./docs/BLoC Architecture - BLoC Overview.svg" width="625" /><br/>
<img alt="Bloc Architecture - Overview" src="./website/static/img/Bloc Architecture - Bloc Overview.svg" width="625" /><br/>

- The `Bloc` (Business Logic Component) encapsulates the application's business logic. It receives `Action(s)` from the view, processes those actions and outputs `Proposals` and optionally `SideEffect(s)`.
- The `BlocState` holds the component's `State`. It's separate from the actual `Bloc` to support different scenarios like:
Expand All @@ -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.7.0")
implementation("com.1gravity:bloc-core:0.8.0")

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

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

Expand Down
2 changes: 2 additions & 0 deletions bloc-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ kotlin {
// Logging (https://github.com/touchlab/Kermit)
implementation(Touchlab.kermit)

implementation("org.jetbrains.kotlinx:atomicfu:_")

// Kotlin Result (https://github.com/michaelbull/kotlin-result)
implementation("com.michael-bull.kotlin-result:kotlin-result:_")
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:_")
Expand Down
27 changes: 0 additions & 27 deletions bloc-core/src/commonMain/kotlin/com/onegravity/bloc/BlocDsl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,6 @@ package com.onegravity.bloc
import com.onegravity.bloc.internal.BlocExtension
import com.onegravity.bloc.utils.*

/**
* Submit an Initializer to a Bloc to be run.
* The initializer will receive the state and a dispatch function.
* The dispatch function dispatches to the first matching thunk/reducer/side-effect in the Bloc.
*/
@BlocDSL
public fun <State : Any, Action : Any, SideEffect : Any> Bloc<State, Action, SideEffect>.onCreate(
initializer: Initializer<State, Action>
) {
// we assume that every class implementing Bloc also implements BlocExtension
// since we provide all concrete Bloc implementations, this is guaranteed
// the proposal is irrelevant for an initializer so we set it to Unit
(this as BlocExtension<State, Action, SideEffect, Unit>).initialize(initializer)
}

/**
* Submit an Initializer to a BlocOwner/Bloc to be run.
* The initializer will receive the state and a dispatch function.
* The dispatch function dispatches to the first matching thunk/reducer/side-effect in the Bloc.
*/
@BlocDSL
public fun <State : Any, Action : Any, SideEffect : Any, Proposal : Any> BlocOwner<State, Action, SideEffect, Proposal>.onCreate(
initializer: Initializer<State, Action>
) {
bloc.onCreate(initializer)
}

/**
* Submit a Thunk to a Bloc to be run.
* The thunk will receive the dispatch and the getState function but no action (since it was
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package com.onegravity.bloc.internal
import com.arkivanov.essenty.lifecycle.*
import com.onegravity.bloc.Bloc
import com.onegravity.bloc.BlocContext
import com.onegravity.bloc.internal.lifecycle.subscribe
import com.onegravity.bloc.internal.builder.MatcherReducer
import com.onegravity.bloc.internal.builder.MatcherThunk
import com.onegravity.bloc.internal.lifecycle.BlocLifecycle
import com.onegravity.bloc.internal.lifecycle.BlocLifecycleImpl
import com.onegravity.bloc.state.BlocState
import com.onegravity.bloc.utils.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.FlowCollector
import kotlin.coroutines.CoroutineContext

/**
* The probably most important class in the framework.
Expand All @@ -23,36 +25,77 @@ internal class BlocImpl<State : Any, Action : Any, SideEffect : Any, Proposal :
initialize: Initializer<State, Action>? = null,
thunks: List<MatcherThunk<State, Action, Action>> = emptyList(),
reducers: List<MatcherReducer<State, Action, Effect<Proposal, SideEffect>>>,
initDispatcher: CoroutineContext = Dispatchers.Default,
thunkDispatcher: CoroutineContext = Dispatchers.Default,
reduceDispatcher: CoroutineContext = Dispatchers.Default
initDispatcher: CoroutineDispatcher = Dispatchers.Default,
thunkDispatcher: CoroutineDispatcher = Dispatchers.Default,
reduceDispatcher: CoroutineDispatcher = Dispatchers.Default
) : Bloc<State, Action, SideEffect>(),
BlocExtension<State, Action, SideEffect, Proposal> {

// ReduceProcessor, ThunkProcessor and InitializeProcessor need to be defined in this exact
// order or we risk NullPointerExceptions
private val blocLifecycle: BlocLifecycle = BlocLifecycleImpl(blocContext.lifecycle)

/* ******************************************************************************************
* ReduceProcessor, ThunkProcessor and InitializeProcessor need to be defined in this exact *
* order or we risk NullPointerExceptions *
********************************************************************************************/

private val reduceProcessor = ReduceProcessor(
blocContext,
blocState,
reducers,
reduceDispatcher
lifecycle = blocLifecycle,
state = blocState,
dispatcher = reduceDispatcher,
reducers = reducers
)

private val thunkProcessor = ThunkProcessor(
blocContext,
blocState,
thunks,
thunkDispatcher,
reduceProcessor::send
lifecycle = blocLifecycle,
state = blocState,
dispatcher = thunkDispatcher,
thunks = thunks,
dispatch = reduceProcessor::send
)

private val initializeProcessor = InitializeProcessor(
blocContext,
blocState,
initialize,
initDispatcher
) { send(it) }
lifecycle = blocLifecycle,
state = blocState,
dispatcher = initDispatcher,
initializer = initialize,
dispatch = { initActionQueue += it }
)

/**
* Queue for actions dispatched by the initializer.
* These actions are processed once the bloc transitions to the Started state.
*/
private val initActionQueue by lazy { ArrayDeque<Action>(10) }

private inner class ActionQueueElement(
val action: Action? = null,
val thunk: ThunkNoAction<State, Action>? = null,
val reducer: ReducerNoAction<State, Effect<Proposal, SideEffect>>? = null
)

/**
* Queue for thunks and reducers submitted while the bloc is being initialized (initializer is
* running). These thunks/reducers are processed once the bloc transitions to the Started state.
*/
private val actionQueue by lazy { ArrayDeque<ActionQueueElement>(10) }

init {
blocLifecycle.subscribe(onStart = {
// process initializer actions first
while (! initActionQueue.isEmpty()) {
val entry = initActionQueue.removeFirst()
send(entry)
}

// before processing action, thunks and reducers
while (! actionQueue.isEmpty()) {
val entry = actionQueue.removeFirst()
entry.action?.run(::send)
entry.thunk?.run(thunkProcessor::thunk)
entry.reducer?.run(reduceProcessor::reduce)
}
})
}

/**
* The current state
Expand All @@ -71,10 +114,16 @@ internal class BlocImpl<State : Any, Action : Any, SideEffect : Any, Proposal :
*/
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
override fun send(action: Action) {
logger.d("emit action ${action.trimOutput()}")
// thunks are always processed first
// ThunkProcessor will send the action to ReduceProcessor if there's no matching thunk
thunkProcessor.send(action)
when {
// we need to cache actions if the initializer is still running
blocLifecycle.isStarting() -> actionQueue += ActionQueueElement(action = action)

// thunks are always processed first
// ThunkProcessor will send the action to ReduceProcessor if there's no matching thunk
blocLifecycle.isStarted() -> thunkProcessor.send(action)

else -> { /* NOP*/ }
}
}

/**
Expand Down Expand Up @@ -128,15 +177,25 @@ internal class BlocImpl<State : Any, Action : Any, SideEffect : Any, Proposal :
* reduce { } -> run a Reducer MVVM+ style
*/
override fun reduce(reduce: ReducerNoAction<State, Effect<Proposal, SideEffect>>) {
reduceProcessor.reduce(reduce)
when {
// we need to cache if the initializer is still running
blocLifecycle.isStarting() -> actionQueue += ActionQueueElement(reducer = reduce)
blocLifecycle.isStarted() -> reduceProcessor.reduce(reduce)
else -> { /* NOP*/ }
}
}

/**
* BlocExtension interface implementation:
* thunk { } -> run a thunk MVVM+ style
*/
override fun thunk(thunk: ThunkNoAction<State, Action>) {
thunkProcessor.thunk(thunk)
when {
// we need to cache if the initializer is still running
blocLifecycle.isStarting() -> actionQueue += ActionQueueElement(thunk = thunk)
blocLifecycle.isStarted() -> thunkProcessor.thunk(thunk)
else -> { /* NOP*/ }
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.onegravity.bloc.internal

import com.onegravity.bloc.utils.CoroutineRunner
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel

/**
* Helper class to manage CoroutineScope and CoroutineRunner
*/
internal class Coroutine(private val dispatcher: CoroutineDispatcher) {
internal var scope: CoroutineScope? = null
private set(value) {
if (value != null) {
field = value
runner = CoroutineRunner(value)
}
}

internal var runner: CoroutineRunner? = null
private set

internal fun onStart() {
scope = CoroutineScope(SupervisorJob() + dispatcher)
}

internal fun onStop() {
scope?.cancel()
}
}
Loading

0 comments on commit 8ead156

Please sign in to comment.