Giter Club home page Giter Club logo

komposable-architecture's Introduction

๐Ÿงฉ Komposable Architecture Maven Central Build Status License

Kotlin implementation of Point-Free's The Composable Architecture

๐Ÿšง Project Status

We've been using the Komposable Architecture in production for years now, and we haven't encountered any major issues. However, the API is still subject to change, at least until we reach version 1.0. We are working to make the setup more straightforward and are considering ways to integrate Jetpack Navigation as well.

๐Ÿ’ก Motivations

When it came time to rewrite Toggl's mobile apps, we chose a native approach instead of continuing with Xamarin. We quickly realized that, despite the apps not sharing a common codebase, we could still share many aspects across them. Using the same architecture allowed us to share specs, GitHub issues, and create a single common language that both Android and iOS developers can use. This approach has even sped up the development of features already implemented on the other platform!

We chose to use Point-Free's Composable Architecture as the apps's architecture, which meant we had to set out to implement it in Kotlin. This repo is the result of our efforts!

๐ŸŽ Differences from iOS

While all the core concepts are the same, the composable architecture is still written with Swift in mind, which means not everything can be translated 1:1 to Kotlin. Here are the problems we faced and the solutions we found:

No KeyPaths

The lack of KeyPaths in Kotlin forces us to use functions in order to map from global state to local state.

No Value Types

There's no way to simply mutate the state in Kotlin like the Composable architecture does in Swift. Instead, the reduced state is returned from the reducer along with any effects in ReduceResult.

Subscriptions

Additionally we decided to extend Point-Free architecture with something called subscriptions. This concept is taken from the Elm Architecture. It's basically a way for us to leverage observable capabilities of different APIs, in our case it's mostly for observing data stored in Room Database.

๐Ÿ“ฒ Sample App

To run the sample app, start by cloning this repo:

git clone [email protected]:toggl/komposable-architecture.git

Next, open Android Studio and open the newly created project folder. You'll want to run the todo-sample app.

For more examples take a look at Point-Free's swift samples

๐Ÿš€ Installation

The latest release is available on Maven Central.

implementation("com.toggl:komposable-architecture:1.0.0-preview01")
testImplementation("com.toggl:komposable-architecture-test:1.0.0-preview01") // optional testing extensions
ksp("com.toggl:komposable-architecture-compiler:1.0.0-preview01'")  // optional compiler plugin (still experimental)

ยฉ Licence

Copyright 2021 Toggl LLC

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.

๐Ÿงญ High-level View

This is a high level overview of the different parts of the architecture.

  • Views This is anything that can subscribe to the store to be notified of state changes. Normally this happens only in UI elements, but other elements of the app could also react to state changes.
  • Action Simple structs that describe an event, normally originated by the user, but also from other sources or in response to other actions (from Effects). The only way to change the state is through actions. Views send actions to the store which handles them in the main thread as they come.
  • Store The central hub of the application. Contains the whole state of the app, handles the actions, passing them to the reducers and fires Effects.
  • State The single source of truth for the whole app. This data class will be probably empty when the application start and will be filled after every action.
  • Reducers Reducers are pure functions that take the state and an action and produce a new state. Simple as that. They optionally result in an array of Effects that will asynchronously send further actions. All business logic should reside in them.
  • Effects As mentioned, Reducers optionally produce these after handling an action. They are classes that return an optional action. All the effects emitted from a reducer will be batched, meaning the state change will only be emitted once all actions are handled.
  • Subscriptions Subscriptions are emitting actions based on some underling observable API and/or state changes.

There's one global Store and one AppState. But we can view into the store to get sub-stores that only work on one part of the state. More on that later.

There's also one main Reducer and multiple sub-reducers that handle a limited set of actions and only a part of the state. Those reducers are then pulled back and combined into the main reducer.

๐Ÿ”Ž Getting into the weeds

Store & State

The Store exposes a flow which emits the whole state of the app every time there's a change and a method to send actions that will modify that state. The State is just a data class that contains ALL the state of the application. It also includes the local state of all the specific modules that need local state. More on this later.

The store interface looks like this:

interface Store<State, Action : Any> {
    val state: Flow<State>
    fun send(actions: List<Action>)
    // more code
}

And you can create a new store using:

createStore(
    initialState = AppState(),
    reducer = reducer,
    subscription = subscription,
    dispatcherProvider = dispatcherProvider,
    storeScopeProvider = application as StoreScopeProvider
)

actions are sent like this:

store.send(AppAction.BackPressed)

