cycle

Kotlin Multi-platform Presentation Layer Design Pattern Library

https://github.com/chrynan/cycle

Science Score: 44.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
    Found CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
    Found .zenodo.json file
  • DOI references
  • Academic publication links
  • Committers with academic emails
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (12.8%) to scientific vocabulary

Keywords

architecture architecture-components design-patterns kotlin kotlin-coroutines kotlin-multi-platform kotlin-multiplatform kotlin-multiplatform-library mvi mvi-android mvi-architecture mvi-kotlin mvvm mvvm-android mvvm-architecture presentation ui viewmodel viewmodels

Keywords from Contributors

dispatchers kotlin-coroutine-dispatchers kotlin-coroutines-library kotlin-library
Last synced: 6 months ago · JSON representation ·

Repository

Kotlin Multi-platform Presentation Layer Design Pattern Library

Basic Info
  • Host: GitHub
  • Owner: chRyNaN
  • License: apache-2.0
  • Language: Kotlin
  • Default Branch: master
  • Homepage:
  • Size: 1.16 MB
Statistics
  • Stars: 8
  • Watchers: 1
  • Forks: 0
  • Open Issues: 0
  • Releases: 20
Topics
architecture architecture-components design-patterns kotlin kotlin-coroutines kotlin-multi-platform kotlin-multiplatform kotlin-multiplatform-library mvi mvi-android mvi-architecture mvi-kotlin mvvm mvvm-android mvvm-architecture presentation ui viewmodel viewmodels
Created over 5 years ago · Last pushed 6 months ago
Metadata Files
Readme Funding License Code of conduct Citation Codeowners

README.md

presentation

cycle

A Kotlin multi-platform presentation layer design pattern. This is a cyclic (hence the name) uni-directional data flow (UDF) design pattern library that is closely related to the MVI (Model-View-Intent) pattern on Android. It utilizes kotlinx.coroutines Flows and is easily compatible with modern UI Frameworks, such as Jetpack Compose.

Perform > Reduce > Compose

This design pattern breaks down complex application logic into three simple parts: Perform the actions, Reduce the changes, and Compose the view from the state. This simple approach to application design is easy to reason about, implement, debug, and test, and very flexible to adapt to any application's specific needs.

GitHub tag (latest by date)

```kotlin fun counterReducer(state: Int?, change: CounterChange): Int { val value = state ?: 0

return when (change) {
    CounterChange.INCREMENT -> value + 1
    CounterChange.DECREMENT -> value - 1
}

}

@Composable fun Counter() { val viewModel = remember { ViewModel.create(reducer = ::counterReducer) }

val state by viewModel.stateChanges()

Text("Count = $state")

LaunchedEffect(Unit) {
    viewModel.dispatch(CounterChange.INCREMENT) // 1
    viewModel.dispatch(CounterChange.INCREMENT) // 2
    viewModel.dispatch(CounterChange.DECREMENT) // 1
}

} ```

Getting Started 🏁

The library is provided through Repsy.io. Checkout the releases page to get the latest version.

GitHub tag (latest by date)

Repository

kotlin repositories { maven { url = uri("https://repo.repsy.io/mvn/chrynan/public") } }

Dependencies

core

kotlin implementation("com.chrynan.cycle:cycle-core:$VERSION")

compose

kotlin implementation("com.chrynan.cycle:cycle-compose:$VERSION")

Usage 👨‍💻

State Management (Perform and Reduce)

The first two parts of a cycle are Perform and Reduce which, together, invoke application logic that produces changes, which then get reduced to create a new state. This process, which is illustrated below, can ultimately be considered as state management since it involves the creation, alteration, and storage of state.

Redux Counter Example

The following is an example of using a StateStore component from this library to implement the same example shown in the Redux Javascript Library's Documentation, but in Kotlin.

```kotlin enum class CounterChange {

INCREMENT,
DECREMENT

}

fun counterReducer(state: Int?, change: CounterChange): Int { val value = state ?: 0

return when (change) {
    CounterChange.INCREMENT -> value + 1
    CounterChange.DECREMENT -> value - 1
}

}

fun testCounter(coroutineScope: CoroutineScope) { val store = MutableStateStore(reducer = ::counterReducer)

store.subscribe(coroutineScope = coroutineScope) { state ->
    println(state)
}

coroutineScope.launch {
    store.dispatch(CounterChange.INCREMENT) // 1
    store.dispatch(CounterChange.INCREMENT) // 2
    store.dispatch(CounterChange.DECREMENT) // 1
}

} ```

The above example is a good simple demonstration, but it isn't very useful for more complex, "real-world" applications. While the fundamentals are the same, applications often require a more complex flow of logic. Coordinating the flow of logic efficiently between different application components is the responsibility of a design pattern.

