Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Important top-level areas:
Core framework module families:

- `scope`, `di-common`
- `presenter`, `presenter-molecule`
- `presenter`, `presenter-compose`
- `renderer`, `renderer-android-view`, `renderer-compose-multiplatform`
- `robot`, `robot-compose-multiplatform`, `robot-internal`
- `kotlin-inject`, `kotlin-inject-extensions`
Expand All @@ -60,7 +60,7 @@ Do not introduce a dependency on an `:impl` module outside these application ass
The framework’s architectural flow is:

1. `Scope` and DI assemble objects for a lifecycle boundary.
2. `MoleculePresenter` implementations produce models.
2. `ComposePresenter` implementations produce models.
3. App-specific `Template` presenters wrap the root model tree.
4. `RendererFactory` resolves platform renderers for those models.
5. Thin platform entrypoints bootstrap the root scope and start rendering.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Changed

- **Breaking change:** Rename the Molecule-specific presenter API to Compose-focused names, including `MoleculePresenter` to `ComposePresenter`, its scope APIs, Gradle DSL options, and `:presenter-molecule:*` artifacts to `:presenter-compose:*`.

### Deprecated

### Removed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ public open class BasePlugin : Plugin<Project> {
"${APP_PLATFORM_GROUP}:presenter-backstack-nav3-testing" to
":presenter-backstack-nav3:testing",
"${APP_PLATFORM_GROUP}:presenter-public" to ":presenter:public",
"${APP_PLATFORM_GROUP}:presenter-molecule-public" to ":presenter-molecule:public",
"${APP_PLATFORM_GROUP}:presenter-molecule-impl" to ":presenter-molecule:impl",
"${APP_PLATFORM_GROUP}:presenter-molecule-testing" to ":presenter-molecule:testing",
"${APP_PLATFORM_GROUP}:presenter-compose-public" to ":presenter-compose:public",
"${APP_PLATFORM_GROUP}:presenter-compose-impl" to ":presenter-compose:impl",
"${APP_PLATFORM_GROUP}:presenter-compose-testing" to ":presenter-compose:testing",
"${APP_PLATFORM_GROUP}:renderer-public" to ":renderer:public",
"${APP_PLATFORM_GROUP}:renderer-android-view-public" to ":renderer-android-view:public",
"${APP_PLATFORM_GROUP}:renderer-compose-multiplatform-public" to
Expand Down
102 changes: 51 additions & 51 deletions docs/presenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
!!! note

While App Platform has a generic `Presenter` interface to remove coupling, we strongly recommend using
`MoleculePresenter` for implementations. `MoleculePresenters` are an opt-in feature through the Gradle DSL.
`ComposePresenter` for implementations. `ComposePresenters` are an opt-in feature through the Gradle DSL.
The default value is `false`.

```groovy
appPlatform {
enableMoleculePresenters true
enableComposePresenters true
}
```

Expand All @@ -18,7 +18,7 @@ App Platform implements the unidirectional dataflow pattern to decouple business
does this allow for better testing of business logic and provides clear boundaries, but individual apps can also
share more code and change the look and feel when needed.

## `MoleculePresenter`
## `ComposePresenter`

In the unidirectional dataflow pattern events and state only travel into one direction through a single stream.
State is produced by `Presenters` and can be observed through a reactive stream:
Expand All @@ -37,11 +37,11 @@ use case of Compose is handling, creating and modifying tree-like data structure
UI frameworks. Molecule reuses Compose to handle state management and state transitions to implement business
logic in the form of `@Composable` functions with all the benefits that Compose provides.

