[compose multiplatform 中級チュートリアル 5]compose multiplatformのアーキテクチャを整える

# 前回
[前回](https://qiita.com/shunk0616/private/a61be6e397534ca393e8)はバックエンドの検討でした。

# はじめに
前回の章でsupabaseを使うことは決まったので、環境を作っていきます。

# supabase側の準備
## プロジェクト作成
1. supabaseのダッシュボードに行きます。（やってなかったら会員登録してください。）
2. NewProjectを選択
3. プロジェクトネームとDatabasePasswordを適当な値で入力
4. RegionはNortheast Asiaを選択
5. Create new Projectを押す

## テストデータを追加
1. Databaseをクリック
![](https://storage.googleapis.com/zenn-user-upload/3c7ca4752d10-20231114.png)
2. Create a new tableをクリック
![](https://storage.googleapis.com/zenn-user-upload/785d187718c1-20231114.png)
3. Nameはtasks、descriptionは適当な値を入力
![](https://storage.googleapis.com/zenn-user-upload/1357c2d6afdf-20231114.png)
4. RLSのチェックボックスを外し、Realtimeをenableで選択（セキュリティガバガバなのでもし公開するとかならちゃんとRLSを有効化しておいてください。)
5. 以下の画像のように、created_at,title,estimated-time,categoryのcolumnを作成してsaveする。(categoryのデフォルト値を”バックログ”にするのを忘れないように)
![](https://storage.googleapis.com/zenn-user-upload/f440b716a45b-20231118.png)
6. Insert→Insert rowをクリック
![](https://storage.googleapis.com/zenn-user-upload/7b8520e316f0-20231114.png)
7. 以下のように「タスク1」と「タスク2」を追加、estimated-timeは両方2とか適当な数値にし、他の情報は入れなくて良いです。
![](https://storage.googleapis.com/zenn-user-upload/a6effd83a4e8-20231114.png)

# アプリをsupabaseと接続
## ライブラリを導入
これ意外とめんどくさいんですが、commonMainに大元のライブラリを追加して、その後、各プラットフォーム向けにKtor-clientを入れてあげないといけないみたいですね。

自分のチュートリアルやってきた人はktorは入っているので大丈夫かな

以下のように今回は、postgrest-ktとrelatime-ktを入れます。
ただしどうせ今後実装することがわかりきっているのでauthentication用のgotrueも入れましょう。

```
commonMain.dependencies {
    implementation(compose.runtime)
    implementation(compose.material3)
    implementation(compose.materialIconsExtended)
    implementation(libs.libres)
    implementation(libs.voyager.navigator)
    implementation(libs.composeImageLoader)
    implementation(libs.napier)
    implementation(libs.kotlinx.coroutines.core)
    implementation(libs.ktor.core)
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.multiplatformSettings)
    // 以下を追加
    implementation("io.github.jan-tennert.supabase:postgrest-kt:1.4.7")
    implementation("io.github.jan-tennert.supabase:realtime-kt:1.4.7")
    implemantation("io.github.jan-tennert.supabase:gotrue-kt:1.4.7")
}
```
sync nowボタンを押してbuild successと出れば完了です。

# フォルダ構造の変更
ここまででやってきてアレなんですが、アプリの構造をちゃんと設計していなかったので、簡単に作り直します。

目的は、今後のコードの拡張性と保守性を一定レベルに保ちたいからです。

参考にしたのは、supabaseの[チャットデモアプリ](https://github.com/supabase-community/supabase-kt/tree/master/demos/chat-demo-mpp)です。

このアプリでは以下の画像のような状態になっていました。
![](https://storage.googleapis.com/zenn-user-upload/1517dc491068-20231115.png)

本当は[clean architecture](https://zenn.dev/shun1997/articles/understand-clean-architecture)とかにした方が良いとは思うのですが、あくまでチュートリアルなので簡単バージョンにします。

今回のアプリは現状、Commonフォルダと、それぞれの画面のフォルダが作られており、それぞれの画面のフォルダの中にScreenファイルとScreenModelファイルが入っているという構造でした。
つまり以下のような構成です。
![](https://storage.googleapis.com/zenn-user-upload/267f2b0380a1-20231118.png)
それぞれ画面ごとでScreenとScreenModelが同じフォルダに入っていた方が自分は好き（UIに関係するところはまとめておきたい）ということで
基本の構造を崩さないようにして、ロジックを処理する部分を付け足していきましょう。
### 基本構造を作る
まずは、今までのUI周りの処理をuiフォルダにまとめます。
App.ktが入っている階層にuiフォルダを作成し、Common.composablesと各画面のフォルダを移動します。

次に、同じ階層にdiとnetフォルダを作成します。

この画像みたいになっていたらOK
![](https://storage.googleapis.com/zenn-user-upload/b4f02031e56b-20231118.png)

### netフォルダの中身を作る
netフォルダは今は空なので、TaskApi.ktを作成します。
中身は何も行わなくて良いのですが、データ型は決まっているのでそれだけ書いときましょうか。

```
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.postgrest.postgrest
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Task(
    @SerialName("id")
    val id: Int?,
    @SerialName("created_at")
    val createdAt: String?,
    @SerialName("title")
    val title: String,
    @SerialName("estimated-time")
    val estimatedTime: Int,
    @SerialName("category")
    val category: String
)

sealed interface TaskApi {
    // :todo CRUD処理用のインターフェースを定義する
}

internal class TaskApiImpl(
    private val client: SupabaseClient
) : TaskApi {
    // :todo CRUD処理を実装する
}
```
中身はこの後書くのですが、何を書くかだけは、コメントアウトで書いておきました。

### 作成したデータ型を使うように変更する
元々はui/List配下に置いてあったデータ型を使っていましたが、今回はnetフォルダに移動したので、それに合わせて変更します。
CommonComposable.ktのimportを以下のように変更します。

```Kotlin
// これを削除
import your.applications.name.ui.List.Task
// これを追加
import your.applications.name.net.Task
```
同じことをListItem.ktとTaskForm.ktにも行います。
もしやり方がよくわからなかったらui.List.Taskのimportを消した後で、Taskのところにカーソルを合わせて、option+enterを押してみてください。
おそらく自動でimportを追加してくれるはずです。

### diフォルダの中身を作る
diフォルダはdependency injectionのためのフォルダです。
細かいことは省略しますが、役割を分割させたファイルたちをつなぐために必要な魔法のファイルくらいに思っておけばとりあえず大丈夫。
細かいやつはあとで記事にします。

基本は先ほどのデモアプリを参考にして作りましょう。

と思ったのですが、ここで大変なミスに気づきました。
dependency injectionのためのライブラリを入れていない、、、
このチュートリアルの一番最初に、wizardからアプリを作ったと思うのですが、ここでKoinを指定していませんでした。
アホすぎた、、、

## koinを導入する
ということで、Koinを導入します。

gradle/libs.versions.tomlに移動して
19行目あたりにkoinのバージョンの情報を追加します。

```
~~~
multiplatformSettings = "1.1.0"
# 以下を追加
koin = "3.5.0"
~~~
```

そして同じファイルの39行目あたりに具体的なモジュール名を指定します。

```
~~~
multiplatformSettings = { module = "com.russhwolf:multiplatform-settings:$multiplatformSettings", version.ref = "multiplatformSettins" }

# 以下を追加
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
```

最後に、build.gradle.ktsに移動して、commonMainのdependenciesのところに以下を追加します。

```
implementation(libs.koin.core)
```

最後にsyncNowボタンを押せば完了です。

### 改めてdiフォルダの中身を実装する
diフォルダに、koin.kt, newModule.kt, supabaseModule.kt, viewModelModule.ktを作成します。
それぞれのファイルの中身はこんな感じです。実際の処理を書くのは後にしますが、まずは基本的なところを埋めていきます。
細かい説明はコメントアウトで書いておきました。


***koin.kt***
```Kotlin
// Koin DIライブラリから必要なクラスをインポート
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin

// Koin DIコンテナの初期化を行う関数
fun initKoin(additionalConfiguration: KoinApplication.() -> Unit = {}) {
    // Koinコンテナを開始する
    startKoin {
        // 使用するモジュールを設定する
        // supabaseModuleとnetModule、viewModelは、DIで使用するクラスやインスタンスを定義したもの
        modules(supabaseModule, netModule, viewModel)

        // 追加の設定を適用する
        // このラムダ関数により、呼び出し側で特別な設定を追加できる
        additionalConfiguration()
    }
}
```

***netModule.kt***
```Kotlin
// 必要なクラスをインポート
import org.company.saikyotodo.net.TaskApi
import org.company.saikyotodo.net.TaskApiImpl
import org.koin.dsl.module

// netModuleという名前のKoin DIモジュールを定義
val netModule = module {
    // TaskApiのインターフェースに対して、TaskApiImplのインスタンスをシングルトンとして提供
    single<TaskApi> { TaskApiImpl(get()) }
}
```

***viewModelModule.kt***
```Kotlin
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import org.company.saikyotodo.ui.List.ListScreenModel
import org.company.saikyotodo.ui.Login.LoginScreenModel
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import org.koin.dsl.module
import org.koin.mp.KoinPlatform.getKoin

@Composable
public inline fun <reified T : ScreenModel> Screen.getScreenModel(
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null
): T {
    // Koin DIフレームワークを使用して依存関係を取得
    val koin = getKoin()
    // rememberScreenModelを使用して画面の状態を保持。画面が再構築されても状態が維持される。
    return rememberScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) }
}

// ViewModel層の依存関係を設定するKoinモジュール
val viewModel = module {
    // ListScreenModelのインスタンスを生成するためのファクトリを定義
    factory {
        ListScreenModel(get())
    }
    // LoginScreenModelのインスタンスを生成するためのファクトリを定義
    factory {
        LoginScreenModel()
    }
}
```

***supabaseModule.kt***
```Kotlin
// Supabase関連のライブラリをインポート
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.postgrest.Postgrest
import io.github.jan.supabase.realtime.Realtime
import io.github.jan.supabase.realtime.createChannel
import io.github.jan.supabase.realtime.realtime
import org.koin.dsl.module

// Koin DIモジュールの定義
val supabaseModule = module {
    // SupabaseClientのインスタンスをシングルトンとして提供
    single {
        createSupabaseClient(
            // SupabaseプロジェクトのURLとキーを設定
            supabaseUrl = "YOUR_URL",
            supabaseKey = "YOUR_KEY"
        ) {
            // PostgRESTとRealtimeをSupabaseクライアントにインストール
            install(Postgrest)
            install(Realtime)
        }
    }
    // Realtimeチャネルのインスタンスをシングルトンとして提供
    // "task"という名前のチャネルを作成し、これを介してリアルタイム通信を行う
    single {
        get<SupabaseClient>().realtime.createChannel("task")
    }
}
```
注意点なのですが、supabaseModule.ktの中にあるsupabaseUrlとsupabaseKeyは自分のプロジェクトのものに変更してください。
具体的にはsettingsのAPIページに行って、表示されているurlとkeyをコピーしてください。
以下の画像の矢印のところですー
![](https://storage.googleapis.com/zenn-user-upload/2a50ad5ed6f2-20231119.png)

結果的に今のディレクトリ構造はこんな感じになっていてください。
![](https://storage.googleapis.com/zenn-user-upload/a16620356447-20231120.png)

## Koinを初期化する処理を呼ぶ
commonMainのApp.kt内、App()の中身を以下のように修正してください。
```Kotlin
internal fun App() = AppTheme {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colorScheme.background
    ) {
        // 以下を追加
        initKoin()
        val screens = listOf(LoginScreen())
        Navigator(screens)
    }
}
```

# Androidの権限を追加しておく
AndroidManifest.xmlに以下のように権限を追加しておきます。
```
~~~
<!--この行を追加-->
<uses-permission android:name="android.permission.INTERNET"/>
<application
~~~
```

### その他追加
もしかしたらTaskForm.ktのところでエラーが起きているかもしれません。
そしたら53行目あたりのonClickの処理の中でonTaskAddedの呼び出しをコメントアウトしてください。
一旦消すだけで後で修正しますが、今はエラーが起きるのでコメントアウトしておきます。


一旦これで準備完了です。
実際のアプリ上では何も変更がないとは思いますが、一度アプリを立ち上げるなどしてビルドができることを確認しておくと良いかも？


# 次
[次の章](https://qiita.com/shunk0616/private/327b67fbe9e27645964c)でsupabaseからデータを読んでみましょう。