and views can subscribe like this:

store.state
    .onEach { Log.d(tag, "The whole state: \($0)") }
    .launchIn(scope)

// or

store.state
    .map { it.email }
    .onEach { emailTextField.text = it }
    .launchIn(scope)

The store can be "viewed into", which means that we'll treat a generic store as if it was a more specific one which deals with only part of the app state and a subset of the actions. More on the Store Views section.

Actions

Actions are sealed classes, which makes it easier to discover which actions are available and also add the certainty that we are handling all of them in reducers.

sealed class EditAction {
    data class TitleChanged(val title: String) : EditAction()
    data class DescriptionChanged(val description: String) : EditAction()
    object CloseTapped : EditAction()
    object SaveTapped : EditAction()
    object Saved : EditAction()
}

These sealed actions are embedded into each other starting with the "root" AppAction

sealed class AppAction {
    class List(override val action: ListAction) : AppAction(), ActionWrapper<ListAction>
    class Edit(override val action: EditAction) : AppAction(), ActionWrapper<EditAction>
    object BackPressed : AppAction()
}

So to send an EditAction to a store that takes AppActions we would do

store.send(AppAction.Edit(EditAction.TitleChanged("new title")))

But if the store is a view that takes EditActions we'd do it like this:

store.send(EditAction.TitleChanged("new title"))

Reducers & Effects

Reducers are classes that implement the following interface:

fun interface Reducer<State, Action> {
    fun reduce(state: State, action: Action): ReduceResult<State, Action>
}

The idea is they take the state and an action and modify the state depending on the action and its payload.

In order to send actions asynchronously we use Effects. Reducers return an array of Effects. The store waits for those effects and sends whatever action they emit, if any.

An effect interface is also straightforward:

interface Effect<out Action> {
    suspend fun execute(): Action?
}

Subscriptions

Subscriptions are similar to effects:

fun interface Subscription<State, Action : Any> {
    fun subscribe(state: Flow<State>): Flow<Action>
}

The difference is that Subscriptions are not triggered by Actions. They start immediately after the store is created and continue emitting as long as the store exists.

Subscriptions are typically used to observe some data in the database:

class ListSubscription @Inject constructor(val todoDao: TodoDao) : Subscription<AppState, AppAction> {
    override fun subscribe(state: Flow<AppState>): Flow<AppAction> =
        todoDao.getAll().map { AppAction.List(ListAction.ListUpdated(it)) }
}

Or some other observable APIs like for example location services. Subscription flow can be also steered by state changes:

class ListSubscription @Inject constructor(val locationProvider: LocationProvider) : Subscription<AppState, AppAction> {
    override fun subscribe(state: Flow<AppState>): Flow<AppAction> =
        if (state.isPermissionGranted) 
          locationProvider.observeCurrentLocation().map { AppAction.Map(MapAction.LocationUpdated(it)) }
        else 
          flowOf()
}

Pullback

There's one app level reducer that gets injected into the store. This reducer takes the whole AppState and the complete set of AppActions.

The rest of the reducers only handle one part of that state, for a particular subset of the actions.

This aids in modularity. But in order to merge those reducers with the app level one, their types need to be compatible. That's what pullback is for. It converts a specific reducer into a global one.

internal class PullbackReducer<LocalState, GlobalState, LocalAction, GlobalAction>(
    private val innerReducer: Reducer<LocalState, LocalAction>,
    private val mapToLocalState: (GlobalState) -> LocalState,
    private val mapToLocalAction: (GlobalAction) -> LocalAction?,
    private val mapToGlobalState: (GlobalState, LocalState) -> GlobalState,
    private val mapToGlobalAction: (LocalAction) -> GlobalAction,
) : Reducer<GlobalState, GlobalAction> {
    override fun reduce(
        state: GlobalState,
        action: GlobalAction,
    ): ReduceResult<GlobalState, GlobalAction> {
        val localAction = mapToLocalAction(action)
            ?: return ReduceResult(state, noEffect())

        val newLocalState = innerReducer.reduce(mapToLocalState(state), localAction)

        return ReduceResult(
            mapToGlobalState(state, newLocalState.state),
            newLocalState.effects.map { effects -> effects.map { e -> e?.run(mapToGlobalAction) } },
        )
    }
}

After we've transformed the reducer we can use combine to merge it with other reducers to create one single reducer that is then injected into the store.

Store Views

Similarly to reducers and pullback, the store itself can be "mapped" into a specific type of store that only holds some part of the state and only handles some subset of actions. Only this operation is not exactly "map", so it's called view.

