Skip to content

Commit

Permalink
Updated README file
Browse files Browse the repository at this point in the history
  • Loading branch information
chRyNaN committed Apr 11, 2023
1 parent 953f09d commit 8226f02
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 158 deletions.
287 changes: 129 additions & 158 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,231 +1,202 @@
![presentation](assets/branding_image.png)

# presentation

Kotlin multi-platform presentation layer design pattern. This is a 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. <br/>
and is easily compatible with modern UI Frameworks, such as Jetpack Compose.

### Perform > Reduce > Compose

<img alt="GitHub tag (latest by date)" src="https://img.shields.io/github/v/tag/chRyNaN/presentation">

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

when (state) {
is HomeState.Loading -> {
CircularProgressIndicator()
}
is HomeState.Loaded -> {
Text("Hello ${state.name}")
}
}
}
```

class HomeLayout : Layout<HomeIntent, HomeState, HomeChange> {
## Getting Started 🏁

override val viewModel = ViewModel(
perform = { intent, state -> ... },
reduce = { state, change -> ... })
The library is provided through [Repsy.io](https://repsy.io/). Checkout the
[releases page](https://github.com/chRyNaN/presentation/releases) to get the latest version. <br/><br/>
<img alt="GitHub tag (latest by date)" src="https://img.shields.io/github/v/tag/chRyNaN/presentation">

@Composable
override fun Content() {
val state by stateChanges()
### Repository

when (state) {
is HomeState.Loading -> {
CircularProgressIndicator()
}
is HomeState.Loaded -> {
Text("Hello ${state.name}")
}
}
```kotlin
repositories {
maven {
url = uri("https://repo.repsy.io/mvn/chrynan/public")
}

override fun onBind() = intent(to = HomeIntent.Load)
}
```

## Usage
### Dependencies

The library introduces components that interact with each other using a uni-directional, cyclic data flow. Roughly, the
flow of data looks like the following:
#### core

```
Intent Change State
View --------> Action --------> Reducer -------> View
```kotlin
implementation("com.chrynan.presentation:presentation-core:$VERSION")
```

A quick breakdown of some components:

* **View** - The View component renders the State to the UI and emits Intents after events, such as User actions.
* **Intent** - The Intent component is simply an indicator of what action the View intends to perform.
* **Action** - The Action component handles the application logic for a particular Intent and outputs a Flow of Changes.
It is responsible for connecting the business logic (UseCases, Repositories, etc) to this design pattern.
* **Change** - The Change component is simply an indicator of what change to the State must be performed.
* **Reducer** - The Reducer component takes the current State and a Change and deduces a new State.
* **State** - The State component contains all the necessary data to completely render the UI.
* **ViewModel** - The ViewModel component is the connecting piece that coordinates the flow of data between each of the
other components.

The communication channel between the `View` and `ViewModel` looks like the following:
#### compose

