📱

[Android] ReduxKotlin + Jetpack Compose + Room + Hilt

2024/04/19に公開

目的

ReduxKotlin + Jetpack Compose + Room + Hiltを組み合わせてローカルなデータを永続化するアプリを作る

ReduxKotlinについては以下を見て頂ければ
https://zenn.dev/giglancer/articles/f34be37dde7723

依存の追加

libs.versions.toml

[versions]
# hilt
hilt-version = "2.50"
# room
room-version = "2.6.1"
# redux
redux-version = "0.5.5"
redux-compose-version = "0.6.0"

[libraries]
# hilt
android-hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt-version"}
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt-version"}

# room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version"}
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version"}
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-version"}

# redux
redux-threadsafe = { module = "org.reduxkotlin:redux-kotlin-threadsafe-jvm", version.ref = "redux-version"}
redux-compose = { module = "org.reduxkotlin:redux-kotlin-compose-jvm", version.ref = "redux-compose-version"}

[bundles]
redux = ["redux-threadsafe", "redux-compose"]

[plugins]
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt-version"}
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

project build.gradle.kts

plugins {
    alias(libs.plugins.hilt) apply false
    alias(libs.plugins.ksp) apply false
}

app build.gradle.kts

plugins {
    alias(libs.plugins.ksp)
    alias(libs.plugins.hilt)
}

dependencies {
    implementation(libs.android.hilt)
    ksp(libs.hilt.compiler)
    implementation(libs.room.runtime)
    implementation(libs.room.ktx)
    ksp(libs.room.compiler)
    implementation(libs.bundles.redux)
}

Room

Entityのdata classを作成する

data>model>Todo

@Entity(tableName = "todo_list")
data class Todo(
    @PrimaryKey(autoGenerate = true)
    val id: Int,

    @ColumnInfo("todo_item")
    val todoItem: String,
)

data>room>TodoDao
Dao interfaceを作成する

@Dao
interface TodoDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTodo(todo: Todo)

    @Query("SELECT * from todo_list")
    fun getTodoList(): List<Todo>
}

データベースの抽象クラスを作成する

@Volatileアノテーションはマルチスレッド処理される変数に対してアクセスされるたび、必ず、共有メモリ上の変数の値とスレッド上の値を一致させる

synchronizedはデータベースに同時にアクセスするさまざまなスレッドを制御し、複数のインスタンスが作成されるのを防ぐためのもの

data>room>TodoListDatabae

@Database(entities = [Todo::class], version = 1, exportSchema = false)
abstract class TodoListDatabase: RoomDatabase() {
    abstract fun todoDao(): TodoDao

    companion object {
        @Volatile
        private var Instance: TodoListDatabase? = null

        fun getDatabase(context: Context): TodoListDatabase {
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, TodoListDatabase::class.java, "todo_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

リポジトリクラスを作成する

domain>TodoListRepository

interface TodoListRepository {
    suspend fun insertTodoList(todoList: List<Todo>)

    fun getTodoList: List<Todo>
}

リポジトリクラスを継承した実装クラスを作成する

data>repository>TodoListRepositoryImpl

class TodoListRepositoryImpl @Inject constructor(private val todoDao: TodoDao) : TodoListRepository {
    override suspend fun insertTodoList(todoList: List<Todo>) = todoDao.insertTodoList(todoList)

    override fun getTodoList(): List<Todo> = todoDao.getTodoList()
}

DI

di>TodoListDatabaseModule

@Module
@InstallIn(SingletonComponent::class)
object TodoListDatabaseModule {
    @Singleton
    @Provides
    fun provideDatabase(@ApplicationContext context: Context): TodoListDatabase {
        return TodoListDatabase.getDatabase(context)
    }

    @Singleton
    @Provides
    fun provideDao(db: TodoListDatabase): TodoDao {
        return db.todoDao()
    }

    @Singleton
    @Provides
    fun provideTodoListRepository(todoDao: TodoDao): TodoListRepository {
        return TodoListRepositoryImpl(todoDao)
    }
}

Redux

State、Action、Reducer、Middlewareを作成していく

以前書いた記事もあるので重複するところは省略する
https://zenn.dev/giglancer/articles/295cf1ceb1368a

redux>state>InputScreenState

data class InputScreenState(
    val todoList: List<Todo> = emptyList()
)

redux>action>screen>InputScreenAction

読み込むため、保存するためのAction

sealed class InputScreenAction {
    data class Load() : InputScreenAction()
    data class InsertTodoList(
        val todoList: List<Todo>
    ) : InputScreenAction()
}

redux>action>middleware>RoomMiddlewareAction

sealed class RoomMiddlewareAction {
    data class ReduceTodoList(val todoList: List<Todo>) : RoomMiddlewareAction()
}

redux>reducer>RoomReducer

fun roomReducer(
    state: State,
    action: Any,
) : State =
    when (action) {
        is RoomMiddlewareAction.ReduceTodoList -> {
            state.copy(
                inputScreenState = InputScreenState().copy(todoList = action.todoList)
            )
        }
        else -> state
    }

redux>middleware>RoomMiddleware

class RoomMiddleWare
@Inject constructor(private val todoListRepository: TodoListRepository) : Middleware<State> {
    override fun invoke(store: TypedStore<State, Any>): (next: Dispatcher) -> (action: Any) -> Any {
        return { next ->
            { action ->
                val coroutineScope = CoroutineScope(Dispatchers.IO)
                when (action) {
                    is InputScreenAction.Load -> {
                        coroutineScope.launch {
                            try {
                                store.dispatch(StatusAction.Processing)
                                val todoList = todoListRepository.getTodoList()
                                store.dispatch(RoomMiddlewareAction.ReduceTodoList())
                                store.dispatch(StatusAction.Success)
                            } catch (e: IOException) {
                                store.dispatch(StatusAction.Error)
                            }
                        }
                    }
                    is InputScreenAction.InsertTodoList -> {
                        coroutineScope.launch {
                            try {
                                store.dispatch(StatusAction.Processing)
                                todoListRepository.insertTodoList(action.todoList)
                                store.dispatch(StatusAction.Success)
                            } catch (e: IOException) {
                                store.dispatch(StatusAction.Error)
                            }
                        }
                    }
                    else -> next(action)
                }
            }
        }
    }
}

UI

とりあえず、画面を開いた時にローカルデータに保存し取得するActionを投げる

@Composable
fun InputScreen(
    modifier: Modifier = Modifier,
) {
    val inputScreenState by selectState<State, InputScreenState> { inputScreenState }
    val status by selectState<State, Status> { status }
    val dispatch = rememberDispatcher()

    LaunchedEffect(Unit) {
        dispatch(InputScreenAction.InsertTodoList(
            todoList = listOf(Todo(id = 0, todoItem = "テスト",)))
        )
        dispatch(InputScreenAction.Load())
    }
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(text = "InputScreen")
        Text(text = inputScreenState.todoList.toString())
    }
}

Discussion