데이터를 안드로이드인 앱 내에서 뭔가 데이터를 사용하고 날아가는 것이 아닌, 앱을 종료하거나 핸드폰을 재시작해도 데이터가 저장되어 있도록 하기위한 방법은 세가지가 존재한다.
간단하게 저장할 수 있는 정보, 예를 들어 자동로그인을 할 것인지에 대한 정보가 폰 안에 저장되어 있어야 앱 실행 시 자동 저장 여부를 불러와 자동 로그인에 대한 정보를 Preference
라는 것을 통해서 저장한다. XML 포멧으로 텍스트 파일에 키-값 세트로 정보를 영구적으로 저장하는 특징이 있다. (알람-true / 알람-false)
응용프로그램 내의
Activity
간에 공유하며, 한쪽Activity
에서 수정하면 다른Activity
에서도 수정된 값을 읽을 수 있다 !
👉 고유 정보로 외부에서는 읽는 것은 불가능
여러가지 데이터를 한꺼번에 저장할 경우 사용한다. 인자로는 name과 mode가 있다. name은 preference 데이터를 저장할 XML 파일의 이름이고, mode는 파일의 공유 모드를 설정해주면 된다.
val sharedPref = activity?.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE)
한 개의 데이터를 저장할 경우 사용한다. 인자로는 mode만 필요하며, 생성한 activtiy의 전용이기 때문에 같은 패키지 내의 다른 activity를 읽는 것은 불가능하다.
val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)
실행했을 때 android!!studio!!
를 입력하고 데이터 저장 버튼을 누른 뒤, 재실행하면 EditText에 그대로 android!!studio!!
가 나오는걸 확인할 수 있다.
data 폴더 안의 data 폴더 안의 현재 생성한 프로젝트의 폴더안의 shared_prefs
를 누르고 pref.xml
을 확인해보면 안에 입력한 텍스트가 저장된 것을 확인할 수 있다.
가볍게 만드는 DB로 SQLite
를 쉽게 사용할 수 있는 DB
의 객체 Mapping
라이브러리로, Query의 경우에도 직접 작성하지 않아도 쉽게 만들어준다는 특징이 있다.
Query
결과를 LivedData
로 하여 DB
가 변경될 때마다 쉽게 UI
를 변경할 수 있다!
@Database
: 클래스를 DB로 지정하는 annotation
, RoomDatabase
를 상속받은 클래스여야 하고, Room.databaseBuilder
를 이용해서 인스턴스를 생성한다.
@Entity
: 클래스를 테이블 스키마로 지정하는 annotation
@Dao
: 클래스를 DAO(Data Access Object)로 지정하는 annotation
Room을 만들 때, 이 3요소는 꼭 필요하다!
여기 안드로이드 공식 문서를 접속해서 Room의 최신화 버전을 체크하고 사용할 것!
plugins {
id 'kotlin-kapt'
}
dependencies {
// room_version은 공식문서에서 항상 최신화 버전으로 사용
val room_version = "2.5.0"
implementation"androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-guava:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
}
Entity는 아까정리했던대로 테이블 스키마를 정의해줄 것이다.
CREATE TABLE student_table(student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
여기서 student_id
는 INTEGER PRIMARY KEY
로 설정할건데 PRIMARY KEY는 유니크한 값으로 중복되지 않는 값을 말하므로 INTEGER PRIMARY KEY는 중복되지 않는 정수형 값이라 생각하면 되고, name
에는 TEXT NOT NULL
로 null이 들어갈 수 없다는 뜻이다.
위 스키마 정의를 Entity 데이터 클래스로 만들면 다음과 같다.
// 테이블 이름 : student_table로 지정
@Entity(tableName = "student_table")
data class Student {
@PrimaryKey
@ColumnInfo(name = "student_id")
val id : Int,
val name : String
}
DAO는 interface
나 abstract class
로 정의해야하며 Annotation에는 SQL쿼리를 정의하고 이 쿼리를 위한 메소드를 선언해줘야 한다. annotation으로는 @Insert
, @Update
, @Delete
,@Query
가 존재한다.
@Query("SELECT * from table") fun getAllData() : List<Data>
@Insert
, @Update
, @Delete
는 SQL 쿼리를 따로 작성하지 않아도 컴파일러가 자동으로 생성해주며, @Insert
, @Update
는 key가 중복되는 경우의 처리를 위해 onConflict
를 지정할수 있다는 특징을 가지고 있다.
즉, @Query를 제외한 annotation은 쿼리를 따로 작성하지 않아도 자동 생성된다!
OnConflictStrategy.ABORT : key 충돌 시 종료
OnConflictStrategy.IGNORE : key 충돌 무시
OnConflictStrategy.REPLACE : key 충돌 시 새로운 데이터로 변경
@Update
와 @Delete
는 고유키값인 primary key
에 해당되는 tuple
(셀 수 있는 수량의 순서 있는 열거)을 찾아서 변경하거나 삭제한다.
@Query
로 리턴되는 데이터의 타입을 LiveData<>
로 하면, 이후에 이 데이터가 업데이트될 때 메인쪽에서 Obsserver를 걸어두면 업데이트 된 시점을 따로 호출하지 않아도 자동으로 업데이트 되는 것을 이용할 수 있다. 참고로, LiveData
는 비동기적으로 동작하기 때문에 coroutine
으로 할 필요가 없다.
@Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>
@Query
에서 SQL
을 정의할 때 메소드의 인자를 사용할 수도 있다. @Query
부분은 내 주전공인 빅데이터 전공 수업을 들을 때 SQL
을 조금 사용해봐서 익숙한 편인 것 같다.
@Query("SELECT * FROM student_table WHERE name = ":name")
supsend fun getStudentBYname($name : String) : List<Student>
여기서 suspend
를 붙이는 이유는 동시실행을 위한 coroutine
사용때문이다.
@Dao
interface MyDAO {
@Insert(onConflict = OnConflictStratgy.REAPLCE) // insert 충돌 시 데이터 교체
@Query("SELECT * FROM student_table")
fun getAllStudents() : LiveData<List<Student>>
@Query("SELECT * FROM student_table WHERE name = ":name")
suspend fun getStudentByName(saname:String) : List<Student>
@Delete
suspend fun deleteStudent(student : Student);
}
RoomDatabase
를 상속해 자신의 Room
클래스를 생성하고 포함되는 Entity
들과 DB 버전을 @Database annotation
에 지정해준다.
예를 들어, 테이블을 하나 만들어서 데이터베이스가 설치되는 앱을 하나 생성했는데, 사용 중 추가개발이 필요해 테이블을 하나 추가해야 할 경우, 테이블이 추가된 채로 앱을 업데이트 시키게 되면 "기존에 깔려있던 앱의 데이터베이스는 업데이트가 되지 않아" 에러가 발생한다.
데이터베이스의 버전 관리가 중요하다 !
기존에 깔려있던 앱에 Migration
을 통해 테이블을 추가 및 변경 등 가능하다.
RoomDatabase
객체의addMigrations()
를 통해 설정 가능
@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun getMyDao() : MyDAO
companion object {
private var INSTANCE: MyDatabase? = null
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) { 생략 }
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) { 생략 }
}
fun getDatabase(context: Context) : MyDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context, MyDatabase::class.java, "school_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
return INSTANCE as MyDatabase
}
}
}
Migration
은 여러개를 지정해 줄 수 있다.
MIGRATION(1,2)
는 1부터 2까지의 Migration을 뜻하고, MIGRATION(2,3)
은 2부터 3까지의 Migration을 뜻한다.
Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
private val MIGRATION_1_2 = object : Migration(1, 2) { // version 1 -> 2
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) { // version 2 -> 3
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
}
}
Repository와 ViewModel 사용을 권장하며, RoomDatabase 객체에게 DAO 객체를 받아오고, 이 DAO 객체의 메소드를 호출해서 DB에 접근한다.
myDao = MyDatabase.getDatabase(this).getMyDao()
runBlocking { // (주의) UI를 블록할 수 있는 DAO 메소드를 UI 스레드에서 바로 호출하면 안됨
myDao.insertStudent(Student(1, "james")) // suspend 지정되어 있음
}
val allStudents = myDao.getAllStudents() // LiveData는 Observer를 통해 비동기적으로 데이터를 가져옴
UI 쓰레드에서의 네트워킹이나 DB를 직접 호출이 불가능하기 때문에 Coroutine
을 사용하므로 runBlocking
으로 걸어두고 insertStudent
를 이용해 Student
객체를 하나 생성해 insert를 해준다. 또한, Observer
를 통해 실시간으로 변동된 사항을 확인할 수 있다.
LiveData<>
타입으로 리턴되는 DAO 메소드의 경우, Observer
메소드를 통해 데이터가 변경하고 있는지 실시간으로 관찰이 가능하고, 데이터의 관찰이 있을 경우 observe
가 호출되어 화면에 업데이트 된다.
val allStudents = myDao.getAllStudents()
allStudents.observe(this) {
// Observer::onChanged() 는 SAM 이기 때문에 lambda로 대체
val str = StringBuilder().apply {
for ((id, name) in it) {
append(id)
append("-")
append(name)
append("\n")
}
}.toString()
binding.textStudentList.text = str
}