Merge pull request #271 from ouchadam/tech/state-submodule

screen-state submodule
This commit is contained in:
Adam Brown 2022-11-28 19:20:06 +00:00 committed by GitHub
commit 4d829ac84e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 102 additions and 715 deletions

View File

@ -17,6 +17,9 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions/setup-java@v3
with:
distribution: 'adopt'

View File

@ -14,6 +14,9 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions/setup-java@v2
with:
distribution: 'adopt'

View File

@ -16,6 +16,9 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions/setup-java@v2
with:
distribution: 'adopt'

View File

@ -16,6 +16,8 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions/setup-node@v3
with:

View File

@ -17,6 +17,8 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- uses: actions/setup-java@v2
with:
distribution: 'adopt'

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "screen-state"]
path = screen-state
url = git@github.com:ouchadam/screen-state.git

View File

@ -163,7 +163,6 @@ ext.firebase = { dependencies, name ->
}
}
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
apply from: 'tools/coverage.gradle'
}

View File

@ -1,64 +0,0 @@
package app.dapk.st.design.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@Composable
fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?) -> Unit, graph: SpiderScope.() -> Unit) {
val pageCache = remember { mutableMapOf<Route<*>, SpiderPage<out T>>() }
pageCache[currentPage.route] = currentPage
val navigateAndPopStack = {
pageCache.remove(currentPage.route)
onNavigate(pageCache[currentPage.parent])
}
val itemScope = object : SpiderItemScope {
override fun goBack() {
navigateAndPopStack()
}
}
val computedWeb = remember(true) {
mutableMapOf<Route<*>, @Composable (T) -> Unit>().also { computedWeb ->
val scope = object : SpiderScope {
override fun <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit) {
computedWeb[route] = { content(itemScope, it as T) }
}
}
graph.invoke(scope)
}
}
Column {
if (currentPage.hasToolbar) {
Toolbar(
onNavigate = navigateAndPopStack,
title = currentPage.label
)
}
BackHandler(onBack = navigateAndPopStack)
computedWeb[currentPage.route]!!.invoke(currentPage.state)
}
}
interface SpiderScope {
fun <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit)
}
interface SpiderItemScope {
fun goBack()
}
data class SpiderPage<T>(
val route: Route<T>,
val label: String,
val parent: Route<*>?,
val state: T,
val hasToolbar: Boolean = true,
)
@JvmInline
value class Route<out S>(val value: String)

View File

@ -5,5 +5,4 @@ dependencies {
implementation project(":features:navigator")
implementation project(":design-library")
api project(":domains:android:core")
api project(":domains:state")
}

View File

@ -21,53 +21,3 @@ inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
}
return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise })
}
inline fun <reified S, E> ComponentActivity.state(
noinline factory: () -> State<S, E>
): Lazy<State<S, E>> {
val factoryPromise = object : Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when(modelClass) {
StateViewModel::class.java -> factory() as T
else -> throw Error()
}
}
}
return KeyedViewModelLazy(
key = S::class.java.canonicalName!!,
StateViewModel::class,
{ viewModelStore },
{ factoryPromise }
) as Lazy<State<S, E>>
}
class KeyedViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
private val key: String,
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory,
) : Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
val viewModel = cached
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
ViewModelProvider(
store,
factory,
CreationExtras.Empty
).get(key, viewModelClass.java).also {
cached = it
}
} else {
viewModel
}
}
override fun isInitialized(): Boolean = cached != null
}

View File

@ -1,48 +0,0 @@
package app.dapk.st.core
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.dapk.state.Action
import app.dapk.state.ReducerFactory
import app.dapk.state.Store
import app.dapk.state.createStore
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
class StateViewModel<S, E>(
reducerFactory: ReducerFactory<S>,
eventSource: MutableSharedFlow<E>,
) : ViewModel(), State<S, E> {
private val store: Store<S> = createStore(reducerFactory, viewModelScope)
override val events: SharedFlow<E> = eventSource
override val current
get() = _state!!
private var _state: S by mutableStateOf(store.getState())
init {
_state = store.getState()
store.subscribe {
_state = it
}
}
override fun dispatch(action: Action) {
store.dispatch(action)
}
}
fun <S, E> createStateViewModel(block: (suspend (E) -> Unit) -> ReducerFactory<S>): StateViewModel<S, E> {
val eventSource = MutableSharedFlow<E>(extraBufferCapacity = 1)
val reducer = block { eventSource.emit(it) }
return StateViewModel(reducer, eventSource)
}
interface State<S, E> {
fun dispatch(action: Action)
val events: SharedFlow<E>
val current: S
}

View File

