본문 바로가기
안드로이드 프로젝트/유튜브 음정 조절 어플리케이션

[Android] #9 컴포넌트 설계 수정

by joh9911 2023. 4. 10.

 

빠르게 피드백을 받아보고자, 플레이리스트 기능을 제외하고 출시를 했었습니다.

해당 기능에 대한 고려 없이 개발을 했던 것 같습니다.

 

추가하려던 찰나 컴포넌트 설계를 잘못했다는 것을 깨달았습니다.

 

기존의 설계는 다음과 같습니다.

 

 

main_activity

하나밖에 없는 액티비티입니다.

액티비티 내 모든 코드가 들어가 있습니다.

 

transpose 페이지

피치와 템포를 조절할 수 있는 transpose 페이지입니다.

fragment처럼 보이지만 사실 액티비티 코드 내에 있으며,

바텀 내비게이션의 변환 버튼을 눌러 visiblity 상태를 바꾸는 형식으로 코드를 작성하였습니다.

 

이렇게 만들었던 이유는, 음악을 감상할 때 언제든지 현재의 피치와 템포 상태를 확인할 수 있게끔 하기 위함이었습니다.

 

 

Fragment 동작 방식

 

위 툴바의 검색 버튼을 누르면, 연관 검색어 RecyclerView 가 visible 상태가 됩니다.

 

검색어를 입력하여 탐색 버튼을 누르면, 검색 결과를 보여주는 SearchResultFragment가 frameLayout의 backStack에 쌓이고,

 

채널을 클릭하면 채널 정보와 채널의 동영상들을 보여주는 ChannelFragment가 backStack에 쌓이게 됩니다.

 

 

뒤로 가기를 누를 경우 하나씩 제거되게끔 만들었습니다.

 

Fragment들의 쌓여있을 때 바텀 내비게이션의 홈버튼을 누르면 모든 backStack이 사라지며,

 

기존의 액티비티 페이지가 보이게 됩니다.

 

 

 

이렇게 하기 위해서는 뒤로 가기에 대한 예외 처리가 필요했습니다.

 

<activity.kt>

override fun onBackPressed() {
    if (toolbar.menu.findItem(R.id.youtube_search_icon).isActionViewExpanded)
        return
    if (supportFragmentManager.findFragmentById(R.id.player_fragment) == null){
        if (transposePage.visibility == View.VISIBLE)
            transposePageInvisibleEvent()
        else
            return super.onBackPressed()
    }
    else{
        val playerFragment 
        = supportFragmentManager.findFragmentById(binding.playerFragment.id) as PlayerFragment
        if (playerFragment.binding.playerMotionLayout.currentState == R.id.end){
            playerFragment.binding.playerMotionLayout.transitionToState(R.id.start)
        }
        else{
            if (transposePage.visibility == View.VISIBLE)
                transposePageInvisibleEvent()
            else
                return super.onBackPressed()
        }
    }
}

 

뒤로가기 버튼을 누를 시 우선순위를 정했습니다.

 

1. 재생 프레그먼트가 최상위에 있을 때 밑으로 내려준다.

 

2. 변환 페이지가 visible 상태에 있을 때 invisible 상태로 바꿔준다.

 

3. 프레그먼트들이 쌓인 backStack을 하나씩 없앤다.

 

 

onBackPressed 방식

 

 

이런 상황에서 플레이리스트(보관함) 페이지를 어떻게 추가할지 고민이 되었습니다.

 

 

처음에는 그냥 backStack에 추가를 하는 식으로 해보자 생각을 했습니다.

 

중복되어 쌓이는 것을 방지하기 위해, for문을 돌려 backStack의 쌓여있는 프레그먼트들 중 보관함 페이지가 존재하는지 확인한 후, 

 

존재하지 않으면 backStack에 추가를 해주는 방식입니다.

 

 

 

코드를 추가하려다 생각해 보니 어색함이 느껴졌습니다.

 

보통 바텀 내비게이션의 버튼을 통해 프레그먼트들을 교체하는 방식을 사용하기 때문입니다.

 

문득 유튜브에서는 어떤 방식을 사용할까 궁금해졌습니다.

 

 

유튜브 동작 방식

 

 

처음에는, 툴바부터 바텀 네비게이션 바 전까지 홈, 구독, 보관함에 대한 프레그먼트들이 backStack에 쌓이는 방식이라 생각했습니다.

 

