티스토리 뷰

앱개발 심화 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 파일 설정

 

방  |  Jetpack  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Room Room 지속성 라이브러리는 SQLite에 추상화 레이어를

developer.android.com

해당 공식문서 참고해서 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에서 데이터 베이스의 변경사항을 확인할 수 있다.

 
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함