android Room 설정

개배박발불지은만자·2024년 10월 26일
1

안드로이드에서 sqlite에서 Room을 덥기

아마 아직도 sqlite 만을 사용하는 개발자가 있을지 모르겠다.
나는 sqlite 에서 room 으로 옮겨온지가 꽤 지나서 sqlite는 이제 가물거린다.
언젠가는 room 설정도 가물거릴까 걱정하는 마음에 현제 사용하는 설정을 기록해 두려한다.

libs.versions.toml

[versions]
room = "2.6.1"
[libraries]
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

build.gradle.kts(:app)

plugins {
//...
    alias(libs.plugins.ksp)
  // Existing plugins
  kotlin("plugin.serialization") version "2.0.21"
}
android{
ksp {
    arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
  }
}
dependencies {
  //room database
  // https://developer.android.com/jetpack/androidx/releases/room?hl=ko
  implementation(libs.androidx.room.runtime)
  ksp(libs.androidx.room.compiler)
  implementation(libs.androidx.room.ktx)
}

class RoomSchemaArgProvider(
  @get:InputDirectory
  @get:PathSensitive(PathSensitivity.RELATIVE)
  val schemaDir: File
) : CommandLineArgumentProvider {
  @Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE") // Kotlin 의 Iterable 와 override 한 java 의 Iterable를 다른것으로 인식하는 버그 수정
  override fun asArguments(): Iterable<String> {
    return listOf("room.schemaLocation=${schemaDir.path}") // ksp
  }
}

Database 객체설정

@TypeConverters(Converters::class)
@Database(
  version = 1,
  entities = [
    LunaInfoEntity::class, SpcdEntity::class, ScheduleEntity::class, ShiftEntity::class,
    ShiftPatternEntity::class,ShiftAddonEntity::class,
    UserEntity::class,
             ],
  exportSchema = false // Todo !!! 배포시 true 로 변경
)

abstract class EodeunCalendarMemo : RoomDatabase() {
  companion object {
    @Volatile  // 중요 정보로 Ram 을 이용함
    private var INSTANCE: EodeunCalendarMemo? = null

    /**
     * Get instance
     * if the INSTANCE is not null, then return it,
     * if it is, then create the database and save in instance variable then return it.
     *
     * @param context
     * @return
     */
    fun getInstance(context: Context): EodeunCalendarMemo {
      return INSTANCE ?: synchronized(this) {
        val instance = Room.databaseBuilder(
          context.applicationContext, EodeunCalendarMemo::class.java, "EodeunCalendarMemo.db"
        ).fallbackToDestructiveMigration() // todo !!! 배포시 삭제. 스키마 관리 포기 하고 강제로 진행
          .build()
        INSTANCE = instance
        instance // return instance
      } //
    } // fun
  } // companion object

  abstract fun lunaInfoDao(): LunaInfoDao
  abstract fun spcdDao(): SpcdDao
  abstract fun scheduleDao(): ScheduleDao
  abstract fun shiftDao(): ShiftDao
  abstract fun shiftPatternDao(): ShiftPatternDao
  abstract fun userDao(): UserDao
  abstract fun shiftAddOnDao(): ShiftAddonDao
}

convertor

Room은 Int,String,Long 같은 프라임타입만 지원하기에 저장시 변환해서 저장해야 한다.

Room에서 지원하는 Type

  • NULL
  • INTEGER (Int, Long)
  • REAL (Float, Double)
  • TEXT (String)
  • BLOB (ByteArray)
class Converters {

  @TypeConverter
  fun intToTermTime(value: Int): TermTime {
    return TermTime(value)
  }

  @TypeConverter
  fun termTimeToInt(value: TermTime): Int {
    return value.toInt()
  }

  @TypeConverter
  fun uLongToLong(value:ULong):Long = value.toLong()

  @TypeConverter
  fun longToULong(value:Long):ULong = value.toULong()

  @TypeConverter
  fun listStringToString(value: List<String>): String = Gson().toJson(value)

  @TypeConverter
  fun stringToShiftEntity(value: String?): ShiftEntity? {
    return value?.let { Gson().fromJson(it, ShiftEntity::class.java) }
  }

  @TypeConverter
  fun shiftEntityToString(value: ShiftEntity?): String? {
    return value?.let { Gson().toJson(it) }
  }

  @TypeConverter
  fun stringToShiftEvent(value: String?): ShiftEvent? {
    return value?.let { Gson().fromJson(it, ShiftEvent::class.java) }
  }

  @TypeConverter
  fun shiftEntityToString(value: ShiftEvent?): String? {
    return value?.let { Gson().toJson(it) }
  } // shiftEntityToString

  @TypeConverter
  fun shiftEventListToString(value: List<ShiftEvent>): String {
    return Gson().toJson(value)
  } // List<ShiftEvent> -> String

