{"id":17450267,"url":"https://github.com/gymbay/gelm","last_synced_at":"2025-06-25T21:33:59.486Z","repository":{"id":247703279,"uuid":"823946426","full_name":"gymbay/GELM","owner":"gymbay","description":"Gelm is Android presentation library foundation on TEA","archived":false,"fork":false,"pushed_at":"2024-10-18T12:21:27.000Z","size":499,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-19T12:46:57.524Z","etag":null,"topics":["android","elm","kotllin","mvi","presentation","tea","udf"],"latest_commit_sha":null,"homepage":"https://central.sonatype.com/artifact/io.github.gymbay/gelm","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gymbay.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-04T04:18:14.000Z","updated_at":"2024-10-18T12:21:29.000Z","dependencies_parsed_at":"2024-07-10T06:27:32.573Z","dependency_job_id":"975d0021-1f04-4736-9627-1991d016a165","html_url":"https://github.com/gymbay/GELM","commit_stats":null,"previous_names":["gymbay/gelm"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gymbay%2FGELM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gymbay%2FGELM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gymbay%2FGELM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gymbay%2FGELM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gymbay","download_url":"https://codeload.github.com/gymbay/GELM/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228568478,"owners_count":17938267,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","elm","kotllin","mvi","presentation","tea","udf"],"created_at":"2024-10-17T22:03:48.694Z","updated_at":"2024-12-07T06:24:30.836Z","avatar_url":"https://github.com/gymbay.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# General\n\nGELM is Android library for the popular presentation approach The ELM Architecture (TEA).\n\nThe library standardizes the work with sending and processing synchronous and asynchronous events.\nAll inputs and outputs to library are strictly defined, which makes it easier to develop and test.\n\n### Gelm architecture schema\n![Gelm schema](/gelm_schema.jpg)\n\n# How to implement (Gradle)\n\nLibrary publicated in Maven Central repository, so you need first define Maven Central repository in\nyour settings.gradle.kts file.\n\n```kotlin\npluginManagement {\n    repositories {\n        mavenCentral()\n    }\n}\ndependencyResolutionManagement {\n    repositories {\n        mavenCentral()\n    }\n}\n```\n\nWhen you defined Maven Central then you need define library in your application module in\nbuild.gradle.kts file.\n\n```kotlin\ndependencies {\n  implementation(\"io.github.gymbay:gelm:1.0.1\")\n}\n```\n\n# Quick start\n\nThis guide helps you to understand how library works.\n\n## Data types\n\nLibrary works with 5 generic types, some types may be optional.\nOptional types can be defined as Kotlin Nothing type.\n\nAll types applied in [GelmStore](gelm/src/main/java/io/github/gymbay/gelm/GelmStore.kt).\n\n- **State** (required) - UI representation as some Kotlin type. In general is Data class;\n- **Effect** (optional) - one shot event. As example, showing alert in Android view, start timer,\n  trigger navigation.\n  In general is Enum class;\n- **Event** (optional) - some external event\n  for [GelmStore](gelm/src/main/java/io/github/gymbay/gelm/GelmStore.kt).\n  As example, user initiated event from UI, event from\n  another [GelmStore](gelm/src/main/java/io/github/gymbay/gelm/GelmStore.kt). In general is Sealed\n  or Enum class;\n- **Command** (optional) - internal async command\n  for [GelmActor](gelm/src/main/java/io/github/gymbay/gelm/GelmActor.kt). May be produced from\n  external and internal reducer. In example, command for load data from server or save to database;\n- **InternalEvent** (optional) - some event from\n  internal [GelmActor](gelm/src/main/java/io/github/gymbay/gelm/GelmActor.kt). As example, response\n  from server or database.\n\n## Main components\n\nGelm works with 4 main and 2 additional components. In main components only 2 required and others\noptional.\n\nMinimum work configuration\nrequired [GelmStore](gelm/src/main/java/io/github/gymbay/gelm/GelmStore.kt)\nand [GelmExternalReducer](gelm/src/main/java/io/github/gymbay/gelm/reducers/GelmExternalReducer.kt).\n\n### GelmStore\n\nCentral entity for GELM architecture. Holder for architecture components. Responsible for coordinate\nflow of external and internal events. Inherited from `ViewModel` android architecture component.\n\n```kotlin\nimport io.github.gymbay.gelm.utils.GelmStore\n\n// Minimum configuration\nGelmStore(\n  initialState = ExampleState(),\n  externalReducer = ExampleExternalReducer()\n)\n```\n\nYou can pass `Events` from UI to `GelmStore` using `sendEvent(event: Event)` function. All that\nevent\nwill be handled by `GelmExternalReducer`.\n\n```kotlin\nOutlinedTextField(\n  onValueChange = {\n    store.sendEvent(ExampleEvent.TypeText(it))\n  }\n)\n```\n\nAnd then you can observe changes in `State` subscribed on `state: StateFlow\u003cState\u003e`.\n\n```kotlin\nval state by store.state.collectAsStateWithLifecycle()\n\nOutlinedTextField(\n  value = state.editField,\n  onValueChange = {\n    store.sendEvent(ExampleEvent.TypeText(it))\n  }\n)\n```\n\nAt the end you can use `effect: Flow\u003cEffect\u003e` to observe one-shot events.\n\n```kotlin\n// A little life-hack to simplify observing in Compose\n@Composable\nfun \u003cT\u003e CollectEffect(\n  effect: Flow\u003cT\u003e,\n  context: CoroutineContext = EmptyCoroutineContext,\n  block: (T) -\u003e Unit\n) {\n  LaunchedEffect(key1 = Unit) {\n    effect.onEach(block).flowOn(context).launchIn(this)\n  }\n}\n\nCollectEffect(store.effect) { effect -\u003e\n  when (effect) {\n    ExampleEffect.NavigateToScreen -\u003e {\n      Toast.makeText(context, \"Button tapped!\", Toast.LENGTH_LONG).show()\n    }\n  }\n}\n```\n\n### GelmExternalReducer\n\nReducer is an entity responsible for handling external events (UI or another GelmStore).\n\nThe reducer is a pure function, and therefore it should be stateless. Adding stored properties is a\nbad\npractice.\n\nTo define your external reducer you can inherit `GelmExternalReducer` abstract class\nand override `processEvent(currentState: State, event: Event)`\nor/and `processInit(currentState: State)` functions.\n\n```kotlin\nimport io.github.gymbay.gelm.reducers.GelmExternalReducer\nimport io.github.gymbay.gelm.reducers.Modifier\n\nclass ExampleExternalReducer :\n  GelmExternalReducer\u003cExampleEvent, ExampleState, ExampleEffect, ExampleCommand\u003e() {\n\n  // Handle GelmStore init. \n  // For example, to mutate state on screen start or start loading.\n  override fun Modifier\u003cExampleState, ExampleEffect, ExampleCommand\u003e.processInit(currentState: ExampleState) {\n    TODO()\n  }\n\n  // Handle events from UI or another observed GelmStore \n  override fun Modifier\u003cExampleState, ExampleEffect, ExampleCommand\u003e.processEvent(\n    currentState: ExampleState,\n    event: ExampleEvent\n  ) {\n    when (event) {\n      ExampleEvent.Reload -\u003e TODO()\n      is ExampleEvent.TypeText -\u003e TODO()\n      is ExampleEvent.Next -\u003e TODO()\n    }\n  }\n\n}\n```\n\nAnd then pass reducer to GelmStore\n\n```kotlin\nimport io.github.gymbay.gelm.utils.GelmStore\n\nGelmStore(\n  initialState = ExampleState(),\n  externalReducer = ExampleExternalReducer()\n)\n```\n\nUse `Modifier` in override functions to produce new state, effects and commands. As a result of\nreducer work will be produced `ReducerResult` data class. More detailed\nin [Modifier and ReducerResult](#modifier-and-reducerresult) section.\n\nOn every invoke `GelmStore` passed actual current state and event to reducer. `currentState` is\nimmutable, state mutations accumulates in `Modifier`.\n\n### GelmActor\n\nOptional entity responsible for handling async or heavy computing commands from reducers. For\nexample, request to server or database. Component use Flow to publish results with InternalEvent\ntype.\n\nTo define your Actor you can inherit GelmActor abstract class and\noverride `suspend execute(command: Command): Flow\u003cInternalEvent\u003e` function.\n\nFor example:\n\n```kotlin\nimport io.github.gymbay.gelm.GelmActor\n\nclass ExampleActor : GelmActor\u003cExampleCommand, ExampleInternalEvent\u003e() {\n  override suspend fun execute(command: ExampleCommand): Flow\u003cExampleInternalEvent\u003e = flow {\n    when (command) {\n      is ExampleCommand.StartLoading -\u003e {\n        delay(3.seconds)\n        val list = mutableListOf\u003cString\u003e()\n        for (i in 1..Random.nextInt(1, 100)) {\n          list.add(\"${command.text} N $i\")\n        }\n        emit(ExampleInternalEvent.LoadedData(list))\n      }\n    }\n  }\n}\n```\n\nBy default all commands executes in `viewModelScope` with `Dispatchers.Default`. If you need another\ncontext use `withContext()` function.\n\nAs a result of actor work `GelmStore` received flow of `InternalEvent`.\n\n### GelmInternalReducer\n\nOptional entity. Internal reducer an entity responsible for handling internal events\nfrom `GelmActor`. Must be stateless.\n\nIn the sense of `GelmInternalReducer` is an analog of `GelmExternalReducer`, but it works only with\ninternal events.\n\nTo define your internal reducer you can inherit `GelmInternalReducer` abstract class and\noverride `processInternalEvent(currentState: State, internalEvent: InternalEvent)` function.\n\n```kotlin\nclass ExampleInternalReducer :\n  GelmInternalReducer\u003cExampleInternalEvent, ExampleState, ExampleEffect, ExampleCommand\u003e() {\n\n  override fun Modifier\u003cExampleState, ExampleEffect, ExampleCommand\u003e.processInternalEvent(\n    currentState: ExampleState,\n    internalEvent: ExampleInternalEvent\n  ) {\n    when (internalEvent) {\n      is ExampleInternalEvent.LoadedData -\u003e TODO()\n    }\n  }\n\n}\n```\n\nUsing `GelmInternalReducer` make sense only with `GelmActor`, so store defining will be looks like:\n\n```kotlin\nGelmStore(\n  initialState = ExampleState(),\n  externalReducer = ExampleExternalReducer(),\n  actor = ExampleActor(),\n  internalReducer = ExampleInternalReducer()\n)\n```\n\nOn every invoke `GelmStore` passed actual current state and internal event to\nreducer. `currentState` is\nimmutable, state mutations accumulates in `Modifier`.\n\n### Modifier and ReducerResult\n\n[Modifier](gelm/src/main/java/io/github/gymbay/gelm/reducers/GelmModifier.kt) and\n[ReducerResult](gelm/src/main/java/io/github/gymbay/gelm/reducers/ReducerResult.kt)\nis a core classes for reducers used in processing events functions.\n\n`Modifier` provides functions for state changing, produce effects, commands, events to subscribed\nstores and cancel long work commands.\n\nAs a result of `Modifier` work always be `ReducerResult`. `ReducerResult` contains all state changes\nand produced commands, events and etc.\n\nLet's take a closer look at what functions the `Modifier` provides and what its typical use looks\nlike:\n\n#### Function `state(modify: State.() -\u003e State)`\n\nFunction for state modifications. Function provides `State` lambda scope and expect new state on\nreturn.\nTypical approach for state modification in Kotlin is using `copy()` function on data classes.\nExample of use:\n\n```kotlin\ndata class State(val loadedCount: Int = 0)\n\nclass TestExternalReducer : GelmExternalReducer\u003cNothing, State, Effect, Nothing\u003e() {\n  override fun Modifier\u003cState, Effect, Nothing\u003e.processInit(currentState: State) {\n    val newLoadedCount = currentState.loadedCount + 1\n    state { copy(loadedCount = newLoadedCount) }\n  }\n}\n\nval reducerResult = TestExternalReducer().processInit(State())\n\nassertEquals(State(loadedCount = 1), reducerResult.state)\n```\n\n#### Function `effect(newEffect: Effect)`\n\nFunction for producing new `Effects` after reducer work. Each time the function is called,\nthe `Modifier` adds a new value to the `internalEffects` mutable list.\n\n```kotlin\nclass TestExternalReducer : GelmExternalReducer\u003cNothing, Unit, Effect, Nothing\u003e() {\n  override fun Modifier\u003cUnit, Effect, Nothing\u003e.processInit(currentState: Unit) {\n    effect(Effect.Alert)\n    effect(Effect.Toast)\n    effect(Effect.Navigation)\n  }\n}\n\nval reducerResult = TestExternalReducer().processInit(Unit)\n\nassertEquals(3, reducerResult.effects.size)\n```\n\n#### Function `command(newCommand: Command)`\n\nFunction for producing async commands to `GelmActor`. Each time the function is called,\nthe `Modifier` adds a new value to the `internalCommands` mutable list.\n\n```kotlin\nclass TestExternalReducer : GelmExternalReducer\u003cNothing, Unit, Nothing, Command\u003e() {\n  override fun Modifier\u003cUnit, Nothing, Command\u003e.processInit(currentState: Unit) {\n    command(Command.LoadClient)\n    command(Command.LoadProducts)\n  }\n}\n\nval reducerResult = TestExternalReducer().processInit(Unit)\n\nassertEquals(2, reducerResult.commands.size)\n```\n\n#### Function `cancelCommand(command: Command)`\n\nFunction for cancelling long work command in `GelmActor`. For example, long computation than might\nbe cancelled when user tap `Cancel` button.\n\n```kotlin\nclass TestExternalReducer : GelmExternalReducer\u003cNothing, Unit, Nothing, Command\u003e() {\n  override fun Modifier\u003cUnit, Nothing, Command\u003e.processInit(currentState: Unit) {\n    cancelCommand(Command.LoadClient)\n    cancelCommand(Command.LoadProducts)\n  }\n}\n\nval reducerResult = TestExternalReducer().processInit(Unit)\n\nassertEquals(2, reducerResult.cancelledCommands.size)\n```\n\n#### Function `event(event: ObserverEvent)`\n\nFunction for sending untyped events that might be handled by subscribed `GelmStore`. Use when you\nneed make interaction with another `GelmStore`.\n\n```kotlin\n// Reducer that will be sending event to another store\nprivate class InitialExternalReducer :\n  GelmExternalReducer\u003cInitialEvent, Unit, Nothing, Nothing\u003e() {\n  override fun Modifier\u003cUnit, Nothing, Nothing\u003e.processEvent(\n    currentState: Unit,\n    event: InitialEvent\n  ) {\n    when (event) {\n      is InitialEvent.StartDelegation -\u003e event(DelegationEvent.Data(title = event.title))\n    }\n  }\n}\n\n// Reducer that will be receiving event from another store\nprivate class DelegationExternalReducer :\n  GelmExternalReducer\u003cDelegationEvent, DelegationState, Nothing, Nothing\u003e() {\n  override fun Modifier\u003cDelegationState, Nothing, Nothing\u003e.processEvent(\n    currentState: DelegationState,\n    event: DelegationEvent\n  ) {\n    when (event) {\n      is DelegationEvent.Data -\u003e state { copy(title = event.title) }\n    }\n  }\n}\n\n// Implementation logic\nval initialStore = GelmStore\u003cUnit, Nothing, InitialEvent, Nothing, Nothing\u003e(\n  initialState = Unit,\n  externalReducer = InitialExternalReducer(),\n  commandsDispatcher = StandardTestDispatcher(testScheduler)\n)\n\nval delegationStore = GelmStore\u003cDelegationState, Nothing, DelegationEvent, Nothing, Nothing\u003e(\n  initialState = DelegationState(),\n  externalReducer = DelegationExternalReducer(),\n)\n\n// Subscribe delegationStore to initialStore\ninitialStore.subscribe(delegationStore)\nassertNull(delegationStore.state.first().title)\n\nval newTitle = \"initial\"\ninitialStore.sendEvent(InitialEvent.StartDelegation(title = newTitle))\nadvanceUntilIdle()\n\nassertEquals(DelegationState(title = newTitle), delegationStore.state.first())\n```\n\n## Additional components\n\n`GelmStore` provides some additional functionality to debugging and state handling.\n\n### GelmSavedStateHandler\n\nIf you need your `State` to survive the system process death event,\nuse [GelmSavedStateHandler](gelm/src/main/java/io/github/gymbay/gelm/utils/GelmSavedStateHandler.kt)\ninterface.\n\n`GelmSavedStateHandler` interface provides two functions:\n\n- `saveState(state: State)` for saving state into long living storage. Invokes on each state\n  changing. Works on main thread;\n- `restoreState(initialState: State): State?` for restoring state from long living storage. Works on\n  init phase of `GelmStore` on main thread.\n\n```kotlin\nval savedStateHandler = object : GelmSavedStateHandler\u003cExampleState\u003e {\n  override fun saveState(state: ExampleState) {\n    TODO()\n  }\n\n  override fun restoreState(initialState: ExampleState): ExampleState? {\n    TODO()\n  }\n}\n\nGelmStore(\n  initialState = ExampleState(),\n  externalReducer = ExampleExternalReducer(),\n  actor = ExampleActor(),\n  internalReducer = ExampleInternalReducer(),\n  savedStateHandler = savedStateHandler\n)\n```\n\n### GelmLogger\n\nFor debugging might be useful\nusing [GelmLogger](gelm/src/main/java/io/github/gymbay/gelm/utils/GelmLogger.kt).\n`GelmLogger` provides `log(eventType: EventType, message: String)` function for log `GelmStore`\nlifecycle events.\nYou can use standard log output or analytics in overrides of log function.\n\nExample of use:\n\n```kotlin\nGelmStore(\n  logger = { eventType, message -\u003e println(\"$eventType = $message\") }\n)\n```\n\nNow `GelmLogger` supports\n10 [EventTypes](gelm/src/main/java/io/github/gymbay/gelm/utils/GelmLogger.kt). Name of event\ndetermine what will be logged.\n\n# How to test\n\nAll components in Gelm covered tests and no need to retest on user side.\n\nOn user side might be tested custom architecture components inherited from:\n\n- GelmExternalReducer\n- GelmInternalReducer\n- GelmActor\n\nThe library offers a standardized approach to testing architectural components.\n\n## Test GelmExternalReducer\n\nBecause  `GelmExternalReducer` stateless component it might be tested like pure function with\ndetermined inputs and outputs.\n\nTypical test looks like:\n\n```kotlin\n@Test\nfun testReloadEvent() {\n  val inputText = \"Text\"\n\n  val reducer = ExampleExternalReducer()\n  val result = reducer.startProcessing(\n    state = ExampleState(editField = inputText),\n    event = ExampleEvent.Reload\n  )\n\n  // state\n  assertEquals(\n    ExampleState(\n      editField = inputText,\n      isLoading = true\n    ),\n    result.state\n  )\n  // effects\n  assertTrue(result.effects.isEmpty())\n  // commands\n  assertTrue(result.commands.size == 1)\n  assertEquals(\n    ExampleCommand.StartLoading(text = inputText),\n    result.commands.first()\n  )\n  // cancelled commands\n  assertTrue(result.cancelledCommands.isEmpty())\n  // observer events\n  assertTrue(result.observersEvents.isEmpty())\n}\n```\n\nEach test might be invoke `startProcessing` function in reducer with specific `Event` and `State`.\nAnd then assert `ReducerResult` output that contains five standardized variables.\n\nThat approach guarantee that test will be stable and check all logic.\n\nAdditional approach for stable test is using default values in `State`.\nThis ensures that if you add a new value to `State`, then most of the tests will not require\ncorrection.\n\n## Test GelmInternalReducer\n\nTesting approach for `GelmInternalReducer` is the same as for `GelmExternalReducer`.\n\n## Test GelmActor\n\n`GelmActor` might be stateless component with external dependencies (for example, use cases,\nrepositories and etc).\nAll actor external dependencies must be replaced by fakes.\n\nSo, typical test on `GelmActor` will be looks like:\n\n```kotlin\nimport kotlinx.coroutines.flow.toList\nimport kotlinx.coroutines.test.runTest\n\n@Test\nfun testStartLoading() = runTest {\n  val inputText = \"test\"\n  val sut = ExampleActor()\n\n  val events = mutableListOf\u003cExampleInternalEvent\u003e()\n  sut.execute(ExampleCommand.StartLoading(text = inputText)).toList(events)\n\n  assertEquals(1, events.size)\n  val items = (events[0] as ExampleInternalEvent.LoadedData).list\n  assertTrue(items.isNotEmpty())\n  assertTrue(items.all { it.contains(inputText) })\n}\n```\n\nWe need to invoke `execute()` with specific variant of `Command` (input) and then check all produced\nevents from `Flow` (output).\n\nThus, you need to consistently check all `Commands` and resulting events for a full covered test.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgymbay%2Fgelm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgymbay%2Fgelm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgymbay%2Fgelm/lists"}