# jellyfish **Repository Path**: ProjectOpenSea/jellyfish ## Basic Information - **Project Name**: jellyfish - **Description**: Build UIs as state machines using Compose. - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-01-27 - **Last Updated**: 2025-07-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Jellyfish ***Build presenters as declarative state machines with Compose*** Heavily inspired by [Square's Workflow library](https://square.github.io/workflow/), Jellyfish has some core properties: - Presenters are declaratively defined state-machines - Separation of internal presenter state and UI models - Allows starting of a presenter in any given state, which makes testing nice and easy - Supports nested composition of presenters Jellyfish is production ready and has been used in the OpenSea Android and iOS apps (via Kotlin Multiplatform) for over a year. ## Setup ```kotlin dependencyResolutionManagement { repositories { maven { url = URI("https://jitpack.io") } } } dependencies { implementation("com.github.ProjectOpenSea:jellyfish:0.1.0") } ``` Since artifact distribution is through Jitpack for now, **Kotlin Multiplatform iOS artifacts are not built.** To use Jellyfish in a iOS KMP project, you'll need to **build the artifacts yourself.** Migrating to Maven is on the roadmap. ## Example usage As a (sort of contrived) example, let's build a simple counter that fetches it's initial count from another source. First, let's define our states, their corresponding UI models, and events. ```kotlin sealed class CounterUiModel data object LoadingUi : CounterUiModel() data class LoadedUi(val text: String) : CounterUiModel() data class ErrorUi(val message: String) : CounterUiModel() sealed class CounterState data object Loading : CounterState() data class Loaded(val count: Int) : CounterState() data object Error : CounterState() sealed class CounterEvent sealed class CounterLoadedEvent : CounterEvent() data object Increment : CounterLoadedEvent() sealed class CounterErrorEvent : CounterEvent() data object Retry : CounterErrorEvent() ``` And this is what our presenter would look like: ```kotlin // note: this class doesn't use Props or Output, so we can set their types to Unit. class CounterPresenter : StateMachinePresenter() { // define the state we should initialize into override fun initialState(props: Unit): CounterState = Loading @Composable override fun render(state: CounterState, props: Unit, events: Flow): CounterUiModel { // map our state to UI models return when (state) { Loading -> LoadingUi is Loaded -> LoadedUi(text = "Count: ${state.count}") Error -> ErrorUi("Couldn't load initial count.") } } @Composable override fun state( state: CounterState, setState: (CounterState) -> Unit, props: Unit, events: Flow ): CounterState { when (state) { Loading -> { LaunchedEffect(Unit) { val initialCountResult = fetchCount() // example suspending call that returns a Result // we use the setState() function to update our state. initialCountResult.getOrNull()?.let { initialCount -> setState(Loaded(count = initialCount)) } ?: setState(Error) } } is Loaded -> { // EventStateTransitions is a helper function that helps enforce we are only accepting the events we declare in this state. EventStateTransitions<_, CounterLoadedEvent>( state = state, setState = setState, props = props, events = events, ) { event, _ -> // this lambda defines what state we should transition to when receiving an event. when (event) { Increment -> Loaded(count = count+1) } } } Error -> { EventStateTransitions<_, CounterErrorEvent>( state = state, setState = setState, props = props, events = events, ) { event, _ -> when (event) { Retry -> Loading } } } } return state } } ``` Finally, to use the presenter, simply call it's `invoke()` function: ```kotlin @Composable fun CounterScreen() { val presenter by remember { CounterPresenter() } val uiModel = presenter() when (uiModel) { // draw your UI } } ``` ## Nested presenters 🚧🚧🚧🚧🚧🚧 under construction 🚧🚧🚧🚧🚧🚧 ## License Apache License Copyright 2023 Ozone Networks Inc. Portions of this software include code from [Jetbrains Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform) which is licensed under the Apache License Version 2.0 and which is copyright 2020-2021 JetBrains s.r.o. and their respective authors and developers