【正社員】還元率83%【フリーランス】マージン一律5万円で案件をご紹介させていただきます。 詳細はこちら

【Android】<Kotlin>アプリ開発におけるクリーンアーキテクチャ入門

【Android】<Kotlin>アプリ開発におけるクリーンアーキテクチャ入門
すだ

みなさまこんにちは〜!
メモリアインクのすだです。

本日は、
Androidアプリ開発における「クリーンアーキテクチャ」の基本について、実際のコードを用いて解説していきます!

この記事を読んでわかること…
・クリーンアーキテクチャについて
・クリーンアーキテクチャでの実装方法

目次

環境

  • Kotlin (ver 1.9.0)
  • Android Studio (Giraffe | 2022.3.1 Patch 3)

クリーンアーキテクチャとは?

クリーンアーキテクチャとは、ソフトウェア開発において「依存関係を内側に向かわせ、ビジネスロジックを守る構造」のことです。

「データ処理」「表示処理」「外部サービス」などを明確に“仕切り”で分けることで、
・変更に強く
・試しやすく
・他のアプリでも使い回せる
という設計になるのがクリーンアーキテクチャです。

MVPMVVMが「UIの構造に関する設計パターン」であるのに対し、
クリーンアーキテクチャは「アプリ全体の依存関係と責務分離のための原則」ということになります。

【Android】<Kotlin>アプリ開発におけるクリーンアーキテクチャ入門

画像引用元:https://github.com/ESchouten/CleanArchitecture

これだけだと少しわかりづらいかと思いますので
より詳細に説明します。↓

特徴①:依存関係を“内側”に向かわせる

クリーンアーキテクチャでは、「重要なロジック(ビジネス処理)を一番内側に置く」という考え方があります。

[ UI / API / DB ] ← 外側(変更されやすい)
        ↓
[ UseCase(ユースケース) ] ← 中間
        ↓
[ Domain(ビジネスロジック) ] ← 内側(絶対に守りたい)

これは、外側の変更(UIのデザインが変わる、APIの仕様が変わる)に内側が影響されないようにするためです。
(内側はアプリの“本質”なので、簡単に壊れない構造にすることが重要 という考え)

内側に行くほど“純粋な処理”があり、外側に行くほど技術的な依存(Android, Retrofit, Roomなど)を含みます。

特徴②: プレゼンテーション(表示)とドメイン(中身)を分離

クリーンアーキテクチャは、以下のように 主に3レイヤー(Presentation, Domain, Data)に分けて実装します。

【Android】<Kotlin>アプリ開発におけるクリーンアーキテクチャ入門

画像引用元:https://qiita.com/koutalou/items/07a4f9cf51a2d13e4cdc

<外側>Presentationレイヤー:
View(ActivityやFragment)はユーザーと直接関わる画面部分。
PresenterはAndroid依存を減らすための中間役(MVVMならViewModel)

<中心>Domainレイヤー:
UseCaseはアプリの振る舞いそのもの。
Model(Translator)はData層から取得したEntityをView向けに加工する場合に使用される中間変換役

<外側>Dataレイヤー:
RepositoryはDomainが依存する唯一の外部接点。
DataStoreはAPIやDBなど具体的な実装場所。Entityはアプリ内部で共通して扱う純粋なデータ型

MVVMやMVPを取り込むパターンも

クリーンアーキテクチャは 以下のように
UI構造の設計にMVVMやMVPというアーキテクチャを取り入れるケースもあります。

MVP / MVVM(UI設計の話)
└─ View
   └─ Presenter / ViewModel(UIロジック)

クリーンアーキテクチャ(アプリ全体の構造)
├─ Presentation Layer(MVPやMVVMを内包)
├─ Domain Layer(ビジネスロジック)
└─ Data Layer(API・DB・キャッシュなど)

MVVMやMVPの設計に関しては、以下の記事で詳細を説明しておりますので、ぜひ合わせてご覧ください!

クリーンアーキテクチャを採用するメリットとデメリット

クリーンアーキテクチャを採用するにあたり、メリットとデメリットが以下のように存在します。

メリット

・責任が明確になり、大規模開発でもコードが破綻しにくい
・UIとビジネスロジックの分離によりテストがしやすい
・Android固有技術(Activity, Retrofit, Room等)に依存しない純粋なロジックが書ける

デメリット

