Jetpack Compose - Room, DataStore Practice

2024. 3. 29. 20:13IT/Android

728x90

<미리 보기>

 

 

<소스 코드>

https://github.com/SeongHyunJeon/android-kotlin-practice/tree/b127e7b047e34e1373e4a144b2cb3470915ac2af/FlightSearch

 

 

<정리>

Flow<T>.collect() - 값이 변경될 때마다 수집하고 방출한다.

Flow<T>.single() - 가장 첫 번째 값을 수집하고 방출하지만, 비어 있거나 두 개 이상의 값을 수집하면 예외를 발생시킨다.

Flow<T>.first() - 가장 첫 번째 값만 수집하고 방출하는데 이후의 값은 무시하고 비어 있는 경우 예외를 발생시킨다.

 

위의 프로그램에선 DataStore<Preferences>를 사용하여 텍스트 필드의 값을 저장하고 프로그램이 시작할 때 초기화 함수로 단 한번만 수집하여 출력하면 되기 때문에 Flow<T>.first()를 사용했다.

init {
    initialiseFlight()
}

private fun initialiseFlight() {
    viewModelScope.launch {
        _uiState.update { flightUiState ->
            flightUiState.copy(
                currentText = flightsPreferencesAccessor.currentText.first()
            ) //flightsPreferencesAccessor.currentText은 Flow<String>을 반환.
        }
        updateFavoriteAirports()
        updateRelatedAirports(_uiState.value.currentText)
    }
}

Room 데이터 베이스에 Kotlin 기본 타입이 아닌 사용자 지정 타입 데이터의 삽입은 변환기를 사용하면 가능하다.

 

Favorite타입의 객체를 데이터 베이스에 삽입하려고 하지만 해당 필드의 타입이 Airport인 관계로 컴파일러는 에러를 발생시킨다.

@Entity(tableName = "airport")
data class Airport(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 1,
    @ColumnInfo(name = "iata_code")
    val iataCode: String,
    val name: String,
    val passengers: Int
)

@Entity(tableName = "favorite")
data class Favorite(
    @PrimaryKey
    val id: String = "",
    @ColumnInfo(name = "departure_airport")
    val departureAirport: Airport,
    @ColumnInfo(name = "destination_airport")
    val destinationAirport: Airport
)

 

컴파일러가 이해할 수 있도록 변환기를 정의하는데, 쉽게 말해 사용자 지정 타입 객체의 프로퍼티들을 하나의 문자열로 변환하여 저장했다가 꺼낼 때 이 문자열을 쪼개서 다시 사용자 타입 객체를 생성하는 방식이다. Airport 객체의 프로퍼티 값들을 하나의 문자열로 변환할 때 각각을 쉼표로 구분하였고, 이를 기준으로 다시 Airport 객체를 생성한다.

class AirportConverter {
    @TypeConverter
    fun fromAirport(airport: Airport): String {
        return "${airport.id},${airport.iataCode},${airport.name},${airport.passengers}"
    } //저장할 때 사용되는 변환기.

    @TypeConverter
    fun toAirport(value: String): Airport {
        val parts = value.split(",")
        return Airport(parts[0].toInt(), parts[1], parts[2], parts[3].toInt())
    } //꺼낼 때 사용되는 변환기.
}

 

정의한 변환기를 데이터 베이스에 어노테이션을 사용하여 적용하면 끝.

@Database(entities = [Airport::class, Favorite::class], version = 4, exportSchema = false)
@TypeConverters(AirportConverter::class)
abstract class FlightDatabase: RoomDatabase() {
    abstract fun airportDao(): AirportDao
    abstract fun favoriteDao(): FavoriteDao

	//...생략
}

파라미터로 값을 받아 해당 텍스트를 포함하는 쿼리문의 작성은 '||' 기호를 사용하면 가능하다. ( '+' 기호는 안됨 )

@Dao
interface AirportDao {
    @Query("""
        SELECT * FROM airport
        WHERE name LIKE '%' || :text || '%' OR iata_code LIKE '%' || :text || '%'
       """)
    suspend fun getRelatedAirports(text: String): List<Airport>

    //...생략
}

 

테이블 정의에서 자동으로 증분하는 기본 키 설정 어노테이션과 초기 값을 설정해도 해당 객체를 생성할 때 기본 키 값을 전달하지 않으면 해당 설정은 적용되지 않는다.

 

이렇게 자동 증분 키 생성 어노테이션과 초기 값을 설정하고,

@Entity(tableName = "favorite")
data class Favorite(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "departure_airport")
    val departureAirport: Airport,
    @ColumnInfo(name = "destination_airport")
    val destinationAirport: Airport
)

 

기본 키를 제외하고 객체를 생성하면,

Favorite(departureAirport = this, destinationAirport = airport)

 

데이터 베이스는 해당 객체를 생성할 때 증분 되는 키의 기준이 없다고 생각해 초기 값을 적용한다. 초기 값을 기준으로 증분되는 키를 생성하지 않으니 꼭 기본 키의 값을 전달하도록 하자.


테이블의 스키마를 변경은 마이그레이션을 정의를 통해 가능하다.

자세한 내용은 https://medium.com/androiddevelopers/understanding-migrations-with-room-f01e04b07929 참고.

@Database(entities = [Airport::class, Favorite::class], version = 2, exportSchema = false)
@TypeConverters(AirportConverter::class)
abstract class FlightDatabase: RoomDatabase() {
    abstract fun airportDao(): AirportDao
    abstract fun favoriteDao(): FavoriteDao

    companion object {
        @Volatile
        private var INSTANCE: FlightDatabase? = null

        val migration1to2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("CREATE TABLE new_favorite (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, departureAirport TEXT NOT NULL, destinationAirport TEXT NOT NULL)")
                database.execSQL("DROP TABLE favorite")
                database.execSQL("ALTER TABLE new_favorite RENAME TO favorite")
            }
        }

        fun getDatabase(context: Context): FlightDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(context, FlightDatabase::class.java, "flight_search")
                    .createFromAsset("database/flight_search.db")
                    .addMigrations(migration1to2)
                    .build()
                    .also { INSTANCE = it }
            }
        }
    }
}

 

비동기적으로 함수를 호출하여 해당 결과를 받아 반환하는 함수를 정의하는 바보는 본인 하나로 충분하다.

fun favoriteIntoAirports(favorite: Favorite): Pair<Airport, Airport> {
        lateinit var airports: Pair<Airport, Airport>
        viewModelScope.launch {
            airports = Pair(flightsAccessor.getAirport(favorite.departureCode), flightsAccessor.getAirport(favorite.destinationCode))
        }
        return airports
}

 

airports는 초기화 되지 않고 반환된다.

728x90

'IT > Android' 카테고리의 다른 글

Jetpack Compose - WorkManager Practice  (0) 2024.04.03
Jetpack Compose - WorkManager  (0) 2024.04.03
Jetpack Compose - DataStore<Preferences>  (0) 2024.03.20
Jetpack Compose - Room Practice  (0) 2024.03.19
Jetpack Compose - Room  (0) 2024.03.19