class MutableStateFlowStore<State, Action : Any> private constructor(
    override val state: Flow<State>,
    private val sendFn: (List<Action>) -> Unit
) : Store<State, Action> {

    override fun <ViewState, ViewAction : Any> view(
        mapToLocalState: (State) -> ViewState,
        mapToGlobalAction: (ViewAction) -> Action?,
    ): Store<ViewState, ViewAction> = MutableStateFlowStore(
        state = state.map { mapToLocalState(it) }.distinctUntilChanged(),
        sendFn = { actions ->
            val globalActions = actions.mapNotNull(mapToGlobalAction)
            sendFn(globalActions)
        },
    )
}

This method on Store takes two functions, one to map the global state into local state and another one to map local action to global action.

Different modules or features of the app use different store views so they are only able to listen to changes to parts of the state and are only able to send certain actions.

Local State

Some features have the need of adding some state to be handled by their reducer, but maybe that state is not necessary for the rest of the application. Consider email & password fields in a theoretical Auth module.

To deal with this kind of state we do the following:

  • In the module's state use a public class with internal properties to store the needed local state
  • We store that property in the global state. So that state in the end is part of the global state and it behaves the same way, but can only be accessed from the module that needs it.

This is how could the AuthState look like:

data class AuthState(
    val user: Loadable<User>,
    val localState: LocalState
) {
    data class LocalState internal constructor(
        internal val email: Email,
        internal val password: Password
    ) {
        constructor() : this(Email.Invalid(""), Password.Invalid(""))
    }
}

This is how it looks in the global app state

data class AppState(
    val authLocalState: AuthState.LocalState = AuthState.LocalState(),
)

High-order reducers

High-order reducers are basically reducers that take another reducer (and maybe also some other parameters). The outer reducer adds some behavior to the inner one, maybe transforming actions, stopping them or doing something with them before sending them forward to the inner reducer.

The simplest example of this is a logging reducer, which logs every action sent to the console:

class LoggingReducer(override val innerReducer: Reducer<AppState, AppAction>)
    : HigherOrderReducer<AppState, AppAction> {
    override fun reduce(
        state: AppState,
        action: AppAction
    ): ReduceResult<AppAction> {
        Log.i(
            "LoggingReducer", when (action) {
                is AppAction.List -> action.list.formatForDebug()
                is AppAction.Edit -> action.edit.formatForDebug()
            }
        )

        return innerReducer.reduce(state, action)
    }
}

โœ… Testing Extensions

If you decide to include com.toggl:komposable-architecture-test to your dependencies, you'll be able to use a small set of Reducer extensions designed to make testing easier.

Take a look at this test from Todo Sample app which is making a good use of testReduce extension method:

@Test
fun `ListUpdated action should update the list of todos and return no effects`() = runTest {
     val initialState = ListState(todoList = emptyList(), backStack = emptyList())
     reducer.testReduce(
         initialState,
         ListAction.ListUpdated(listOf(testTodoItem))
     ) { state, effects ->
         assertEquals(initialState.copy(todoList = listOf(testTodoItem)), state)
         assertEquals(noEffect(), effects)
     }
}

komposable-architecture's People

Contributors

daividssilverio avatar heytherewill avatar pdrj avatar semanticer avatar yshrsmz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

komposable-architecture's Issues

๐Ÿ”ฅ Get rid of the current complex TODO sample app

Description

Our current Todo sample app is too complex and confusing for purposes of sample code. Let's get rid of it in favor of a new sample code, which should be very closely modeled after Point-Free's Todos sample

This will also help everyone better understand the differences between KA and TCA in their current iterations.

Definition of done

  • Old sample code is gone

Correct and improve PUBLISHING.md

Description

I had the pleasure of following PUBLISHING.md today since I forgot how to publish, and I have a new machine with no keys or any other local setup. It turns out PUBLISHING.md is missing some important information and even contains some misinformation ๐Ÿ™ˆ

We need to fix this so that the future me or you have an easier time publishing next time.

Dependency update

Description

We need to update a lot of stuff including AGP and SDK levels.

๐Ÿ†™ Effects 1.0.0

Description

We aim to enhance our effects to match The Composable Architecture.

New run API

TCA's version is using this .run { } method to construct effects. We can do the same or similar but we definitely want to have more options to construct effect. I came up with these three so far:

fun runFlow(flow: Flow<Int>) { /* ... */ }
fun runSuspend(func: suspend () -> Int) { /* ... */ }
fun runProducer(@BuilderInference block: suspend ProducerScope<Int>.() -> Unit) { /* ... */ }

