Hun's Blog

Android Dagger-Hilt 적용기 (2) - Local Database 본문

Android

Android Dagger-Hilt 적용기 (2) - Local Database

jhk-im 2020. 11. 6. 23:49

아래의 오픈소스를 통해서 분석하고 학습하여 개인프로젝트에 Dagger-Hilt적용

github.com/android/architecture-samples/tree/dev-hilt

 

android/architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - android/architecture-samples

github.com

github.com/skydoves/Pokedex

 

skydoves/Pokedex

🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture. - skydoves/Pokedex

github.com

 

MVVM - database 

이미지 출처 https://codelabs.developers.google.com/codelabs/kotlin-android-training-room-database#0

위 이미지는 MVVM 패턴을 나타내고 있다.  Model (Repository) / View (UI) / ViewModel으로 나뉘어져있으며 검은색 화살표는 Injection을 의미한다. (ex -> Repository는 Database의 DI이다.) 최하단에 있는 Database 먼저 구현하도록 하자. 

Model class

* data class -> 데이터만 담기위한 클래스 , getter/setter를 자동으로 작성해준다. 자바에는 없는 기능. 

@Entity
@Parcelize
@JsonClass(generateAdapter = true)
data class Pokemon(
  var page: Int = 0,
  @field:Json(name = "name") @PrimaryKey val name: String,
  @field:Json(name = "url") val url: String
) : Parcelable {

  fun getImageUrl(): String {
    val index = url.split("/".toRegex()).dropLast(1).last()
    return "https://pokeres.bastionbot.org/images/pokemon/$index.png"
  }
}

@Entity는 해당 클래스가 Room데이터 베이스의 Entity임을 나타내는 annotaion이다. 

 

@Parcelize와 Parcelable

Parcelable은 intent로 데이터 전달 시 Model 객체 자체를 전달 할 수 있도록 해준다. Parcelable은 상당히 긴 보일러플레이트를 작성해야하는데 @Parcelize는 해당 보일러플레이트를 자동으로 생성해준다. 

자세한 내용은 다음 링크에 정리해 두었다. 

jroomstudio.tistory.com/49

 

Parcelable in Kotlin

joaoalves.dev/posts/kotlin-playground/parcelable-in-kotlin-here-comes-parcelize/ Parcelable in Kotlin? Here comes Parcelize Hey everyone, welcome to article number 5 in the series where we’re going..

jroomstudio.tistory.com

@JsonClass(generateAdapter = true)

Moshi의 anotation이다. 

` Moshi는 Android 및 Java 용 최신 JSON 라이브러리이다.  JSON을 Java 객체로, Java를 JSON으로 쉽게 파싱 할 수 있다. `

컴파일시 해당 annotation이 추가된 클래스에 대해 어댑터를 생성한다. 

JSON으로 인코딩할 클래스에 annotation을 달아 활성화한다. 

 

@field:JSON

Moshi의 anotation이다.

해당 anntation으로 선언된 매개변수 즉, Entity field는 JSON으로 인코딩되는 모델의 json 필드에 포함된다. 

 

page 

매개변수인 page는 room database의 쿼리를 통해 pagination을 구현하기 위한 int 값이다. 

  @Query("SELECT * FROM Pokemon WHERE page = :page_")

 

 

intent에는 원시 자료형만 전달할 수 있으나 아래와 같이 모델(객체)을 전달할 수 있게 되었다. 

    fun startActivity(transformationLayout: TransformationLayout, pokemon: Pokemon) {
      val context = transformationLayout.context
      if (context is Activity) {
        val intent = Intent(context, DetailActivity::class.java)
        intent.putExtra(EXTRA_POKEMON, pokemon)
        TransformationCompat.startActivity(transformationLayout, intent)
      }
    }

해당 오픈소스를 통해 Parcelable과 Moshi에 대해 간접적으로 알게되었다. 개인 프로젝트의 API도 JSON으로 제공되기 때문에 그대로 활용해도 무방할 것 같다. 먼저 League 모델을 생성하고 동일하게 설정 한 후 넘어가도록 하겠다. 