그러나 몇 가지 실험을 해본 결과 제 생각이 틀렸다는 것을 깨달았습니다.

 

 

 

 

모든 프레그먼트가 한 backStack에 들어가 있을 경우, 바텀 내비게이션의 버튼을 클릭할 때마다

 

해당 버튼에 관련된 프레그먼트들을 최상위로 올려줘야 합니다.

 

구현은 할 수 있지만, 너무 복잡해집니다.

 

 

저는 홈, 구독, 보관함 이렇게 세 개의 프레그먼트가 있으며,

 

각각의 프레그먼트에 frameLayout을 둬, 거기에 검색 결과 등의 프레그먼트들을 쌓는 것이 아닐까 생각을 했습니다.

 

프레그먼트를 전환할 때마다 검색창이 바뀌는 것을 보아, 프레그먼트마다 toolBar를 정의해야겠다 생각했습니다.

 

 

감이 잘 잡히지 않았지만, 일단 시도를 해봤습니다.

다음은 수정할 컴포넌트 설계입니다.

 

액티비티의 frameLayout에 홈 프레그먼트, 보관함 프레그먼트를 넣어주며,

 

각 프레그먼트의 frameLayout에 검색결과 프레그먼트, 채널프레그먼트 들을 백스택으로 추가해 주는 방식입니다.

 

 

홈 프레그먼트, 보관함 프레그먼트는 전환이 되더라도 각각의 정보를 기억해야 합니다.

 

따라서 replace가 아닌 add로 frameLayout에 추가를 해주었고,

 

show, hide를 통해 화면에 보이지 않도록 처리를 해주었습니다.

 

<activity.kt>

lateinit var homeFragment: HomeFragment
var myPlaylistFragment: MyPlaylistFragment? = null 

fun initFragment(){ // onCreate 에서 호출됨
        homeFragment = HomeFragment()
        supportFragmentManager.beginTransaction()
            .add(binding.basicFrameLayout.id,homeFragment)
            .commit()
    }
    
fun initBottomNavigationView(){  // onCreate 에서 호출됨
    bottomNavigationView = binding.bottomNavigationView
    // 바텀 네비게이션 클릭 리스너
    bottomNavigationView.setOnItemSelectedListener { item -> 
        when (item.itemId) {
            R.id.home_icon -> { // 홈 버튼을 누를 시 
                supportFragmentManager.beginTransaction().hide(myPlaylistFragment!!).commit()
                supportFragmentManager.beginTransaction().show(homeFragment).commit()
                transposePage.visibility = View.INVISIBLE

            }
            R.id.transpose_icon -> {
                transposePage.visibility = View.VISIBLE
            }
            R.id.my_playlist_icon -> { // 보관함 버튼을 누를 시
                if (myPlaylistFragment == null){
                    myPlaylistFragment = MyPlaylistFragment()
                    supportFragmentManager.beginTransaction()
                        .add(binding.basicFrameLayout.id,myPlaylistFragment!!)
                        .commit()
                }
                supportFragmentManager.beginTransaction().show(myPlaylistFragment!!).commit()
                supportFragmentManager.beginTransaction().hide(homeFragment).commit()
            }
        }
        true
    }
}

 

홈 프레그먼트, 보관함 프레그먼트에서는 검색 결과 프레그먼트를 백스택에 추가해줘야 합니다.

fragment의 자식이므로, childFragmentManger를 이용하였습니다.

 

 

<HomeFragment.kt>