Usage:

runFlow(flowOf(1))
runSuspend { longRunningSuspendFunction() }
runProducer {
    send(1)
    send(2)
}

It'd be nice to keep the option of dedicated effect classes, such as:

class SomeBigEffect() : SuspendEffect<Int> {
    override suspend fun execute(): Int {
        return 1
    }
}

Cancelation

Take a look at TCA's cancellation case study.

Basically, there should be an option to specify a cancellable key for each effect:

return .run { [count = state.count] send in
  await send(.factResponse(TaskResult { try await self.factClient.fetch(count) }))
}
.cancellable(id: CancelID.factRequest)

And they cancel the effects with something like:

case .cancelButtonTapped:
  state.isFactRequestInFlight = false
  return .cancel(id: CancelID.factRequest)

cancellable has one extra parameter cancelInFlight which determines if any in-flight effect with the same identifier should be canceled before starting this new one.

 .cancellable(id: CancelID.timer, cancelInFlight: true)

Definition of done

  • Effects can emit more than one action
  • Effects are built on top of Flow
  • Effects are cancellable
  • API is closer to TCA but provides unique features of Kotlin coroutines/flows

โ“ How to handle some advanced features

Hi! I'm a big fan of TCA in SwiftUI and I'm working on a Kotlin multiplatform project in which we want to use this architecture for both platforms.

We've managed to code a lot of the app but we are starting to face some rough edges, so I'd like to get some inspiration on how to solve them.

The challenges we (mostly) related to state restoration once the app comes back from background after some time. We create our substores in runtime depending on the user navigation, so once the reference is destroyed we cannot recreate them and the app crashes.

Some examples:

State is a sealed class and we need to use a composable function for handling every when branch so we don't deal with nullables: (TCA's SwitchStore)

sealed class ProfileState {
  object Loading : ProfileState()
  data class Empty(val state: EmptyProfileState) : ProfileState()
  data class Filled(val state: FilledProfileState): ProfileState()
}

val profileReducer = Reducer.combine(
  Reducer { ... },
  EmptyProfileState.reducer.pullback(...),
  FilledProfileState.reducer.pullback(...)
)

@Composable
fun HomeTab() {
  val store = hiltViewModel<HomeStore>() // State isn't too deep here so we can create this viewmodel in build time
  
 // WhenStore and Case allows us to use stores with non-nullable state for the current case. But these stores
 // can only be created in runtime by passing through this piece of code.
  WhenStore(store) {
    Case<ProfileState.Loading, ProfileAction>(
      deriveState = ...,
      embedAction = ::identity
    ) { loadingProfileStore ->
      Navigator(HomeLoadingProfilePage(loadingProfileStore))
    }
    
    Case<EmptyProfileState, EmptyProfileAction>(
      deriveState = ...,
      embedAction = ...
    ) { emptyProfileStore ->
      Navigator(HomeEmptyProfilePage(emptyProfileStore))
    }
    
    Case<FilledProfileState, FilledProfileAction>(
      deriveState = ...,
      embedAction = ...
    ) { filledProfileStore ->
      Navigator(HomeFilledProfilePage(filledProfileStore))
    }    
  }
}

Once in one of those pages, if the activity is destroyed, when the framework tries to recreate the screen it cannot create the store again as it isn't parcelable. Creating a store manually is possible, but it wouldn't make sense as the global state won't be updated.


Recursive state and navigation.

data class TaskDetailState(
  val title: String,
  val body: String,
  val subtasks: List<TaskDetailState>,
  val childDetail: TaskDetailState? = null
)

sealed class TaskDetailAction {
  ...
  
  data class ChildTaskDetailAction(val action: TaskDetailAction) : TaskDetailAction()
}

val reducer = Reducer.recurse { ... } 

// Code for creating substores would be
val store: Store<TaskDetailState, TaskDetailAction> = ...

store.derived<TaskDetailState?, TaskDetailAction>(
  deriveState = { it.childDetail },
  embedAction = { TaskDetailAction.ChildTaskDetailAction(it) } 
)

Again, these kind of substores must be created in runtime as we don't know how many levels of nesting the user will navigate. Besides, they all have the same type signature, so a parameter is needed for retrieving them from DI. The most obvious one being the Task id, which we don't know until runtime.


TLDR How would you work with store which can only be created in runtime? How would you make them survive process death?

Thanks for your time โค๏ธ