・ファイルやクラスが増えるので設計の理解が必要
・小規模アプリだと構造が大げさになることがある

→これらのことから、クリーンアーキテクチャの採用に向いているのは、
機能や画面数が多い「中〜大規模な開発」、かつ長期的な運用を前提としたアプリであると言えるでしょう。

サンプル処理で学ぶMVP実装(Kotlin)

それでは、クリーンアーキテクチャを採用して「画面にあるボタンを押すとユーザー名を取得して表示するる」という簡単な処理を作ってみます。

図でまとめると以下のような構成になるようにします。

Presentation Layer
├─ MainActivity (View)
└─ MainViewModel (ViewModel)

Domain Layer
├─ GetUserNameUseCase (UseCase)
└─ UserModelTranslator (Model / Translator)

Data Layer
├─ UserRepository (interface)
├─ UserRepositoryImpl (実装)
├─ UserDataStore (APIやDBのような実装)
└─ UserEntity (Entity)

Presentation Layer(View / ViewModel)

Activity

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 依存関係を手動で注入(本来はDIを使う)
        val dataStore = UserDataStore()
        val repository = UserRepositoryImpl(dataStore)
        val translator = UserModelTranslator()
        val useCase = GetUserNameUseCase(repository, translator)
        viewModel = MainViewModel(useCase)

        val button = findViewById<Button>(R.id.button)
        val textView = findViewById<TextView>(R.id.textView)

        button.setOnClickListener {
            viewModel.loadUserName()
        }

        viewModel.userName.observe(this) { name ->
            textView.text = name
        }
    }
}

↑ここでユーザーのアクションをクリックリスナーで検知し、ボタン押下後の処理をViewModelに委ねます。
LiveData(ViewModel)を監視して、「データの変更があれば行う処理」を記述します。

ViewModel

class MainViewModel(
    private val useCase: GetUserNameUseCase
) : ViewModel() {

    private val _userName = MutableLiveData<String>()
    val userName: LiveData<String> get() = _userName

    fun loadUserName() {
        val model = useCase.execute()
        _userName.value = model.displayName
    }
}

↑ViewModelはUIの状態(トーストメッセージ)をLiveDataで持ちます。
ボタンが押されたらModelからデータを取得し、Viewに通知します。

Domain Layer(UseCase&モデル変換)

Model(UI表示用データ)

data class UserModel(val displayName: String)

ModelTranslator

class UserModelTranslator {
    fun translate(entity: UserEntity): UserModel {
        return UserModel(displayName = "ユーザー名:${entity.name}")
    }
}

UseCase

class GetUserNameUseCase(
    private val repository: UserRepository,
    private val translator: UserModelTranslator
) {
    fun execute(): UserModel {
        val entity = repository.getUser()
        return translator.translate(entity)
    }
}

取得したデータ(Entity)を、表示用の形式(UserModel)に変換して返している処理です。

Data Layer(データ層)

Entity

data class UserEntity(val id: Int, val name: String)

DataStore

class UserDataStore {
    fun fetchUser(): UserEntity {
        // 本来はAPIやDB呼び出しなどをここに
        return UserEntity(id = 1, name = "太郎")
    }
}

Repository

interface UserRepository {
    fun getUser(): UserEntity
}

RepositoryImpl

class UserRepositoryImpl(
    private val dataStore: UserDataStore
) : UserRepository {
    override fun getUser(): UserEntity {
        return dataStore.fetchUser()
    }
}

まとめ

おつかれさまでした。いかがでしたでしょうか!

用意するclassが多くて大変と感じる人は、最初は「UseCase」「ViewModel」「Repository」の3層だけでも十分です。
大切なのは、ドメインを“ビジネスロジックの中心”とする考えを持つということ。
Viewがロジックを持たないだけでもアプリの質は大きく上がるので、ぜひ開発に取り入れてみてください!

すだ

技術者としてのキャリアパスを次のレベルへと進めたい皆様、
未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】
を通じて、
自分の市場価値をさらに向上させてみませんか?

それではまた次回の記事でお会いしましょう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

弊社テックブログをご愛読いただきありがとうございます。
当テックブログを運用している株式会社メモリアインクは、
【正社員】還元率83%
【フリーランス】マージン一律5万円で案件のご紹介
と、エンジニアの皆様に分かりやすい形で稼げる仕組みを構築し提供させていただいております。

コメント

コメントする

目次