HOME > android > jetpack

안드로이드 ViewModel, LiveData, Room을 사용하는 좋은 예제

JSFollow29 Dec 2018

Android Architecture Component(AAC)인 ViewModel, LiveData, Room을 함께 사용하는 샘플을 소개하려고 합니다. Paging을 소개하는 CodeLab 튜토리얼이 있는데, 여기서 다양한 AAC의 기능들을 함께 사용합니다. 이 샘플은 GitHub의 API를 이용하여 검색하는 키워드의 결과를 화면에 출력해줍니다.

android jetpack sample

코드를 보시면 Retrofit과 GitHub API를 이용하여 키워드에 대한 검색 결과를 가져옵니다. 그리고 Jetpack의 ViewModel, LiveData, Room을 사용하여 데이터를 다루고, RecyclerView에 결과를 출력합니다.

이 샘플을 분석해보시면, ViewModel, LiveData, Room을 어떻게 사용하는지 알 수 있습니다. 다른 샘플들 보다 간단하기 때문에 Jetpack의 새로운 기능들에 익숙하지 않다면 먼저 이 샘플을 분석해보시는 것이 도움이 될 것 같습니다.

기존의 CodeLab샘플은 Dagger가 포함되어있는데, Dagger를 제거하고, AndroidX로 변경한 코드를 GitHub에 올려두었습니다.

프로젝트 구성

프로젝트는 5개의 패키지로 구성되어 있습니다.

  • api - Retrofit를 사용하여 GitHub에 쿼리하는 GitHub API 코드가 있습니다.
  • db - 로컬 DB에 대한 구현이 있습니다. Room을 사용하여 구현하였습니다.
  • data - 데이터를 다루는 Repository 클래스가 있습니다. GitHub API로부터 받는 데이터와 로컬 DB에 대한 모든 것을 관리합니다.
  • model - Json, Room 등 에서 사용하는 모델들이 있습니다.
  • ui - UI에 대한 구현이 있습니다.

API

Retrofit을 이용하여 GitHub에 쿼리를 합니다.

interface GithubService {
    /**
     * Get repos ordered by stars.
     */
    @GET("search/repositories?sort=stars")
    fun searchRepos(@Query("q") query: String,
                    @Query("page") page: Int,
                    @Query("per_page") itemsPerPage: Int): Call<RepoSearchResponse>

    companion object {
        private const val BASE_URL = "https://api.github.com/"

        fun create(): GithubService {
            val logger = HttpLoggingInterceptor()
            logger.level = Level.BASIC

            val client = OkHttpClient.Builder()
                    .addInterceptor(logger)
                    .build()
            return Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                    .create(GithubService::class.java)
        }
    }
}

결과는 RepoSearchResponse로 받습니다. @SerializedName는 Gson으로 Json을 자바 객체로 직렬화, 역직렬화 해줄 때 사용됩니다.

data class RepoSearchResponse(
        @SerializedName("total_count") val total: Int = 0,
        @SerializedName("items") val items: List<Repo> = emptyList(),
        val nextPage: Int? = null
)

@SerializedName의 "..."은 Json에서 아래와 같이 표현됩니다.

{
    "total_count": "10",
    "items": "......"
}

Database

로컬 DB는 Room을 이용하여 구현을 하였습니다. GitHub API로 Repo에 대한 객체를 가져오기 때문에 Annotation SerializedName도 함께 정의되어 있습니다.

@Entity(tableName = "repos")
data class Repo(
        @PrimaryKey @field:SerializedName("id") val id: Long,
        @field:SerializedName("name") val name: String,
        @field:SerializedName("full_name") val fullName: String,
        @field:SerializedName("description") val description: String?,
        @field:SerializedName("html_url") val url: String,
        @field:SerializedName("stargazers_count") val stars: Int,
        @field:SerializedName("forks_count") val forks: Int,
        @field:SerializedName("language") val language: String?
)

Room에 대한 자세한 내용은 Android Jetpack Room 살펴보기를 참고해주세요.

DATA

이 프로젝트는 GithubRepository라는 인터페이스를 두어 모든 데이터를 처리합니다. 이 클래스는 GitHub API로 데이터를 얻고, 로컬 DB에 데이터를 저장합니다. UI에 보여지는 내용은 모두 로컬 DB에서 가져옵니다.

fun search(query: String): RepoSearchResult {
    Log.d("GithubRepository", "New query: $query")
    lastRequestedPage = 1
    requestAndSaveData(query)

    // Get data from the local cache
    val data = cache.reposByName(query)

    return RepoSearchResult(data, networkErrors)
}

private fun requestAndSaveData(query: String) {
    if (isRequestInProgress) return

    isRequestInProgress = true
    searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos ->
        cache.insert(repos, {
            lastRequestedPage++
            isRequestInProgress = false
        })
    }, { error ->
        networkErrors.postValue(error)
        isRequestInProgress = false
    })
}

UI

ViewModel과 RecyclerView에서 사용하는 클래스들이 정의되어 있습니다. ViewModel은 Repository를 통해 데이터를 가져옵니다. Repository는 LiveData로 데이터를 전달하기 때문에, UI는 옵저버를 붙여 데이터의 변화를 알 수 있습니다.

class SearchRepositoriesViewModel(application: Application) : AndroidViewModel(application) {
    .....
    private val repository: GithubRepository = GithubRepository(application)

    private val queryLiveData = MutableLiveData<String>()
    private val repoResult: LiveData<RepoSearchResult> = Transformations.map(queryLiveData, {
        repository.search(it)
    })

    val repos: LiveData<List<Repo>> = Transformations.switchMap(repoResult,
            { it -> it.data })
    fun searchRepo(queryString: String) {
        queryLiveData.postValue(queryString)
    }
    ......
}

ViewModel에 대한 자세한 내용은 Android Jetpack ViewModel 살펴보기를 참고해주세요. LiveData에 대한 자세한 내용은 Android Jetpack LiveData 살펴보기를 참고해주세요.

참고