  @TypeConverter
  fun stringToShiftEventList(value: String): List<ShiftEvent> {
    return Gson().fromJson(
      value, object : TypeToken<List<ShiftEvent>>() {}.type
    ) // return Gson().fromJson(value, Array<ShiftEvent>::class.java).toList() // Array -> List 로 기능은 같은데 메모리를 약간 더 씀
  } // String -> List<ShiftEvent>

  @TypeConverter
  fun shiftPatternToString(value: ShiftPattern?): String? {
    return value?.let { Gson().toJson(it) }
  } // ShiftPattern -> String

  @TypeConverter
  fun stringToShiftPattern(value: String?): ShiftPattern? {
    return value?.let { Gson().fromJson(it, ShiftPattern::class.java) }
  }

  @TypeConverter
  fun workHistoryItemListToString(value: List<WorkHistoryItem>): String? {
    return value.let { Gson().toJson(it) }
  }

  @TypeConverter
  fun stringToWorkHistoryItemList(value: String?): List<WorkHistoryItem>? {
    return Gson().fromJson(
      value, object : TypeToken<List<WorkHistoryItem>>() {}.type
    )
  }
}

@Entity

Table 을 @Entity(tableName="String")으로 저장한다.

@Entity(tableName = "luna_info_table")
data class LunaInfoEntity(
  @PrimaryKey(autoGenerate = true)
  val rowId: Long = 0L, // 값이 0 이면 자동 증가
  // 양력 epoch 값
  val epoch: Long = 0L,
  // 음력정보
  val lunLeapmonth: Boolean = false, // 음력 평달 여부
  val lunYear: Int = 0,
  val lunMonth: Int = 0,
  val lunDay: Int = 0,
  val solLeapyear: Boolean = false,
  //  val solYear: Int = 0,
  //  val solMonth: Int = 0,
  //  val solDay: Int = 0,
  val lunIljin: String = "",
  val lunNday: Int = 0,
  val lunSecha: String = "",
  val lunWolgeon: String = "",
  val solJd: Long = 0L,
  //  val solWeek: String = ""
) {
  fun toLog(): String {
    val solDate = LocalDate.ofEpochDay(epoch)
    return "solYMD:${solDate.year}.${solDate.monthValue}.${solDate.dayOfMonth}.(${solDate.dayOfWeek}요일[${if (solLeapyear) "" else ""}년])는 " +
            "lunYMD:${lunYear}.${lunMonth}.${lunDay}.(${if (lunLeapmonth) "" else ""}달) (lunIljin:${lunIljin}/lunSecha:${lunSecha}/lunWolgeon:${lunWolgeon})"
  }

  fun toLunaShort():String{
    return "${lunMonth}.${lunDay}."
  }

  fun toLabel(): String {
    return " ${lunYear}년 (${if (lunLeapmonth) "" else ""}) ${lunMonth}${lunDay}일"
  }

  fun toGanJi(): String {
    return "${lunSecha}${lunWolgeon}${lunIljin}일"
  }
}

@Dao

DataAccessObject 는 Interface로 선언한다.

@Dao
interface LunaInfoDao {

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insert(lunaInfoEntity: LunaInfoEntity): Long

  @Query("SELECT EXISTS (SELECT * FROM  luna_info_table WHERE epoch  = :epoch) ")
  suspend fun isExists(epoch:DateLong): Boolean

  @Query("SELECT * FROM  luna_info_table WHERE epoch = :epoch ")
  fun readFlow(epoch:DateLong): Flow<LunaInfoEntity?>

  @Query("SELECT * FROM  luna_info_table WHERE epoch = :dateLong ")
  suspend fun read(dateLong: DateLong): LunaInfoEntity?

  @Query("SELECT * FROM  luna_info_table WHERE epoch = :epoch ")
  fun fetch(epoch:DateLong): Flow<LunaInfoEntity?>

  @Query("SELECT * FROM luna_info_table ")
  fun fetchAll(): Flow<List<LunaInfoEntity>>
}

Repository

DB 는 Repository 를 경유해서 접근한다.
repository 를 interface로 하여 구현체를 별도로 관리한다.

interface LunaRepo {
  suspend fun isExist(dateLong: DateLong): Boolean
  fun fetchFlow(dateLong: DateLong): Flow<LunaInfoEntity?>
  suspend fun isExistMonth(year: Int, month: Int)
}

Repo 구현체
실제처리를 구현하고 간단한 처리를 담당해서 상단의 Viewmodel에서 처리하도록 돕니다.

