Slack Circuitの`@CircuitInject`周りのドキュメントとコードを読むメモ

Hookの仕組みや、Jetpack Composeを使うと、宣言的にもっといい感じにアプリ全体をよくできるはずです。
[molecule](https://github.com/cashapp/molecule)でも同じようなことができるのですが、実装によっては画面が裏に行っても、ViewModelが生きている間は生き続けてしまうという問題があります。
そこで [Retaining beyond ViewModels](https://chrisbanes.me/posts/retaining-beyond-viewmodels/) の記事があり、Circuitを使うと、Viewと同じライフサイクルでEventを受け取って値を返す仕組みを実現でき、かつ、ViewModelと同じスコープで値を持つことができるということで、Circuitを見てみようと思いました。

https://slackhq.github.io/circuit/

# Overview

UDF(Unidirectional Data Flow)とかの原則に従いて、`Cash App’s Broadway architecture` に影響されていることなどが書いてあり、以下があります。


> Circuit’s core components are its Presenter and Ui interfaces.

CircuitのコアコンポーネントはPresenterとUi interface。

> 1. A Presenter and a Ui cannot directly access each other. They can only communicate through state and event emissions.

PresenterとUiは互いに直接やり取りしない。stateとeventの排出によってのみやり取りできる。 
(このあたりがピンとこない人は https://qiita.com/takahirom/items/99cfa342a00ba9a99ef8#unidirectional-data-flowudf%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E7%AE%A1%E7%90%86%E3%82%92%E3%81%99%E3%82%8B このあたりを読んでも良いかも )

> `2.` UIs are compose-first.

UIはCompose

> `3.` Presenters are also compose-first. They do not emit Compose UI, but they do use the Compose runtime to manage and emit state.

PresenterもComposeを利用する。ComposeのUIを排出はしないが、Compose runtimeを使ってstateを排出する。

> `4.` Both Presenter and Ui each have a single composable function.

PresenterとUiはそれぞれ一つのComposable関数を持つ。

> `5.` In most cases, Circuit automatically connects presenters and UIs.

ほとんどのケースでCircuitは自動的にpresenterとUIをつなげる。

> `6.` Presenter and Ui are both generic types, with generics to define the UiState types they communicate with.

PresenterとUIは両方とも通信するUiStateの型でGeneric typeになっている。

> `7.` They are keyed by Screens. One runs a new Presenter/Ui pairing by requesting them with a given Screen that they understand.

PresenterとUiは画面によって紐づけられる。画面でPresenterとUiが要求されることによって新しいPresenterとUiが動く


いい感じですね。

https://slackhq.github.io/circuit/ より引用

# Counterの例

https://slackhq.github.io/circuit/

この例がなんともむずいんですよね。。 :sweat_smile: 

## CounterScreen

```kotlin
@Parcelize
data object CounterScreen : Screen {
  data class CounterState(
    val count: Int,
    val eventSink: (CounterEvent) -> Unit,
  ) : CircuitUiState
  sealed interface CounterEvent : CircuitUiEvent {
    data object Increment : CounterEvent
    data object Decrement : CounterEvent
  }
}
```

これは通信の型を決めるものっぽいですね。UIからPresenterのイベントがCounterEvent。PresenterからUIへ渡すStateがCounterStateですね。

## CounterPresenter

```kotlin
@CircuitInject(CounterScreen::class, AppScope::class)
@Composable
fun CounterPresenter(): CounterState {
  var count by rememberSaveable { mutableStateOf(0) }

  return CounterState(count) { event ->
    when (event) {
      CounterEvent.Increment -> count++
      CounterEvent.Decrement -> count--
    }
  }
}
```

## Counter UI

```kotlin
@CircuitInject(CounterScreen::class, AppScope::class)
@Composable
fun Counter(state: CounterState) {
  Box(Modifier.fillMaxSize()) {
    Column(Modifier.align(Alignment.Center)) {
      Text(
        modifier = Modifier.align(CenterHorizontally),
        text = "Count: ${state.count}",
        style = MaterialTheme.typography.displayLarge
      )
      Spacer(modifier = Modifier.height(16.dp))
      Button(
        modifier = Modifier.align(CenterHorizontally),
        onClick = { state.eventSink(CounterEvent.Increment) }
      ) { Icon(rememberVectorPainter(Icons.Filled.Add), "Increment") }
      Button(
        modifier = Modifier.align(CenterHorizontally),
        onClick = { state.eventSink(CounterEvent.Decrement) }
      ) { Icon(rememberVectorPainter(Icons.Filled.Remove), "Decrement") }
    }
  }
}
```

### `CircuitInject` とは

あー 上のカウンターの例で、`CircuitInject` でてきましたね。。あー難しいですね。なんだこれってなりますね。

ここに説明があるみたいです。
https://slackhq.github.io/circuit/code-gen/

まずPresenterからですが、下の上のCircuitInjectがContributesMultibindingに置き換わるっぽいですね。これは[anvil](https://github.com/square/anvil)のもので、Dagger Hiltも選べるみたいですね。

```kotlin
// 生成元のコード
@CircuitInject(HomeScreen::class, AppScope::class)
@Composable
fun HomePresenter(): HomeState { ... }

// 生成後のコード
@ContributesMultibinding(AppScope::class)
class HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }
```

https://slackhq.github.io/circuit/code-gen/ より

えっと、HomeScreenの情報生成後のコードから消えたけど大丈夫？ってなりますね。 ちょっとわからなすぎるので、コード読みにいきましょう。
こういうのはテストを見るとわかりやすいです。ちゃんとHomeScreenは生成後のコードにいますね。


```kotlin
  @Test
  fun simplePresenterFunction_withObjectScreen() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun HomePresenter(): HomeScreen.State {

          }
        """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomePresenterFactory.kt",
      expectedContent =
        """
          package test

          import com.slack.circuit.runtime.CircuitContext
          import com.slack.circuit.runtime.Navigator
          import com.slack.circuit.runtime.presenter.Presenter
          import com.slack.circuit.runtime.presenter.presenterOf
          import com.slack.circuit.runtime.screen.Screen
          import com.squareup.anvil.annotations.ContributesMultibinding
          import javax.inject.Inject

          @ContributesMultibinding(AppScope::class)
          public class HomePresenterFactory @Inject constructor() : Presenter.Factory {
            override fun create(
              screen: Screen,
              navigator: Navigator,
              context: CircuitContext,
            ): Presenter<*>? = when (screen) {
              HomeScreen -> presenterOf { HomePresenter() }
              else -> null
            }
          }
        """
          .trimIndent(),
    )
  }
```

https://github.com/slackhq/circuit/blob/33b5e286248d08b9e3f43cc8df358654caf6abd6/circuit-codegen/src/test/kotlin/com/slack/circuit/codegen/CircuitSymbolProcessorTest.kt#L499 より

この中身のwhen文は後で見ていきますが、
でも次はContributesMultibindingが分からんってなりますね。

```kotlin
          @ContributesMultibinding(AppScope::class)
          public class HomePresenterFactory @Inject constructor() : Presenter.Factory
```

生成されたコードより

で、ContributesMultibindingを使うと https://github.com/square/anvil?tab=readme-ov-file#contributed-multibindings によると、
さらに以下のようなコードを生成します。

```kotlin
@Binds @IntoSet
abstract fun bindHomePresenterFactory(listener: HomePresenterFactory): Presenter.Factory
```

これにはIntoSetが使われていて、つまり、**HomePresenterFactoryを作ってSetにいれます。**

ってことなので、ContributesMultibindingにより、AppScopeにHomePresenterFactoryを紐づけているということは分かると思います。どう使われるんでしょうか。

以下のように使われているようです。

```kotlin
@ContributesTo(AppScope::class)
@Module
interface CircuitModule {
  @Multibinds fun presenterFactories(): Set<Presenter.Factory>

  @Multibinds fun viewFactories(): Set<Ui.Factory>

  companion object {
    @Provides
    fun provideCircuit(
      presenterFactories: @JvmSuppressWildcards Set<Presenter.Factory>,
      uiFactories: @JvmSuppressWildcards Set<Ui.Factory>,
    ): Circuit {
      return Circuit.Builder()
        .addPresenterFactories(presenterFactories)
        .addUiFactories(uiFactories)
        .build()
    }
  }
}
```

https://github.com/slackhq/circuit/blob/3187f88c03920ceecfc367fcd69906da77853d0e/samples/star/src/jvmCommonMain/kotlin/com/slack/circuit/star/di/CircuitModule.kt#L15 より

先程のIntoSetで入れたインスタンスを配布する`@Multibinds`がいて、それを受け取って更にCircuitのインスタンスをProvideするprovideCircuit()がいます。

### `CircuitInject` がDaggerの配布関連で何をしているのかをまとめると？
`@CircuitInject(HomeScreen::class, AppScope::class)` をつけると、 `@ContributesMultibinding(AppScope::class)` が作られて(Circuit genのkspにより)、これが、`@Binds @IntoSet` に変換される(anvilのkspにより)ことで、配布時にSetにインスタンスをいれるようになります。そして、そのインスタンスを `@Multibinds` によリ配布(Daggerのkspにより)して、Circuitの中で使われるFactoryとして機能するわけです。 

### 配布周り、難しいんだけど一言でいうと？？

以下から

```kotlin
          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun HomePresenter(): HomeScreen.State {

          }
```
    

以下のインスタンスを作って、配布する。

```kotlin
          @ContributesMultibinding(AppScope::class)
          public class HomePresenterFactory @Inject constructor() : Presenter.Factory {
            override fun create(
              screen: Screen,
              navigator: Navigator,
              context: CircuitContext,
            ): Presenter<*>? = when (screen) {
              HomeScreen -> presenterOf { HomePresenter() }
              else -> null
            }
          }
```



### PresenterFactory


配布周りがなんとなくわかったところで、この中身のwhen文を見ていくんですが、まあスクリーンを受け取って、それに対応するPresenterを返す。ですね。なければnullを返すと。


```kotlin
            override fun create(
              screen: Screen,
              navigator: Navigator,
              context: CircuitContext,
            ): Presenter<*>? = when (screen) {
              HomeScreen -> presenterOf { HomePresenter() }
              else -> null
            }
```

で次はこれがどうやって利用されるのか。が気になってくるわけです。


# MainActivityからPresenterFactory.create()までの流れを追う

以下のサンプルを見ていきます。このサンプルではDaggerによって作られるところを自分で作っているようです。これのcreate()はOrderTacosPresenter()を作っているところになります。create()までの流れを追いましょう。
https://github.com/slackhq/circuit/blob/72350bf9334b243d11450fb179c3250c35a19469/samples/tacos/src/main/kotlin/com/slack/circuit/tacos/MainActivity.kt#L22

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

    val circuit: Circuit =
      Circuit.Builder()
        .addPresenterFactory(buildPresenterFactory())
        .addUiFactory(buildUiFactory())
        .build()

    setContent {
      TacoTheme { CircuitCompositionLocals(circuit) { CircuitContent(OrderTacosScreen) } }
    }
  }
}

private fun buildPresenterFactory(): Presenter.Factory =
  Presenter.Factory { _, _, _ ->
    OrderTacosPresenter(
      fillingsProducer = FillingsProducerImpl(IngredientsRepositoryImpl),
      toppingsProducer = ToppingsProducerImpl(IngredientsRepositoryImpl),
      confirmationProducer = { details, _ -> confirmationProducer(details) },
      summaryProducer = { _, sink -> summaryProducer(sink) },
    )
  }

private fun buildUiFactory(): Ui.Factory =
  Ui.Factory { _, _ ->
    ui<OrderTacosScreen.State> { state, modifier -> OrderTacosUi(state, modifier) }
  }
```

## MainActivity: CircuitContentを呼び出す

呼び出してます。


```kotlin
    setContent {
      TacoTheme { CircuitCompositionLocals(circuit) { CircuitContent(OrderTacosScreen) } }
    }
```


## MainActivity: CircuitContentを呼び出す

呼び出してます。ただ、 **CircuitCompositionLocalsにcircuitを渡していることで、PresenterFactoryとかをComposable関数内で使える余地を残しています。** 
また **OrderTacosScreenを渡していることもポイントです**

```kotlin
    val circuit: Circuit =
      Circuit.Builder()
        .addPresenterFactory(buildPresenterFactory())
        .addUiFactory(buildUiFactory())
        .build()

    setContent {
      TacoTheme { CircuitCompositionLocals(circuit) { CircuitContent(OrderTacosScreen) } }
    }
```

# CircuitContent: Presenter.createの呼び出し

```kotlin
@Composable
internal fun CircuitContent(
  screen: Screen,
  modifier: Modifier,
  navigator: Navigator,
  circuit: Circuit,
  unavailableContent: (@Composable (screen: Screen, modifier: Modifier) -> Unit),
  context: CircuitContext,
  key: Any? = screen,
) {
  val eventListener = rememberEventListener(screen, context, factory = circuit.eventListenerFactory)
  DisposableEffect(eventListener, screen, context) { onDispose { eventListener.dispose() } }

  // ↓ ここで Presenterの取得
  val presenter = rememberPresenter(screen, navigator, context, eventListener, circuit::presenter)

  // ↓ ここで UIの取得
  val ui = rememberUi(screen, context, eventListener, circuit::ui)
...
}

```

https://github.com/slackhq/circuit/blob/44713592c494301e0a0b2fbf533687000325cfed/circuit-foundation/src/commonMain/kotlin/com/slack/circuit/foundation/CircuitContent.kt#L96 より

で、nextPresenterを呼び出して、

```kotlin
  @OptIn(InternalCircuitApi::class)
  public fun presenter(
    screen: Screen,
    navigator: Navigator,
    context: CircuitContext = CircuitContext(null).also { it.circuit = this },
  ): Presenter<*>? {
    return nextPresenter(null, screen, navigator, context)
  }
  ```

  **一個ずつ、渡されたpresenterFactoriesのcreateを呼んでみて、nullじゃなければ返します！**


```kotlin
  public fun nextPresenter(
    skipPast: Presenter.Factory?,
    screen: Screen,
    navigator: Navigator,
    context: CircuitContext,
  ): Presenter<*>? {
...
    for (i in start until presenterFactories.size) {
      val presenter = presenterFactories[i].create(screen, navigator, context)
      if (presenter != null) {
        return presenter
      }
    }

    return null
  }
```

そういえば以下で思ったのですが、CashApp/Moleculeとの大きな違いとして、Circuitを使うと同じCompositionの中にPresenterがいることになりますね。これはかなり大きな違いになると思います。 (Chrisさんの記事でも触れられていましたが。)


```kotlin
  // ↓ ここで Presenterの取得
  val presenter = rememberPresenter(screen, navigator, context, eventListener, circuit::presenter)

  // ↓ ここで UIの取得
  val ui = rememberUi(screen, context, eventListener, circuit::ui)
...
```

# メモまとめ

* CircuitInject自体はPresenterやUIをScreenに紐づけて、Screenをキーにして、取り出せるようにするためのもの。であんまり怖くない。
* CircuitのPresenterはComposeのライフサイクルで動く。