0Pricing
Kotlin Academy · レッスン

Repository層とUse Case層の共有

ビジネスロジックとデータアクセスをcommonMainへ移し、再利用性を最大化します。

「Repository層とUse Case層の共有」はCoddyKit上の無料Kotlin Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはKotlin Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Kotlin Academyコースには全4レッスンが含まれています。

アーキテクチャの目標

KMPでは、ドメインモデル、リポジトリインターフェース、ユースケース、ViewModelなど、できるだけ多くのロジックをcommonMainに移すことが目標です。プラットフォームI/Oだけを各プラットフォームのソースセットに残します。

// Target architecture:
// commonMain:
//   domain/    - data classes, interfaces
//   data/      - repository implementations using shared clients
//   usecase/   - business logic, composing repositories
// androidMain / iosMain:
//   DI wiring, platform drivers (DB, network)

commonMainのドメイン層

ドメインエンティティとリポジトリインターフェースをcommonMainで定義します。プラットフォームのインポートは使用しません。

// commonMain/domain/User.kt
data class User(val id: String, val name: String, val email: String)

// commonMain/domain/UserRepository.kt
interface UserRepository {
    suspend fun getUser(id: String): User?
    suspend fun getAllUsers(): List<User>
    suspend fun saveUser(user: User)
}

Ktorを使った共有リポジトリ実装

マルチプラットフォーム対応のKtorクライアントを使って、commonMainにリポジトリを実装します。実際のHTTPエンジンは、プラットフォームごとに提供します。

// commonMain/data/RemoteUserRepository.kt
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class RemoteUserRepository(private val client: HttpClient) : UserRepository {
    override suspend fun getUser(id: String): User? =
        client.get("https://api.example.com/users/$id").body()
    override suspend fun getAllUsers(): List<User> =
        client.get("https://api.example.com/users").body()
    override suspend fun saveUser(user: User) {
        client.post("https://api.example.com/users") { setBody(user) }
    }
}

ユースケース層

ユースケース(インタラクター)はリポジトリを調整し、ビジネスロジックを含みます。プラットフォームに依存しない純粋なKotlinとして実装します。

// commonMain/usecase/GetUserUseCase.kt
class GetUserUseCase(private val repo: UserRepository) {
    suspend operator fun invoke(id: String): Result<User> =
        runCatching { repo.getUser(id) ?: error("User not found: $id") }
}

// commonMain/usecase/SaveUserUseCase.kt
class SaveUserUseCase(private val repo: UserRepository) {
    suspend operator fun invoke(user: User): Result<Unit> =
        runCatching { repo.saveUser(user) }
}

共有ViewModel

ViewModelはkotlinx-coroutinesとStateFlowを使ってcommonMainに配置できます。Androidでは直接使用し、iOSではSwiftUI用のラッパーを作成します。

// commonMain/viewmodel/UserViewModel.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class UserViewModel(private val getUser: GetUserUseCase) {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    private val _user = MutableStateFlow<User?>(null)
    val user = _user.asStateFlow()
    fun load(id: String) {
        scope.launch { _user.value = getUser(id).getOrNull() }
    }
    fun clear() = scope.cancel()
}

Ktor HttpClientの設定

共有設定を使って、commonMainでKtorのHttpClientを作成します。エンジン(OkHttp / Darwin)は、注入するか、プラットフォームごとに作成します。

// commonMain/network/HttpClientFactory.kt
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
fun createHttpClient(engine: io.ktor.client.engine.HttpClientEngine? = null) =
    HttpClient(engine ?: io.ktor.client.engine.cio.CIO) {
        install(ContentNegotiation) { json() }
    }

共有永続化のためのSQLDelight

SQLDelightは、commonMainの.sqファイルから、すべてのプラットフォーム向けに型安全なKotlinクエリを生成します。実際のDBドライバーはプラットフォーム固有です。

// commonMain/db/UserQueries.sq:
// CREATE TABLE User (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL);
// selectAll:
// SELECT * FROM User;