There are many application level design patterns (MVC, MVP, MVVM, MVI, to name a few), but this library focuses on MVVM and MVI design patterns, since those are easily reactive (using Kotlin Coroutine Flows) and easily supportive of the UDF (uni-directional data flow) design principal. There is a ViewModel component provided by this library which can encapsulate component specific functionality. The above example can be updated to utilize a ViewModel and perform more complex actions at the call-site:

```kotlin fun testCounter() { val viewModel = ViewModel.create(reducer = ::counterReducer).apply { bind() }

viewModel.subscribe { state ->
    println(state)
}

viewModel.dispatch(CounterChange.INCREMENT) // 1
viewModel.dispatch(CounterChange.INCREMENT) // 2
viewModel.dispatch(CounterChange.DECREMENT) // 1

// The provided action will be invoked and must return a Flow of changes
// 2
viewModel.perform {
    flow {
        emit(CounterChange.INCREMENT)

        if ((viewModel.currentState ?: 0) > 2) {
            emit(CounterChange.DECREMENT)
        }
    }
}

viewModel.unbind()

} ```

The above example illustrates the usage of the ViewModel.perform function, which takes an Action value as a parameter. An Action is simply a typealias for a suspending function that takes the current State as a parameter and returns a Flow of Changes. This function is typically not invoked at the call-site, as in the example above, but instead invoked by ViewModel implementing classes. This forces the logic to be well-defined, encapsulated within a single component, and easily testable. The above example re-written to use a custom ViewModel might look like the following:

```kotlin class CounterViewModel : ViewModel( stateStore = MutableStateStore(reducer = ::counterReducer) ) {

fun increment() = dispatch(CounterChange.INCREMENT)

fun decrement() = dispatch(CounterChange.DECREMENT)

fun incrementIfLessThanTwo() = perform {
    flow {
        emit(CounterChange.INCREMENT)

        if ((currentState ?: 0) > 2) {
            emit(CounterChange.DECREMENT)
        }
    }
}

}

fun testCounter() { val viewModel = CounterViewModel().apply { bind() }

viewModel.subscribe { state ->
    println(state)
}

// Note: The dispatch function is no longer public, so we can't access it here.
viewModel.increment() // 1
viewModel.increment() // 2
viewModel.decrement() // 1

// Note: The perform function is no longer public, so we can't access it here.
viewModel.incrementIfLessThanTwo() // 2

viewModel.unbind()

} ```

Another common design pattern is MVI (Model-View-Intent). With this design pattern, an Intent model is emitted on the ViewModel's reactive stream, which triggers an associated Action, resulting in a Flow of Changes being emitted and reduced to produce new States. This is similar to the above example, but instead of having separate functions on the ViewModel for each action, we will have a single intent(to:) function on the ViewModel that takes an Intent model and performs the appropriate action based on that value. This approach can easily be implemented with this library by extending the IntentViewModel class:

```kotlin enum class CounterIntent {

INCREMENT,
DECREMENT,
INCREMENT_IF_LESS_THAN_TWO

}

enum class CounterChange {

INCREMENTED,
DECREMENTED,
NO_CHANGE

}

fun counterReducer(state: Int?, change: CounterChange): Int { val value = state ?: 0

return when (change) {
    CounterChange.INCREMENTED -> value + 1
    CounterChange.DECREMENTED -> value - 1
    CounterChange.NO_CHANGE -> value
}

}

class CounterViewModel : IntentViewModel( stateStore = MutableStateStore(reducer = ::counterReducer) ) {

override fun performIntentAction(state: Int?, intent: CounterIntent): Flow<CounterChange> = flow {
    val change = when (intent) {
        CounterIntent.INCREMENT -> CounterChange.INCREMENTED
        CounterIntent.INCREMENT_IF_LESS_THAN_TWO -> CounterChange.NO_CHANGE
        CounterIntent.DECREMENT -> CounterChange.DECREMENTED
    }

    emit(change)
}

}

fun testCounter() { val viewModel = CounterViewModel().apply { bind() }

viewModel.subscribe { state ->
    println(state)
}

// Note: The dispatch function is no longer public, so we can't access it here.
viewModel.intent(to = CounterIntent.INCREMENT) // 1
viewModel.intent(to = CounterIntent.INCREMENT) // 2
viewModel.intent(to = CounterIntent.DECREMENT) // 1

// Note: The perform function is no longer public, so we can't access it here.
viewModel.intent(to = CounterIntent.INCREMENT_IF_LESS_THAN_TWO)

viewModel.unbind()

} ```

UI Management (Compose)

The third and final part of a cycle is Compose which is responsible for listening to new states and updating a UI view accordingly. This part's implementation is dependent on the UI framework used, but can easily be adapted to fit most modern UI frameworks.

The easiest way to subscribe to state changes to update the UI, is to use the subscribe function:

