수현이 찾기 뷰, 홈 뷰 구현

→ 홈 화면 로티, 건수, 카테고리 이동, 게시글 조회

→ 수현이 찾기 조회 삭제 업로드 전부

1️⃣ 네비게이션 공유 뷰모델 문제

두개의 스크린 사이에서 뷰모델을 공유하기 위해 어떻게 할 지 고민을 하다가 처음에는 Pager로 하나의 스크린을 두개처럼 보이도록 구현하려했다.

하지만 navController에서 제공하는 previousBackStackEntry을 활용하면 뷰모델을 공유할 수 있다고 해서

NavGraph에서 뷰모델을 구해서 라우트로 넘기고, 이를 스크린에서 내려주니 두개의 스크린 사이에서 뷰모델을 공유할 수 있었다.

물론 이때 공유하고자 하는 스크린이 3개 이상이 아닐 때 활용이 가능한 방법인 것 같다. 세번째 스크린에서 뷰모델이 다시 생성되기 때문에 공유가 이루어지지 않는 것을 확인했다. 내가 맡은 수현이 찾기에서는 다행히 두개의 스크린에서만 공유하면 됐기 때문에 이 방법을 통해 뷰모델을 공유할 수 있었다.

findSuhyeonNavGraph(
                padding = padding,
                onNavigateToFindSuheyonUpload = navigator::navigateToFindSuhyeonUpload,
                onNavigateToFindSuhyeon = { navigator.navigateToFindSuhyeon() },
                onNavigateToFindSuheyonUploadDetail = navigator::navigateToFindSuhyeonUploadDetail,
                onNavigateToFindSuhyeonPost = { navigator.navigateToFindSuhyeonPost(it) },
                getBackStackUploadViewModel = { navBackStackEntry ->
                    navigator.navController.previousBackStackEntry?.let { previousEntry ->
                        hiltViewModel<FindSuhyeonUploadViewModel>(previousEntry)
                    } ?: hiltViewModel(navBackStackEntry)
                }
            )

2️⃣ 메인 탭 라우트(바텀바 라우트)가 data class일때 인자가 다르면 네비게이션이 안되는 문제 & 바텀바 탭이 선택됐는지 인식을 못하는 문제

class MainNavigator(
    val navController: NavHostController,
) {
    private val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    val currentTab: MainTab?
        @Composable get() = MainTab.entries.find { tab ->
            when (tab.route) {
                is MainTabRoute.Gallery -> currentDestination?.route?.startsWith(MainTabRoute.Gallery::class.qualifiedName!!) == true
                else -> currentDestination?.route == tab.route::class.qualifiedName
            }
        }

    fun navigate(tab: MainTab) {
        val navOptions = navOptions {
            popUpTo(MainTab.HOME.route) {
                saveState = true
            }
            launchSingleTop = true
            restoreState = true
        }

        when (tab) {
            MainTab.HOME -> navController.navigateToHome(navOptions)
            MainTab.FINDSUHYEON -> navController.navigateToFindSuhyeon(navOptions)
            MainTab.GALLERY -> navController.navigateToGallery(navOptions)
            MainTab.CHAT -> navController.navigateToChat(navOptions)
            MainTab.MYPAGE -> navController.navigateToMyPage(navOptions)
        }
    }

    
    fun navigateToGallery(navOptions: NavOptions? = null, category: String? = null) {
        navController.navigateToGallery(
            navOptions ?: navOptions {
                popUpTo(navController.graph.findStartDestination().id) {
                    inclusive = true
                }
                launchSingleTop = true
            },
            category
        )
    }
    
    fun navigateToGalleryPostDetail(galleryId: Long) {
        navController.navigateToGalleryPostDetail(galleryId)
    }

    private inline fun <reified T : Route> isSameCurrentDestination(): Boolean =
        navController.currentDestination?.route == T::class.qualifiedName

    @Composable
    fun shouldShowBottomBar(): Boolean {
        return currentDestination?.route?.let { currentRoute ->
            MainTab.entries.any { tab ->
                when (tab.route) {
                    is MainTabRoute.Home -> currentRoute.startsWith(MainTabRoute.Home::class.qualifiedName!!)
                    is MainTabRoute.FindSuhyeon -> currentRoute.startsWith(MainTabRoute.FindSuhyeon::class.qualifiedName!!)
                    is MainTabRoute.Gallery -> currentRoute.startsWith(MainTabRoute.Gallery::class.qualifiedName!!)
                    is MainTabRoute.Chat -> currentRoute.startsWith(MainTabRoute.Chat::class.qualifiedName!!)
                    is MainTabRoute.MyPage -> currentRoute.startsWith(MainTabRoute.MyPage::class.qualifiedName!!)
                }
            }
        } ?: false
    }
}

sealed interface MainTabRoute : Route {
    @Serializable
    data object Home : MainTabRoute
    @Serializable
    data object FindSuhyeon : MainTabRoute
    @Serializable
    data class Gallery(val category: String?) : MainTabRoute
    @Serializable
    data object Chat : MainTabRoute
    @Serializable
    data object MyPage : MainTabRoute
}

