[6주차 챌린지반 강의] 복습 TIL
핵심 내용
- Domain Layer(usecase 적용)
- RecyclerView에 ListAdapter 사용 예시
- StateFlow, SharedFlow 소개
디렉토리 구조 및 역할 정리 - 이해한 대로 작성하여 의역의 여지가 있음
- network(data에서 분리함)
- RetrofitClient : RetrofitBuilder object 정의
- AuthInterceptor : 네트워크 통신 시, 가로채서 API 호출에 필요한 공통 Header(인증 Key값 등)를 심어준다
- data
- model(SearchResponse - data class) : Response의 Json 타입에 맞게 data calss 셋업
- remote(SearchRemoteDatasource - interface) : API 또는 내부 DB로 부터 데이터를 가져오기 위한 명세
- repository(SearchRepositoryImpl - class) : UI Layer와 Data Layer의 연결을 위한 구현체
(1) 생성자로 (Api 호출 기능이 구현된) network/remote의 interface 객체를 전달 받는다.
(2) ui/repository의 interface를 상속받아 구현한다.
(3) 전달 받은 interface 객체에 접근해 Api Call Method를 호출한다.
- domain(optinal) - 선택적 이지만, 다양한 usecase가 존재할 경우에는 유지보수를 위해 필수 구현
- model : SearchEntity - data class : network의 model과 구성은 동일함 = 중간 백업 역할
SearchMapper - function : Response를 SearchEntity 타입으로 매핑하는 역할
-> "규모가 클 때, 효과적인 로직이다" ?!
- repository(SearchRepository - interface) : UI Layer에서 Data Layer에 접근하기 위한 interface 구현
Data Layer에서 이 interface를 오버라이드 하여 함수 재구현함 -> Layer 간의 연결
api 쿼리에서 optinal한 속성을 default value 설정 -> viewModel 에서는 query 값만 파라미터로 주고 함수 호출
- usecase(SearchGetImageUseCase - class) :
usecase 사용 시, viewModel에서 repository를 생성자로 사용하지 않고, usecase를 사용한다. usecase에서 repository를 생성자로 씀~
ViewModel(repository) -> data/remote 에서 ViewModel(usecase) -> usecase(repository) -> data/remote로 중간에 추가됨
- ui (presentation)
- viewmodel(SearchViewModel - class) : 생성자로 usecase 객체를 가지고 있음
-> ViewModel Factory를 활용하여 외부에서 usecase 객체를 주입 받는다.
1. [05:20] RecyclerView의 어댑터로 ListAdapter를 더 많이 사용한다.
2. [09:40] usecase 도입
-> ViewModel의 생성자를 domain/repository 에서 domain/usecase로 변경함 / ViewModelFactory도 이에 맞게 수정
-> usecase의 생성자에는 data/repository의 구현체를 둔다.
3. [14:31] TabLayout을 관리하기 위한 SearchMainTabModel data class 구현
파라미터로는 fragment와 title인데 추가로 icon 등도 넣을 수 있을 듯
data class SearchTabModel(
val fragment: Fragment,
@StringRes val title: Int, // 분석 필요
@DrawableRes val icon: Int, // 이건 알겠음
)
4. [18:15] RecyclerView에 ListAdapter를 사용한 이유
- 좀 더 간결하고, 기존 RecyclerViewAdapter 보다 Item 갱신에 있어 효율이 좋다.
-> RecyclerViewAdapter는 notify를 통해 데이터를 다 갱신하지만, ListAdapter는 데이터 간의 비교를 통해서 일치하지 않는 부분만 갱신(적용)해준다.
- Tip. ListAdapter도 RecyclerViewAdapter를 상속 받기 때문에 사용 가능함~
class SearchListAdapter(
private val onClick: (SearchListItem) -> Unit,
) : ListAdapter<SearchListItem, SearchListAdapter.ViewHolder>(
// ref: https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil.ItemCallback#public-constructors_1
object : DiffUtil.ItemCallback<SearchListItem>() {
// 현재 리스트에 노출하고 있는 아이템과 새로운 아이템이 서로 같은지 비교
// 권장 사항 : Item의 파라미터에 고유한 ID 값이 있는 경우, 이 메서드는 ID를 기준으로 동일성을 반환 해야함
override fun areItemsTheSame(oldItem: SearchListItem, newItem: SearchListItem): Boolean {
return if (oldItem is SearchListItem.ImageItem && newItem is SearchListItem.ImageItem) {
oldItem.title == newItem.title
} else {
oldItem == newItem
}
}
// areContentsTheSame : 현재 리스트에 노출하고 있는 아이템과 새로운 아이템의 equals를 비교한다.
// 역할 : 한 Item의 내용이 변경되었는지 감지 (ex 좋아요 state 감지)
// 호출 경로 : areItemsTheSame 메서드가 true를 반환하는 경우
override fun areContentsTheSame(oldItem: SearchListItem, newItem: SearchListItem): Boolean {
return oldItem == newItem
}
}
)
5. [22:22] Enum Class의 oridinal 프로퍼티 -> 각 item의 서수(orifinal, index)를 반환한다.
enum class SearchItemViewType {
IMAGE, VIDEO
}
SearchItemViewType.IMAGE.ordinal // 0
SearchItemViewType.VIDEO.ordinal // 1
6. [35:00] StateFlow와 SharedFlow의 차이점에 대해 공부해봐라
Flow가 뭔지부터 알아보자 - > 간단하게 한 줄로 정의 하려 했으나 실패, depth가 꽤 깊은 것 같다...
특징
- Flow 타입의 생성은 flow {} 빌더를 이용함 (이외에도 컬렉션에 .asFlow()를 쓴다거나 flowOf() 등의 메서드도 있었음)
- flow에서 방출된 값들은 collect 함수를 이용하여 수집됨
- 결과 값들은 flow에서 emit() 함수를 이용하여 방출됨
- 그리고, 타이머를 지정해서 일정시간 동안만 비동기 처리를 하거나, take등으로 원하는 데이터의 개수를 받아오면 중지하는 등 아주 다양한 활용이 가능해보인다.
뭔가 신세계다.... 추후에 무조건 학습 해야할 것 같다
핵심적인 로직의 차이
출력 결과
- 메서드에서는 일반적인 List를 다뤘다. 출력 결과는 당연히 모든 List값을 셋업한 뒤, map 함수를 통해 차례로 출력도니다.
- 하지만, flow 에서는 collect 메서드를 통해 flow 내부의 코드가 돌아가고 emit을 할 때마다 collect 메서드로 값을 방출한다는 차이점이 있었다.
// 기존
fun foo1(): List<Int> {
var arr = MutableList(3) { 0 }
for (i in 1..3) {
println("Emitting $i")
arr[i-1] = i
}
return arr
}
// flow
fun foo(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
println("Emitting $i")
emit(i)
}
}
fun main() = runBlocking<Unit> {
foo1().map { value -> println("$value 처리 완료") }
println("Done")
val f = foo()
withTimeoutOrNull(250) { // Timeout after 250ms
f.collect { value -> println("$value 처리 완료") }
}
}
// 출력 결과
Emitting 1
Emitting 2
Emitting 3
1 처리 완료
2 처리 완료
3 처리 완료
Done
Emitting 1
1 처리 완료
Emitting 2
2 처리 완료
그렇다면 둘의 차이점은?
(1) StateFlow : UI 반영에 최적화 (현재 상태와 새로운 상태에 특화된 Flow)
- 기본값(초기 상태)을 가지고, 상태가 변하면 모든 구독자에게 최신 상태를 전달함
- 신규 구독 시 가장 최근 값(상태)을 받는다.
(2) SharedFlow : Event 처리에 최적화된 Flow
- 이벤트 이므로 기본값이 없다.
- 새롭게 구독 한 뒤 이벤트가 발생해야 값이 정의된다.
- 생성자로 3가지 값을 전달할 수 있음
replay: Int, 새로운 구독자에게 구독 전 이벤트들 중 몇 개를 전달할지? -> default value = 0
extraBufferCapacity: Int, 추가 버퍼를 생성하여 emit한 데이터가 버퍼에 유지되도록 한다. - default value = 0
onBufferOverflow: BufferOverflow, 버퍼가 가득 찼을 때 어느 데이터를 제거할지? -> 오래된 데이터 제거( BufferOverflow.DROP_OLDEST) - default value = BufferOverflow.SUSPEND
Flow들의 내부 계층 구조 : Flow < SharedFlow < StateFlow(SharedFlow + 기본 값)
ref: https://myungpyo.medium.com/stateflow-%EC%99%80-sharedflow-32fdb49f9a32
LiveData가 있는데 Flow를 써야 하는 이유가 있을까?
- 굳이 그럴 필요는 없지만, Compose와 연계했을 때 조합이 좋다고 함
Tip1. RcyclerView의 예상 UI를 미리 보는 방법 - tools 속성 활용
(1) rv의 item에서 ImageView 등에는 아래 속성을 이용해 아바타 이미지가 보이도록 한다.
tools:background="@tools:sample/avatars"
(2) rv에서 tools의 listitem 속성을 사용하여 item 레이아웃을 지정하면 된다.
tools:listitem="@layout/item_rv_image"
Tip2. RecyclerView에서 ListAdapter 사용하기
class MyListAdapter(
private val onClick1: (View, Int) -> Unit
) : ListAdapter<MyItem, MyListAdapter.ViewHolder>(ItemDiffCallBack) {
// object class : 해당 클래스의 유일한 인스턴스를 자동으로 생성하고 관리한다.
object ItemDiffCallBack: DiffUtil.ItemCallback<MyItem>() {
// 현재 리스트에 노출하고 있는 아이템과 새로운 아이템이 서로 같은지 비교
// 권장 사항 : Item의 파라미터에 고유한 ID 값이 있는 경우, 이 메서드는 ID를 기준으로 동일성을 반환 해야함
override fun areItemsTheSame(oldItem: SearchListItem, newItem: SearchListItem): Boolean {
return if (oldItem is SearchListItem.ImageItem && newItem is SearchListItem.ImageItem) {
oldItem.title == newItem.title
} else {
oldItem == newItem
}
}
// areContentsTheSame : 현재 리스트에 노출하고 있는 아이템과 새로운 아이템의 equals를 비교한다.
// 역할 : 한 Item의 내용이 변경되었는지 감지 (ex 좋아요 state 감지)
// 호출 경로 : areItemsTheSame 메서드가 true를 반환하는 경우
override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem == newItem
}
}
class ViewHolder(
val binding: ItemRvBinding,
onClickParameter: (View, Int) -> Unit
): RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
onClickParameter(it, adapterPosition)
}
}
fun onBind(myItem: MyItem) {
with(binding) {
ivImg.setImageResource(myItem.icon)
tvName.text = myItem.name
tvAge.text = myItem.age
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemRvBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onClick1)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// checkPoint: getItem(position) 또는 currentList[position]를 통해서 binding 한다!!
holder.onBind(getItem(position))
}
}
그렇다면 adapter의 데이터 셋업은 어떻게 하는가?
- viewmodel의 LiveData, 각종 Flow 등을 옵저빙하는 방식
myListAdapter.submitList(dataLists) // adapter에 데이터 세팅하는 메서드 submitList()
Tip3. adapter 이름을 adapter라고 명명하지 말 것
- 그냥 adapter라는 이름으로 쓸 경우 RecyclerView에 adapter를 할당할 때, 제대로 지정되지 않고 오류도 나지 않는다.
// 귀찮다고 adapter로 명명하지 말 것.
// private val adapter: SearchListAdapter by lazy {
// SearchListAdapter()
// }
private val searchListAdapter: SearchListAdapter by lazy {
SearchListAdapter()
}
// with(binding) 까지만 했으면... adapter 파라미터에 접근이 안돼서 오류를 금방 찾았을 텐데,,,
with(binding.rvImage) {
// adapter = adapter // 여기서 오른쪽의 adapter는 위에서 선언한 adapter가 아닌 RV의 파라미터 adapter를 가져옴
adapter = searchListAdapter
layoutManager = LinearLayoutManager(context)
}
- 이유 : RecyclerView에는 아래와 같은 getter()가 있어서 할당된 adapter를 adapter 파라미터에 다시 넣는 코드가 되버림
public Adapter getAdapter() {
return mAdapter;
}
데이터는 제대로 전달하는 데 왜 onBindViewHolder가 호출되지 않느지 의문이였다. - 몇 시간을 날린 건지,,, ㅠㅠ
Tip4. 여러 Fragment에서 싱글톤 객체를 만드는 메서드들의 중복 코드를 제거하기 위해 확장함수로 분리해보자 -> 실패
inline fun <reified T: Any> newInstance(): T {
// INSTANCE Field에 접근
val fragClass = T::class.java.getDeclaredField("INSTANCE")
return synchronized(fragClass) {
val instance = fragClass.get(null) as? T
// 이미 생성된 객체가 있는 경우
if (instance != null) {
return instance
}
// 생성된 객체가 없는 경우 -> 객체 생성 및 생성된 객체를 필드에 저장
val newInstance = T::class.java.newInstance()
return newInstance.also {
fragClass.set(null, it)
}
}
}
사용법
newInstance<SearchListFragment>() // 기존 code : SearchListFragment.newInstance()
newInstance<SearchMainBookmarkFragment>() // 기존 code : SearchMainBookmarkFragment.newInstance()
하지만, .set() 메서드에서 cannot access private static volatile field 오류가 나고 있다. -> pass
'Android' 카테고리의 다른 글
GPS Location, 사용자 위치 정보 가져오기 (0) | 2024.05.08 |
---|---|
SharedPreferences, Room 사용법 (0) | 2024.05.08 |
ch 5주차 정리 (0) | 2024.04.30 |
앱 개발 숙련 과제 후기 (1) | 2024.04.18 |
Retrofit2 (2) | 2024.04.15 |