[Team 박카스] 팀 프로젝트 2022.02.08 코드 리뷰 - 1

ErroredPasta·2022년 2월 13일
0

코드리뷰

목록 보기
2/4
post-thumbnail

이번 코드리뷰에서 다룰 코드는 제가 구현한 코드입니다. 제가 구현한 부분은 처음에 나타나는 화면으로 구현을 한 내용은 다음과 같습니다.

  • 새로운 할인 상품의 정보를 불러와서 사용자에게 출력
  • 근처 마켓 정보를 불러와서 가까운 순으로 사용자에게 출력
  • 카테고리별 버튼을 생성하여 클릭 시 해당 카테고리의 상품들을 출력
  • 처음 화면(HomeMainFragment)과 상품 목록을 보여주는 화면(HomeFragment)의 이동
  • 근처 마켓 정보와 새로운 할인 상품의 정보를 출력할 때 사용할 ViewHolder

아래는 해당 구현 내용들에 대한 상세한 설명입니다. ViewHolder는 정보를 불러와서 binding을 해주는 단순한 동작만 하므로 설명을 생략하였습니다.

구현 내용

HomeMainViewModel 구현

HomeMainViewModel의 property

// HomeMainViewModel.kt
private val _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState> = _marketData

private val _itemData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val itemData: LiveData<HomeMainState> = _itemData

private lateinit var allNewSaleItemsList: List<HomeItemModel>

marketDataitemData는 각각 근처 마켓 정보와 새로운 할인 상품을 나타낼 LiveData입니다. Backing property를 이용하여 LiveData를 필요한 곳에서 observe 가능하게 하면서 value의 변경은 ViewModel에서만 가능하도록 하였습니다. 처음에는 마켓과 상품의 정보를 불러오기 전이므로 초기 상태를 Uninitialized로 설정하였습니다.
allNewSaleItemsList는 모든 새로운 할인 상품을 저장하고 있을 List로 카테고리별로 상품을 보여줄 때 사용을 하게 됩니다.

HomeMainViewModel의 Method

override fun fetchData(): Job = viewModelScope.launch {
    if (LocationData.locationStateLiveData.value is LocationState.Success) {
        fetchMarketData()
        fetchItemData()
    }
}

FragmentView가 생성될 때 근처 마켓 정보와 새로운 할인 상품 정보를 불러 올 때 사용할 Method입니다. 사용자의 위치 정보를 기반으로 정보를 불러오기 때문에 위치 정보를 성공적으로 불러왔을 때만 동작하도록 하였습니다. fetchMarketData()fetchItemData()의 상세 내용은 아래에서 설명을 하였습니다.

fun reloadData(): Job {
    _marketData.value = HomeMainState.Loading
    _itemData.value = HomeMainState.Loading
    return fetchData()
}

추후 정보를 새로 불러올 때 사용할 method입니다. 근처 마켓 정보와 새로운 할인 상품 정보의 상태를 Loading으로 전환 후 fetchData()를 호출하여 Job을 반환하도록 하였습니다.

근처 마켓 정보 가져오기

// HomeMainViewModel.kt
private suspend fun fetchMarketData() {
    if (marketData.value !is HomeMainState.Success<*>) {
        _marketData.value = HomeMainState.Loading

		// 거리가 가까운 순으로 정렬
        // 임시로 ViewModel에서 type을 HOME_CELL로 변경
        _marketData.value = HomeMainState.Success(
            modelList = homeRepository.getAllMarketList().map {
                it.copy(type = CellType.HOME_CELL)
            }.sortedBy { it.distance }
        )
    }
}

homeRepsitory에서 근처 마켓의 정보를 가져와서 거리가 가까운 순으로 정렬하는 method 입니다. Success가 아닐 때만 정보를 가져오도록 하여 화면 이동과 같은 상황에서 불필요하게 계속 정보를 불러오는 것을 방지했습니다.

// HomeMainFragment.kt
marketData.observe(viewLifecycleOwner) {
    when (it) {
		...

        is HomeMainState.Success<*> -> {
            nearbyMarketAdapter.submitList(it.modelList)
        }

		...
    }
}

HomeMainFragment에서 marketData를 observe하고 있다가 Success 상태가 되면 성공적으로 불러온 데이터를 nearbyMarketAdapter에 리스트를 submitList(it.modelList)을 이용하여 업데이트 하여 RecyclerView에 정보를 출력하게 됩니다.

새로운 할인 상품 정보 가져오기

// HomeMainViewModel.kt
private suspend fun fetchItemData() {
    if (itemData.value !is HomeMainState.Success<*>) {
        _itemData.value = HomeMainState.Loading

        allNewSaleItemsList = homeRepository.getAllNewSaleItems()
        _itemData.value = HomeMainState.ListLoaded
    }
}