class LunaRepoImpl @Inject constructor(
  private val lunaDb: LunaDBRepo
) : LunaRepo {
  override suspend fun isExist(dateLong: DateLong): Boolean {
    return lunaDb.isExist(dateLong)
  }

  override fun fetchFlow(dateLong: DateLong): Flow<LunaInfoEntity?> {
    return lunaDb.fetchFlow(dateLong)
  }

  private val _requestedLunaYearMonths = mutableListOf<Int>()

  override suspend fun isExistMonth(year: Int, month: Int) {
    val seed = year * 12 + month
    if (_requestedLunaYearMonths.contains(seed)) return //
    _requestedLunaYearMonths.add(seed)
    if (lunaDb.isExist(
        LocalDate.of(year, month, 1).toEpochDay()
      )
    ) return // Api로 부터 holiday 를 가져 오도록 함
    try {
      DataGoKr.getLunCalInfo(year, month).response?.body?.items?.item?.forEach { it: LunaItem? ->
        it?.let { lunaDb.insert(it.toEntity()) }
      } //forEach
    } catch (e: Exception) {
      Log.e("Jim", "${e.message}")
    } //try-catch
  } //
}

Injection

코드주입을 위해 Hilt를 사용하게 되는데 그 주입설정이다.

@InstallIn(SingletonComponent::class)
@Module
object Injections {

  //   DB 와 관련된 인젝션들
  @Singleton
  @Provides
  fun providesAppDB(@ApplicationContext context: Context): EodeunCalendarMemo {
    return EodeunCalendarMemo.getInstance(context = context)
  }

  /* Dao 와 관련된 인젝션들 */
  @Singleton
  @Provides
  fun provideLunaInfoDao(db: EodeunCalendarMemo): LunaInfoDao {
    return db.lunaInfoDao()
  }

  @Singleton
  @Provides
  fun provideSpcdDao(db: EodeunCalendarMemo): SpcdDao {
    return db.spcdDao()
  }

  @Singleton
  @Provides
  fun provideScheduleDao(db: EodeunCalendarMemo): ScheduleDao {
    return db.scheduleDao()
  }

  @Singleton
  @Provides
  fun provideShiftDao(db: EodeunCalendarMemo): ShiftDao {
    return db.shiftDao()
  }

  @Singleton
  @Provides
  fun provideShiftPatternDao(db: EodeunCalendarMemo): ShiftPatternDao {
    return db.shiftPatternDao()
  }

  @Singleton
  @Provides
  fun provideShiftAddonDao(db: EodeunCalendarMemo): ShiftAddonDao {
    return db.shiftAddOnDao()
  }

  @Singleton
  @Provides
  fun provideUserDao(db: EodeunCalendarMemo): UserDao {
    return db.userDao()
  }

  /**
  Repository 주입
   */

  // It is DB repository
  @Singleton
  @Provides
  fun provideLunaInfoRepository(dao: LunaInfoDao): LunaDBRepo {
    return LunaDBRepoImpl(dao)
  } // LunaDb

  // It is not DB repository (over DB)
  @Singleton
  @Provides
  fun provideLunaRepository(lunaDBRepo: LunaDBRepo): LunaRepo {
    return LunaRepoImpl(lunaDBRepo)
  } // Luna

  // it is DB repository
  @Singleton
  @Provides
  fun provideSpcdRepository(dao: SpcdDao): SpcdDBRepo {
    return SpcdDBRepoImpl(dao)
  }  // SpcdDB

  // It is not DB repository (over DB)
  @Singleton
  @Provides
  fun provideHolidayRepository(spcdRepo: SpcdDBRepo): SpcdRepo {
    return SpcdRepoImpl(spcdRepo)
  } //Spcd

  @Singleton
  @Provides
  fun provideShiftRepository(shiftDao: ShiftDao): ShiftRepo {
    return ShiftRepoImpl(shiftDao)
  }

  @Singleton
  @Provides
  fun provideScheduleRepository(scheduleDao: ScheduleDao): ScheduleRepo {
    return ScheduleRepoImpl(scheduleDao)
  }

  @Singleton
  @Provides
  fun provideShiftPatternRepository(dao: ShiftPatternDao): ShiftPatternRepo {
    return ShiftPatternRepoImpl(dao)
  }

  @Singleton
  @Provides
  fun provideShiftAddonRepository(dao: ShiftAddonDao): ShiftAddonRepo {
    return ShiftAddonRepoImpl(dao)
  }

  @Singleton
  @Provides
  fun provideUserRepository(dao: UserDao): UserRepo {
    return UserRepoImpl(dao)
  }

  /**
   * Viewmodel 주입
   */

  @Singleton
  @Provides
  fun provideCoreViewmodel(
    spcdRepo: SpcdRepo,
    lunaRepo: LunaRepo,
    userRepo: UserRepo,
    scheduleRepo: ScheduleRepo
  ): CoreVM {
    return CoreVM(spcdRepo, lunaRepo, userRepo, scheduleRepo)
  }
}

0개의 댓글