AndroidXでクリーンアーキテクチャ（LiveData, Room, coroutines）


この記事は [Android #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/android-2) 12日目の記事です。

# はじめに
こんにちは [たけのこ](https://twitter.com/TakenokoTech) です！

早速ですが、業務でゴリゴリと小規模なAndroidアプリを作って溜まった
~~うっぷん~~ナレッジを共有させてください。

クリーンアーキテクチャと題名で言っていますが、
解釈違いがあるかもしれませんがご了承ください。
（本を少し読んでなんとなく分かった気でいる感じなので、すいません・・・）

## そもそも AndroidX って何？
Android開発をモダンな状態にするために元のSupport Libraryに破壊的変更を行わずに刷新した新しいライブラリ群の総称です。
https://developer.android.com/jetpack/androidx/

## AndroidX のアーキテクチャでは何を使うの？
Android Developers のアーキテクチャガイドがあるので一読してもらうと良いかと思います。
https://developer.android.com/jetpack/docs/guide?hl=ja

`ViewModel`に`LiveData`が使われていたり、
`Model`に`Room`、`Remote Data Source`に`webservice`が使われていますね。

**AndroidXではViewModelのバインディングにLiveData、SQLiteへのアクセスにはRoomを使っていきましょう！**


# AndroidX でクリーンアーキテクチャ

作成したアプリはこちら
[![TakenokoTech/CleanArchitectureX - GitHub](https://gh-card.dev/repos/TakenokoTech/CleanArchitectureX.svg)](https://github.com/TakenokoTech/CleanArchitectureX)

## 使用したライブラリの紹介

| ライブラリ | 解説|
|:-----------------|:------------------|
| [lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle?hl=ja)| ViewModelやLiveData |      
| [Room](https://developer.android.com/training/data-storage/room?hl=ja)|SQLite|   
| [Coroutine](https://kotlinlang.org/docs/reference/coroutines-overview.html) | 軽量スレッド（Rxの代わり） |    
| [koin](https://insert-koin.io/)|DI（Dagger2の代わり）|    
| [fuel](https://fuel.gitbook.io/documentation/) | 通信（Retrofitの代わり） |
|[Moshi](https://github.com/square/moshi)|JSONパース用|
| [Navigation](https://developer.android.com/guide/navigation) |iOSのStoryboardのようにGUIで画面遷移が書ける| 
|[Mockito-Kotlin](https://github.com/nhaarman/mockito-kotlin)|モックテスト|
|[Robolectric](http://robolectric.org/)|UnitTestでエミュレータを起動|
|[ktlint](https://ktlint.github.io/)|フォーマッター|

## 実現したいアーキテクチャ

### まず、単方向の依存になるようにする
![CleanArchitectureX__1@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/2f370a97-2402-0398-bd52-1ed2f32a408d.png)

ドメイン駆動設計の原則「複雑なドメインの設計は、モデルベースで行うべき」を
実現するためにザックリと三層に分けてみます。

- プレゼン層　＝ Androidアプリ依存のコンポーネント（[アプリコンポーネント](https://developer.android.com/guide/components/fundamentals.html?hl=ja#Components)）＋ [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=JA)
- ドメイン層　＝ ビジネスロジック（UseCase）
- インフラ層　＝ 永続化処理の集約（Repository） ＋ ローカルDBやAPIなど

### 次に、依存性逆転の原則(DIP)を意識して書き直す

![CleanArchitectureX__2@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/262c727d-5e47-3320-c16d-080a7bd08f51.png)

クリーンアーキテクチャにするためにレイヤーをまたぐ場合は
インターフェースを挟んで依存関係が制御の流れに逆転するようにします。
→ [Dependency Inversion Principle](https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87)

また、ヘルパークラスなどを作成して再利用しやすい形にしてあげるのが良さそうです。
→ [ネットワークステータスの公開](https://developer.android.com/jetpack/docs/guide?hl=ja#addendum)


# コーディング（インターフェース）

先に完成形のクラス図を貼っておきます。

![CleanArchitecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/5ce98254-a218-f279-18db-1e802dd8edbc.png)

だいたいのイメージは掴めそうでしょうか？
では、インターフェースから順に作ってみます！

## UseCase
まずは、プレゼン層とドメイン層の間に挟むUseCaseのテンプレートを作ってみます。
このクラスを継承することでI/Oが共通化されるので変更の多いプレゼン層からも扱いやすくなります。
また、UseCaseの実装以降のレイヤーはワーカースレッドで動くようにしました。
別途、SupervisorJobでジョブ管理しても良いかもしれません。

```kotlin:usecase/Usecase.kt
abstract class Usecase<Q : Any, P : Any>(private val context: Context, private val scope: CoroutineScope) : KoinComponent {

    protected var result = MediatorLiveData<UsecaseResult<P>>()
    val source: LiveData<UsecaseResult<P>> = result

    @MainThread
    open fun execute(param: Q) {
        result.postValue(UsecaseResult.Pending())
        scope.launch {
            runCatching {
                call(param).await()
            }.fold({
                result.postValue(UsecaseResult.Resolved(it))
            }, {
                result.postValue(UsecaseResult.Rejected(it))
            })
        }
    }

    @WorkerThread
    protected abstract suspend fun call(param: Q): Deferred<P>
}
```

MediatorLiveDataは変更可能であり、LiveDataは参照のみ可能です。
なので、外部参照可能なプロパティとする場合は上記のような実装が必要なようです。
さらに、UseCaseが返すLiveDataには、`データと状態` の両方をカプセル化したものを返すようにします。

```kotlin:entity/UsecaseResult.kt
sealed class UsecaseResult<P> {
    class Pending<P> : UsecaseResult<P>()
    data class Resolved<P>(val value: P) : UsecaseResult<P>()
    data class Rejected<P>(val reason: Throwable) : UsecaseResult<P>()
}
```

![Usecase.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/4216cfa6-0162-3e44-d728-398ab74362cb.png)

ViewModelなどからUseCaseを呼ぶ場合は`Usecase.source`をCoroutineScopeでオブザーブ後に
`Usecase.execute()`で発火することで気持ちいい動きをしてくれます。


## Repository

Repositoryでは、下のLocal Data Source と Remote Data Source の処理を集約します。
ただのinterfaceクラスです。

```kotlin:repository/UserRepository.kt
interface UserRepository {
    suspend fun getAllUser(): List<UserLocalDataSource.UserEntity>
    suspend fun addUser(user: UserLocalDataSource.UserEntity)
}
```

## Local Data Source

### Room (SQLite)

Roomのヘルパークラスはこんな感じ。
LocalDataSourceとRoomを繋ぐ役割を担います。

```kotlin:AppDatabase.kt
interface AppDatabase {
    fun userDao(): UserDao

    companion object {
        @Volatile private var instance: AppDatabaseImpl? = null
        fun getDatabase(context: Context): AppDatabase = synchronized(this) {
            instance = if (instance == null) Room.databaseBuilder(context, AppDatabaseImpl::class.java, "CleanArchitectureX-DB").build() else instance
            return instance!!
        }
    }
}

@Database(entities = [UserLocalDataSource.UserEntity::class], version = 1)
abstract class AppDatabaseImpl : RoomDatabase(), AppDatabase {
    abstract override fun userDao(): UserDao
}
```

![local.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/87b72e20-6c81-d3f1-edc2-a93d03a73fd9.png)


## Remote Data Source

### Fuel (Rest API)

Fuelのヘルパークラスはこんな感じ。
RemoteDataSourceとFuelを繋ぐ役割を担います。

```kotlin:AppRestApi.kt
interface AppRestApi {
    suspend fun <T : Any> execute(param: ApiParameter<T>, clazz: KClass<T>): ApiResult<T>
}
suspend inline fun <reified T : Any> AppRestApi.fetch(param: ApiParameter<T>) = execute(param, T::class)

@Suppress("OVERRIDE_BY_INLINE")
class AppRestApiImpl : AppRestApi {

    @WorkerThread
    override suspend inline fun <T : Any> execute(param: ApiParameter<T>, clazz: KClass<T>): ApiResult<T> {
        val bodyStr = if (param.body != null) OBJECT_MAPPER.adapter(Any::class.java).toJson(param.body) else ""
        val requestBuilder = param.call().header(param.header).jsonBody(bodyStr)
        val adapter = param.adapter ?: planeAdapter(clazz.java)

        val (request, response, result) = runCatching {
            requestBuilder.awaitResponseResult(adapter)
        }.getOrElse {
            Triple(requestBuilder, Response.error(), Result.error(Exception(it)))
        }

        return result.fold({ ApiResult.Success(it) }, { ApiResult.Failed(it, response.statusCode) })
    }
}
```

```kotlin:ApiResult.kt
sealed class ApiResult<P> {
    data class Success<P>(val value: P) : ApiResult<P>()
    data class Failed<P>(val cause: Exception, val statusCode: Int) : ApiResult<P>()
}
```


Moshiでリスト型のJSONを変換する場合は`listAdapter`のようにデシリアライズの処理を書かないといけないので、[AppRestApiのパラメータ](https://github.com/TakenokoTech/CleanArchitectureX/blob/master/app/src/main/java/tech/takenoko/cleanarchitecturex/entities/ApiParameter.kt)でMoshiAdapterを渡すような記述をしています。

```kotlin:Moshi+.kt
val OBJECT_MAPPER: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()

/**
 * moshi adapter
 */
inline fun <reified T : Any> planeAdapter() = planeAdapter(T::class.java)
fun <T : Any> planeAdapter(clazz: Class<T>) = object : ResponseDeserializable<T> {
    override fun deserialize(content: String): T? = OBJECT_MAPPER.adapter(clazz).fromJson(content)
}

/**
 * moshi list adapter
 */
inline fun <reified T : Any> listAdapter() = listAdapter(T::class.java)
fun <T : Any> listAdapter(clazz: Class<T>) = object : ResponseDeserializable<List<T>> {
    override fun deserialize(content: String): List<T>? {
        val type = Types.newParameterizedType(List::class.java, clazz)
        val listAdapter = OBJECT_MAPPER.adapter<List<T>>(type)
        return listAdapter.fromJson(content)
    }
}

/**
 * moshi map adapter
 */
inline fun <reified K : Any, reified V : Any> mapAdapter() = mapAdapter(K::class.java, V::class.java)
fun <K : Any, V : Any> mapAdapter(clazzK: Class<K>, clazzV: Class<V>) = object : ResponseDeserializable<Map<K, V>> {
    override fun deserialize(content: String): Map<K, V>? {
        val type = Types.newParameterizedType(Map::class.java, clazzK, clazzV)
        val listAdapter = OBJECT_MAPPER.adapter<Map<K, V>>(type)
        return listAdapter.fromJson(content)
    }
}
```

![remote.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/c69c66e1-d3b6-1443-4d35-f895d0dd7e62.png)



ベースの実装はここまで！

# コーディング（中身）

早速、VIewModelから順番に作ったクラスを使って実装してみます。


## Implement ViewModel

```kotlin:usecase/TopViewModel.kt
class TopViewModel : ViewModel(), KoinComponent {

    private val registerUserUsecase: RegisterUserUsecase by inject { parametersOf(viewModelScope) }
    private val loadUserUsecase: LoadUserUsecase by inject { parametersOf(viewModelScope) }

    private val _userNameList: MediatorLiveData<List<String>> = MediatorLiveData()
    val userNameList: LiveData<List<String>> = _userNameList

    private val _isLoading: MediatorLiveData<Boolean> = MediatorLiveData()
    val isLoading: LiveData<Boolean> = _isLoading

    init {
        _userNameList.addSource(loadUserUsecase.source) { loadUserUsecaseHandlerToList(it) }
        _isLoading.addSource(loadUserUsecase.source) { usecaseHandlerIsLoading() }
        _isLoading.addSource(registerUserUsecase.source) { usecaseHandlerIsLoading().run { registerUserUsecaseHandler(it) } }
    }

    fun load() {
        loadUserUsecase.execute(Unit)
    }

    fun register() {
        val user = UserLocalDataSource.UserEntity("UserName", "DisplayName")
        val param = RegisterUserUsecase.Param(user)
        registerUserUsecase.execute(param)
    }

    private fun loadUserUsecaseHandlerToList(result: UsecaseResult<List<String>>): Any = when (result) {
        is UsecaseResult.Pending -> Unit
        is UsecaseResult.Resolved -> _userNameList.value = result.value
        is UsecaseResult.Rejected -> Log.e("TopViewModel", result.reason.localizedMessage)
    }

    private fun registerUserUsecaseHandler(result: UsecaseResult<Unit>): Any = when (result) {
        is UsecaseResult.Pending -> Unit
        is UsecaseResult.Resolved -> load()
        is UsecaseResult.Rejected -> Log.e("TopViewModel", result.reason.localizedMessage)
    }

    private fun usecaseHandlerIsLoading() {
        val registerUserUsecaseIsLoading = registerUserUsecase.source.value.isLoading()
        val loadUserUsecaseLoading = loadUserUsecase.source.value.isLoading()
        _isLoading.value = registerUserUsecaseIsLoading || loadUserUsecaseLoading
    }
}
```

ViewModelでも`Activity`または`Layout resource`から参照するLiveDataや関数はpublicにして、それ以外はprivateにしています。
こちらも破壊的変更可能なMediatorLiveDataは外部から参照できないようにしています。

![CleanArchitectureX@2x(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/d9f0ad33-816f-007e-ef01-7fc3f50da029.png)



## Implement Usecase

```kotlin:usecase/RegisterUserUsecase.kt
class RegisterUserUsecase(context: Context, private val scope: CoroutineScope) : Usecase<RegisterUserUsecase.Param, Unit>(context, scope) {

    private val userRepository: UserRepository by inject()

    @WorkerThread
    override suspend fun call(param: Param): Deferred<Unit> = scope.async(Dispatchers.IO) {
        Thread.sleep(1000)
        return@async userRepository.addUser(param.user)
    }

    data class Param(val user: UserLocalDataSource.UserEntity)
}
```

```kotlin:usecase/LoadUserUsecase.kt
class LoadUserUsecase(context: Context, private val scope: CoroutineScope) : Usecase<Unit, List<String>>(context, scope) {

    private val userRepository: UserRepository by inject()

    @WorkerThread
    override suspend fun call(param: Unit): Deferred<List<String>> = scope.async(Dispatchers.IO) {
        Thread.sleep(1000)
        return@async userRepository.getAllUser().map { "${it.id}: ${it.displayName}" }
    }
}
```

Usecaseではプレゼン層の依存を断ち切り、インフラストラクチャー層をブラックボックスとして扱うようにします。
また、UseCaseは永続化しないビジネスロジックを書くと良いかと思います。


## Implement Repository

```kotlin:repository/UserRepositoryImpl.kt
class UserRepositoryImpl : UserRepository, KoinComponent {

    private val local: UserLocalDataSource by inject()
    private val network: UserRemoteDataSource by inject()

    @WorkerThread
    override suspend fun getAllUser(): List<UserLocalDataSource.UserEntity> {
        val users = network.getUser()
        local.deleteAll()
        local.insertAll(*users.map { UserLocalDataSource.User(it.user_name, it.display_name) }.toTypedArray())
        return local.getAll()
    }

    @WorkerThread
    override suspend fun addUser(user: UserLocalDataSource.UserEntity) {
        network.postUser(user = UserEntity(user.userName, user.displayName))
        return local.insertAll(user)
    }
}
```

RepositoryではLocalのデータとRemoteのデータを集約します。
どのくらいの責務を持たせるか謎です。

## Implement RemoteDataSource

```kotlin:repository/remote/UserRemoteDataSource.kt
class UserRemoteDataSource : KoinComponent {

    private val restApi: AppRestApi by inject()

    @WorkerThread
    suspend fun getUser(): List<UserEntity> {
        val param = Get<List<UserEntity>>(
            url = getUserUrl,
            adapter = listAdapter()
        )
        return when (val it = restApi.fetch(param)) {
            is ApiResult.Success -> it.value
            is ApiResult.Failed -> when (it.statusCode) {
                else -> throw it.cause
            }
        }
    }

    @WorkerThread
    suspend fun postUser(user: UserEntity): ResultEntity {
        val param = Post<ResultEntity>(
            url = addUserUrl,
            body = user
        )
        return when (val it = restApi.fetch(param)) {
            is ApiResult.Success -> it.value
            is ApiResult.Failed -> when (it.statusCode) {
                else -> throw it.cause
            }
        }
    }

    @JsonClass(generateAdapter = true)
    data class UserEntity(
        @Json(name = "user_name") val userName: String,
        @Json(name = "display_name") val displayName: String
    )

    @JsonClass(generateAdapter = true)
    data class ResultEntity(
        @Json(name = "status") val status: String
    )
}
```

APIを定義します。
エンティティはパッケージを分けた方が良いかと思います。

## Implement LocalDataSource

```kotlin:repository/local/UserLocalDataSource.kt
class UserLocalDataSource : UserDao, KoinComponent {

    private val database: AppDatabase by inject()

    @WorkerThread
    override suspend fun getAll(): List<UserEntity> {
        return database.userDao().getAll()
    }

    @WorkerThread
    override suspend fun insertAll(vararg users: UserEntity) {
        val result = database.userDao().insertAll(*users)
        return result
    }

    @WorkerThread
    override suspend fun deleteAll() {
        val result = database.userDao().deleteAll()
        return result
    }

    @Entity
    data class UserEntity(
        @ColumnInfo(name = "userName") val userName: String,
        @ColumnInfo(name = "displayName") val displayName: String,
        @PrimaryKey(autoGenerate = true) val id: Int = 0
    )
}
```

Rooｍを定義します。また、Daoは別で用意します。

```kotlin:entity/room/UserDao.kt
@Dao
interface UserDao {
    @Insert
    suspend fun insertAll(vararg users: UserLocalDataSource.UserEntity)

    @Query(QUERY_GET_ALL)
    suspend fun getAll(): List<UserLocalDataSource.UserEntity>

    @Query(QUERY_DELETE_ALL)
    suspend fun deleteAll()
}

/** ユーザー全件取得 */
const val QUERY_GET_ALL = """
    SELECT *
    FROM user
"""

/** ユーザー全件削除 */
const val QUERY_DELETE_ALL = """
    DELETE FROM user
"""
```

以上で、ViewModel層以下の実装は完了です！
あとは、TopViewModelのpubliicなプロパティや関数を使ってUIを作ってください。

サンプルコードでは下のような簡単な画面を作りました。
今まで通りUIはdatabindingを使って貰えれば大丈夫かと思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/bd501f6b-d532-37cf-fd60-f6c06db11841.png" width=400>

# +αでやってみたこと

ほかに盛り込んでみたものを紹介します。

## koinでDIしてみる

コードの中に `by inject()` ってのがあったと思うんですが、koinを使ってDIするためにつけてました。
Applicationの中で`startKoin`すればオブジェクトが注入されます。

```kotlin:App.kt
class App : Application() {

    override fun onCreate() {
        super.onCreate()

        // Dependency Injection
        startKoin {
            androidContext(this@App)
            modules(diModules)
        }
    }
}

private val viewmodelModules = module {
    factory { TopViewModel() }
}

private val usecaseModules = module {
    factory { RegisterUserUsecase(androidContext() as Application, it.get<CoroutineScope>() as CoroutineScope) }
    factory { LoadUserUsecase(androidContext() as Application, it.get<CoroutineScope>() as CoroutineScope) }
    factory { BackgroundUsecase(androidContext() as Application, it.get<CoroutineScope>() as CoroutineScope) }
}

private val repositoryModules = module {
    factory { UserRepositoryImpl() as UserRepository }
}

private val localModules = module {
    single { AppDatabase.getDatabase(androidContext() as Application) }
    single { UserLocalDataSource() }
}

private val remoteModules = module {
    single { AppRestApiImpl() as AppRestApi }
    single { UserRemoteDataSource() }
}

val diModules = listOf(
    viewmodelModules,
    usecaseModules,
    repositoryModules,
    localModules,
    remoteModules
)
```

はじめて触ったのですが、Dagger2よりシンプルでした！
これでテストも書きやすくなるので使っていきたいですね。

# 最後に

今回はLiveDataとRoomを業務で使ってみて上手くいったアーキテクチャを書いてみました。
LiveDataとかコルーチンを使ったアーキテクチャの記事があまりなくて習得するのに苦労しました（汗
この記事が誰かの役に立てば幸いです。

AndroidXに入ってからはRxからコルーチンに移行していくようなので、これからも勉強が必要ですね。
まだ、`@Experimental`がついているAPIが多いので本番で使うのは怖いですが、恐れずに触りましょう！