homeRepository에서 새로운 할인 상품 정보를 가져와서 allNewSaleItemsList로 저장하는 method입니다. 성공적으로 정보를 가져오면 새롭게 추가한 ListLoaded 상태가 됩니다.

// HomeMainFragment.kt
itemData.observe(viewLifecycleOwner) {
    when (it) {
		...

        is HomeMainState.ListLoaded -> with(binding.newSaleItemSpinner) {
            viewModel.setItemFilter(categories[selectedItemPosition])
        }

        is HomeMainState.Success<*> -> {
            newSaleItemsAdapter.submitList(it.modelList)
        }

		...
    }
}

ListLoaded 상태가 되면 HomeMainViewModelsetItemFilter(category)를 이용하여 알맞은 카테고리의 상품을 출력하게 됩니다.

// HomeMainViewModel.kt
fun setItemFilter(category: HomeListCategory) {
    if (::allNewSaleItemsList.isInitialized) {
        _itemData.value = HomeMainState.Success(
            modelList = allNewSaleItemsList.filter { it.homeListCategory == category }
        )
    }
}

allNewSaleItemList가 초기화 되었을 경우에만 실행되도록 하였고 Listfilter method를 이용하여 parameter로 넘겨받은 카테고리와 일치하는 상품만 나타내도록 하고 상태를 Success로 전환하여 newSaleItemsAdaptersubmitList(it.modelList)List를 넘겨주어 RecyclerView에 출력하게 됩니다.

새로운 할인 상품 Spinner 구현

// HomeListCategory.kt
enum class HomeListCategory(
    @StringRes val categoryNameId: Int,
    @StringRes val categoryTypeId: Int
) {
    TOWN_MARKET(R.string.all, R.string.all_type), // Not needed
    FOOD(R.string.food, R.string.food_type),
    MART(R.string.mart, R.string.mart_type),
    SERVICE(R.string.service, R.string.service_type),
    FASHION(R.string.fashion, R.string.fashion_type),
    ACCESSORY(R.string.accessory, R.string.accessory_type),
    ETC(R.string.etc, R.string.etc_type)
}
// HomeMainFragment.kt
private val categories = HomeListCategory.values().drop(1)

Spinner를 이용하여 상품을 카테고리별로 보여주어야 하므로 첫 번째인 마켓 카테고리는 필요가 없어서 drop(1)을 이용하여 제외하였습니다.

// HomeMainFragment.kt
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        viewModel.setItemFilter(categories[position])
}

setItemFilter(categories[position])를 이용하여 선택된 Spinner item 카테고리의 상품만 출력하게 됩니다.

위와 같이 Spinner의 item이 설정된 것을 확인할 수 있고 제대로 동작하는 것을 확인 하였습니다.

Navigation component에 대해서 자세히 설명하기에는 많은 내용이 필요하여 나중에 따로 글을 작성할 예정입니다.

Navigation component를 이용하여 BottonNavigationView에서 사용할 Navigation graph입니다.
home.xml, order_list.xml, like.xml, map.xml, my_info.xml을 include하고 있고 각각은 BottomNavigationView의 menu item들에 대응되는 Navigation graph입니다.
Navigation component를 BottomNavigationView에서 사용하기 위해서 Navigation graph와 BottomNavigationView의 menu item들의 android:id를 일치시켜 주어야합니다.

현재 home.xml을 제외한 graph에는 하나의 Fragment만 존재하므로 home.xml만 설명을 하도록 하겠습니다. home.xml에는 homeMainFragmenthomeFragment가 존재하며 homeMainFragment에서 homeFragment로의 action이 존재하는 것을 볼 수 있습니다.

<!-- home.xml -->
<fragment
    android:id="@+id/homeFragment"
    android:name="com.example.YUmarket.screen.home.HomeFragment"
    android:label="HomeFragment" >
    <argument
        android:name="goToTab"
        app:argType="com.example.YUmarket.model.homelist.category.HomeListCategory"
        android:defaultValue="TOWN_MARKET" />
</fragment>

homeFragmentargument를 설정하여 homeMainFragment에서 homeFragment로 이동시에 어떤 탭을 표시할지 HomeListCategory의 값을 넘겨주도록 하여 argument를 이용하여 알맞은 탭을 출력하도록 하였습니다.

<!-- activitiy_main.xml -->
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragmentContainer"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:defaultNavHost="true"
    app:layout_constraintBottom_toTopOf="@id/bottomNav"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/toolbar"
    app:navGraph="@navigation/navigation_graph" />

FragmentContainerViewandroid:nameNavHostFragment로 명시를 하여 containerFragment를 설정해주고 app:navGraph에 사용할 Navigation graph를 설정합니다. app:defaultNavHosttrue로 하여 back button의 동작을 intercept하도록 하였습니다.