@ -1,95 +0,0 @@
package app.dapk.st.core.page
import app.dapk.st.design.components.SpiderPage
import app.dapk.state.*
import kotlin.reflect.KClass
sealed interface PageAction<out P> : Action {
data class GoTo<P : Any>(val page: SpiderPage<P>) : PageAction<P>
}
sealed interface PageStateChange : Action {
data class ChangePage<P : Any>(val previous: SpiderPage<out P>, val newPage: SpiderPage<out P>) : PageAction<P>
data class UpdatePage<P : Any>(val pageContent: P) : PageAction<P>
}
data class PageContainer<P>(
val page: SpiderPage<out P>
)
interface PageReducerScope<P> {
fun <PC : Any> withPageContent(page: KClass<PC>, block: PageDispatchScope<PC>.() -> Unit)
fun rawPage(): SpiderPage<out P>
}
interface PageDispatchScope<PC> {
fun ReducerScope<*>.pageDispatch(action: PageAction<PC>)
fun getPageState(): PC?
}
fun <P : Any, S : Any> createPageReducer(
initialPage: SpiderPage<out P>,
factory: PageReducerScope<P>.() -> ReducerFactory<S>,
): ReducerFactory<Combined2<PageContainer<P>, S>> = shareState {
combineReducers(createPageReducer(initialPage), factory(pageReducerScope()))
}
private fun <P : Any, S : Any> SharedStateScope<Combined2<PageContainer<P>, S>>.pageReducerScope() = object : PageReducerScope<P> {
override fun <PC : Any> withPageContent(page: KClass<PC>, block: PageDispatchScope<PC>.() -> Unit) {
val currentPage = getSharedState().state1.page.state
if (currentPage::class == page) {
val pageDispatchScope = object : PageDispatchScope<PC> {
override fun ReducerScope<*>.pageDispatch(action: PageAction<PC>) {
val currentPageGuard = getSharedState().state1.page.state
if (currentPageGuard::class == page) {
dispatch(action)
}
}
override fun getPageState() = getSharedState().state1.page.state as? PC
}
block(pageDispatchScope)
}
}
override fun rawPage() = getSharedState().state1.page
}
@Suppress("UNCHECKED_CAST")
private fun <P : Any> createPageReducer(
initialPage: SpiderPage<out P>
): ReducerFactory<PageContainer<P>> {
return createReducer(
initialState = PageContainer(
page = initialPage
),
async(PageAction.GoTo::class) { action ->
val state = getState()
if (state.page.state::class != action.page.state::class) {
dispatch(PageStateChange.ChangePage(previous = state.page, newPage = action.page))
} else {
dispatch(PageStateChange.UpdatePage(action.page.state))
}
},
change(PageStateChange.ChangePage::class) { action, state ->
state.copy(page = action.newPage as SpiderPage<out P>)
},
change(PageStateChange.UpdatePage::class) { action, state ->
val isSamePage = state.page.state::class == action.pageContent::class
if (isSamePage) {
val updatedPageContent = (state.page as SpiderPage<Any>).copy(state = action.pageContent)
state.copy(page = updatedPageContent as SpiderPage<out P>)
} else {
state
}
},
)
}
inline fun <reified PC : Any> PageReducerScope<*>.withPageContext(crossinline block: PageDispatchScope<PC>.(PC) -> Unit) {
withPageContent(PC::class) { getPageState()?.let { block(it) } }
}

View File

@ -1,14 +0,0 @@
plugins {
id 'kotlin'
id 'java-test-fixtures'
}
dependencies {
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
testFixturesImplementation testFixtures(project(":core"))
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
testFixturesImplementation Dependencies.mavenCentral.kluent
testFixturesImplementation Dependencies.mavenCentral.mockk
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
}

View File

