MapViewModel 추가 : 비즈니스 로직 분리
이전 코드에서는 MainActivity에서 네트워크 호출을 하는 MainService 객체를 생성해
앱에 필요한 데이터를 불러와 마커를 생성했다.
데이터를 불러오는 로직을 ViewModel이 수행하게끔 수정했다.
ViewModel은 데이터를 가져오고 앱의 상태 데이터들을 보유하고 관리한다.
그리고 UI는 ViewModel의 앱 상태에 의해 UI 요소를 변경한다.
즉, ViewModel은 UI와 데이터간의 상호작용을 담당하는 역할이다.
Store 리스트 데이터를 저장하고 관리할 수 있는 ViewModel 클래스를 추가해줬다.
Jetpack의 ViewModel를 상속받는 MapViewModel 클래스 타입을 정의해줬다.
private const val RADIUS_METER = 1000 // 1km
class MapViewModel : ViewModel() {
private val repository = MaskAlarmiRepositoryImpl.get()
private val _stores: MutableLiveData<List<Store>> = MutableLiveData()
val stores: LiveData<List<Store>> get() = _stores
private val _isLoading: MutableLiveData<Boolean> = MutableLiveData(false)
val isLoading: LiveData<Boolean> get() = _isLoading
private val _errorMessage: MutableLiveData<String> = MutableLiveData("")
val errorMessage: LiveData<String> get() = _errorMessage
fun getStoresByGeo(latLng: LatLng) {
_isLoading.postValue(true)
storeRepository.getStoresByGeo(
latLng.latitude,
latLng.longitude,
RADIUS_METER,
onSuccess = { stores ->
_stores.postValue(stores)
_isLoading.postValue(false)
}, onFailure = { _ ->
_isLoading.postValue(false)
}
)
}
}
ViewModel 안에서 MaskAlarmiRepository 싱글톤 객체를 통해 Store 리스트 데이터를 요청한다.
ViewModel은 응답받은 데이터가 내부 리소스 저장소(내부 DB, 파일 등)에서 온 데이터인지
외부 리소스 저장소(원격 서버)에서 온 데이터인지 모른다.
그저 필요한 데이터를 레포지토리에게 전달받아 앱 상태 데이터를 업데이트하면 된다.
앱 상태 데이터는 LiveData로 UI 컨트롤러인 MapFragment가 관찰하게게 했다.
LiveData의 값을 변경(postValue)하면 UI 컨트롤러가 이를 감지하고 필요한 일을 수행한다.
또한, 데이터 레이어게게 요청의 성공, 실패를 처리하는 콜백 함수를 인자로 전달해 결과를 처리한다.
추후 리팩토링 사항
- 레포지토리를 Hilt를 적용해 의존성 주입 패턴으로 리팩토링
- LiveData를 kotlin 비동기 스트림 라이브러리인 Flow로 리팩토링
- 콜백 함수가 아닌 Coroutine를 활용해 비동기 제어하기
MapFragment : 이벤트 발생 및 UI 요소 변경
UI 컨트롤러인 MapFragment는 ViewModel에게 getStoresByGeo를 요청하고
stores 데이터를 관찰하고 있다가 데이터 변경이 감지되면 마커를 생성하게 된다.
(이전 코드에서 UI 컨트롤러는 MainActivity였지만 개선하면서
MainActivity는 MapFragment를 호스팅하고 맵과 관련되지 않은 뷰만 관리하게끔 수정하였다.
따라서 현재 코드에서 Store 데이터와 관련된 UI 컨트롤러는 MapFragment 이다)
class MapFragment : Fragment(), OnMapReadyCallback {
private lateinit var viewModel: MapViewModel
private var storeMarkers: List<StoreMarker> = emptyList()
private var locationUtil: LocationUtil? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[MapViewModel::class.java]
locationUtil = LocationUtil(requireActivity() as AppCompatActivity)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.fragment_map, container, false)
locationUtil?.getLastLocation { latlng ->
latlng?.let { viewModel.getStoresByGeo(latlng) }
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeStores()
}
private fun observeStores() {
viewModel.stores.observe(
viewLifecycleOwner
) { stores ->
storeMarkers = stores?.let { makeStoreMarkers(it) } ?: emptyList()
if (isMapReady) {
storeMarkers.forEach { it.marker.map = this.naverMap }
}
}
}
private fun makeStoreMarkers(stores: List<Store>): List<StoreMarker> {
return stores.map { store ->
StoreMarker(
store.code,
store.remainState,
coordinate = LatLng(store.lat, store.lng),
) { storeCode, isClicked ->
onStoreMarkerClicked(storeCode, isClicked)
}.newInstance()
}
}
}
코드가 돌아가는 순서를 살펴보면 (설명에 불필요한 코드 부분은 생략)
MapFragment가 생성되며 MapViewModel를 가져오고
onCreateView 생명주기에서 MapViewModel에게 stores 데이터를 가져오라는 이벤트를 발생시킨다.
MapFragment는 MapViewModel의 stores LiveData를 관찰하다가
stores 데이터가 변경되면 makeStoreMarkers 메서드를 호출해
Store 리스트를 StoreMarker 리스트로 변환하여 Map UI에 렌더링을 수행한다.
추후 리팩토링 사항
- ViewModel을 Kotlin의 'by' 속성 위임으로 리팩토링
- 앱 상태 변경이 UI 요소를 바로 변경시킬 수 있도록 DataBinding 적용
'프로젝트 > 마스크 알라미' 카테고리의 다른 글
4년 전 코드 리팩토링하기 (4) - Repository 패턴 (0) | 2023.10.29 |
---|---|
4년 전 코드 리팩토링하기 (2) - 아키텍처 재설계 (0) | 2023.10.26 |
4년 전 코드 리팩토링하기 (1) (0) | 2023.10.20 |