Android Architecture Component(AAC)인 ViewModel, LiveData, Room을 함께 사용하는 샘플을 소개하려고 합니다. Paging을 소개하는 CodeLab 튜토리얼이 있는데, 여기서 다양한 AAC의 기능들을 함께 사용합니다. 이 샘플은 GitHub의 API를 이용하여 검색하는 키워드의 결과를 화면에 출력해줍니다.
코드를 보시면 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 살펴보기를 참고해주세요.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력