@ -1,194 +0,0 @@
package app.dapk.state
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.reflect.KClass
fun <S> createStore(reducerFactory: ReducerFactory<S>, coroutineScope: CoroutineScope): Store<S> {
val subscribers = mutableListOf<(S) -> Unit>()
var state: S = reducerFactory.initialState()
return object : Store<S> {
private val scope = createScope(coroutineScope, this)
private val reducer = reducerFactory.create(scope)
override fun dispatch(action: Action) {
coroutineScope.launch {
state = reducer.reduce(action).also { nextState ->
if (nextState != state) {
subscribers.forEach { it.invoke(nextState) }
}
}
}
}
override fun getState() = state
override fun subscribe(subscriber: (S) -> Unit) {
subscribers.add(subscriber)
}
}
}
interface ReducerFactory<S> {
fun create(scope: ReducerScope<S>): Reducer<S>
fun initialState(): S
}
fun interface Reducer<S> {
fun reduce(action: Action): S
}
private fun <S> createScope(coroutineScope: CoroutineScope, store: Store<S>) = object : ReducerScope<S> {
override val coroutineScope = coroutineScope
override fun dispatch(action: Action) = store.dispatch(action)
override fun getState(): S = store.getState()
}
interface Store<S> {
fun dispatch(action: Action)
fun getState(): S
fun subscribe(subscriber: (S) -> Unit)
}
interface ReducerScope<S> {
val coroutineScope: CoroutineScope
fun dispatch(action: Action)
fun getState(): S
}
sealed interface ActionHandler<S> {
val key: KClass<Action>
class Async<S>(override val key: KClass<Action>, val handler: suspend ReducerScope<S>.(Action) -> Unit) : ActionHandler<S>
class Sync<S>(override val key: KClass<Action>, val handler: (Action, S) -> S) : ActionHandler<S>
class Delegate<S>(override val key: KClass<Action>, val handler: ReducerScope<S>.(Action) -> ActionHandler<S>) : ActionHandler<S>
}
data class Combined2<S1, S2>(val state1: S1, val state2: S2)
fun interface SharedStateScope<C> {
fun getSharedState(): C
}
fun <S> shareState(block: SharedStateScope<S>.() -> ReducerFactory<S>): ReducerFactory<S> {
var internalScope: ReducerScope<S>? = null
val scope = SharedStateScope { internalScope!!.getState() }
val combinedFactory = block(scope)
return object : ReducerFactory<S> {
override fun create(scope: ReducerScope<S>) = combinedFactory.create(scope).also { internalScope = scope }
override fun initialState() = combinedFactory.initialState()
}
}
fun <S1, S2> combineReducers(r1: ReducerFactory<S1>, r2: ReducerFactory<S2>): ReducerFactory<Combined2<S1, S2>> {
return object : ReducerFactory<Combined2<S1, S2>> {
override fun create(scope: ReducerScope<Combined2<S1, S2>>): Reducer<Combined2<S1, S2>> {
val r1Scope = createReducerScope(scope) { scope.getState().state1 }
val r2Scope = createReducerScope(scope) { scope.getState().state2 }
val r1Reducer = r1.create(r1Scope)
val r2Reducer = r2.create(r2Scope)
return Reducer {
Combined2(r1Reducer.reduce(it), r2Reducer.reduce(it))
}
}
override fun initialState(): Combined2<S1, S2> = Combined2(r1.initialState(), r2.initialState())
}
}
private fun <S> createReducerScope(scope: ReducerScope<*>, state: () -> S) = object : ReducerScope<S> {
override val coroutineScope: CoroutineScope = scope.coroutineScope
override fun dispatch(action: Action) = scope.dispatch(action)
override fun getState() = state.invoke()
}
fun <S> createReducer(
initialState: S,
vararg reducers: (ReducerScope<S>) -> ActionHandler<S>,
): ReducerFactory<S> {
return object : ReducerFactory<S> {
override fun create(scope: ReducerScope<S>): Reducer<S> {
val reducersMap = reducers
.map { it.invoke(scope) }
.groupBy { it.key }
return Reducer { action ->
val result = reducersMap.keys
.filter { it.java.isAssignableFrom(action::class.java) }
.fold(scope.getState()) { acc, key ->
val actionHandlers = reducersMap[key]!!
actionHandlers.fold(acc) { acc, handler ->
when (handler) {
is ActionHandler.Async -> {
scope.coroutineScope.launch {
handler.handler.invoke(scope, action)
}
acc
}
is ActionHandler.Sync -> handler.handler.invoke(action, acc)
is ActionHandler.Delegate -> when (val next = handler.handler.invoke(scope, action)) {
is ActionHandler.Async -> {
scope.coroutineScope.launch {
next.handler.invoke(scope, action)
}
acc
}
is ActionHandler.Sync -> next.handler.invoke(action, acc)
is ActionHandler.Delegate -> error("is not possible")
}
}
}
}
result
}
}
override fun initialState(): S = initialState
}
}
fun <A : Action, S> sideEffect(klass: KClass<A>, block: suspend (A, S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> {
return {
ActionHandler.Async(key = klass as KClass<Action>) { action -> block(action as A, getState()) }
}
}
fun <A : Action, S> change(klass: KClass<A>, block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> {
return {
ActionHandler.Sync(key = klass as KClass<Action>, block as (Action, S) -> S)
}
}
fun <A : Action, S> async(klass: KClass<A>, block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> {
return {
ActionHandler.Async(key = klass as KClass<Action>, block as suspend ReducerScope<S>.(Action) -> Unit)
}
}
fun <A : Action, S> multi(klass: KClass<A>, block: Multi<A, S>.(A) -> (ReducerScope<S>) -> ActionHandler<S>): (ReducerScope<S>) -> ActionHandler<S> {
val multiScope = object : Multi<A, S> {
override fun sideEffect(block: suspend (S) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = sideEffect(klass) { _, state -> block(state) }
override fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S> = change(klass, block)
override fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S> = async(klass, block)
override fun nothing() = sideEffect { }
}
return {
ActionHandler.Delegate(key = klass as KClass<Action>) { action ->
block(multiScope, action as A).invoke(this)
}
}
}
interface Multi<A : Action, S> {
fun sideEffect(block: suspend (S) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
fun nothing(): (ReducerScope<S>) -> ActionHandler<S>
fun change(block: (A, S) -> S): (ReducerScope<S>) -> ActionHandler<S>
fun async(block: suspend ReducerScope<S>.(A) -> Unit): (ReducerScope<S>) -> ActionHandler<S>
}
interface Action

View File

@ -1,20 +0,0 @@
package fake
import org.amshove.kluent.internal.assertEquals
class FakeEventSource<E> : (E) -> Unit {
private val captures = mutableListOf<E>()
override fun invoke(event: E) {
captures.add(event)
}
fun assertEvents(expected: List<E>) {
assertEquals(expected, captures)
}
fun assertNoEvents() {
assertEquals(emptyList(), captures)
}
}

View File

@ -1,153 +0,0 @@
package test
import app.dapk.state.Action
import app.dapk.state.Reducer
import app.dapk.state.ReducerFactory
import app.dapk.state.ReducerScope
import fake.FakeEventSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.shouldBeEqualTo
interface ReducerTest<S, E> {
operator fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit)
}
fun <S, E> testReducer(block: ((E) -> Unit) -> ReducerFactory<S>): ReducerTest<S, E> {
val fakeEventSource = FakeEventSource<E>()
val reducerFactory = block(fakeEventSource)
return object : ReducerTest<S, E> {
override fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit) {
runReducerTest(reducerFactory, fakeEventSource, block)
}
}
}
fun <S, E> runReducerTest(reducerFactory: ReducerFactory<S>, fakeEventSource: FakeEventSource<E>, block: suspend ReducerTestScope<S, E>.() -> Unit) {
runTest {
val expectTestScope = ExpectTest(coroutineContext)
block(ReducerTestScope(reducerFactory, fakeEventSource, expectTestScope))
expectTestScope.verifyExpects()
}
}
class ReducerTestScope<S, E>(
private val reducerFactory: ReducerFactory<S>,
private val fakeEventSource: FakeEventSource<E>,
private val expectTestScope: ExpectTestScope
) : ExpectTestScope by expectTestScope, Reducer<S> {
private var invalidateCapturedState: Boolean = false
private val actionSideEffects = mutableMapOf<Action, () -> S>()
private var manualState: S? = null
private var capturedResult: S? = null
private val actionCaptures = mutableListOf<Action>()
private val reducerScope = object : ReducerScope<S> {
override val coroutineScope = CoroutineScope(UnconfinedTestDispatcher())
override fun dispatch(action: Action) {
actionCaptures.add(action)
if (actionSideEffects.containsKey(action)) {
setState(actionSideEffects.getValue(action).invoke(), invalidateCapturedState = true)
}
}
override fun getState() = manualState ?: reducerFactory.initialState()
}
private val reducer: Reducer<S> = reducerFactory.create(reducerScope)
override fun reduce(action: Action) = reducer.reduce(action).also {
capturedResult = if (invalidateCapturedState) manualState else it
}
fun actionSideEffect(action: Action, handler: () -> S) {
actionSideEffects[action] = handler
}
fun setState(state: S, invalidateCapturedState: Boolean = false) {
manualState = state
this.invalidateCapturedState = invalidateCapturedState
}
fun setState(block: (S) -> S) {
setState(block(reducerScope.getState()))
}
fun assertInitialState(expected: S) {
reducerFactory.initialState() shouldBeEqualTo expected
}
fun assertEvents(events: List<E>) {
fakeEventSource.assertEvents(events)
}
fun assertOnlyStateChange(expected: S) {
assertStateChange(expected)
assertNoDispatches()
fakeEventSource.assertNoEvents()
}
fun assertOnlyStateChange(block: (S) -> S) {
val expected = block(reducerScope.getState())
assertStateChange(expected)
assertNoDispatches()
fakeEventSource.assertNoEvents()
}
fun assertStateChange(expected: S) {
capturedResult shouldBeEqualTo expected
}
fun assertDispatches(expected: List<Action>) {
assertEquals(expected, actionCaptures)
}
fun assertNoDispatches() {
assertEquals(emptyList(), actionCaptures)
}
fun assertNoStateChange() {
assertEquals(reducerScope.getState(), capturedResult)
}
fun assertNoEvents() {
fakeEventSource.assertNoEvents()
}
fun assertOnlyDispatches(expected: List<Action>) {
assertDispatches(expected)
fakeEventSource.assertNoEvents()
assertNoStateChange()
}
fun assertOnlyEvents(events: List<E>) {
fakeEventSource.assertEvents(events)
assertNoDispatches()
assertNoStateChange()
}
fun assertNoChanges() {
assertNoStateChange()
assertNoEvents()
assertNoDispatches()
}
}
fun <S, E> ReducerTestScope<S, E>.assertOnlyDispatches(vararg action: Action) {
this.assertOnlyDispatches(action.toList())
}
fun <S, E> ReducerTestScope<S, E>.assertDispatches(vararg action: Action) {
this.assertDispatches(action.toList())
}
fun <S, E> ReducerTestScope<S, E>.assertEvents(vararg event: E) {
this.assertEvents(event.toList())
}
fun <S, E> ReducerTestScope<S, E>.assertOnlyEvents(vararg event: E) {
this.assertOnlyEvents(event.toList())
}

View File

@ -3,7 +3,7 @@ applyAndroidComposeLibraryModule(project)
dependencies {
implementation project(":chat-engine")
implementation project(":domains:android:compose-core")
implementation project(":domains:state")
implementation 'screen-state:screen-android'
implementation project(":features:messenger")
implementation project(":core")
implementation project(":design-library")
@ -11,9 +11,9 @@ dependencies {
kotlinTest(it)
testImplementation 'screen-state:state-test'
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:state"))
androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))

View File

@ -2,7 +2,7 @@ package app.dapk.st.directory
import android.content.Context
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.createStateViewModel
import app.dapk.st.state.createStateViewModel
import app.dapk.st.core.JobBag
import app.dapk.st.directory.state.DirectoryState
import app.dapk.st.directory.state.directoryReducer

View File

@ -1,6 +1,6 @@
package app.dapk.st.directory.state
import app.dapk.st.core.State
import app.dapk.st.state.State
import app.dapk.st.engine.DirectoryState
typealias DirectoryState = State<DirectoryScreenState, DirectoryEvent>

View File

@ -9,7 +9,7 @@ dependencies {
implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel")
implementation project(':domains:store')
implementation project(':domains:state')
implementation 'screen-state:screen-android'
implementation project(":core")
implementation project(":design-library")
implementation Dependencies.mavenCentral.coil

View File

@ -13,11 +13,11 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module
import app.dapk.st.core.state
import app.dapk.st.core.viewModel
import app.dapk.st.directory.DirectoryModule
import app.dapk.st.login.LoginModule
import app.dapk.st.profile.ProfileModule
import app.dapk.st.state.state
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

View File

@ -6,7 +6,7 @@ dependencies {
implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel")
implementation project(":domains:store")
implementation project(":domains:state")
implementation 'screen-state:screen-android'
implementation project(":core")
implementation project(":features:navigator")
implementation project(":design-library")
@ -14,10 +14,10 @@ dependencies {
kotlinTest(it)
testImplementation 'screen-state:state-test'
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:state"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))

View File

@ -10,15 +10,17 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import app.dapk.st.core.*
import app.dapk.st.core.AndroidUri
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.MimeType
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.core.module
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.messenger.gallery.GetImageFromGallery
import app.dapk.st.messenger.state.ComposerStateChange
import app.dapk.st.messenger.state.MessengerEvent
import app.dapk.st.messenger.state.MessengerScreenState
import app.dapk.st.messenger.state.MessengerState
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.state.state
import coil.request.ImageRequest
import kotlinx.parcelize.Parcelize
@ -27,7 +29,7 @@ val LocalImageRequestFactory = staticCompositionLocalOf<ImageRequest.Builder> {
class MessengerActivity : DapkActivity() {
private val module by unsafeLazy { module<MessengerModule>() }
private val state by state { module.messengerState(readPayload()) }
private val state: MessengerState by state { module.messengerState(readPayload()) }
companion object {
@ -87,4 +89,4 @@ data class MessagerActivityPayload(
fun <T : Parcelable> Activity.readPayload(): T = intent.getParcelableExtra("key") ?: intent.getStringExtra("shortcut_key")!!.let {
MessagerActivityPayload(it) as T
}
}

View File

@ -5,7 +5,7 @@ import android.content.Context
import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.JobBag
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.createStateViewModel
import app.dapk.st.state.createStateViewModel
import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId

View File

@ -15,16 +15,17 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.lifecycleScope
import app.dapk.st.core.*
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.design.components.GenericError
import app.dapk.st.messenger.gallery.state.ImageGalleryState
import app.dapk.st.state.state
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class ImageGalleryActivity : DapkActivity() {
private val module by unsafeLazy { module<ImageGalleryModule>() }
private val imageGalleryState by state {
private val imageGalleryState: ImageGalleryState by state {
val payload = intent.getParcelableExtra("key") as? ImageGalleryActivityPayload
val module = module<ImageGalleryModule>()
module.imageGalleryState(payload!!.roomName)
}
@ -94,4 +95,4 @@ class GetImageFromGallery : ActivityResultContract<ImageGalleryActivityPayload,
@Parcelize
data class ImageGalleryActivityPayload(
val roomName: String,
) : Parcelable
) : Parcelable

View File

@ -6,7 +6,7 @@ import android.provider.MediaStore
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.JobBag
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.createStateViewModel
import app.dapk.st.state.createStateViewModel
import app.dapk.st.messenger.gallery.state.ImageGalleryState
import app.dapk.st.messenger.gallery.state.imageGalleryReducer

View File

@ -21,13 +21,14 @@ import androidx.compose.ui.unit.sp
import app.dapk.st.core.Lce
import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.page.PageAction
import app.dapk.st.design.components.GenericError
import app.dapk.st.design.components.Spider
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.design.components.Toolbar
import app.dapk.st.messenger.gallery.state.ImageGalleryActions
import app.dapk.st.messenger.gallery.state.ImageGalleryPage
import app.dapk.st.messenger.gallery.state.ImageGalleryState
import app.dapk.state.SpiderPage
import app.dapk.state.page.PageAction
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
@ -44,7 +45,7 @@ fun ImageGalleryScreen(state: ImageGalleryState, onTopLevelBack: () -> Unit, onI
}
}
Spider(currentPage = state.current.state1.page, onNavigate = onNavigate) {
Spider(currentPage = state.current.state1.page, onNavigate = onNavigate, toolbar = { navigate, title -> Toolbar(navigate, title) }) {
item(ImageGalleryPage.Routes.folders) {
ImageGalleryFolders(
it,

View File

@ -2,15 +2,15 @@ package app.dapk.st.messenger.gallery.state
import app.dapk.st.core.JobBag
import app.dapk.st.core.Lce
import app.dapk.st.core.page.PageAction
import app.dapk.st.core.page.PageStateChange
import app.dapk.st.core.page.createPageReducer
import app.dapk.st.core.page.withPageContext
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.messenger.gallery.FetchMediaFoldersUseCase
import app.dapk.st.messenger.gallery.FetchMediaUseCase
import app.dapk.state.SpiderPage
import app.dapk.state.async
import app.dapk.state.createReducer
import app.dapk.state.page.PageAction
import app.dapk.state.page.PageStateChange
import app.dapk.state.page.createPageReducer
import app.dapk.state.page.withPageContext
import app.dapk.state.sideEffect
import kotlinx.coroutines.launch

View File

@ -1,12 +1,12 @@
package app.dapk.st.messenger.gallery.state
import app.dapk.st.core.Lce
import app.dapk.st.core.State
import app.dapk.st.design.components.Route
import app.dapk.st.state.State
import app.dapk.st.messenger.gallery.Folder
import app.dapk.st.messenger.gallery.Media
import app.dapk.st.core.page.PageContainer
import app.dapk.state.Combined2
import app.dapk.state.Route
import app.dapk.state.page.PageContainer
typealias ImageGalleryState = State<Combined2<PageContainer<ImageGalleryPage>, Unit>, Unit>

View File

@ -1,7 +1,7 @@
package app.dapk.st.messenger.state
import app.dapk.st.core.Lce
import app.dapk.st.core.State
import app.dapk.st.state.State
import app.dapk.st.design.components.BubbleModel
import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent

View File

@ -2,15 +2,15 @@ package app.dapk.st.messenger.gallery.state
import android.net.Uri
import app.dapk.st.core.Lce
import app.dapk.st.core.page.PageAction
import app.dapk.st.core.page.PageContainer
import app.dapk.st.core.page.PageStateChange
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.messenger.gallery.FetchMediaFoldersUseCase
import app.dapk.st.messenger.gallery.FetchMediaUseCase
import app.dapk.st.messenger.gallery.Folder
import app.dapk.st.messenger.gallery.Media
import app.dapk.state.Combined2
import app.dapk.state.SpiderPage
import app.dapk.state.page.PageAction
import app.dapk.state.page.PageContainer
import app.dapk.state.page.PageStateChange
import fake.FakeJobBag
import fake.FakeUri
import io.mockk.coEvery
@ -18,7 +18,6 @@ import io.mockk.mockk
import org.junit.Test
import test.assertOnlyDispatches
import test.delegateReturn
import test.expect
import test.testReducer
private const val A_ROOM_NAME = "a room name"

View File

@ -4,16 +4,16 @@ dependencies {
implementation project(":chat-engine")
implementation project(":features:settings")
implementation project(':domains:store')
implementation project(':domains:state')
implementation 'screen-state:screen-android'
implementation project(":domains:android:compose-core")
implementation project(":design-library")
implementation project(":core")
kotlinTest(it)
testImplementation 'screen-state:state-test'
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:state"))
androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))

View File

@ -2,7 +2,7 @@ package app.dapk.st.profile
import app.dapk.st.core.JobBag
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.createStateViewModel
import app.dapk.st.state.createStateViewModel
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.engine.ChatEngine
import app.dapk.st.profile.state.ProfileState

View File

@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
import app.dapk.st.core.Lce
import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.page.PageAction
import app.dapk.st.design.components.*
import app.dapk.st.engine.RoomInvite
import app.dapk.st.engine.RoomInvite.InviteMeta
@ -28,6 +27,8 @@ import app.dapk.st.profile.state.Page
import app.dapk.st.profile.state.ProfileAction
import app.dapk.st.profile.state.ProfileState
import app.dapk.st.settings.SettingsActivity
import app.dapk.state.SpiderPage
import app.dapk.state.page.PageAction
@Composable
fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) {
@ -47,7 +48,7 @@ fun ProfileScreen(viewModel: ProfileState, onTopLevelBack: () -> Unit) {
}
}
Spider(currentPage = viewModel.current.state1.page, onNavigate = onNavigate) {
Spider(currentPage = viewModel.current.state1.page, onNavigate = onNavigate, toolbar = { navigate, title -> Toolbar(navigate, title) }) {
item(Page.Routes.profile) {
ProfilePage(context, viewModel, it)
}

View File

@ -3,14 +3,14 @@ package app.dapk.st.profile.state
import app.dapk.st.core.JobBag
import app.dapk.st.core.Lce
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.page.PageAction
import app.dapk.st.core.page.PageStateChange
import app.dapk.st.core.page.createPageReducer
import app.dapk.st.core.page.withPageContext
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.engine.ChatEngine
import app.dapk.state.SpiderPage
import app.dapk.state.async
import app.dapk.state.createReducer
import app.dapk.state.page.PageAction
import app.dapk.state.page.PageStateChange
import app.dapk.state.page.createPageReducer
import app.dapk.state.page.withPageContext
import app.dapk.state.sideEffect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

View File

@ -1,12 +1,12 @@
package app.dapk.st.profile.state
import app.dapk.st.core.Lce
import app.dapk.st.core.State
import app.dapk.st.core.page.PageContainer
import app.dapk.st.design.components.Route
import app.dapk.st.state.State
import app.dapk.st.engine.Me
import app.dapk.st.engine.RoomInvite
import app.dapk.state.Combined2
import app.dapk.state.Route
import app.dapk.state.page.PageContainer
typealias ProfileState = State<Combined2<PageContainer<Page>, Unit>, Unit>

View File

@ -1,13 +1,13 @@
package app.dapk.st.profile.state
import app.dapk.st.core.Lce
import app.dapk.st.core.page.PageAction
import app.dapk.st.core.page.PageContainer
import app.dapk.st.core.page.PageStateChange
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.engine.Me
import app.dapk.st.matrix.common.HomeServerUrl
import app.dapk.state.Combined2
import app.dapk.state.SpiderPage
import app.dapk.state.page.PageAction
import app.dapk.state.page.PageContainer
import app.dapk.state.page.PageStateChange
import fake.FakeChatEngine
import fake.FakeErrorTracker
import fake.FakeJobBag

View File

@ -7,15 +7,16 @@ dependencies {
implementation project(':domains:android:push')
implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel")
implementation 'screen-state:screen-android'
implementation project(":design-library")
implementation project(":core")
kotlinTest(it)
testImplementation 'screen-state:state-test'
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:state"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))

View File

@ -4,11 +4,15 @@ import android.os.Bundle
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import app.dapk.st.core.*
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module
import app.dapk.st.core.resetModules
import app.dapk.st.settings.state.SettingsState
import app.dapk.st.state.state
class SettingsActivity : DapkActivity() {
private val settingsState by state { module<SettingsModule>().settingsState() }
private val settingsState: SettingsState by state { module<SettingsModule>().settingsState() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -10,6 +10,7 @@ import app.dapk.st.push.PushModule
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
import app.dapk.st.settings.state.SettingsState
import app.dapk.st.settings.state.settingsReducer
import app.dapk.st.state.createStateViewModel
class SettingsModule(
private val chatEngine: ChatEngine,

View File

@ -41,7 +41,6 @@ import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.components.Header
import app.dapk.st.core.extensions.takeAs
import app.dapk.st.core.getActivity
import app.dapk.st.core.page.PageAction
import app.dapk.st.design.components.*
import app.dapk.st.engine.ImportResult
import app.dapk.st.navigator.Navigator
@ -51,6 +50,8 @@ import app.dapk.st.settings.state.ComponentLifecycle
import app.dapk.st.settings.state.RootActions
import app.dapk.st.settings.state.ScreenAction
import app.dapk.st.settings.state.SettingsState
import app.dapk.state.SpiderPage
import app.dapk.state.page.PageAction
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
@ -66,7 +67,7 @@ internal fun SettingsScreen(settingsState: SettingsState, onSignOut: () -> Unit,
else -> settingsState.dispatch(PageAction.GoTo(it))
}
}
Spider(currentPage = settingsState.current.state1.page, onNavigate = onNavigate) {
Spider(currentPage = settingsState.current.state1.page, onNavigate = onNavigate, toolbar = { navigate, title -> Toolbar(navigate, title) }) {
item(Page.Routes.root) {
RootSettings(
it,

View File

@ -2,10 +2,10 @@ package app.dapk.st.settings
import android.net.Uri
import app.dapk.st.core.Lce
import app.dapk.st.design.components.Route
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.engine.ImportResult
import app.dapk.st.push.Registrar
import app.dapk.state.Route
import app.dapk.state.SpiderPage
internal data class SettingsScreenState(
val page: SpiderPage<out Page>,
@ -26,9 +26,9 @@ internal sealed interface Page {
object Routes {
val root = Route<Root>("Settings")
val encryption = Route<Page.Security>("Encryption")
val pushProviders = Route<Page.PushProviders>("PushProviders")
val importRoomKeys = Route<Page.ImportRoomKey>("ImportRoomKey")
val encryption = Route<Security>("Encryption")
val pushProviders = Route<PushProviders>("PushProviders")
val importRoomKeys = Route<ImportRoomKey>("ImportRoomKey")
}
}

View File

@ -3,10 +3,8 @@ package app.dapk.st.settings.state
import android.content.ContentResolver
import app.dapk.st.core.JobBag
import app.dapk.st.core.Lce
import app.dapk.st.core.State
import app.dapk.st.state.State
import app.dapk.st.core.ThemeStore
import app.dapk.st.core.page.*
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.domain.StoreCleaner
import app.dapk.st.domain.application.eventlog.LoggingStore
import app.dapk.st.domain.application.message.MessageOptionsStore
@ -16,10 +14,8 @@ import app.dapk.st.push.PushTokenRegistrars
import app.dapk.st.settings.*
import app.dapk.st.settings.SettingItem.Id.*
import app.dapk.st.settings.SettingsEvent.*
import app.dapk.state.Combined2
import app.dapk.state.async
import app.dapk.state.createReducer
import app.dapk.state.multi
import app.dapk.state.*
import app.dapk.state.page.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

View File

@ -1,10 +1,6 @@
package app.dapk.st.settings
import app.dapk.st.core.Lce
import app.dapk.st.core.page.PageAction
import app.dapk.st.core.page.PageContainer
import app.dapk.st.core.page.PageStateChange
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.engine.ImportResult
import app.dapk.st.push.Registrar
import app.dapk.st.settings.state.ComponentLifecycle
@ -12,6 +8,10 @@ import app.dapk.st.settings.state.RootActions
import app.dapk.st.settings.state.ScreenAction
import app.dapk.st.settings.state.settingsReducer
import app.dapk.state.Combined2
import app.dapk.state.SpiderPage
import app.dapk.state.page.PageAction
import app.dapk.state.page.PageContainer
import app.dapk.state.page.PageStateChange
import fake.*
import fixture.aRoomId
import internalfake.FakeSettingsItemFactory

View File

@ -1,7 +1,7 @@
package internalfixture
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.settings.Page
import app.dapk.state.SpiderPage
internal fun aImportRoomKeysPage(
state: Page.ImportRoomKey = Page.ImportRoomKey()

1
screen-state Submodule

@ -0,0 +1 @@
Subproject commit 4e92f14031cc8be907cba09b3bfc1d9dbd380072

View File

@ -6,6 +6,9 @@ dependencyResolutionManagement {
}
}
rootProject.name = "SmallTalk"
includeBuild 'screen-state'
include ':app'
include ':design-library'