Jetpack navigation plan

From the README:

we are thinking about ways to integrate jetpack navigation

What is the plan for this?

Naming convention

Hi,

We were looking at using this but noticed that Store has a method called dispatch whereas TCA nomenclature for it, as far as we understood it, was send, is there any reason in particular for this difference? It was rather confusing when some of us had (some) previous knowledge from iOS examples and were trying to make our way through the Android TODO app example.

Thank you!

Implement ViewStateProvider

Description

Implement ViewStateProvider to fix: Flow operator functions should not be invoked within composition error

Kotlin Multiplatform

I have rewritten a version of this library for Kotlin Multiplatform that is in use in a production app. My version has some of the same items you are currently iterating on, Effects using Flows, Effect cancellation (by id and by scope) among other changes. You can find a version of it here.

Since there is very little in this framework that is JVM specific, in the name of avoiding duplication of effort, I would be more than happy to contribute to this library and use this framework if it supported KMP.

Is there any chance you would consider supporting KMP in this library? I'd be happy to contribute to or do the implementation.

Code generation proposal

Proposal

Since the inception of this library I've been meaning to write a compiler plugin which could generate a lot of the boilerplate needed to overcome the lack of Keypaths in Kotlin. I have a Proof of concept plugin but I need to discuss the details to make sure I'm covering all details. Since I don't have access to the Toggl codebase anymore, I will need internal help to find edge cases

My idea is simple:

  • Introduce annotations that allow you to indicate State and action mappings
  • Introduce a KSP SymbolProcessor that will generate the mapping and pullback functions for you
  • We delete inline fun <From, reified To> From.unwrap(): To? = everywhere so I can finally have peace of mind

This is the first draft I have of the wishful thinking outcome of this proposal. I'd love to hear feedback on naming, but also on possible ways of improving the proposal, edge cases I didn't consider and boilerplate we could automate that I'm not touching here yet.

/*
    Newly added annotations
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class WrapsAction

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class ChildStates(vararg val childStates: KClass<*>)

/*
    Code the users write
 */
data class SettingsState(
    val booleanSetting: Boolean = true
)

sealed interface SettingsAction {
    data class ChangeSomeSetting(val newValue: Boolean) : SettingsAction
}

// We need to annotate the parent because the
// child might not have access to the parent due to modularization.
@ChildStates(SettingsState::class)
data class AppState(
    val listOfItems: List<String> = emptyList(),
    val booleanSetting: Boolean = true,
)

sealed interface AppAction {
    object ClearList : AppAction

    @WrapsAction
    data class Settings(val settingsAction: SettingsAction) : AppAction
}

class SettingsReducer : Reducer<SettingsState, SettingsAction> {
    override fun reduce(state: Mutable<SettingsState>, action: SettingsAction): List<Effect<SettingsAction>> =
        when (action) {
            is SettingsAction.ChangeSomeSetting -> state.mutateWithoutEffects {
                copy(booleanSetting = action.newValue)
            }
        }
}

fun mainReducer(settingsReducer: SettingsReducer): Reducer<AppState, AppAction> =
    settingsReducer.pullback()

/*
    Auto-generated code
 */

// This we can infer by checking that `SettingsReducer` implements `Reducer<SettingsState, SettingsAction>`
// and also by checking that both `SettingsState` and `SettingsAction` have generated action mappings.
fun Reducer<SettingsState, SettingsAction>.pullback() = pullback(
    ::mapAppStateToSettingsState,
    ::mapAppActionToSettingsAction,
    ::mapSettingsStateToAppState,
    ::mapSettingsActionToAppAction,
)

// This function is super simple to generate. We will throw a compiler error if
// the wrapper action has more than one property (meaning it's not truly wrapping a child action).
fun mapSettingsActionToAppAction(settingsAction: SettingsAction): AppAction =
    AppAction.Settings(settingsAction)

// This one we can generate easily if we are doing name matching BUT we need to allow overrides.
// We can add something like `ParentPropertyPath` so one can annotate the child state. While we
// can output compiler errors to ensure refactors don't break mapping, you are still just
// using a string to tell where to find the parent property.
fun mapSettingsStateToAppState(appState: AppState, settingsState: SettingsState): AppState =
    appState.copy(
        booleanSetting = settingsState.booleanSetting
    )

// Same as the other action mapping function.
fun mapAppActionToSettingsAction(appAction: AppAction): SettingsAction? =
    if (appAction is AppAction.Settings) appAction.settingsAction else null