// Usage in commonMain:
// val db = UserDatabase(driver)
// val users = db.userQueries.selectAll().executeAsList()

プラットフォームDIの組み立て

プラットフォーム固有のDIモジュールですべてを組み立てます。インターフェースは共通で、コンストラクター引数だけが異なります。

// androidMain:
val androidModule = module {
    single { AndroidSqliteDriver(UserDatabase.Schema, androidContext(), "users.db") }
    single { UserDatabase(get()) }
    single<UserRepository> { RemoteUserRepository(get()) }
    single { GetUserUseCase(get()) }
    viewModel { UserViewModel(get()) }
}

// iosMain:
// Same pattern with NativeSqliteDriver

commonTestでのユースケースのテスト

ユースケースはプラットフォームに依存しないため、フェイクのリポジトリを使ってcommonTestで簡単にユニットテストできます。

// commonTest/usecase/GetUserUseCaseTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
class FakeUserRepo : UserRepository {
    override suspend fun getUser(id: String) = if (id == "1") User("1", "Alice", "a@b.com") else null
    override suspend fun getAllUsers() = emptyList<User>()
    override suspend fun saveUser(user: User) {}
}
class GetUserUseCaseTest {
    private val useCase = GetUserUseCase(FakeUserRepo())
    @Test
    fun testGetExistingUser() = kotlinx.coroutines.runBlocking {
        val result = useCase("1")
        assertEquals("Alice", result.getOrNull()?.name)
    }
}

ユースケースでのエラーハンドリング

ユースケースからResultまたはsealed classを返してエラーを伝播させ、共有コードとプラットフォームコードの境界を例外が越えないようにします。

// commonMain:
sealed class UserResult {
    data class Success(val user: User) : UserResult()
    data class NotFound(val id: String) : UserResult()
    data class Error(val cause: Throwable) : UserResult()
}
class SafeGetUser(private val repo: UserRepository) {
    suspend operator fun invoke(id: String): UserResult = try {
        val user = repo.getUser(id)
        if (user != null) UserResult.Success(user) else UserResult.NotFound(id)
    } catch (e: Exception) { UserResult.Error(e) }
}

プラットフォームの慣習を損なわない共有

UIではなくロジックを共有します。AndroidではComposeを、iOSではSwiftUIを使用します。どちらも、プラットフォーム固有のバインディングを通じて同じ共有ViewModelの状態を監視します。

// Android (Compose):
// val user by viewModel.user.collectAsState()

// iOS (SwiftUI):
// @State var user = viewModel.user.value
// viewModel.load(id: "1")

// Shared ViewModel:
// _user.value = ... (commonMain)
fun main() { println("Share logic; use platform UI frameworks") }

確認問題

適切に構成されたKMPプロジェクトで、プラットフォーム固有のインポートを持つべきでないレイヤーはどれですか?

まとめ

ドメインモデル、リポジトリインターフェース、ユースケース、共有ViewModelをcommonMainに配置します。ネットワークにはKtor、データベースにはSQLDelightを使用し、プラットフォームのエンジンはDIレイヤーで注入します。フェイクのリポジトリを使って、commonTestでビジネスロジックをテストします。

よくある質問

「Repository層とUse Case層の共有」レッスンは無料ですか?

はい。「Repository層とUse Case層の共有」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Kotlin Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Kotlin Academyコースには全4レッスンが含まれています。

「Repository層とUse Case層の共有」で何を学びますか?

ビジネスロジックとデータアクセスをcommonMainへ移し、再利用性を最大化します。 ブラウザで直接実行するハンズオンコードでKotlin Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Kotlin Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのKotlin Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「Repository層とUse Case層の共有」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このKotlin Academyレッスンでコードを書いて実行できますか?

はい。すべてのKotlin Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. KMPプロジェクト構造:commonMain、androidMain、iosMain
  2. プラットフォームAPI向けexpect/actual機構
  3. Repository層とUse Case層の共有
  4. KoinによるKMPの依存性注入
← Kotlin Academyに戻る