Memoアプリ自体の基礎的な実装はこちらを参考に。

今回はアプリを開き直してもデータが保存されるようにRoom を使用してローカルデータベースにデータを保存するようにします。
Dependencies
build.gradle.kts (アプリ単位)
以下を追加
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
// 追加
id("com.google.dagger.hilt.android") version "2.51.1" apply false
}
build.gradle.kts (モジュール単位)
dependenciesに以下を追加、syncする。
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
// 追加 -- ここから
// ViewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)
// Hilt
implementation(libs.hilt.android)
kapt(libs.hilt.android.compiler)
// Room
implementation(libs.androidx.room.runtime)
annotationProcessor(libs.androidx.room.compiler)
kapt(libs.androidx.room.compiler)
implementation("androidx.room:room-ktx:2.2.1")
// Coroutines
implementation(libs.kotlinx.coroutines.android)
// 追加 -- ここまで
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
Util系 (UUID, DateなどをDatabaseに保存するための変換処理)
Utils.kt
fun formatDate(time: Long): String {
// タイムスタンプ(ミリ秒)からDateオブジェクトを生成
val date = Date(time)
// 表示形式を指定するSimpleDateFormatオブジェクトを生成
// "EEE, d MM hh:mm aaa" は、曜日, 日 月 時:分 午前/午後 を表す形式
// Locale.getDefault() は、システムのデフォルトのロケールを使用
val format = SimpleDateFormat("EEE, d MM hh:mm aaa", Locale.getDefault())
// Dateオブジェクトを指定された形式の文字列に変換して返す
return format.format(date)
}
Converter.kt
class DateConverter {
// Date型をLong型のタイムスタンプに変換する関数
@TypeConverter
fun timeStampFromDate(date: Date): Long {
return date.time // Dateオブジェクトをミリ秒単位のタイムスタンプに変換
}
// Long型のタイムスタンプをDate型に変換する関数
@TypeConverter
fun dateToTimeStamp(timeStamp: Long): Date? {
return Date(timeStamp) // タイムスタンプをDateオブジェクトに変換
}
}
class UUIDConverter {
// UUID型をString型に変換する関数
@TypeConverter
fun fromUUID(uuid: UUID): String? {
return uuid.toString() // UUIDオブジェクトを文字列に変換
}
// String型をUUID型に変換する関数
@TypeConverter
fun toUUID(uuidString: String?): UUID? {
return UUID.fromString(uuidString) // 文字列をUUIDオブジェクトに変換
}
}
Database管理ファイル系
NoteDatabase.kt
Noteエンティティを扱うためのデータベースを定義し、データへのアクセスを提供するファイル。
TypeConvertersとNoteDatabaseDaoでエラーが出ますが以下で作成するので一旦スルーでokです。
@Database(entities = [Note::class], version = 1, exportSchema = false)
@TypeConverters(DateConverter::class, UUIDConverter::class)
abstract class NoteDatabase: RoomDatabase() {
abstract fun noteDao(): NoteDatabaseDao
}
NoteDatabaseDao
データベース(倉庫)に対して、ノート(情報)を出し入れするための設計図(DAO)というイメージ。
notes_tblも以下で定義していくので一旦スルーでOK。
@Dao
interface NoteDatabaseDao {
@Query("SELECT * FROM notes_tbl")
fun getNotes(): Flow<List<Note>>
@Query("SELECT * FROM notes_tbl WHERE id = :id")
suspend fun getNoteId(id: String): Note
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: Note)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(note: Note)
@Query("DELETE FROM notes_tbl")
suspend fun deleteAll()
@Delete
suspend fun deleteNote(note: Note)
}
DI (Dependency Injection)系
AppModule.kt
@InstallIn(SingletonComponent::class) // アプリケーションのライフサイクル全体でインスタンスを保持
@Module
object AppModule {
@Singleton
@Provides // 依存性を提供するための関数であることを示すアノテーション
// NoteDatabaseからNoteDatabaseDaoを生成し、提供
fun provideNotesDao(noteDatabase: NoteDatabase): NoteDatabaseDao = noteDatabase.noteDao()
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context): NoteDatabase =
Room.databaseBuilder(
context, // アプリケーションのコンテキスト
NoteDatabase::class.java, // データベースクラス
"notes_db" // データベース名
)
.fallbackToDestructiveMigration() // スキーマ変更時にデータベースを再構築
.build() // データベースインスタンスを生成し、提供
}
Repository系
NoteRepository
データベース操作の仲介ポジション。
class NoteRepository @Inject constructor(private val noteDatabaseDao: NoteDatabaseDao) {
suspend fun addNote(note: Note) = noteDatabaseDao.insert(note = note)
suspend fun updateNote(note: Note) = noteDatabaseDao.update(note)
suspend fun deleteNote(note: Note) = noteDatabaseDao.deleteNote(note)
suspend fun deleteAllNotes(note: Note) = noteDatabaseDao.deleteAll()
fun getAllNotes(): Flow<List<Note>> = noteDatabaseDao.getNotes().flowOn(Dispatchers.IO).conflate()
}
Model
Note.kt
データベースとして扱えるようにtable名やprimarykeyなどを指定する。
@Entity(tableName = "notes_tbl")
data class Note(
@PrimaryKey
val id: UUID = UUID.randomUUID(),
@ColumnInfo(name = "note_title")
val title: String,
@ColumnInfo(name = "note_description")
val description: String,
@ColumnInfo(name = "note_entryDate")
val entryDate: Date = Date.from(Instant.now())
)
ViewModel
NoteViewModel.kt
@HiltViewModel // HiltにViewModelであることを伝えるアノテーション
class NoteViewModel @Inject constructor(private val repository: NoteRepository): ViewModel() {
private val _noteList = MutableStateFlow<List<Note>>(emptyList()) // ノートのリストを保持するMutableStateFlow
val noteList = _noteList.asStateFlow() // 外部からの変更を防ぐために、asStateFlowで公開
init {
// ViewModelの初期化時に実行される処理
viewModelScope.launch(Dispatchers.IO) {
// IOスレッドでデータベースからノートのリストを取得
repository.getAllNotes()
.distinctUntilChanged() // 同じリストが連続して流れてくるのを防ぐ
.collect { listOfNotes ->
// 取得したノートのリストをcollectで処理
if (listOfNotes.isEmpty()) {
// リストが空の場合、ログを出力
Log.d("Empty", "Empty List")
} else {
// リストが空でない場合、_noteListの値を更新
_noteList.value = listOfNotes
}
}
}
}
// ノートを追加する関数
fun addNote(note: Note) = viewModelScope.launch {
repository.addNote(note) // リポジトリのaddNote関数を呼び出し
}
// ノートを更新する関数
fun updateNote(note: Note) = viewModelScope.launch {
repository.updateNote(note) // リポジトリのupdateNote関数を呼び出し
}
// ノートを削除する関数
fun removeNote(note: Note) = viewModelScope.launch {
repository.deleteNote(note) // リポジトリのdeleteNote関数を呼び出し
}
}
View
MainActivity.kt
@AndroidEntryPoint // HiltにこのActivityで依存性注入を行うことを伝えるアノテーション
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyNoteAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
val noteViewModel = viewModel<NoteViewModel>()
NotesApp(noteViewModel = noteViewModel)
}
}
}
}
}
@Composable
private fun NotesApp(noteViewModel: NoteViewModel) {
val notesList = noteViewModel.noteList.collectAsState().value
NoteScreen(
notes = notesList,
// 追加処理
onAddNote = {
noteViewModel.addNote(it)
},
// 削除処理
onRemoveNote = {
noteViewModel.removeNote(it)
}
)
}
NoteScreen.kt
日付表示部分のTextを以下に置き換え
Text(
text = formatDate(note.entryDate.time),
style = MaterialTheme.typography.bodySmall
)
これでビルドを行い、データの取得 / 追加 / 削除ができることを確認。
まとめ
それぞれの機能の役割をポケモンに例えてみる。
Room Database: ポケモンセンター
- ポケモンセンターは、ポケモンを預かったり、回復させたり、道具を管理したりする場所です。Room Databaseも、アプリのデータを永続的に保存し、管理する役割を担います。
Data Access Objects (DAO): ジョーイさん
- ジョーイさんは、ポケモンセンターでポケモンの回復や道具の受け渡しなど、トレーナーとポケモンセンターの仲介役を務めます。DAOも、アプリとデータベースの間に立ち、データの読み書きなどの操作を仲介します。
Entities: ポケモン
- ポケモンは、それぞれ固有のデータ(名前、種類、技など)を持っています。Entitiesも、アプリで扱うデータの構造を定義し、各データの属性(フィールド)を保持します。
Rest of The App: トレーナー
- トレーナーは、ポケモンと共に冒険し、様々な指示を出します。Rest of The Appも、アプリの主要な機能を提供し、ユーザーの操作に応じてデータを操作します。