[Android/Kotlin]Udemy 강의 정리: #8: App Architecture with Android Jetpack(1) (UI Layer, Data Layer)
목차
- 시작하기 전에
- UI Layer란?
- ViewModel의 데이터 관리 방법
- Data Layer란?
- UI Layer 구현법
- Data Layer 구현법
- ViewModelFactory
- 정리
1. 시작하기 전에:
아키텍처: 변경하는데 드는 비용과 관련. (공식문서) << 꼭 읽고 이해하기!!
앱 구성요소에 애플리케이션 데이터나 상태를 저장해서는 안 되며 앱 구성요소가 서로 종속되면 안 됩니다.
=> 그러면 어케 앱 설계하라는 거야!?
==> 관심사 분리를 위해 레이어 2개로 나누기
- UI Layer: 화면에 애플리케이션 데이터를 표시
- Data Layer: 앱의 비즈니스 로직을 포함하고 애플리케이션 데이터를 노출
2. UI Layer란?
UI Layer: 화면에 애플리케이션 데이터를 표시함 + 데이터가 변할 때마다 변경사항을 반영.
UI 레이어는 다음 두 가지로 구성됨
- UI elements: 화면에 데이터를 렌더링함
- State holders: 데이터를 보유하고 이를 UI에 노출하며 로직을 처리함
+ State holder에 ViewModel이 사용되는데, 이때 ViewModel이란?
ViewModel:
액티비티는 상태가 다양한 반면, ViewModel은 생성 이후 같은 상태를 가짐
== 같은 데이터 유지하기 때문에 액티비티의 상태가 어떻든 같은 데이터 불러올 수 있음
=> ViewModel을 State holder로 사용할 수 있음
3. ViewModel의 데이터 관리 방법
LiveData와 함께 사용됨
주석에 설명!!
MyViewModel.kt
class MyViewModel : ViewModel() {
// user 객체: MutableLiveData객체를 생성 + lodeUser 사용해서 데이터 로드
// by laze 사용: MyViewModel 생성 시 초기화되지 않음(자세한 설명 아래에)
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also { loadUsers() }
}
// 위의 users 변수 그대로 리턴 + 데이터 타입을 LiveData로 변경
fun getUsers(): LiveData<List<User>> { return users }
// Data layer의 Repository에 데이터 요청
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
/*
MutableLiveData와 LiveData의 차이는 수정 가능/불가능
*/
+ by lazy란?: 클래스가 생성될 때가 아니라 최초 호출 시에 초기화 되는 예약어
// by lazy: 클래스가 생성될 때 함께 초기화 되지 않고, 최초 호출 시 초기화됨.
// => 최초에 초기화된 값 계속 재사용
// 언제 사용? 초기화 비용 많이 드는 연산이지만, 이를 계속 재사용하는 경우
val lazyValue: String by lazy {
println("초기화")
"ㅎㅇ"
}
fun main() {
println(lazyValue) // lazyValue 초기화됨, "초기화" 출력되고 lazyValue에 "ㅎㅇ" 저장됨
println(lazyValue) // 저장된 "ㅎㅇ" 출력
}
// 실행 결과:
// 초기회
// ㅎㅇ
// ㅎㅇ
Activity, Fragment에서 ViewModel 참조 방법
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// MyViewModel 객체 생성해야 함.
// 아래 코드는 by viewModels() 이용해서 ViewModel 객체 생성함
// by viewModels() 호출하려면 라이브러리 추가해야 함(activity-ktx artifact)
/* (ktx: kotlin 개발자들을 위해 기존 Android SDK가 지원하던 기능에
수신 객체를 확장하는 코틀린 문법을 적용해서
해당 객체가 호출할 수 있는 함수화 속성 추가한 라이브러리) */
// getUsers 호출해서 user에 대한 정보 얻고 +
// user의 데이터 변경에 대해 알림 받는 observe 메서드 호출
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
4. Data Layer
Data Layer는 0개부터 여러 개의 데이터 소스를 각각 포함할 수 있는 저장소로 구성됨.
앱에서 처리하는 다양한 유형의 데이터마다 저장소 클래스를 만들어야 함
화살표는 종속 관계를 뜻함: Repository는 Data Soutce로부터 데이터 받음
5. UI layer 구현
1. ui 패키지 생성(ui 렌더링 당담하는 파일 담는 곳)
2. ViewModel 클래스 생성하기
- Fragment는
- life cycle에 맞춰 layout을 inflate하는 책임만 가지게 하기
- ViewModel의 데이터를 받아서 view에 할당하는 코드만 남기기
// home 화면 그리는 데 필요한 데이터의 state holder 역할
class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() {
// 외부 접근 불가 변수: _로 시작
private val _title = MutableLiveData<Title>()
val title: LiveData<Title> = _title
private val _topBanners = MutableLiveData<List<Banner>>()
val topBanners: LiveData<List<Banner>> = _topBanners
// 데이터 요청
fun loadHomeDate() {
val homeData = homeRepository.getHomeData()
homeData?.let { homeData ->
_title.value = homeData.title
_topBanners.value = homeData.topBanners
}
}
}
3. Fragment에서 by viewModels() 사용 위해 라이브러리 추가
implementation("androidx.collection:collection-ktx:1.2.0")
3. Activity나 Fragment에 viewModel 객체 생성하고 observe 연결하여 data 관리
private val viewModel: HomeViewModel by viewModels()
// 데이터 변경 시 어떤 처리하리는 Fragemnt가 구현
viewModel.title.observe(viewLifecycleOwner) { title ->
// view 업데이트
toolbarTitle.text = title.text
GlideApp.with(this@HomeFragment)
.load(title.iconUrl)
.into(toolbarIcon)
}
viewModel.topBanners.observe(viewLifecycleOwner) { banners ->
viewpager.adapter = HomeBannerAdapter().apply {
submitList(banners)
}
}
4. Adapter생성해서 Adapter에서 ViewModel에 data bind하기
: layout.xml의 <data> 태그 안에 사용된 data bind하기 < 다음 게시글 참조
<data>
<variable
name="title"
type="com.example.shoppi.model.Title" />
</data>
// ListAdapter: 데이터의 리스트 받아서 순차적으로 viewholder와 바인딩함.
// 레이아웃 유지한 채로 데이터만 업데이트
// BannerDiffCallback: 스크롤 변경됨에 따라서 데이터 변경 확인하고 업데이트
class HomeBannerAdapter(): ListAdapter<Banner, HomeBannerAdapter.HomeBannerViewHolder>(BannerDiffCallback()) {
private lateinit var binding: ItemHomeBannerBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeBannerViewHolder {
binding = ItemHomeBannerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return HomeBannerViewHolder(binding)
}
override fun onBindViewHolder(holder: HomeBannerViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class HomeBannerViewHolder(private val binding: ItemHomeBannerBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(banner: Banner) {
binding.banner = banner
binding.executePendingBindings()
}
}
}
// BannerDiffCallback: 스크롤 변경됨에 따라서 데이터 변경 확인하고 업데이트
// Banner 객체가 같은지 다른지 체크 기준 정하기
class BannerDiffCallback: DiffUtil.ItemCallback<Banner>() {
// 제품 id 같으면 같은 제품
override fun areItemsTheSame(oldItem: Banner, newItem: Banner): Boolean {
return oldItem.productDetail.productId == newItem.productDetail.productId
}
// 제품 id도 같을 때 다른 내용들도 모두 같은지?(업데이트 되었을 수도 있으니까)
override fun areContentsTheSame(oldItem: Banner, newItem: Banner): Boolean {
return oldItem == newItem
}
}
6. Data Layer 구현
1. repository 패키지 생성
2. repository > HomeDataSource.kt 인터페이스 생성
왜 interface??: Data Source는 여러 유형이 있을 수 있기 때문
interface HomeDataSource {
fun getHomeData(): HomeData
}
3.repository > HomeAssetDataSource 클래스 생성: HomeDataSource의 구현체
class HomeAssetDataSource(private val assetLoader: AssetLoader) : HomeDataSource {
private val gson = Gson()
override fun getHomeData(): HomeData? {
return assetLoader.getJsonString("home.json").let { homeJsonString ->
gson.fromJson(homeJsonString, HomeData::class.java)
}
}
}
4. repository > HomeRepository.kt 클래스 생성
: Home 화면에서 보여줄 데이터를 HomeAssetDataSource를 사용해 실제로 가져오고 반환함
class HomeRepository(private val assetDataSource: HomeAssetDataSource) {
fun getHomeData(): HomeData? {
return assetDataSource.getHomeData()
}
}
5. HomeFragment.kt에서 ViewModel 객체 생성하고 observe 달고 data 변경 관리하기
+ by viewModels을 다른 생성자로 생성하는 법: ViewModelFactory를 구현한 객체 전달하기
1. ui 패키지에 ViewModelFactory.kt 생성
class ViewModelFactory(private val context: Context): ViewModelProvider.Factory {
// HomeViewModel 생성하고 반환하기
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
// HomeViewModel 타입인지 검사
if (modelClass.isAssignableFrom(HomeViewModel::class.java)) {
// 의존성 관리: 'Hilt' 라이브러리 사용할 수도 있음
val repository = HomeRepository(HomeAssetDataSource(AssetLoader(context)))
return HomeViewModel(repository) as T
} else {
throw IllegalArgumentException("Failed to create ViewModel: ${modelClass.name}")
}
}
}
변경 전:
private val viewModel: HomeViewModel by viewModels
변경 후:
private val viewModel: HomeViewModel by viewModels { ViewModelFactory(requireContext()) }
7. 정리
- AData: data class
- ui
- AAdapter: layout의 <data>에 사용된 data bind하기
- AViewModel: LiveData 관리, 인자로 받은 ARepository로 AData 불러와서 LiveData에 data 할당
- AFragment(AActivity): AViewModel을 생성하고, observer로 data의 변경을 관리함
- repository
- ADataSource: interface, AData 가져오는 함수 가짐
- ARemoteDataSource: ADataSource를 구현한 객체, AData 가져오는 구체적 함수 구현
- AReposity: 인자로 ARemoteDataSource를 받고 ARemoteDataSource의 함수 사용해 실제로 AData 얻고 반환