// 기존 코드 
@Composable
    fun shouldShowBottomBar() = MainTab.contains {
        currentDestination?.route == it::class.qualifiedName
    }

// 변경 코드 
@Composable
    fun shouldShowBottomBar(): Boolean {
        return currentDestination?.route?.let { currentRoute ->
            MainTab.entries.any { tab ->
                when (tab.route) {
                    is MainTabRoute.Home -> currentRoute.startsWith(MainTabRoute.Home::class.qualifiedName!!)
                    is MainTabRoute.FindSuhyeon -> currentRoute.startsWith(MainTabRoute.FindSuhyeon::class.qualifiedName!!)
                    is MainTabRoute.Gallery -> currentRoute.startsWith(MainTabRoute.Gallery::class.qualifiedName!!)
                    is MainTabRoute.Chat -> currentRoute.startsWith(MainTabRoute.Chat::class.qualifiedName!!)
                    is MainTabRoute.MyPage -> currentRoute.startsWith(MainTabRoute.MyPage::class.qualifiedName!!)
                }
            }
        } ?: false
    }

기존에는 네비게이션의 라우트 이름과 MainTabRoute 클래스의 qualifiedName를 비교하여 일치하면 매인 탭의 라우트라고 인식해 바텀바를 표시하는 등의 작업이 들어갔었다. 하지만 기능을 구현하다보니 MainTabRoute중 Gallery가 인자가 필요하게 되었다. 이때 문제는 MainTabRoute의 Gallery이름은 Gallery이고, currentDestination?.route는 Gallery/{category}라서 일치하지 않아 문제가 발생했다.

이를 해결하기 위해 라우트 이름인 currentDestination?.route가 Gallery로 시작하는지로 판단하여 메인 바텀바 탭인지를 인식시켰다.

3️⃣ 바텀바의 선택 상태 반영이 안되는 문제

@Composable
fun MainBottomBar(
    modifier: Modifier = Modifier,
    visible: Boolean,
    tabs: PersistentList<MainTab>,
    currentTab: MainTab?,
    onTabSelected: (MainTab) -> Unit,
) {
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn() + slideIn { IntOffset(0, it.height) },
        exit = fadeOut() + slideOut { IntOffset(0, it.height) }
    ) {
        val borderColor = defaultWithSuhyeonColors.Grey200
        Row(
            modifier = modifier
                .fillMaxWidth()
                .height(68.dp)
                .drawBehind {
                    val borderThickness = 1.dp.toPx()

                    drawLine(
                        color = borderColor,
                        start = Offset(0f, 0f),
                        end = Offset(size.width, 0f),
                        strokeWidth = borderThickness
                    )
                }
                .background(
                    color = colors.White,
                ),
        ) {
            tabs.forEach { tab ->
                MainBottomBarItem(
                    tab = tab,
                    selected = **tab == currentTab,**
                    onClick = { onTabSelected(tab) },
                )
            }
        }
    }
}

enum class MainTab(
    @DrawableRes val defaultIconResId: Int,
    @DrawableRes val selectIconResId: Int,
    @StringRes val descriptionResId: Int,
    val route: MainTabRoute,
) {
    HOME(
        defaultIconResId = R.drawable.ic_home_default,
        selectIconResId =  R.drawable.ic_home_select,
        descriptionResId = R.string.bottom_navigation_bar_item_home,
        route = MainTabRoute.Home,
    ),
    FINDSUHYEON(
        defaultIconResId = R.drawable.ic_find_suhyeon_default,
        selectIconResId =  R.drawable.ic_find_suhyeon_select,
        descriptionResId = R.string.bottom_navigation_bar_item_find_suhyeon,
        MainTabRoute.FindSuhyeon
    ),
    GALLERY(
        defaultIconResId = R.drawable.ic_gallery_default,
        selectIconResId =  R.drawable.ic_gallery_select,
        descriptionResId = R.string.bottom_navigation_bar_item_gallery,
        MainTabRoute.Gallery(null),
    ),
    CHAT(
        defaultIconResId = R.drawable.ic_chat_default,
        selectIconResId =  R.drawable.ic_chat_select,
        descriptionResId = R.string.bottom_navigation_bar_item_chat,
        MainTabRoute.Chat,
    ),
    MYPAGE(
        defaultIconResId = R.drawable.ic_my_default,
        selectIconResId =  R.drawable.ic_my_select,
        descriptionResId = R.string.bottom_navigation_bar_item_my,
        MainTabRoute.MyPage,
    );

    companion object {
        @Composable
        fun find(predicate: @Composable (MainTabRoute) -> Boolean): MainTab? {
            return entries.find { predicate(it.route) }
        }

        @Composable
        fun contains(predicate: @Composable (Route) -> Boolean): Boolean {
            return entries.map { it.route }.any { predicate(it) }
        }
    }
}