The [MoleculePresenter](https://github.com/vRallev/app-platform/blob/main/presenter-molecule/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/molecule/MoleculePresenter.kt)
The [ComposePresenter](https://github.com/vRallev/app-platform/blob/main/presenter-compose/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/compose/ComposePresenter.kt)
interface looks like this:

```kotlin
fun interface MoleculePresenter<InputT : Any, ModelT : BaseModel> {
fun interface ComposePresenter<InputT : Any, ModelT : BaseModel> {
@Composable
fun present(input: InputT): ModelT
}
Expand All @@ -52,7 +52,7 @@ represent the state of a `Presenter`. Usually, they’re implemented as immutabl
Using sealed hierarchies is a good practice to allow to differentiate between different states:

```kotlin
interface LoginPresenter : MoleculePresenter<Model> {
interface LoginPresenter : ComposePresenter<Model> {
sealed interface Model : BaseModel {
data object LoggedOut : Model

Expand All @@ -78,7 +78,7 @@ Observers of the state of a `Presenter`, such as the UI layer, communicate back
Events are sent through a lambda in the `Model`, which the `Presenter` must provide:

```kotlin hl_lines="16"
interface LoginPresenter : MoleculePresenter<Unit, Model> {
interface LoginPresenter : ComposePresenter<Unit, Model> {

sealed interface Event {
data object Logout : Event
Expand Down Expand Up @@ -123,10 +123,10 @@ class LoginPresenterImpl : LoginPresenter {

!!! note

`MoleculePresenters` are never singletons. They are automatically bound to an API using
`ComposePresenters` are never singletons. They are automatically bound to an API using
`@ContributesBinding`, but they don't use the `@SingleIn` annotation. Metro can instantiate a
contributed presenter with a single constructor without `@Inject`; `kotlin-inject-anvil` users
should still use `@Inject` for constructor injection. `MoleculePresenters` manage their state in
should still use `@Inject` for constructor injection. `ComposePresenters` manage their state in
the `@Composable` function with the Compose runtime. Therefore, it's strongly discouraged to have
any class properties.

Expand Down Expand Up @@ -204,7 +204,7 @@ While the pattern isn’t used frequently, parent presenters can provide input t
returned model from the child presenter can be used further to change the control flow.

```kotlin
interface ChildPresenter : MoleculePresenter<Input, Model> {
interface ChildPresenter : ComposePresenter<Input, Model> {
data class Input(
val argument: String,
)
Expand Down Expand Up @@ -272,7 +272,7 @@ on presenter inputs.

## Launching

`MoleculePresenters` can inject other presenters and call their `present()` function inline. If you are already in a
`ComposePresenters` can inject other presenters and call their `present()` function inline. If you are already in a
composable UI context, then you can simply call the presenter to compute the model:

```kotlin
Expand All @@ -288,12 +288,12 @@ In this example the `LoginPresenter` model is computed from an iOS Compose Multi
In other scenarios a composable context may not be available and it's necessary to turn the `@Composable` functions
into a `StateFlow` for consumption.

[`MoleculeScope`](https://github.com/vRallev/app-platform/blob/main/presenter-molecule/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/molecule/MoleculeScope.kt)
helps to turn a `MoleculePresenter` into a `Presenter`, which then exposes a `StateFlow`:
[`ComposePresenterScope`](https://github.com/vRallev/app-platform/blob/main/presenter-compose/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/compose/ComposePresenterScope.kt)
helps to turn a `ComposePresenter` into a `Presenter`, which then exposes a `StateFlow`:

```kotlin
val stateFlow = moleculeScope
.launchMoleculePresenter(
val stateFlow = composePresenterScope
.launchComposePresenter(
presenter = myPresenter,
input = Unit,
)
Expand All @@ -302,52 +302,52 @@ val stateFlow = moleculeScope

!!! warning

`MoleculeScope` wraps a `CoroutineScope`. The presenter keeps running, recomposing and producing new models
until the `MoleculeScope` is canceled. If the `MoleculeScope` is never canceled, then presenters leak and will
`ComposePresenterScope` wraps a `CoroutineScope`. The presenter keeps running, recomposing and producing new models
until the `ComposePresenterScope` is canceled. If the `ComposePresenterScope` is never canceled, then presenters leak and will
cause issues later.

Use [`MoleculeScopeFactory`](https://github.com/vRallev/app-platform/blob/main/presenter-molecule/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/molecule/MoleculeScopeFactory.kt)
to create a new `MoleculeScope` instance and call `cancel()` when you don't need it anymore.
Use [`ComposePresenterScopeFactory`](https://github.com/vRallev/app-platform/blob/main/presenter-compose/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/compose/ComposePresenterScopeFactory.kt)
to create a new `ComposePresenterScope` instance and call `cancel()` when you don't need it anymore.

On Android an implementation using `ViewModels` may look like this:

```kotlin
class MainActivityViewModel(
moleculeScopeFactory: MoleculeScopeFactory,
composePresenterScopeFactory: ComposePresenterScopeFactory,
myPresenter: MyPresenter,
) : ViewModel() {

private val moleculeScope = moleculeScopeFactory.createMoleculeScope()
private val composePresenterScope = composePresenterScopeFactory.createComposePresenterScope()

// Expose the models for consumption.
val models = moleculeScope
.launchMoleculePresenter(
val models = composePresenterScope
.launchComposePresenter(
presenter = myPresenter,
input = Unit
)
.models

override fun onCleared() {
moleculeScope.cancel()
composePresenterScope.cancel()
}
}
```

!!! info

By default `MoleculeScope` uses the main thread for running presenters and
By default `ComposePresenterScope` uses the main thread for running presenters and
[`RecompositionMode.ContextClock`](https://github.com/cashapp/molecule/blob/trunk/molecule-runtime/src/commonMain/kotlin/app/cash/molecule/RecompositionMode.kt),
meaning a new model is produced only once per UI frame and further changes are conflated.

This behavior can be changed by creating a custom `MoleculeScope`, e.g. tests make use of this:
This behavior can be changed by creating a custom `ComposePresenterScope`, e.g. tests make use of this:

```kotlin
fun TestScope.moleculeScope(
fun TestScope.composePresenterScope(
coroutineContext: CoroutineContext = EmptyCoroutineContext
): MoleculeScope {
val scope = backgroundScope + CoroutineName("TestMoleculeScope") + coroutineContext
): ComposePresenterScope {
val scope = backgroundScope + CoroutineName("TestComposePresenterScope") + coroutineContext

return MoleculeScope(scope, RecompositionMode.Immediate)
return ComposePresenterScope(scope, RecompositionMode.Immediate)
}
```

Expand All @@ -365,7 +365,7 @@ Use `presentDetached()` when a child presenter should run in its own Molecule hi
class ParentPresenter(
private val busyPresenter: BusyPresenter,
private val expensivePresenter: ExpensivePresenter,
) : MoleculePresenter<Unit, ParentPresenter.Model> {
) : ComposePresenter<Unit, ParentPresenter.Model> {

@Composable
override fun present(input: Unit): Model {
Expand Down Expand Up @@ -398,8 +398,8 @@ in that case, child presenter updates are driven by the detached hierarchy's own

## Testing

A [`test()`](https://github.com/vRallev/app-platform/blob/main/presenter-molecule/testing/src/commonMain/kotlin/software/ralf/app/platform/presenter/molecule/TestPresenter.kt)
utility function is provided to make testing `MoleculePresenters` easy using the [Turbine](https://github.com/cashapp/turbine/)
A [`test()`](https://github.com/vRallev/app-platform/blob/main/presenter-compose/testing/src/commonMain/kotlin/software/ralf/app/platform/presenter/compose/TestPresenter.kt)
utility function is provided to make testing `ComposePresenters` easy using the [Turbine](https://github.com/cashapp/turbine/)
library:

```kotlin
Expand Down Expand Up @@ -466,7 +466,7 @@ Platform in the application scope and can be injected:
@Inject
class RootPresenter(
private val backGestureDispatcherPresenter: BackGestureDispatcherPresenter,
) : MoleculePresenter<Unit, Model> {
) : ComposePresenter<Unit, Model> {
@Composable
override fun present(input: Unit): Model {
return withCompositionLocal(
Expand Down Expand Up @@ -732,16 +732,16 @@ is called with their initial state. These presenters only remember their state,

The Compose runtime provides `rememberSaveable { }` and `SaveableStateHolder` as a solution to save and restore small
pieces of UI state. App Platform provides the experimental
[`ReturningSaveableStateHolder`](https://github.com/vRallev/app-platform/blob/main/presenter-molecule/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/molecule/saveable/ReturningSaveableStateHolder.kt)
API for `@Composable` functions that return a value. This matters for `MoleculePresenter` functions, because a presenter
[`ReturningSaveableStateHolder`](https://github.com/vRallev/app-platform/blob/main/presenter-compose/public/src/commonMain/kotlin/software/ralf/app/platform/presenter/compose/saveable/ReturningSaveableStateHolder.kt)
API for `@Composable` functions that return a value. This matters for `ComposePresenter` functions, because a presenter
doesn't render UI directly; it returns a model.

`Presenters` wrapped with `ReturningSaveableStateHolder` can use `rememberSaveable { }` to restore state even after they
weren't part of the hierarchy anymore:

```kotlin
import software.ralf.app.platform.ExperimentalAppPlatform
import software.ralf.app.platform.presenter.molecule.saveable.rememberReturningSaveableStateHolder
import software.ralf.app.platform.presenter.compose.saveable.rememberReturningSaveableStateHolder

@OptIn(ExperimentalAppPlatform::class)
@Composable
Expand Down Expand Up @@ -797,13 +797,13 @@ This pattern can be generalized:

```kotlin
interface NavigationManager {
val currentPresenter: StateFlow<MoleculePresenter<Unit, BaseModel>>
val currentPresenter: StateFlow<ComposePresenter<Unit, BaseModel>>

fun navigateTo(presenter: MoleculePresenter<Unit, BaseModel>)
fun navigateTo(presenter: ComposePresenter<Unit, BaseModel>)
}

@Inject
class NavigationPresenter(val navigationManager: NavigationManager) : MoleculePresenter<Unit, BaseModel> {
class NavigationPresenter(val navigationManager: NavigationManager) : ComposePresenter<Unit, BaseModel> {

@Compose
fun present(input: Unit): BaseModel {
Expand All @@ -822,20 +822,20 @@ The easiest way to import it is the Gradle plugin option:

```groovy
appPlatform {
enableMoleculePresenterBackstack true
enableComposePresenterBackstack true
}
```

This option adds the presenter backstack module and also enables Molecule presenters and Compose UI. The API keeps the
This option adds the presenter backstack module and also enables Compose presenters and Compose UI. The API keeps the
backstack in presenter code, while the renderer integration delegates rendering, back gestures, retained entries, and
transitions to Navigation 3.

The Recipes app wraps the shared API in a small app-specific presenter:

```kotlin
class CrossSlideBackstackPresenter(
private val initialPresenter: MoleculePresenter<Unit, out BaseModel>
) : MoleculePresenter<Unit, CrossSlideBackstackPresenter.Model> {
private val initialPresenter: ComposePresenter<Unit, out BaseModel>
) : ComposePresenter<Unit, CrossSlideBackstackPresenter.Model> {
@Composable
override fun present(input: Unit): Model {
return presenterBackstack(initialPresenter) { backstack ->
Expand Down Expand Up @@ -977,7 +977,7 @@ class YourType

public val LocalYourType: ProvidableCompositionLocal<YourType?> = compositionLocalOf { null }

class ParentPresenter : MoleculePresenter<Unit, Model> {
class ParentPresenter : ComposePresenter<Unit, Model> {
@Composable
override fun present(input: Unit): Model {
val yourType = remember { YourType() }
Expand All @@ -990,7 +990,7 @@ class ParentPresenter : MoleculePresenter<Unit, Model> {
}
}

class ChildPresenter : MoleculePresenter<Unit, Model> {
class ChildPresenter : ComposePresenter<Unit, Model> {
@Composable
override fun present(input: Unit): Model {
val yourType = checkNotNull(LocalYourType.current)
Expand All @@ -1010,7 +1010,7 @@ layer without depending on Compose Foundation's `TextFieldState`.

```kotlin
@OptIn(ExperimentalAppPlatform::class)
class SearchPresenter : MoleculePresenter<Unit, SearchPresenter.Model> {
class SearchPresenter : ComposePresenter<Unit, SearchPresenter.Model> {
@Composable
override fun present(input: Unit): Model {
val query = remember { PresenterTextFieldState() }
Expand Down Expand Up @@ -1070,8 +1070,8 @@ sealed interface SampleAppTemplate : Template {

class SampleAppTemplatePresenter(
private val appBarPresenter: AppBarPresenter,
private val rootPresenter: MoleculePresenter<Unit, BaseModel>,
) : MoleculePresenter<Unit, SampleAppTemplate> {
private val rootPresenter: ComposePresenter<Unit, BaseModel>,
) : ComposePresenter<Unit, SampleAppTemplate> {
@Composable
fun present(input: Unit): SampleAppTemplate {
val contentModel = rootPresenter.present(Unit)
Expand All @@ -1092,7 +1092,7 @@ specific [`AppBarConfigModel`](https://github.com/vRallev/app-platform/blob/main
interface, which provides the configuration for the app bar. Implementing this interface is optional:

```kotlin
class MenuPresenter : MoleculePresenter<Unit, Model> {
class MenuPresenter : ComposePresenter<Unit, Model> {
@Composable
override fun present(input: Unit): Model {
...
Expand Down Expand Up @@ -1354,7 +1354,7 @@ The root `Presenter` responsible for the `Presenter` backstack computes the `Mod
@Composable
override fun present(input: Unit): Model {
val backstack = remember {
mutableStateListOf<MoleculePresenter<Unit, out BaseModel>>().apply {
mutableStateListOf<ComposePresenter<Unit, out BaseModel>>().apply {
// There must be always one element.
add(SwiftUiChildPresenter(index = 0, backstack = this))
}
Expand Down
Loading
Loading