ROOMを使わずにSQLiteを扱う、Androidアプリ開発

はじめに
AndroidアプリでSQLiteを操作する際、最近ではROOMが推奨されている。しかし、ライブラリの設定は面倒であり、特にライブラリを導入する必要性を感じない場合もある。そうした状況では、デフォルトで提供されているSQLiteへのアクセス方法を使用して操作するのが一つの解決策だ。
ここでは、現在製作中のメモアプリを例に、AndroidアプリでSQLiteのデータベースを扱うまでを、ステップごとに解説していく。
ゴール
ここで紹介するアーキテクチャというか、MainActivity
から
SQLite Database
までのフローは次のイメージで制作してく。
【ステップ1】SQLiteOpenHelperを継承したクラスを作る
ソースコード
package com.apppppp.supermemo2.data
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
const val SQL_CREATE_ENTRIES = """
CREATE TABLE `memo` (
`id` INTEGER PRIMARY KEY,
`parent_id` INTEGER,
`title` TEXT DEFAULT NULL,
`done_flg` INTEGER DEFAULT 0
);
"""
const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS memo"
class DbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
.execSQL(SQL_CREATE_ENTRIES)
db}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// db.execSQL(SQL_DELETE_ENTRIES)
// onCreate(db)
if (oldVersion < 2) {
// バージョン1から2へのアップグレード処理
val addCreatedColumn = "ALTER TABLE memo ADD COLUMN `created` TEXT DEFAULT NULL"
val addModifiedColumn = "ALTER TABLE memo ADD COLUMN `modified` TEXT DEFAULT NULL"
.execSQL(addCreatedColumn)
db.execSQL(addModifiedColumn)
db}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
(db, oldVersion, newVersion)
onUpgrade}
companion object {
const val DATABASE_VERSION = 2
const val DATABASE_NAME = "SuperMemo2.db"
}
}
解説
このコードは、SQLiteOpenHelper
を継承したDbHelper
クラスを定義し、データベース作成とバージョン管理のロジックを実装している。
SQL_CREATE_ENTRIES
定数では、memo
テーブルの作成に必要なSQL文を定義。onCreate
メソッド内で、データベースが初めて作成される際にmemo
テーブルを作成するためのSQL文が実行される。
onUpgrade
メソッドでは、データベースのバージョンが上がった場合の処理を記述。ここでは、バージョン1から2へのアップグレード時にcreated
列とmodified
列をmemo
テーブルに追加する処理を行う。
DATABASE_VERSION
とDATABASE_NAME
の定数は、それぞれデータベースのバージョンとファイル名を指定するために使われる。
【ステップ2】Repositoryクラスでデータソースを抽象化する
ソースコード
class MemoRepository(private val context: Context) {
private val dbHelper: DbHelper = DbHelper(context)
val db = dbHelper.writableDatabase
/**
* メモを新規作成
*
* @param title メモのタイトル
* @param parentId 親メモのID
* @return 作成したメモ
*/
fun insertMemo(title: String, parentId:Int? = null): Memo {
val now = LocalDateTime.now()
val formattedDate = now.formatToString() // "2023-01-01T12:00:00" など
val values = ContentValues().apply {
("parent_id", parentId)
put("title", title)
put("done_flg", 0)
put("created", formattedDate)
put}
// 新しい行を挿入し、その行のIDを返す
val newRowId = db.insert("memo", null, values)
return Memo(
= newRowId.toInt(),
id = parentId,
parentId = title,
title = false
doneFlg )
}
/**
* parentIdのdoneFlgを更新
*/
fun updateDoneFlg(memoId: Int, doneFlg:Boolean) {
val now = LocalDateTime.now()
val formattedDate = now.formatToString() // "2023-01-01T12:00:00" など
val values = ContentValues().apply {
("done_flg", if (doneFlg) 1 else 0)
put("modified", formattedDate)
put}
.update("memo", values, "id = ?", arrayOf(memoId.toString()))
db}
/**
* parentIdに一致するメモを取得
*
* @param parentId 親メモのID
* @return parentIdに一致するメモのリスト
*/
fun searchMemos(parentId: Int?): List<Memo> {
val memos = mutableListOf<Memo>()
// parentIdがnullかどうかに応じたSQLクエリ
val sql = if (parentId == null) {
"SELECT * FROM memo WHERE parent_id IS NULL ORDER BY created ASC"
} else {
"SELECT * FROM memo WHERE parent_id = ? ORDER BY created ASC"
}
// rawQueryメソッドを使用してSQLクエリを実行
val cursor = if (parentId == null) {
.rawQuery(sql, null)
db} else {
.rawQuery(sql, arrayOf(parentId.toString()))
db}
(cursor) {
withwhile (moveToNext()) {
val id = getInt(getColumnIndexOrThrow("id"))
val obtainedParentId = if (!isNull(getColumnIndexOrThrow("parent_id"))) getInt(getColumnIndexOrThrow("parent_id")) else null
val title = getString(getColumnIndexOrThrow("title"))
val doneFlg = getInt(getColumnIndexOrThrow("done_flg")) != 0
.add(Memo(id, obtainedParentId, title, doneFlg))
memos}
()
close}
return memos
}
/**
* dbを閉じる
*/
fun close() {
.close()
dbHelper}
}
解説
データベース操作に必要なメソッドをRepositoryに実装していく。
このコードでは、SQLiteデータベースを使ってメモデータのCRUD操作(作成、読み取り、更新、削除)を行うためのメソッドを提供している。DbHelper
クラスのインスタンスを使用してデータベースとの接続を管理し、メモデータを操作する。
close
メソッドは、DbHelper
を通じてデータベース接続を閉じる。これはリソースの解放を確実にするために重要である。
このクラスは、メモアプリケーションのデータ層を担い、アプリケーションの残りの部分がデータベースに直接アクセスすることなく、必要なデータ操作を行えるようにする。MemoRepository
を通じて、アプリケーションのロジックはデータベースの詳細から抽象化され、データ管理の責任がはっきりと分離される。
【ステップ3】コンテナを作って、ViewModelへDIする
ソースコード
object AppContainer {
private var repository: MemoRepository? = null
fun initialize(context: Context) {
= MemoRepository(context)
repository }
private val uiStateRepository: UiStateRepository by lazy {
()
UiStateRepository}
val viewModel by lazy {
if (repository == null) throw IllegalStateException("Repository must be initialized")
(repository!!, uiStateRepository)
MemoViewModel}
}
class MainActivity : ComponentActivity() {
private lateinit var memoRepository: MemoRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
.initialize(applicationContext)
AppContainerval viewModel = AppContainer.viewModel
{
setContent {
AppTheme (viewModel)
MainApp}
}
}
override fun onDestroy() {
.close()
memoRepositorysuper.onDestroy()
}
}
解説
AppContainer
は、プログラム実行中にただ一つのインスタンスしか存在しないシングルトンオブジェクトとして初期化される。MainActivity
から渡されるcontext
でMemoRepository
インスタンスを初期化し、ViewModel
へ依存性注入(Dependency
Injection、DI)している。DIを使うことで、コードのテスト性が向上し、モジュール間の結合が緩くなり、拡張性とメンテナンス性が向上される。
MainActivity
のライフサイクルの
onCreate
メソッド内で、AppContainer
を使用してアプリケーションの依存関係を初期化し、ViewModelを取得する。その後、setContent
を呼び出してUIを設定し、AppTheme
を適用してMainApp
コンポーザブル関数をレンダリングする。
onDestroy
メソッドでは、memoRepository
を閉じることで、データベースとの接続を適切に解放し、リソースのクリーンアップを行う。
【ステップ4】ViewModelからRepositoryを利用する
ソースコード
class MemoViewModel(
private val dbMemoRepos: MemoRepository,
private val uiStateRepository: UiStateRepository
) : ViewModel() {
val uiState: StateFlow<UiState> = uiStateRepository.uiState
{
init .d("mopi", "MemoViewModel init")
Log()
setParentMemos}
/**
* 親メモのuiStateを更新
*/
fun setParentMemos() {
.launch {
viewModelScopeval memos = dbMemoRepos.searchMemos()
...
}
}
/**
* リポジトリへ新規メモを追加
*/
fun insertMemo(title: String, parentId: Int? = null) {
.launch {
viewModelScopeval newMemo = dbMemoRepos.insertMemo(title, parentId)
}
}
// メモのチェックフラグを更新
fun updateMemoCheckedState(memoId: Int, checked: Boolean) {
.launch {
viewModelScope.updateDoneFlg(memoId, checked)
dbMemoRepos.setChildMemoDoneFlg(memoId, checked)
uiStateRepository}
}
...
}
解説
このコードは、アプリのビジネスロジックとUIの状態管理を担当するViewModelの一部を示している。MemoRepository
とUiStateRepository
の二つのリポジトリに依存しており、これらを介してデータベースの操作やUI状態の更新を行っている。
ここでは詳しく解説しないが、
UiStateRepository
はuiState
プロパティを通じて、UIの状態をリアクティブに反映させている。
Repository
を作成したことで、SQLite操作をViewModelで実装せずに済み、コードが簡潔になり役割分担が明確になった。
関連記事
- iOSアプリ開発でSQLiteを使う FMDB
- AndroidのカメラをUIに表示する【Androidアプリ開発】
- Java開発が爆速に!超便利なJShellの使い方
- Laravel で実行されたSQLをログに表示したい
- SQLiteでデータベースをはじめる macOS/Linux/Unix