Skip to content

Commit

Permalink
Added quick start docs
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Dec 29, 2022
1 parent 4568e67 commit a77411b
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 2 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

---

Decompose is a Kotlin Multiplatform library for breaking down your code into lifecycle-aware business logic components (aka BLoC), with routing functionality and pluggable UI (Jetpack Compose, Android Views, SwiftUI, JS React, etc.).
Decompose is a Kotlin Multiplatform library for breaking down your code into tree-structured lifecycle-aware business logic components (aka BLoC), with routing functionality and pluggable UI (Jetpack Compose, Android Views, SwiftUI, JS React, etc.).

Please see the [project website](https://arkivanov.github.io/Decompose/) for documentation and APIs.

Expand Down Expand Up @@ -90,9 +90,13 @@ Here are some key concepts of the library, more details can be found in the docu

<img src="docs/media/ComponentStructure.png" width="512">

## Quick start

Please refer to the [Quick start](https://arkivanov.github.io/Decompose/getting-started/quick-start/) section of the docs.

## Samples

Check out the [project website](https://arkivanov.github.io/Decompose/samples/) for a full description of each sample.
Check out the [Samples](https://arkivanov.github.io/Decompose/samples/) section of the docs for a full description of each sample.

## Articles

Expand Down
2 changes: 2 additions & 0 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Installation

Decompose provides a number of modules, they are all published to Maven Central Repository.

## The main Decompose module
Expand Down
253 changes: 253 additions & 0 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Quick start

## A simplest component

```kotlin
class RootComponent
```

Yes, a simples component is just a normal class. No need to extend a class from the library, or implement an interface.

## Extracting an interface

```kotlin
interface RootComponent

class DefaultRootComponent : RootComponent
```

It's often useful to extract an interface for a component. It makes it possible to create test doubles for integration tests (e.g. testing navigation in a container component), or fake implementations for UI previews (e.g. for Jetpack Compose or SwiftUI).

## ComponentContext

[ComponentContext](/Decompose/component/overview/#componentcontext) is probably the most important concept of Decompose. It is an interface that provides access to various tools, like lifecycle, state preservation, instance retaining (aka Android ViewModel), back button handling, etc. Each component has its own `ComponentContext` provided by Decompose.

If your component requires `ComponentContext`, just pass it via constructor. You can also use the delegation pattern to add `ComponentContext` to `this` scope.

```kotlin
class DefaultRootComponent(
componentContext: ComponentContext,
) : RootComponent, ComponentContext by componentContext {

init {
lifecycle... // Access the Lifecycle
stateKeeper... // Access the StateKeeper
intanceKeeper... // Access the InstanceKeeper
backHandler... // Access the BackHandler
}
}
```

## Observable state and callbacks

There are multiple ways of exposing an observable state from a component.

### Using Value from Decompose

Decompose provides an observable state holder - `Value`. It offers great integration with various UI frameworks, such as Jetpack Compose, SwiftUI, React, etc. You can also convert Reaktive `Observable` or coroutines `Flow` to `Value`, if needed.

```kotlin
interface ListComponent {
val model: Value<Model>

fun onItemClicked(item: String)

data class Model(
val items: List<String>,
)
}

class DefaultListComponent(
componentContext: ComponentContext,
private val onItemSelected: (item: String) -> Unit,
) : ListComponent {
override val model: Value<ListComponent.Model> =
MutableValue(Model(items = List(100) { "Item $it" }))

override fun onItemClicked(item: String) {
onItemSelected(item)
}
}
```

Observing `Value` in Jetpack Compose is easy, just use `subscribeAsState` extension function.

```kotlin
@Composable
fun ListContent(component: ListComponent, modifier: Modifier = Modifier) {
val model by component.model.subscribeAsState()

LazyColumn {
items(items = model.items) { item ->
Text(
text = item,
modifier = Modifier.clickable {
component.onItemClicked(item = item)
},
)
}
}
}
```

Please refer to the [docs](/Decompose/component/overview/) for information about other platforms and UI frameworks.

### Using Reaktive or coroutines

The state can be also exposed using Reaktive `Observable` or coroutines `Flow`, or any other reactive library. Follow best practices recommended for the reactive library of your choice.

## Navigation

Decompose provides various ways to navigate, you can find more information in the [docs](/Decompose/navigation/overview/). The most common navigation pattern is [Child Stack](/Decompose/navigation/stack/overview/).

### Component configurations

Child component configurations is another important concepts of Decompose. It allows supplying type safe arguments, as well as any kind of dependencies to child components.

Each child component is represented by a persistent configuration class. A configuration class denotes which child component should be instantiated, and holds persistent arguments required for instantiation. A configuration class must be defined for every child component.


### Using the Child Stack

```kotlin
interface RootComponent {

val stack: Value<ChildStack<*, Child>>

// Defines all possible child components
sealed class Child {
class ListChild(val component: ListComponent) : Child()
class DetailsChild(val component: DetailsComponent) : Child()
}
}

class DefaultRootComponent(
componentContext: ComponentContext,
) : RootComponent, ComponentContext by componentContext {

private val navigation = StackNavigation<Config>()

private val _stack =
childStack(
source = navigation,
initialConfiguration = Config.List, // The initial child component is List
handleBackButton = true, // Automatically pop from the stack on back button presses
childFactory = ::child,
)

override val stack: Value<ChildStack<*, RootComponent.Child>> = _stack

private fun child(config: Config, componentContext: ComponentContext): RootComponent.Child =
when (config) {
is Config.List -> ListChild(listComponent(componentContext))
is Config.Details -> DetailsChild(detailsComponent(componentContext, config))
}

private fun listComponent(componentContext: ComponentContext): ListComponent =
DefaultListComponent(
componentContext = componentContext,
onItemSelected = { item: String -> // Supply dependencies and callbacks
navigation.push(Config.Details(item = item)) // Push the details component
},
)

private fun detailsComponent(componentContext: ComponentContext, config: Config.Details): DetailsComponent =
DefaultDetailsComponent(
componentContext = componentContext,
item = config.item, // Supply arguments from the configuration
onFinished = navigation::pop, // Pop the details component
)

@Parcelize // The `kotlin-parcelize` plugin must be applied if you are targeting Android
private sealed interface Config : Parcelable {
object List : Config
data class Details(val item: String) : Config
}
}
```

### Child Stack with Jetpack Compose

```kotlin
@Composable
fun RootContent(component: RootComponent, modifier: Modifier = Modifier) {
Children(
stack = component.stack,
modifier = modifier,
animation = stackAnimation(fade() + scale()),
) {
when (val child = it.instance) {
is ListChild -> ListContent(component = child.component)
is DetailsChild -> DetailsContent(component = child.component)
}
}
}
```

Please refer to [samples](/Decompose/samples/) for integrations with other UI frameworks.

## Initialising a root component

### Android with Jetpack Compose

Use `defaultComponentContext` extension function to create the root `ComponentContext` in an `Activity`.

```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Always create the root component outside of Compose
val root =
DefaultRootComponent(
componentContext = defaultComponentContext(),
)

setContent {
MaterialTheme {
Surface {
RootContent(component = root, modifier = Modifier.fillMaxSize())
}
}
}
}
}
```

### Desktop with Jetpack Compose

Use `LifecycleController` to bind the root lifecycle with the main window state.

```kotlin
fun main() {
val lifecycle = LifecycleRegistry()

// Always create the root component outside of Compose
val root =
DefaultRootComponent(
componentContext = DefaultComponentContext(lifecycle = lifecycle),
)

application {
val windowState = rememberWindowState()

LifecycleController(lifecycle, windowState)

Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "My Application"
) {
MaterialTheme {
Surface {
RootContent(component = root, modifier = Modifier.fillMaxSize())
}
}
}
}
}
```

### Other platforms and UI frameworks

Please refer to [samples](/Decompose/samples/) for integrations with other platforms and UI frameworks.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ nav:
- Getting started:
- Overview: index.md
- Installation: getting-started/installation.md
- Quick start: getting-started/quick-start.md
- License: getting-started/license.md
- Contributing: getting-started/contributing.md

Expand Down

0 comments on commit a77411b

Please sign in to comment.