[Android] ReduxKotlin + Jetpack Compose + Room + Hilt
目的
ReduxKotlin + Jetpack Compose + Room + Hiltを組み合わせてローカルなデータを永続化するアプリを作る
ReduxKotlinについては以下を見て頂ければ
依存の追加
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を作成していく
以前書いた記事もあるので重複するところは省略する
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