// Same as the other state mapping function.
fun mapAppStateToSettingsState(appState: AppState): SettingsState =
    SettingsState(
        booleanSetting = appState.booleanSetting
    )

Tasks

  • Introduce new komposable-architecture-compiler artifact
  • Create a SymbolProcessor for generating Action boilerplate
  • Create a SymbolProcessor for generating State boilerplate
  • Create a SymbolProcessor for generating Pullback boilerplate

Add ForEach composition method

Description

The Composable Architecture has this handy ForEach method of composing reducers. See this in action in Todos sample app.
We should be able to do the same, although it will probably look a bit uglier.

Definition of done

  • ForEach reducer extension method
  • ForEachReducer
  • Tests are written

Update dependencies (Compose, Kotlin and more)

Description

There are a bunch of new library versions we should update, most importantly Compose and Kotlin.

Definition of done

  • Kotlin is updated
  • Compose is updated and included using bom
  • Any other low hanging fruit

mergeSubscriptions function with subscription list parameter

Description

It's nice to provide a convenience method for subscription lists, our own codebase will benefit from this.

Definition of done

  • fun <State, Action : Any> mergeSubscriptions(subscriptions: List<Subscription<State, Action>>) is created

๐Ÿ’š Add testing artifact with testing extensions

Description

Add a new testing artifact with some helper functions for testing similar to what we have in https://github.com/toggl/komposable-architecture/blob/main/todo-sample/src/test/java/com/toggl/komposable/sample/todo/common/ReducerTestExtensions.kt

We could add more extensions specifically for testing effects

This is probably how it's gonna be used:

testImplementation 'com.toggl:komposable-architecture-test:0.1.1'

Definition of done

  • Testing module is created
  • Testing module is successfully used in todo-sample application
  • Testing module is published as a separate artifact
  • Readme is updated to reflect this change

Move away from Mutable<T> and return State + Effect(s) instead

Description

The original decision behind Mutable<T> was made to maintain syntactical similarity with iOS. We agreed this is not necessary, and we'd prefer to make things simpler.

fun reduce(state: Mutable<State>, action: Action): List<Effect<Action>>
// into 
fun reduce(state: State, action: Action): ResultOrSomething<State, List<Effect<Action>>>

after #51 it should become

fun reduce(state: State, action: Action): ResultOrSomething<State, Effect<Action>>

Let's make sure we can do the transition as painless as possible, let's provide @deprecated if it makes sense

Definition of done

  • The old code is deleted or reasonably deprecated
  • The new reducer signature is implemented
  • The rest of the code works as before

Add tests for ReducerExtension methods

Description

Currently, our codebase lacks adequate testing for reducer composition. It's time to address this gap and implement comprehensive tests for reducer composition.

Definition of done

  • Tests for combine are added
  • Tests for pullback are added
  • Tests for optionalPullback are added

Recommendation for state access in composables (warnings in todo-example)

Hi there,

Thank you so much for making this! I'm pretty new to Android development, and I have a question regarding the todo-example. In the Page composables there are warnings that say Flow operator functions should not be invoked within composition. Can these safely be ignored, or is there some other pattern that should be use when accessing state in a composable?

image

Navigation compose

Hi,
I'm using navigation-compose version 2.4.1 and 2.4.0-alpha07
I have error when navigating back (pop()) reached the init route

But in [komposable-architecture]/ todo-sample this version 2.4.0-alpha07 working fine and version 2.4.1 error as the same

What is that problem?
How can I fix it?

Thank you!

๐Ÿ’š Introduce TestStore and make effects easier to test

Description

We should provide a TestStore similar to what TCA offers to make more complex tests including effect testing easier.

Here's a proposal of how this could look like:

store.test {
    send(MyCoolAction) shouldResultInState CoolerState
    send(MyEffectLaungingAction).shouldntChangeState()
    receive(MyEffectAction) shouldResultInState EffectModifiedState
    receive(MySecondEffectAction) shouldResultInState AnotherEffectModifiedState
}

Let's use Turbine under the hood to make our lives easier.

Dependencies

Definition of done

  • [ ]
  • The documentation is updated to reflect the changes done in this PR

Test library doesn't provide latest changes

Version used : komposable-architecture-test-1.0.0-preview01

Test extensions provided by the library only gives some parts of the extensions that are supposed to be visible in the code.
image

Todo's example uses extensions from komposable-architecture-test/src/main/java/com/toggl/komposable/test/store/ReducerTestExtensions.kt, but it's not available on my side

README regarding testing also doesn't seem up to date

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.