티스토리 뷰
앱개발 심화 1주차 정리 - 1
SharedPreferences
Preference란?
프로그램의 설정 정보를 영구적으로 저장하는 용도로 사용된다.
데이터를 XML 포맷의 텍스트 파일에 키-값 세트로 정보를 저장한다.
앱을 삭제하면 저장된 데이터들도 전부 삭제된다.
SharedPreferences란?
SharedPreferences 클래스는 Preferences의 데이터를 관리하는 클래스로,
변경사항을 액티비티간에 공유하며 데이터는 외부에서 읽을 수 없다.
XML 파일 생성
getSharedPreferences(name, mode)
여러개의 SharedPreference 파일들을 사용하는 경우
해당 함수를 사용하여 SharedPreference 객체를 불러올 수 있다.
name에는 프레퍼런스 데이터를 저장할 XML 파일의 이름이 들어가고,
mode에는 해당 XML 파일의 공유 모드가 들어간다.
val prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
mode를 MODE_PRIVATE로 주면 생성된 XML 파일을
해당 애플리케이션 내에서만 읽고 쓰기가 가능하게 된다.
즉 다른 앱은 해당 XML 파일에 접근할 수 없다.
사용 가능한 데이터 타입
SharedPreferences는 간단한 데이터 타입만을 지원한다.
하지만 Json 객체를 이용하여 데이터 클래스 값을 저장하는 것도 가능하다.
getPreferences
val prefs = activity.getPreferences(Context.MODE_PRIVATE)
한 개의 SharedPreference 파일을 사용하는 경우 사용한다.
액티비티 클래스에 정의된 메소드이므로 Activity 인스턴스를 통해 접근이 가능하다.
생성한 액티비티 전용이므로 같은 패키지의 다른 액티비티는 읽을 수 없다.
텍스트 저장하고 불러오기
private fun saveText() {
val prefs = context.getSharedPreferences("prefs", 0)
prefs.edit { putString("text", binding.editText.text.toString()) }
}
private fun loadText() {
val prefs = context.getSharedPreferences("prefs", 0)
binding.editText.setText(prefs.getString("text",""))
}
저장 버튼을 눌렀을 때 saveText를 호출하고, onCreate에서 loadText를 호출하면
앱을 껐다 켜도 텍스트필드에 입력한 텍스트가 그대로 유지된다.
생성된 XML 파일 확인
애뮬레이터를 실행시키거나 디바이스를 연결한 뒤
안드로이드 스튜디오 우측 하단에 위치한 Device Explorer 창을 연다.
(디바이스를 연결할 땐 보안 폴더가 제거된 상태여야 한다.)
/data/data/[패키지명]/shared_prefs 경로로 이동해
SharedPreferences에 저장된 XML 파일을 확인 및 제거할 수 있다.
Room
Room이란?
Android에서 사용하는 데이터베이스인 SQLite를 쉽게 사용할 수
있는 데이터베이스 객체 매핑 라이브러리이다.
Room의 주요 3요소
- @Database: 클래스를 데이터베이스로 지정하는 annotation,
RoomDatabase를 상속 받은 클래스여야 하며 Room.databaseBuilder를 이용하여 인스턴스를 생성한다.
- @Entity: 클래스를 테이블 스키마로 지정하는 annotation
- @Dao: 클래스를 DAO(Data Access Object)로 지정하는 annotation,
기본적인 insert, delete, update SQL은 자동으로 만들어주며, 복잡한 SQL은 직접 만들 수 있다.
어노테이션을 통해 Room의 주요 3요소를 지정할 수 있다.
gradle 파일 설정
해당 공식문서 참고해서 gradle 파일 설정을 진행해준다.
plugins {
...
id("androidx.room") version "2.6.1" apply false
}
프로젝트 수준의 그래들 파일에 해당 종속성을 추가해준다.
plugins {
...
id("kotlin-kapt")
}
dependencies {
val room_version = "2.6.1"
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-ktx:$room_version")
...
}
앱 수준의 그래들 파일에 해당 종속성을 추가해준다.
Entity 생성
// CREATE TABLE student_table (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
@Entity(tableName = "student_table")
data class Student (
@PrimaryKey
@ColumnInfo(name = "student_id")
val id: Int,
val name: String
)
@Entity 어노테이션을 활용해 테이블을 생성할 수 있다.
해당 테이블의 이름은 "student_table"이고 고유한 키는 "student_id"에 해당한다.
id에는 정수형 값이 들어가고 name에는 문자열 값이 들어가면 null값을 할당할 수 없다.
DAO 생성
// 테이블의 모든 데이터를 가져옴
@Query("SELECT * from table") fun getAllData() : List<Data>
Annotation에 SQL 쿼리를 정의하고 해당 쿼리를 호출하기 위한 메소드를 선언한다.
가능한 annotation으로 @Insert, @Update, @Delete, @Query가 있으며,
@Insert, @Update, @Delete는 쿼리를 작성하지 않아도 컴파일러가 자동으로 생성해준다.
@Update나 @Delete는 primary key를 기반으로 튜플을 찾아서 변경/삭제 한다.
- OnConflictStrategy.ABORT: key 충돌시 종료
- OnConflictStrategy.IGNORE: key 충돌 무시
- OnConflictStrategy.REPLACE: key 충돌시 새로운 데이터로 변경
@Insert나 @Update는 key가 중복되는 경우 처리를 위해 onConflict를 지정할 수 있다.
@Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>
@Query로 리턴되는 데이터의 타입을 LiveData로 설정하면,
해당 데이터가 변경될 때 Observer를 통해 데이터의
변경사항을 감지하여 UI를 업데이트할 수 있다.
// name필드가 sname에 해당하는 studuent 데이터 클래스를 반환함
@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>
@Query에 SQL을 정의할 때 메소드의 인자를 사용할 수 있다.
suspend 키워드는 Kotlin에서 비동기 함수(코루틴)를 정의할 때 사용되며,
추후에 해당 함수를 호출하려면 runBlocking 블럭 내에서 호출해야 한다.
LiveData는 비동기적으로 동작하기 때문에 coroutine을 사용할 필요가 없다.
@Dao
interface MyDAO {
// INSERT, key 충돌이 나면 새 데이터로 교체
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertStudent(student: Student)
@Query("SELECT * FROM student_table")
fun getAllStudents(): LiveData<List<Student>>
@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>
@Delete
suspend fun deleteStudent(student: Student); // primary key를 기반으로 학생 데이터를 찾음
...
}
DAO 클래스를 추상 클래스로 생성하고 그 안에
쿼리를 호출해 CRUD 작업을 하는 추상 메소드들을 작성한다.
데이터 베이스 생성
@Database(entities = [Student::class], exportSchema = false, version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun getMyDao() : MyDAO
// 싱글톤 패턴 사용
companion object {
private var INSTANCE: MyDatabase? = null
// Migration 방법 지정
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 현재 테이블에 last_update라는 이름의 열을 INTEGER 타입으로 추가
database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
}
}
// 데이터베이스 객체를 불러옴
fun getDatabase(context: Context) : MyDatabase {
// 인스턴스를 중복하여 초기화하지 않도록 함
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context, MyDatabase::class.java, "school_database")
.addMigrations(MIGRATION_1_2)
.build()
}
return INSTANCE as MyDatabase
}
}
}
RoomDatabase를 상속하여 데이터베이스(Room) 클래스를 만들 수 있다.
포함되는 Entity들과 데이터베이스 버전을 @Database annotation에 지정한다.
버전이 기존에 저장되어 있는 데이터베이스보다 높으면
데이터베이스를에 접근할 때 migration을 수행한다.
DAO 메소드 호출
myDao = MyDatabase.getDatabase(this).getMyDao() // DAO 객체 인스턴스 선언
CoroutineScope(Dispatchers.IO).launch {
myDao.insertStudent(Student(1, "james")) // 비동기 함수
}
UI 스레드에서 DAO 메소드를 호출하면 UI 블로킹이 발생할 수 있다.
따라서 CRUD 함수는 비동기로 작성하고 코루틴을 사용하여 호출해야 한다.
LiveData를 활용한 UI 업데이트
val allStudents = myDao.getAllStudents()
allStudents.observe(this) {
val str = StringBuilder().apply {
for ((id, name) in it) {
append(id)
append("-")
append(name)
append("\n")
}
}.toString()
binding.textStudentList.text = str
}
student_table의 변화를 감지하여 테이블이 업데이트 된다면 텍스트를 초기화한다.
가변현 문자열 객체를 (학번 - 이름) 형식으로 학생 리스트를 출력한다.
LiveData 타입으로 리턴되는 DAO 메소드는 Observer를 통해 비동기적으로
데이터를 받아오기에, UI 스레드에서 직접 호출해도 문제가 없다.
예제
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
lateinit var myDao: MyDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
myDao = MyDatabase.getDatabase(this).getMyDao()
// 학생 리스트 출력
val allStudents = myDao.getAllStudents()
allStudents.observe(this) {
val str = StringBuilder().apply {
for ((id, name) in it) {
append(id)
append("-")
append(name)
append("\n")
}
}.toString()
binding.textStudentList.text = str
}
// 학생 추가
binding.addStudent.setOnClickListener {
val id = binding.editStudentId.text.toString().toInt()
val name = binding.editStudentName.text.toString()
if (id > 0 && name.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
myDao.insertStudent(Student(id, name))
}
}
binding.editStudentId.text = null
binding.editStudentName.text = null
}
// 이름 기반으로 학생 정보 가져오기
binding.queryStudent.setOnClickListener {
val name = binding.editStudentName.text.toString()
CoroutineScope(Dispatchers.IO).launch {
val results = myDao.getStudentByName(name)
if (results.isNotEmpty()) {
val str = StringBuilder().apply {
results.forEach { student ->
append(student.id)
append("-")
append(student.name)
}
}
withContext(Dispatchers.Main) {
binding.textQueryStudent.text = str
}
} else {
withContext(Dispatchers.Main) {
binding.textQueryStudent.text = ""
}
}
}
}
}
}
Dispatchers.IO는 주로 입출력 작업(I/O)과 관련된 작업을 처리하는 데 사용되고,
Dispatchers.Main은 Android의 UI 스레드에서 작업을 수행하는 데 사용된다.
Dispatchers.IO에선 데이터 베이스 관련 비동기 작업을 수행하고,
UI 업데이트 작업은 반드시 Dispatchers.Main에서 수행해야한다.
실행화면
하단 탭의 App Inspection에서 데이터 베이스의 변경사항을 확인할 수 있다.
'내일배움캠프 > Android 국비지원' 카테고리의 다른 글
TIL 59일차 (앱개발 심화 1주차 정리 - 3) (0) | 2024.08.12 |
---|---|
TIL 58일차 (앱개발 심화 1주차 정리 - 2) (0) | 2024.08.12 |
TIL 56일차 (챌린지반 6주차 세션 정리) (0) | 2024.08.08 |
TIL 55일차 (Compose 특강 2회차 정리) (0) | 2024.08.08 |
TIL 54일차 (동적 layoutManager 적용) (0) | 2024.08.08 |