코딩 일기장/Android(Kotlin)

[Android/Kotlin]Udemy 강의 정리: #8: App Architecture with Android Jetpack(1) (UI Layer, Data Layer)

minWachya 2022. 6. 15. 18:59
반응형

목차

  1. 시작하기 전에
  2. UI Layer란?
  3. ViewModel의 데이터 관리 방법
  4. Data Layer란?
  5. UI Layer 구현법
  6. Data Layer 구현법
    1. ViewModelFactory
  7. 정리

1. 시작하기 전에:

아키텍처: 변경하는데 드는 비용과 관련. (공식문서) << 꼭 읽고 이해하기!!

앱 구성요소에 애플리케이션 데이터나 상태를 저장해서는 안 되며 앱 구성요소가 서로 종속되면 안 됩니다.

=> 그러면 어케 앱 설계하라는 거야!?

==> 관심사 분리를 위해 레이어 2개로 나누기

  1. UI Layer: 화면에 애플리케이션 데이터를 표시
  2. Data Layer: 앱의 비즈니스 로직을 포함하고 애플리케이션 데이터를 노출


2. UI Layer란?

UI Layer: 화면에 애플리케이션 데이터를 표시함 + 데이터가 변할 때마다 변경사항을 반영.

 

UI 레이어는 다음 두 가지로 구성됨

  1. UI elements: 화면에 데이터를 렌더링함
  2. 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 얻고 반환
반응형