Lesson 9: App architecture (persistence)

11943 단어 androidkotlinandroid

💡 Teach Android Development

구글에서 제공하는 교육자료를 정리하기 위한 포스트입니다.

Android Development Resources for Educators

Storing data

Ways to store data in an Android app

  • App-specific storage
    • 앱전용 파일
  • Shared storage (files to be shared with other apps)
    • 다른 앱과 공유하려는 파일을 저장
  • Preferences
    • key-value 쌍으로 저장 (SharedPreferences)
  • Databases
    • 앱 전용 데이터베이스에 저장

What is a database?

쉽게 엑세스, 검색 및 구성할 수 있는 구조화된 데이터 모음. 다음과 같이 구성되어 있습니다.

  • Tables (person, car)
  • Rows (Columns가 저장된 행)
  • Columns (_id, name, make)

Structured Query Language (SQL)

SQL을 사용하여 관계형 데이터베이스에 액세스하고 수정합니다.

  • Create new tables
  • Query for data
  • Insert new data
  • Update data
  • Delete data

SQLite in Android

모바일 기기는 하드웨어와 컴퓨터 능력이 제한적이기 때문에 안드로이드에서는 SQL 표준을 기반으로하는 SQLite를 사용합니다. SQLite는 SQL의 대부분의 기능을 지원합니다.

Example SQLite commands

  • CREATE : INSERT INTO colors VALUES ("red", "#FF0000");
  • SELECT : SELECT * from colors;
  • UPDATE : UPDATE colors SET hex="#DD0000" WHERE name="red";
  • DELETE : DELETE FROM colors WHERE name = "red";

Interacting directly with a database

  • 원시 SQL 쿼리의 컴파일 타임 검증을 받지 못합니다.
  • 변환하려면 많은 사용구 코드가 필요합니다.
    • SQL 쿼리 <-> data 객체

Room persistence library

SQLite의 모든기능을 활용합니다.

Add Gradle dependencies