@Entity(tableName = "leagues")
@JsonClass(generateAdapter = true)
data class League(
    @PrimaryKey @ColumnInfo(name = "id") var id: String = UUID.randomUUID().toString(),
    @field:Json(name = "name") val name: String,
    @field:Json(name = "url") val url: String,
    var page: Int = 0
)

@Entity에 테이블명을 지정한 것과 @PrimaryKey를 id로 지정하였다.

 

 

DAO 

Room database 데이터를 활용하기 위한 SQLite 쿼리 인터페이스를 등록한다. 

@Dao
interface PokemonDao {

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertPokemonList(pokemonList: List<Pokemon>)

  @Query("SELECT * FROM Pokemon WHERE page = :page_")
  suspend fun getPokemonList(page_: Int): List<Pokemon>
}

@Dao

해당 인터페이스가 Room database에 활용될 쿼리 인터페이스임을 명시함

 

@Insert(onConflict = OnconflictStrategy.REPLACE)

데이터를 추가하는 쿼리를 자동으로 생성한다. 

onConflict = OnconflictStrategy.REPLACE를 추가하면 자동으로 중복체크하여 데이터의 추가/변경/삭제를 관리한다.

 

@Query

SQLite 쿼리를 입력할 수 있다. 

 

suspend fun 

코루틴의키워드이다.  

* 코루틴 

서브 루틴은 메서드와 같다. 특정 로직을 호출하고 완료 되어야만 다음 단계로 넘어가는 점에서 같지만 다른점이 있다. 메서드는 CPU의 레지스터를 통해 반환하지만 서브루틴은 반환하지 않는다. 

메서드는 시작점과 종료지점이 정해져있고 메인 스레드에서 호출하면 내부 로직이 끝날때 까지 종료되지 않으며 다른 외부요인에 의해서 중지되지 않는다. 코루틴은 로직 중간의 어느 지점이든 시작점과 종료지점이 될 수 있으며 일시중지도 가능하고 다른 코루틴으로 이동할수도 있다. 기존의 스레드보다 훨씬 적은 자원을 소비한다.  

suspend 키워드로 선언된 메서드는 코루틴에 의해 컨트롤될 수 있는 함수임을 의미한다. 

SQLite 쿼리 자체가 메인스레드에서 실행되지 않기때문에 로컬과 네트워크 각각의 스레드를 생성해서 구현해야한다. 

두 개의 참고자료가 모두 코루틴을 활용하고 있다. 이번 기회에 코루틴을 사용해보자. 

 

Database

@Database(entities = [Pokemon::class, PokemonInfo::class], version = 1, exportSchema = true)
@TypeConverters(value = [TypeResponseConverter::class])
abstract class AppDatabase : RoomDatabase() {

  abstract fun pokemonDao(): PokemonDao
  abstract fun pokemonInfoDao(): PokemonInfoDao
}

@Databae

entities -> 작성 완료된 Model을 입력한다. list이므로 여러 model을 입력할 수 있다. 

version -> 추후 Model에 변경이 있는경우 version을 설정하여 각각 저장한다. 

exportSchema -> 룸 스키마를 파일형태로 내보내도록 하는 설정여부이다. 어떻게 활용되는지 추후 지켜보도록 하겠다.

 

RoomDatabase() 상속받아 추상클래스로 선언해야한다. 

그 안에 각각의 모델들의 dao를 추상메서드로 선언한다. 

Database를 생성하면 각각의 dao를 사용하여 데이터에 접근할 수 있다. 

 

@TypeConverters(value = [TypeResponseConverter::class])

Room의 annotation이다. 

참고한 프로젝트의 모델은 Moshi로 변환되는 JSON데이터이고 Room은 해당 데이터를 String으로 저장해야 하기 때문에 컨버터가 필요한 것이다. value 에는 리스트로 입력할 수 있고 Date to Long , Json to String 등 여러 타입변환이 필요한 경우 각각의 변환로직을 구현한 클래스를 작성하여 입력하면 된다. 

다음은 참고자료의 JSON to String / String to JSON converter이다. 

