Skip to content

Commit

Permalink
Test ComposableTarget on Presenters (#1366)
Browse files Browse the repository at this point in the history
This helps prevent use of compose UI in presenter logic as it will emit
a compile-time warning.


![image](https://github.com/slackhq/circuit/assets/1361086/bd823536-6f32-4e3c-8cd4-96aaacea54e6)
  • Loading branch information
ZacSweers authored Apr 25, 2024
1 parent 2ee1da3 commit 6d70c62
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 4 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ Changelog
--------------

- **New**: Add WASM targets.
- **Behaviour Change**: `NavigatorImpl.goTo` no longer navigates if the `Screen` is equal to `Navigator.peek()`
- **Change**: `Navigator.goTo` now returns a Bool indicating navigation success
- **Behaviour Change**: `NavigatorImpl.goTo` no longer navigates if the `Screen` is equal to `Navigator.peek()`.
- **Behaviour Change**: `Presenter.present` is now annotated with `@ComposableTarget("presenter")`. This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do. Note this does not appear in the IDE, so it's recommended to use `allWarningsAsErrors` to fail the build on this event.
- **Change**: `Navigator.goTo` now returns a Bool indicating navigation success.
- Mark `Presenter.Factory` as `@Stable`.
- Mark `Ui.Factory` as `@Stable`.
- Mark `CircuitContext` as `@Stable`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.slack.circuit.runtime.presenter

import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposableTarget
import androidx.compose.runtime.Stable
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.CircuitUiState
Expand Down Expand Up @@ -93,8 +94,44 @@ public interface Presenter<UiState : CircuitUiState> {
*
* Note that Circuit's test artifact has a `Presenter.test()` helper extension function for the
* above case.
*
* ```
* @Test
* fun `emit initial state and refresh`() = runTest {
* val favorites = listOf("Moose", "Reeses", "Lola")
* val repository = FakeFavoritesRepository(favorites)
* val presenter = FavoritesPresenter(repository)
*
* presenter.test {
* assertThat(awaitItem()).isEqualTo(State.Loading)
* val successState = awaitItem()
* // ...
* }
* }
* ```
*
* ## No Compose UI
*
* Presenter logic should _not_ emit any Compose UI. They are purely for presentation business
* logic. To help enforce this, [present] is annotated with
* [@ComposableTarget("presenter")][ComposableTarget]. This helps prevent use of Compose UI in the
* presentation logic as the compiler will emit a warning if you do.
*
* This warning does not appear in the IDE, so it's recommended to use `allWarningsAsErrors` in
* your build configuration to fail the build on this event.
*
* ```kotlin
* // In build.gradle.kts
* kotlin.compilerOptions.allWarningsAsErrors.set(true)
* ```
*/
@Composable public fun present(): UiState
@Composable
// Prevent compose UI from running in presenters, these only produce state
// The name here is a little funny, but intended to help make the warning printed a little easier
// to understand.
// "Calling a presenter composable function where a UI Composable composable was expected"
@ComposableTarget("presenter")
public fun present(): UiState

/**
* A factory that produces [presenters][Presenter] for a given [Screen]. `Circuit` instances use
Expand Down
15 changes: 14 additions & 1 deletion docs/presenter.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ The core Presenter interface is this:

```kotlin
interface Presenter<UiState : CircuitUiState> {
@Composable fun present(): UiState
@ComposableTarget("presenter")
@Composable
fun present(): UiState
}
```

Expand Down Expand Up @@ -67,6 +69,17 @@ fun ProfilePresenter(

Presenters can present other presenters by injecting their assisted factories/providers, but note that this makes them a composite presenter that is now assuming responsibility for managing state of multiple nested presenters.

## No Compose UI

Presenter logic should _not_ emit any Compose UI. They are purely for presentation business logic. To help enforce this, `Presenter.present` is annotated with `@ComposableTarget("presenter")`. This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do.

!!! tip
This warning does not appear in the IDE, so it's recommended to use `allWarningsAsErrors` in your build configuration to fail the build on this event.
```kotlin
// In build.gradle.kts
kotlin.compilerOptions.allWarningsAsErrors.set(true)
```

## Retention

There are three types of composable retention functions used in Circuit.
Expand Down

0 comments on commit 6d70c62

Please sign in to comment.