dependencies {
  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  // Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}

ROOM

데이터베이스의 데이터 표현을 앱에서 직접 사용할 수 있는 객체로 변환하는 객체 관계형 매핑 라이브러리입니다.

Three major components in Room

  • Entitiy : 데이터베이스 내의 테이블을 나타냅니다.(ex: Color)
  • DAO : 데이터베이스에 엑세스하는데 사용되는 메서드를 포함합니다.(ex: ColorDao)
  • Database : 앱에 데이터베이스에 대한 연결을 위한 기본 엑세스 지점입니다. (ex: ColoeDatabase)

ColorClass

Color 저장을 위해 필요한 클래스를 정의합니다. Room database에서 사용하려면 annotation이 필요합니다.

data class Color {
    val hex: String,
    val name: String
}

Annotations

  • 컴파일러에 추가 정보를 제공합니다.
    • @Entity, @DAO, @Database
  • 매개변수를 사용할 수 있습니다.
    • @Entity(tableName = "colors")
  • 어노테이션을 정보로 사용하여 자동으로 코드를 생성합니다.

Entity

SQLite 데이터베이스 테이블에 매핑되는 클래스.

  • @Entity : 해당 클래스가 엔티티임을 알립니다.
  • @PrimaryKey : 테이블의 기본키를 지정합니다.
  • @ColumnInfo : 기본적으로 Property 이름을 사용하여 column이 만들어 지지만 해당 어노테이션을 사용하여 변경할 수 있습니다.

Example entity

@Entity(tableName = "colors")
data class Color {
    @PrimaryKey(autoGenerate = true) val _id: Int,
    @ColumnInfo(name = "hex_color") val hex: String,
    val name: String
}

Data access object (DAO)

데이터베이스에 액세스 하기 위한 클래스 작업

  • DAO에서 데이터베이스 상호작용을 정의합니다.
  • DAO를 인터페이스, 추상클래스로 선언합니다.
  • 컴파일 타임에 DAO 구현체를 생성합니다.
  • 컴파일 타임에 모든 DAO 쿼리를 확인합니다.

Example DAO

@Dao
interface ColorDao {

    @Query("SELECT * FROM colors")
    fun getAll(): Array<Color>
    @Insert
    fun insert(vararg color: Color)
    @Update
    fun update(color: Color)
    @Delete
    fun delete(color: Color)

Create a Room database

  • @Database 과 함께 엔티티 리스트를 포함합니다.
    @Database(entities = [Color::class], version = 1)
  • 추상 클래스인 RoomDatabase를 확장합니다.
    abstract class ColorDatabase : RoomDatabase()
    • DAO를 반환하는 추상 메서드를 선업합니다.
      abstract fun colorDao(): ColorDao

Example Room database

RoomDatabase 인스턴스는 무겁고 단일 프로세스 내에서 여러 인스턴스에 액세스할 필요가 거의 없기 때문에 싱글톤 패턴을 따라야합니다.

@Database(entities = [Color::class], version = 1)
abstract class ColorDatabase : RoomDatabase() {
    abstract fun colorDao(): ColorDao
    companion object {
        @Volatile
        private var INSTANCE: ColorDatabase? = null
        fun getInstance(context: Context): ColorDatabase {
            ...
        }
    }
    ...

Create database instance

Room.databaseBuilder()를 사용하여 데이터베이스를 만듭니다.

synchronized : 하나의 실행 스레드만 이 코드 블록을 수행하므로 데이터베이스가 한 번만 초기화됩니다.

fallbackToDestructiveMigration() : 마이그레이션 경로가 누락되었을 때 기존 데이터가 손실되는 것을 허용할 경우.

fun getInstance(context: Context): ColorDatabase {
    return INSTANCE ?: synchronized(this) {
        INSTANCE ?: Room.databaseBuilder(
            context.applicationContext,
            ColorDatabase::class.java, "color_database"
        )
        .fallbackToDestructiveMigration()
        .build()
        .also { INSTANCE = it }
    }
}

Get and use a DAO

데이터베이스에서 DAO를 가져옵니다.

val colorDao = ColorDatabase.getInstance(application).colorDao()

새로운 Color를 만들고 DAO를 사용하여 데이터베이스에 입력합니다.

val newColor = Color(hex = "#6200EE", name = "purple")
colorDao.insert(newColor)

Asynchronous programming

Long-running tasks

  • 정보 다운로드
  • 서버와 동기화
  • 파일에 쓰기
  • 무거운 계산
  • 데이터베이스에서 읽거나 쓰기

장기 작업을 수행하는 경우 기본 스레드에서 수행하면 안됩니다. 기본 스레드에서 수행할 경우 앱이 사용자에게 응답하지 않을 수 있습니다.

Need for async programming

  • 작업을 수행하고 응답을 유지하는데 제한된 시간.
  • 장기 작업을 실행해야 하는 필요성과 균형.
  • 작업 실행 방법 및 위치 제어.

Async programming on Android

  • Threading
  • Callbacks
  • 그외 많은 옵션

Coroutines

안드로이드 비동기 프로그래밍에 권장되는 방법입니다.

  • 장기 실행 작업을 관리하는 동안 앱의 응답성을 유지합니다.
  • 안드로이드 앱에서 비동기 코드를 단순화합니다.
  • 코드를 순차적으로 작성합니다.
  • try/catch 블록으로 예외 처리를 진행합니다.

Benefits of coroutines

  • 경량
  • 메모리 누수 감소
  • cancellation 내장 지원
  • Jetpack integration
    • Jetpack 라이브러리에는 전체 코투틴을 지원하는 확장 기능이 포함되어 있습니다.

Suspend functions

  • suspend modifier 추가
  • 다른 suspend 함수 또는 coroutines에 의해 호출되어야 합니다.
suspend fun insert(word: Word) {
    wordDao.insert(word)
}

Suspend and resume

  • suspend : 현재 코루틴의 실행을 일시 중지하고 지역 변수를 저장합니다.

  • resume : 저장된 상태를 자동으로 로드하고 일시 중단된 지점부터 실행합니다.

코루틴이 suspend 함수를 호출하면 해당 함수가 일반 함수 호출처럼 반환될 때까지 차단하는 것이 아니라 결과가 준비될 때까지 실행을 일시 중단한 다음 결과와 함께 중단된 곳에서 다시 시작합니다.

결과를 기다리는 동안 스레드의 차단을 해제하여 다른 기능이나 코루틴을 실행할 수 있습니다.

suspend 와 resume 작업을 함께 사용하여 콜백을 대체합니다.

Add suspend modifier to DAO methods

Room은 코루틴을 지원합니다. suspend 수정자를 추가 하여, 코루틴이나 다른 suspend 함수에서 호출할 수 있게 합니다.

@Dao
interface ColorDao {

    @Query("SELECT * FROM colors")
    suspend fun getAll(): Array<Color>
    @Insert
    suspend fun insert(vararg color: Color)
    @Update
    suspend fun update(color: Color)
    @Delete
    suspend fun delete(color: Color)

Control where coroutines run

withContext

withContext를 사용하여 감싸진 코드를 실행할 디스패쳐를 지정할 수 있습니다.

suspend fun get(url: String) {

	// Start on Dispatchers.Main

    withContext(Dispatchers.IO) {
        // Switches to Dispatchers.IO
        // Perform blocking network IO here
    }

    // Returns to Dispatchers.Main
}

CoroutineScope

코루틴은 CoroutineScope에서 실행되어야 합니다.

  • 그 안에서 실행된 모든 코루틴을 추적합니다.
  • 스코프에서 코루틴을 취소하는 방법을 제공합니다
  • 일반 함수와 코루틴 사이의 bridge를 제공합니다.

Examples:

  • GlobalScope
  • viewModelScope
  • lifecycleScope

Start new coroutines

  • launch : 결과가 필요없을 경우
fun loadUI() {
    launch {
        fetchDocs()
    }
}
  • async : 결과가 필요할 경우
    • async를 사용하면 코루틴을 시작하고 await 키워드로 값을 반환할 수 있습니다.

ViewModelScope

  • ViewModel이 지워지면 자동으로 취소됩니다.
  • ViewModel이 활성화된 경우 수행해야 하는 작업이 있을 때 유용합니다.
class MyViewModel: ViewModel() {

    init {
        viewModelScope.launch {
            // Coroutine that will be canceled
            // when the ViewModel is cleared
        }
    }
    ...

Example viewModelScope

class ColorViewModel(val dao: ColorDao, application: Application)
    : AndroidViewModel(application) {

fun save(color: Color) {
    viewModelScope.launch {
        colorDao.insert(color)
    }
}
 
...

Testing databases

Add Gradle dependencies

android {
    defaultConfig {
        ...
        testInstrumentationRunner "androidx.test.runner
         .AndroidJUnitRunner"
        testInstrumentationRunnerArguments clearPackageData: 'true'
    }
}
dependencies {
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

Testing Android code

  • @RunWith(AndroidJUnit4::class)
  • @Before
  • @After
  • @Test

Create test class

테스트에 필요한 DAO와 데이터베이스를 정의합니다.
나중에 사용할 Color 인스턴스를 생성해 줍니다.

@RunWith(AndroidJUnit4::class)
class DatabaseTest {

    private lateinit val colorDao: ColorDao
    private lateinit val db: ColorDatabase

    private val red = Color(hex = "#FF0000", name = "red")
    private val green = Color(hex = "#00FF00", name = "green")
    private val blue = Color(hex = "#0000FF", name = "blue")

    ...

Create and close database for each test

@Before : 테스트 이전에 수행할 행동으로 데이터베이스와 DAO 인스턴스를 생성합니다.
(실제 사용자 데이터를 유지하기 위해 더 이상 사용하지 않을 때 폐기되는 테스트 목적으로 inMemoryDatabaseBuilder를 사용합니다.)

@After : 테스트 완료된 후 수행됩니다.

@Before
fun createDb() {
    val context: Context = ApplicationProvider.getApplicationContext()
    db = Room.inMemoryDatabaseBuilder(context, ColorDatabase::class.java)
        .allowMainThreadQueries()
        .build()
    colorDao = db.colorDao()
}
@After
@Throws(IOException::class)
fun closeDb() = db.close()

Test insert and retrieve from a database

    @Test
    @Throws(Exception::class)
    fun insertAndRetrieve() {
        colorDao.insert(red, green, blue)
        val colors = colorDao.getAll()
        assert(colors.size == 3)
    }

좋은 웹페이지 즐겨찾기