@ProvidedTypeConverter
class TypeResponseConverter @Inject constructor(
  private val moshi: Moshi
) {

  @TypeConverter
  fun fromString(value: String): List<PokemonInfo.TypeResponse>? {
    val listType = Types.newParameterizedType(List::class.java, PokemonInfo.TypeResponse::class.java)
    val adapter: JsonAdapter<List<PokemonInfo.TypeResponse>> = moshi.adapter(listType)
    return adapter.fromJson(value)
  }

  @TypeConverter
  fun fromInfoType(type: List<PokemonInfo.TypeResponse>?): String {
    val listType = Types.newParameterizedType(List::class.java, PokemonInfo.TypeResponse::class.java)
    val adapter: JsonAdapter<List<PokemonInfo.TypeResponse>> = moshi.adapter(listType)
    return adapter.toJson(type)
  }
}

@ProvidedTypeConverter

Room의 annotaion으로 해당 클래스가 타입 컨버터임을 나타낸다. 

 

@TypeConverter

Room의 annotation으로 해당 메서드가 타입을 변환하는 메서드임을 나타낸다. 

 

@Inject constructor

@Inject는 javax 의 annotaion이고 constructor는 코틀린의 생성자 키워드이다. 

즉, @Inject로 주입요청을 하면 해당하는 @Module을 찾아 @Provides로 제공되는 Moshi 객체를 입력과 동시에 생성한다. 

@Module과 @Provides는 아래와 같이 구현되어있다. 

@Module
@InstallIn(SingletonComponent::class)
object PersistenceModule {

  @Provides
  @Singleton
  fun provideMoshi(): Moshi {
    return Moshi.Builder().build()
  }
  ...
 }

 

@Module @Provides는 Dagger의 annotation이다. 

@Inject로 요청 -> @Module : 객체를 생성하는 메서드가 있는 object -> @Provides : 객체를 제공할 메서드의 흐름이 Dagger역할이다.

 

@Singleton

javax의 annotation

제공할 메서드의 싱글턴을 구현한다. 

 

@InstallIn(SingletonComponent::class)

hilt의 annotation

각각의 모듈을 사용할 클래스를 Hilt에 알려주는 역할을 한다. 

 

SingltonComponent는 hilt의 클래스로서 @Singleton이 붙어있는 메서드를 찾는다. 

@Inject로 요청받아 생성된 객체를 주입하는 @Module 사이에서 다리역할을 한다고 생각하면 된다. 

`hilt version - 2.29.1-alpha` 부터 제공된다. 

 

 

'@Typeconvert를 분석하다보니 dagger-hilt 까지 연결된것을 확인할 수 있었다. TypeConvert는 네트워크를 구현할 때 함께 구현하도록 하자. 아래는 위 내용을 바탕으로 개인프로젝트에 로컬 데이터베이스를 setup하고 간단하게 테스트한 것이다. 모델이 추가되어 room database에 insert 되고 getList로 가져오는 로직이다.'

 

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)

        val textView = findViewById<TextView>(R.id.test_text)

        val db = Room.inMemoryDatabaseBuilder(applicationContext, AppDatabase::class.java)
            .allowMainThreadQueries()
            .build()

        val league = League("EPL","epl.url")
        val league2 = League("SerieA","seriea.url")
        val league3 = League("LaLiga", "laliga.url")
        val leagues = ArrayList<League>()
        leagues.add(league)
        leagues.add(league2)
        leagues.add(league3)

        val scope = CoroutineScope(Dispatchers.IO)
        scope.launch {
            CoroutineScope(Dispatchers.IO).launch {
                db.leagueDao().saveLeagueList(leagues)
                textView.text = db.leagueDao().getLeagueList(0).toString()
            }
        }
    }
}

테스트 결과

 

 

Dagger2

medium.com/@dlgksah/dagger2-kotlin-example-4c90d3d56edc

 

Dagger2 + Kotlin Example

Dagger2로 의존성 주입하는 예제입니다.

medium.com

코루틴

medium.com/@jeho1335/kotlin-coroutine-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-c2c0a063b2fd

 

Kotlin Coroutine 튜토리얼

2018년 10월 29일 Kotlin 1.3이 정식 릴리즈 되면서, 몇가지 새로운 기능이 추가되었는데, 이 중에서 가장 화제가 된 부분은 코루틴이다.

medium.com