Skip to content

Commit

Permalink
Merge branch 'master' into v2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Jun 30, 2023
2 parents 47abf16 + b63a3ba commit 474637a
Show file tree
Hide file tree
Showing 26 changed files with 873 additions and 55 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# These are supported funding model platforms

github: arkivanov
custom: ["https://www.buymeacoffee.com/arkivanov", "https://btc.com/1DXjn9e6rmbVvac3TH8hG3LdLtoA1CUvsM", "https://etherscan.io/address/0xf027f5738f45676a54c15cf7753a0f66553947b9"]
14 changes: 14 additions & 0 deletions docs/component/back-button.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ class SomeComponent(
}
```

### Callback order

By default, registered callbacks are checked in reverse order, the last registered enabled callback is called first. Various navigation models may also register back button callbacks, e.g. `Child Stack` uses `BackHandler` to automatically pop the stack on back button press. If you want your callback to be called first, make sure to register it as later as possible. Similarly, if you want your callback to be called last, make sure to register it as early as possible.

Since Essenty version `1.2.0-alpha`, it is also possible to specify a priority for your back callback.

```kotlin
// This will make sure your callback is always called first
private val backCallback = BackCallback(priority = Int.MAX_VALUE) { ... }

// This will make sure your callback is always called last
private val backCallback = BackCallback(priority = Int.MIN_VALUE) { ... }
```

## Predictive Back Gesture

Decompose experimentally supports the new [Android Predictive Back Gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture), not only on Android. The UI part is covered by Compose extensions, please see the [related docs](../../extensions/compose#predictive-back-gesture).
12 changes: 12 additions & 0 deletions docs/component/custom-component-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ class DefaultAppComponentContext(
}
```

## Custom child ComponentContext