kotlin viewModel.subscribe { state -> // Update the UI or trigger a UI refresh here using the new state. }

Note: That a ViewModel has a lifecycle which is defined by the invocation of its bind/unbind functions. Therefore, the ViewModel.bind function must be called before the ViewModel.subscribe function is invoked, otherwise no states will be emitted to the subscribe function closure.

Alternatively, you can use the cycle-compose dependency when targeting Jetpack Compose for a simple integration. Use the stateChanges() to convert the Flow of State changes to a Jetpack Compose State. This approach also handles binding and unbinding of the ViewModel for you.

```kotlin @Composable fun Home(viewModel: HomeViewModel) { val state by viewModel.stateChanges()

// Use the state to construct the UI.

} ```

In the example above, the stateChanges function binds the ViewModel to the lifecycle of the composable function and listens to changes in the State. The type is converted from a Flow of States to a Jetpack Compose State, so when a state change occurs, it triggers recomposition of the composable function.

View

The View interface represents a UI component that contains a ViewModel and properly binds its lifecycle to that of the UI component. This interface can be used to encapsulate lifecycle and logic within the framework defined UI component implementation.

Documentation 📃

More detailed documentation is available in the docs folder. The entry point to the documentation can be found here.

Security 🛡️

For security vulnerabilities, concerns, or issues, please responsibly disclose the information either by opening a public GitHub Issue or reaching out to the project owner.

Contributing ✍️

Outside contributions are welcome for this project. Please follow the code of conduct and coding conventions when contributing. If contributing code, please add thorough documents. and tests. Thank you!

Sponsorship ❤️

Support this project by becoming a sponsor of my work! And make sure to give the repository a ⭐

License ⚖️

``` Copyright 2021 chRyNaN

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. ```

Owner

  • Name: Christopher
  • Login: chRyNaN
  • Kind: user
  • Location: Austin, TX
  • Company: Starry

Citation (CITATION.cff)

cff-version: 1.2.0
message: "If you use this software, please cite it as below."
authors:
    - family-names: "Keenan"
      given-names: "Christopher"
      alias: chRyNaN
      website: "https://chrynan.codes"
contact:
    - family-names: "Keenan"
      given-names: "Christopher"
      alias: chRyNaN
      website: "https://chrynan.codes"
title: "presentation"
type: "software"
abstract: "Kotlin Multi-platform Presentation Layer Design Pattern Library"
license: Apache-2.0
keywords:
    - kotlin
    - ui
    - presentation
    - "design-patterns"
    - "design-pattern"
    - mvi
    - mvvm
    - viewmodel
    - viewmodels
    - "kotlin-library"
    - "kotlin-multiplatform"
    - architecture
    - "kotlin-multiplatform-library"
    - "kotlin-multiplatform-mobile"
    - "mvi-kotlin"
repository-code: "https://github.com/chRyNaN/presentation"
url: "https://github.com/chRyNaN/presentation"

GitHub Events

Total
  • Push event: 16
Last Year
  • Push event: 16

Committers

Last synced: 7 months ago

All Time
  • Total Commits: 191
  • Total Committers: 3
  • Avg Commits per committer: 63.667
  • Development Distribution Score (DDS): 0.136
Past Year
  • Commits: 0
  • Committers: 0
  • Avg Commits per committer: 0.0
  • Development Distribution Score (DDS): 0.0
Top Committers
Name Email Commits
Christopher Keenan c****n@s****m 165
Chris Keenan c****n@p****e 15
Chris c****n@p****m 11
Committer Domains (Top 20 + Academic)

Issues and Pull Requests

Last synced: 7 months ago


Dependencies

presentation-compose/build.gradle.kts maven
  • androidx.appcompat:appcompat 1.5.0 implementation
  • androidx.core:core-ktx 1.8.0 implementation
  • androidx.fragment:fragment-ktx 1.5.2 implementation
  • androidx.lifecycle:lifecycle-viewmodel-compose 2.5.1 implementation
presentation-core/build.gradle.kts maven
  • androidx.lifecycle:lifecycle-viewmodel-ktx 2.5.1 api
  • com.chrynan.dispatchers:dispatchers 0.4.0 api
  • com.chrynan.mapper:mapper-core 1.7.0 api
  • org.jetbrains.kotlinx:kotlinx-coroutines-core 1.6.4 api
  • androidx.activity:activity-ktx 1.5.1 implementation
  • androidx.appcompat:appcompat 1.5.0 implementation
  • androidx.core:core-ktx 1.8.0 implementation
  • androidx.fragment:fragment-ktx 1.5.2 implementation
  • org.jetbrains.kotlin:kotlin-stdlib-common * implementation
  • org.jetbrains.kotlinx:kotlinx-coroutines-android 1.6.4 implementation
kotlin-js-store/yarn.lock npm
  • 279 dependencies