```
States
┌─────────────◄─────────────┐
│ ▲
│ │
┌──▼───┐ ┌─────┴─────┐
│ View │ │ ViewModel │
└──┬───┘ └─────▲─────┘
│ │
▼ │
└─────────────►─────────────┘
Intent
```kotlin
implementation("com.chrynan.presentation:presentation-compose:$VERSION")
```

### Create the State and models
#### processor

Typically, the `Intent`, `State`, and `Change` are sealed classes per screen and defined in the same file. These
components are platform independent, meaning they can live in shared code.
```kotlin
ksp("com.chrynan.presentation:presentation-ksp:$VERSION")
```

For example, consider the following items for a home screen that displays a list of items.
**Note:** This dependency requires you setup the [KSP plugin](https://kotlinlang.org/docs/ksp-quickstart.html).

```kotlin
sealed class HomeIntent : Intent {

object LoadInitial : HomeIntent()
plugins {
id("com.google.devtools.ksp") version "1.8.10-1.0.9"
}
```

data class LoadMore(val currentItems: List<Item>) : HomeIntent()
**Note:** It may be required
to [add the generated sources](https://kotlinlang.org/docs/ksp-quickstart.html#make-ide-aware-of-generated-code) of the
KSP processor for the IDE to recognize the generated code and resources. The following is an example of adding the
generated sources for a Kotlin JVM project:

data class Refresh(val currentItems: List<Item>) : HomeIntent()
```kotlin
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
resources.srcDir("build/generated/ksp/main/resources/")
}
}
```

sealed class HomeChange : Change {

data class Loaded(val items: List<Item>) : HomeChange()
## Usage 👨‍💻

data class StartedLoading(val currentItems: List<Item>) : HomeChange()
### State Definition

data class StartedRefreshing(val currentItems: List<Item>) : HomeChange()
}
Define your state models for a component with a sealed class or interface that inherits from the `State` interface:

```kotlin
sealed class HomeState : State {

object LoadingInitial : HomeState()
abstract val items: List<Item>

data class LoadingMore(val currentItems: List<Item>) : HomeState()
data class Loading(override val items: List<ListItem> = emptyList()) : HomeState()

data class Refreshing(val currentItems: List<Item>) : HomeState()
data class Refreshing(override val items: List<ListItem> = emptyList()) : HomeState()

object DisplayingEmpty : HomeState()
data class Success(override val items: List<Item>) : HomeState()

data class DisplayingLoaded(val items: List<Item>) : HomeState()
data class Error(
override val items: List<ListItem> = emptyList(),
val message: String
)
}
```

### Create the ViewModel
### Perform

The `ViewModel` is a platform independent component that coordinates the flow of data between the other components.
All application logic related to a `State` is encapsulated in separate functions for each responsibility that are
annotated with `@Perform`. These functions can be member functions of a class or interface (such as an `Action`), or
they can be globally scoped functions. The functions can optionally have an extension receiver of the state model which
represents the current state. The return type of these functions can be any value except for `Unit` and `Nothing`; you
can even return a `Flow` of items!

```kotlin
class HomeViewModel @Inject constructor(
...
) : ViewModel<HomeIntent, HomeState, HomeChange>() {

override fun onBind() {
super.onBind()

this.intents
.perform { intent, state ->
when (intent) {
is HomeIntent.LoadInitial -> loadInitialAction(intent)
is HomeIntent.Refresh -> refreshAction(intent)
is HomeIntent.LoadMore -> loadMoreAction(intent)
}
}
.reduce { state, change ->
// deduce new State
}
.render()
.launchIn(this)
}
}
```
@Perform(HomeState::class)
fun HomeState.loadMoreItems(count: UInt = 25): Flow<HomeChange> =
flow {
emit(HomeChange.StartedLoading)

### Create the View
val items = ItemRepo().get(after = this.items.lastOrNull()?.id, count = count)

The `View` implementation is a platform specific class. For instance, the `View` implementation on Android might be
a `Fragment` or this library's `Layout` class if using Jetpack Compose.

#### Jetpack Compose

Let's consider a Jetpack Compose implementation of the `View` interface. In this scenario, we can extend from
the `Layout` class to simplify the implementation.

```kotlin
class HomeLayout : Layout<HomeIntent, HomeState, HomeChange> {

override val viewModel: ViewModel<HomeIntent, HomeState, HomeChange> = ... // Get the ViewModel

@Composable
override fun Content() {
val state by stateChanges()

// Render the UI based on the state that is available
// Emit intents using the intent(to) function
emit(HomeChange.FinishedLoading(items = items))
}
}
```

Then we can include this `Layout` implementation in any `@Composable` function with the `composeLayout` function:
### Reduce

```kotlin
@Composable
fun App() {
ComposeLayout(HomeLayout())
}
```

For convenience, we can also use the `unaryPlus` function which delegates to the `composeLayout` function:
For every `@Perform` annotated function return type, there must be a corresponding `@Reduce` annotated function.
Functions annotated with `@Reduce` are responsible for creating new `States` from the previous `State` and the value
returned from a `@Perform` annotated function.

```kotlin
@Composable
fun App() {
+HomeLayout()
}
@Reduce(HomeState::class)
fun HomeState.reduce(change: HomeChange): HomeState =
when (change) {
is HomeChange.StartedLoading -> HomeState.Loading(items = this.items)
is HomeChange.FinishedLoading -> HomeState.Success(items = this.items)
}
```

## Navigation

This library no longer provides a way to handle the navigation for an application. The navigation components have been
moved to their own [library]("https://github.com/chRyNaN/navigation").

## Building the library

The library is provided through [Repsy.io](https://repsy.io/). Checkout
the [releases page](https://github.com/chRyNaN/presentation/releases) to get the latest version. <br/>
<img alt="GitHub tag (latest by date)" src="https://img.shields.io/github/v/tag/chRyNaN/presentation">
### Compose

### Repository
The `@Perform` and `@Reduce` annotated functions associated with a `State` cause the auto-generation of a `ViewModel`
that can be instantiated and whose state changes can be observed within a Jetpack Compose composable function:

```groovy
repositories {
maven {
url = uri("https://repo.repsy.io/mvn/chrynan/public")
```kotlin
@Compose
fun Home(viewModel: HomeViewModel = HomeViewModel()) {
val state = viewModel.stateChanges()
val lazyListState = rememberLazyListState()

LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState
) {
items(
items = state.items,
key = { it.value.id }
) { item ->
ItemLayout(item)
}
}

LaunchLazyLoader(
state = lazyListState,
onLoadMore = {
viewModel.loadMoreItems()
}
)
}
```

### Dependencies
## Documentation 📃

#### core
More detailed documentation is available in the [docs](docs/) folder. The entry point to the documentation can be
found [here](docs/index.md).

```groovy
implementation("com.chrynan.presentation:presentation-core:VERSION")
```
## Security 🛡️

#### compose
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.

```groovy
implementation("com.chrynan.presentation:presentation-compose:VERSION")
```
## Contributing ✍️

## Documentation
Outside contributions are welcome for this project. Please follow the [code of conduct](CODE_OF_CONDUCT.md)
and [coding conventions](CODING_CONVENTIONS.md) when contributing. If contributing code, please add thorough documents.
and tests. Thank you!

More detailed documentation is available in the [docs](docs/) folder. The entry point to the documentation can be
found [here](docs/index.md).
## Sponsorship ❤️

Support this project by [becoming a sponsor](https://www.buymeacoffee.com/chrynan) of my work! And make sure to give the
repository a ⭐

## License
## License ⚖️

```
Copyright 2021 chRyNaN
Expand Down
Binary file added assets/branding_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8226f02

Please sign in to comment.