// 검색창에서 탐색 버튼을 눌렀을 경우
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{ 
    override fun onQueryTextSubmit(query: String?): Boolean {
        searchView.clearFocus() // 탐색창에 포커스 없애기
        childFragmentManager.beginTransaction() // 백스택에 결과 프레그먼트를 추가
            .replace(binding.searchResultFrameLayout.id,SearchResultFragment(query!!))
            .addToBackStack(null)
            .commit()
        binding.searchRecyclerView.visibility = View.INVISIBLE // 연관검색어 창 없애기
        return false
    }
    
// 연관검색어 창 클릭 이벤트    
    searchAdapter.setItemClickListener
    (object: SearchSuggestionKeywordRecyclerViewAdapter.OnItemClickListener{
            override fun onClick(v: View, position: Int) {
                val searchWord = suggestionKeywords[position]
                suggestionKeywords.clear()
                searchAdapter.submitList(suggestionKeywords.toMutableList())
                searchView.setQuery(searchWord,false) // 검색한 키워드 텍스트 설정
                searchView.clearFocus()
                
                childFragmentManager.beginTransaction()
                    .replace(binding.searchResultFrameLayout.id,SearchResultFragment(searchWord))
                    .addToBackStack(null)
                    .commit()
                binding.searchRecyclerView.visibility = View.INVISIBLE
            }
        })

 

 

또한 홈 프레그먼트, 보관함 프레그먼트에서 뒤로 가기 버튼을 누를 경우, 각 프레그먼트의 backStack에 쌓인 프레그먼트들이 pop 되어야 합니다.

 

따라서 각 프레그먼트에 OnBackPressedCallback을 등록해 주었습니다.

 

해당 프레그먼트 상태가 show인지, hide인지에 따라 callBack 상태를 관리하였습니다.

 

private lateinit var callback: OnBackPressedCallback

// 프레그먼트 상태가 hide인지 show인지 알려주는 콜백 함수
override fun onHiddenChanged(hidden: Boolean) { 
    super.onHiddenChanged(hidden)
    
// hide 상태일 경우 콜백 해제 -> 뒤로가기 버튼을 누르면 activity의 onBackPressed가 호출된다. 
    if (hidden){ 
        callback.remove()
    }
    else{ // show 상태일 경우
    // backStack에 프레그먼트가 존재할 때
        if (childFragmentManager.backStackEntryCount != 0) 
            activity.onBackPressedDispatcher.addCallback(this, callback) // 콜백 등록
    }
}

override fun onAttach(context: Context) {
        super.onAttach(context)
        activity = context as Activity

        callback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                childFragmentManager.popBackStack()                
            }
         // 뒤로가기를 누르다 backStack이 비게 되면 콜백을 해제해야 한다.
        childFragmentManager.addOnBackStackChangedListener {
            if (childFragmentManager.backStackEntryCount == 0) {
                Log.d("콜백 ", "해제")
                callback.remove()
            }
            else
                activity.onBackPressedDispatcher.addCallback(this, callback)
        }
    }

 

결과입니다.

결과

잘 작동이 되는 줄 알았습니다.

 

하지만 보관함 프레그먼트로 이동할 경우, 검색 키워드가 있는 검색창이 꺼지는 이슈가 발생했습니다.

 

뿐만 아니라, 다시 홈 프레그먼트로 이동할 경우 검색창에 관한 코드의 일부만 작동하는 것을 확인하였습니다.

 

로그창을 켜서 여러 실험을 해봤고, 프레그먼트를 전환할 때, 정의해 놨던 searchView 변수가 작동을 하지 않는다는 것을 알 수 있었습니다.

 

 

프레그먼트의 상태가 변할 때 각 검색창의 입력된 text를 로그창에 띄어 봤습니다.

searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
            override fun onQueryTextSubmit(query: String?): Boolean {
                Log.d("homeFragment","쿼리를 보냈어요")
                Log.d("homeFragment","${searchView.query}")
                searchView.clearFocus()
                ...
                }

playlistSearchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
                override fun onQueryTextSubmit(query: String?): Boolean {
                    Log.d("playlist","쿼리를 보냈어요")
                    Log.d("playlist","${playlistSearchView.query}")
                    playlistSearchView.clearFocus()
                    ...
                }

 

1. 홈 프레그먼트에서의 처음으로 검색을 했을 때 결과입니다.

 

2023-03-10 17:33:38.718 29523-29523/com.myFile.transpose D/homeFragment: 쿼리를 보냈어요
2023-03-10 17:33:38.718 29523-29523/com.myFile.transpose D/homeFragment: omg

 

2. 보관함 프레그먼트로 전환한 후 검색을 했을 때 결과입니다.

 

2023-03-10 17:34:13.682 29523-29523/com.myFile.transpose D/playlist: 쿼리를 보냈어요
2023-03-10 17:34:13.682 29523-29523/com.myFile.transpose D/playlist: cookie

 

3. 다시 홈 프레그먼트로 전환한 후 "newJeans" 키워드로 검색을 했을 때 결과입니다.

 

2023-03-10 17:37:34.346 29523-29523/com.myFile.transpose D/homeFragment: 쿼리를 보냈어요
2023-03-10 17:37:34.346 29523-29523/com.myFile.transpose D/homeFragment:

 

 

쿼리 로그가 뜨지 않았습니다. 정의했던 변수가 다시 초기화된 것이라 생각합니다.

 

다음 포스팅에 정리하여 올려보도록 하겠습니다.

 

댓글