The default [ComponentContext#childContext](../child-components/#adding-a-child-component-manually) extension function returns the default `ComponentContext`. In order to create custom child `ComponentContext`, a special extension function is required.

```kotlin
fun AppComponentContext.childAppContext(key: String, lifecycle: Lifecycle? = null): AppComponentContext =
DefaultAppComponentContext(
componentContext = childContext(key = key, lifecycle = lifecycle),
// Supply additional dependencies here
)
```

## Navigation with custom ComponentContext

- [Using Child Stack](../navigation/stack/component-context.md)
Expand Down
19 changes: 10 additions & 9 deletions docs/component/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Using `Value` is not mandatory, you can use any other state holders, e.g. [State
If you are using Jetpack/JetBrains Compose, `Value` can be observed in Composable functions using one of the Compose [extension modules](/Decompose/extensions/compose/).

!!!warning
`Value` is not thread-safe, it should be accessed only from the main thread.
Even though both `Value` and `MutableValue` are thread-safe, it's recommended to subscribe and update it only on the main thread.

### Why not StateFlow?

Expand All @@ -169,7 +169,7 @@ class Counter {
val state: Value<State> = _state

fun increment() {
_state.reduce { it.copy(count = it.count + 1) }
_state.update { it.copy(count = it.count + 1) }
}

data class State(val count: Int = 0)
Expand Down Expand Up @@ -198,26 +198,27 @@ fun CounterUi(counter: Counter) {
```swift
struct CounterView: View {
private let counter: Counter
@ObservedObject
private var state: ObservableValue<CounterState>

@StateValue
private var state: CounterState

init(_ counter: Counter) {
self.counter = counter
self.state = ObservableValue(counter.state)
_state = StateValue(counter.state)
}

var body: some View {
VStack(spacing: 8) {
Text(self.state.value.text)
Button(action: self.counter.increment, label: { Text("Increment") })
Text(state.value.text)
Button(action: counter.increment, label: { Text("Increment") })
}
}
}
```

#### What is ObservableValue?
#### What is StateValue

[ObservableValue](https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/ObservableValue.swift) is a wrapper around `Value` that makes it compatible with SwiftUI. It is a simple class that conforms to `ObservableObject` protocol. Unfortunately it [does not look possible](https://github.com/arkivanov/Decompose/issues/206) to publish utils for SwiftUI as a library or framework, so it has to be copied to your project.
[StateValue](https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/StateValue.swift) is a property wrapper for `Value` that makes it observable in SwiftUI. Unfortunately it [does not look possible](https://github.com/arkivanov/Decompose/issues/206) to publish utils for SwiftUI as a library or framework, so it has to be copied in your project.

#### More Swift utilities

Expand Down
8 changes: 8 additions & 0 deletions docs/extensions/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ Extensions for JetBrains Compose are provided by the `extensions-compose-jetbrai
implementation("com.arkivanov.decompose:extensions-compose-jetbrains:<version>")
```

#### ProGuard rules for Compose for Desktop (JVM)

If you support Compose for Desktop, you will need to add the following rule for ProGuard, so that the app works correctly in release mode. See [Minification & obfuscation](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials/Native_distributions_and_local_execution#minification--obfuscation) section in Compose docs for more information.

```
-keep class com.arkivanov.decompose.extensions.compose.jetbrains.mainthread.SwingMainThreadChecker
```

## Content

As mentioned above both modules provide similar functionality. Most of the links in this document refer to the Jetpack module, however there usually a mirror in the JetBrains module.
Expand Down
3 changes: 3 additions & 0 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ The main functionality is provided by the `decompose` module. It contains some c

Some functionality is actually provided by [Essenty](https://github.com/arkivanov/Essenty) library. Essenty is implemented by the same author and provides very basic things like `Lifecycle`, `StateKeeper`, etc. Most important Essenty modules are added to the `decompose` module as `api` dependency, so you don't have to add them manually to your project. Please familiarise yourself with Essenty library.

!!! note
If you are targetting Android, make sure you applied the [kotlin-parcelize](https://developer.android.com/kotlin/parcelize) Gradle plugin.

## Extensions for Jetpack/JetBrains Compose

The Compose UI is currently published in two separate variants:
Expand Down
78 changes: 60 additions & 18 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ class DefaultListComponent(
}
```

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

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

```kotlin
@Composable
Expand All @@ -90,6 +92,34 @@ fun ListContent(component: ListComponent, modifier: Modifier = Modifier) {
}
```

### Observing Value in SwiftUI

```swift
struct DetailsView: View {
private let list: ListComponent

@StateValue
private var model: ListComponentModel

init(_ list: ListComponent) {
self.list = list
_model = StateValue(list.model)
}

var body: some View {
List(model.items, ...) { item in
// Display the item
}
}
}
```

#### What is StateValue

[StateValue](https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/StateValue.swift) is a property wrapper for `Value` that makes it observable in SwiftUI. Unfortunately it [does not look possible](https://github.com/arkivanov/Decompose/issues/206) to publish utils for SwiftUI as a library or framework, so it has to be copied in your project.

### Observing Value in other UI Frameworks

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

### Using Reaktive or coroutines
Expand All @@ -114,6 +144,9 @@ interface RootComponent {

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

// It's possible to pop multiple screens at a time on iOS
fun onBackClicked(toIndex: Int)

// Defines all possible child components
sealed class Child {
class ListChild(val component: ListComponent) : Child()
Expand All @@ -127,16 +160,14 @@ class DefaultRootComponent(

private val navigation = StackNavigation<Config>()

private val _stack =
override val stack: Value<ChildStack<*, RootComponent.Child>> =
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))
Expand All @@ -157,6 +188,10 @@ class DefaultRootComponent(
item = config.item, // Supply arguments from the configuration
onFinished = navigation::pop, // Pop the details component
)

override fun onBackClicked(toIndex: Int) {
navigation.popTo(index = toIndex)
}

@Parcelize // The `kotlin-parcelize` plugin must be applied if you are targeting Android
private sealed interface Config : Parcelable {
Expand Down Expand Up @@ -190,29 +225,36 @@ fun RootContent(component: RootComponent, modifier: Modifier = Modifier) {
struct RootView: View {
private let root: RootComponent

@ObservedObject
private var childStack: ObservableValue<ChildStack<AnyObject, RootComponentChild>>

private var activeChild: RootComponentChild { childStack.value.active.instance }

init(_ root: RootComponent) {
self.root = root
childStack = ObservableValue(root.childStack)
}

var body: some View {
switch activeChild {
case let child as RootComponentChild.ListChild: ListView(child.component)
case let child as RootComponentChild.DetailsChild: DetailsView(child.component)
default: EmptyView()
}
StackView(
stackValue: StateValue(root.stack),
getTitle: {
switch $0 {
case is RootComponentChild.ListChild: return "List"
case is RootComponentChild.DetailsChild: return "Details"
default: return ""
}
},
onBack: root.onBackClicked,
childContent: {
switch $0 {
case let child as RootComponentChild.ListChild: ListView(child.component)
case let child as RootComponentChild.DetailsChild: DetailsView(child.component)
default: EmptyView()
}
}
)
}
}
```

#### What is ObservableValue?
#### What is StackView?

[ObservableValue](https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/ObservableValue.swift) is a wrapper around `Value` that makes it compatible with SwiftUI. It is a simple class that conforms to `ObservableObject` protocol. Unfortunately it [does not look possible](https://github.com/arkivanov/Decompose/issues/206) to publish utils for SwiftUI as a library or framework, so it has to be copied to your project.
[StackView](https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/StackView.swift) is a view that displays `Child Stack` using the native SwiftUI navigation and providing the native UX. For the same reason, it has to be copied in your project.

### Child Stack with other UI Frameworks

Expand All @@ -222,7 +264,7 @@ Please refer to [samples](/Decompose/samples/) for integrations with other UI fr

### Android with Jetpack Compose

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

```kotlin
class MainActivity : AppCompatActivity() {
Expand Down
Binary file added docs/media/SampleCardsAndroid.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 8 additions & 5 deletions docs/samples.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ Content:

* [shared](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared) - this is a shared module that contains the following components:
* [Root](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/root) - the root (top-most) component, it displays the bottom navigation bar and the currently selected tab.
* [Counters](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters) - the Counters tab, contains a stack of `Counter` component.
* [Counter](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter) - the `Counter` component, it just increments the counter every 250 ms. It starts counting once created and stops when destroyed. So `Counter` continues counting while in the back stack, unless recreated. It uses the `InstanceKeeper`, so counting continues after Android configuration changes. The `StateKeeper` is used to preserve the state when the process is recreated on Android.
* [MultiPane](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane) - the Multi-Pane tab, it displays `List` and `Details` components either in a stack (one on top of another) or side by side. **Please note that this sample is for advanced single-pane/multi-pane navigation and layout, for generic master-detail navigation please refer to the Sample Todo List App described below.**
* [ArticleList](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/list) - displays a random list of articles. Clicking on an item triggers the `ArticleDetails` component.
* [ArticleDetails](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/details) - displays the content of the selected article.
* [Counters](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters) - the Counters tab, contains a stack of `CounterComponent`.
* [Counter](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/counters/counter) - contains `CounterComponent`, it just increments the counter every 250 ms. It starts counting once created and stops when destroyed. So `CounterComponent` continues counting while in the back stack, unless recreated. It uses the `InstanceKeeper`, so counting continues after Android configuration changes. The `StateKeeper` is used to preserve the state when the process is recreated on Android.
* [Cards](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards) - the (draggable) Cards tab, contains a stack of [Card] components that can be dragged and thrown to the back of the stack. The top component is resumed and running, and components in the back stack are stopped. This sample demonstrates how the navigation can be controlled by gestures.
* [Card](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/cards/card) - contains `CardComponent` - a draggable card with some text information.
* [MultiPane](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane) - the Multi-Pane tab, it displays `ArticleListComponent` and `ArticleDetailsComponent` components either in a stack (one on top of another) or side by side. **Please note that this sample is for advanced single-pane/multi-pane navigation and layout, for generic master-detail navigation please refer to the Sample Todo List App described below.**
* [ArticleListComponent](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/list) - displays a random list of articles. Clicking on an item triggers the `ArticleDetails` component.
* [ArticleDetailsComponent](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/multipane/details) - displays the content of the selected article.
* [DynamicFeatures](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures) - the Dynamic Features tab, it demonstrates the usage of [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery) on Android, while using classing integration on other platforms. There are two simple feature components - `Feature1` and `Feature2` - they are located in separate modules described below.
* [DynamicFeature](https://github.com/arkivanov/Decompose/tree/master/sample/shared/shared/src/commonMain/kotlin/com/arkivanov/sample/shared/dynamicfeatures/dynamicfeature) - a helper component responsible for loading dynamic feature components.
* [compose](https://github.com/arkivanov/Decompose/tree/master/sample/shared/compose) - this module contains Jetpack Compose UI.
Expand Down Expand Up @@ -49,6 +51,7 @@ Content:
### Counters Screenshots

<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/SampleCountersAndroid.gif" width="196">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/SampleCardsAndroid.gif" width="196">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/SampleCountersIos.png" width="196">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/SampleCountersDesktop.png" width="294">
<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/SampleCountersWeb.png" width="294">
Expand Down
1 change: 1 addition & 0 deletions sample/app-ios/app-ios/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class PreviewRootComponent : RootComponent {
simpleChildStack(RootComponentChild.CountersChild(component: PreviewCountersComponent()))

func onCountersTabClicked() {}
func onCardsTabClicked() {}
func onMultiPaneTabClicked() {}
func onDynamicFeaturesTabClicked() {}
func onCustomNavigationTabClicked() {}
Expand Down
Loading

0 comments on commit 474637a

Please sign in to comment.