// MainActivity.kt
private val navController by lazy {
    val hostContainer =
        supportFragmentManager.findFragmentById(R.id.fragmentContainer) as NavHost

    hostContainer.navController
}

NavHostFragment에서 NavController를 lazy하게 가져오는 코드입니다.
Activity의 extension 함수인 Activity.findNavController(viewId: Int)를 사용하지 않는 이유는 FragmentContainerView를 이용하여 NavHostFragment를 생성하였기 때문입니다.

When creating the NavHostFragment using FragmentContainerView or if manually adding the NavHostFragment to your activity via a FragmentTransaction, attempting to retrieve the NavController in onCreate() of an Activity via Navigation.findNavController(Activity, @IdRes int) will fail. You should retrieve the NavController directly from the NavHostFragment instead. [1]

이와 같은 경우 해당 함수가 제대로 동작하지 않아서 NavController를 제대로 가져오지 못하게 되어 에러가 발생합니다.
Android Developers 공식 페이지에서는 위와 같이 NavHostFragment에서 NavController를 직접 가져오라고 적혀있습니다.

// MainActivity.kt
bottomNav.setupWithNavController(navController)

NavController를 이용하여 BottonNavigationView의 동작을 설정하여 알맞은 Fragment가 출력되도록 하였습니다.

// HomeMainFragment.kt
override fun initViews() {
		...

        showMoreTextView.setOnClickListener {
            findNavController().navigate(
                HomeMainFragmentDirections
                    .actionHomeMainFragmentToHomeFragment()
            )
        }

        ...

        setCategoryButtonListener()
    }
}

private fun setCategoryButtonListener() = with(binding) {

    foodCategoryListButton.setOnClickListener {
        findNavController().navigate(
            HomeMainFragmentDirections
                .actionHomeMainFragmentToHomeFragment(HomeListCategory.FOOD)
        )
    }

    martCategoryListButton.setOnClickListener {
        findNavController().navigate(
            HomeMainFragmentDirections
                .actionHomeMainFragmentToHomeFragment(HomeListCategory.MART)
        )
    }

    ...
}

버튼별로 HomeFragment에서 알맞은 탭을 보여줄 수 있도록 알맞은 argument를 넘겨주고 HomeMainFragment에서 HomeFragment로 이동하도록 OnClickListener를 설정해주었습니다. showMoreTextViewactionargument의 기본값이 TOWN_MARKET으로 설정되어 있기 때문에 아무것도 넘겨주지 않도록 했습니다.

private val args by navArgs<HomeFragmentArgs>()

...

override fun observeData() = with(binding) {
    LocationData.locationStateLiveData.observe(viewLifecycleOwner) {
        when (it) {
            is LocationState.Success -> {
                initViewPager()
                viewPager.currentItem = args.goToTab.ordinal
            }
        }
    }
}

by navArgs<HomeFragmentArgs>()argument를 lazy하게 가져오도록 하였습니다.
Fragment 이동시에 argument로 받은 HomeListCategory의 값을 이용하여 viewPager.currentItem = args.goToTab.ordinal로 알맞은 카테고리의 화면을 출력합니다.

왜 Navigation component를 이용하였는가?

  1. Navigation graph를 이용하여 Fragment간 관계를 파악하기 쉬워진다.
    원래 Fragment간 관계를 파악하기 위해서는 직접 구현한 코드들을 살펴봐야합니다.
    다른 동작을 구현한 코드들 사이에서 Fragment 이동 코드들을 찾고 만약 관련된 코드들이 분산되어 있을 경우 코드를 읽는 것은 쉬운일이 아닙니다.
    Navigation component를 이용하면 Navigation graph를 살펴봄으로써 한눈에 어떻게 Fragment간 이동이 이루어지는지 그리고 이동시에 어떤 argument를 넘겨주어야 하는지 알 수 있습니다.

  2. Boilerplate 코드를 줄일 수 있다.
    원래는 매번 FragmentManager를 통해 FragmentTransaction을 이용하여 직접 어떻게 화면을 전환할지 boilerplate 코드를 작성하여야 했습니다. 하지만 Navigation component를 사용함으로써 NavControllernavigate(resId)를 이용하여 boilerplate 코드를 줄일 수 있습니다.

  3. SafeArgs를 이용하여 type safe하게 Fragmentargument를 넘겨줄 수 있다.

다음 내용

다음 글에서는 코드 리뷰를 진행하면서 받았던 질문들과 해당 질문들의 답변에 대해서 작성할 예정입니다.

Reference

[1] "Get started with the Navigation component," Android Developers, last modified Feb 09, 2022, accessed Feb 13, 2022, https://developer.android.com/guide/navigation/navigation-getting-started#navigate.

profile
Hola, Mundo

0개의 댓글