마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

로그인화면과 Navigation popupto 관련

0 추천

파이어베이스로 회원가입 및 로그인까지 구현을 성공을 했고 다음 화면까지 넘어가는것까지 했습니다.

로그인후 나오는 화면이 Navigation Component를 사용한 Bottom Navigation인데요,

로그인을 한후에 바텀 내비게이션에서 뒤로가기를 하면 로그인화면으로 돌아가지 않아야하는것이

맞는데, 이것을 위해 내비게이션 그래프에서 action에 popupTo와 popuptoInclusive를 사용하려고 합니다.
 

로그인후에 바텀 내비게이션 화면이 나오고 여기서 뒤로가기를 누르면 더이상 로그인화면으로 가지않고

앱을 종료하는 것이죠.

이 옵션은 <action>의 속성이고 휴대폰의 뒤로가기를 통해 동작이 되어야합니다.

궁금한것은 바텀 내비게이션 메뉴가 4개일때, 내비게이션 그래프에서 각각의 메뉴(프래그먼트)에 대해

모두 <action>을 구현하고 이 속성을 적용해야하는건가요?

 

++)))) 이것을 위해 LoginFragment를 Global Action으로 지정하고 바텀메뉴의 모든 프래그먼트에서 뒤로가기시 로그인 화면으로 돌아올 수 있게 하려했지만 뒤로가기를  눌러도 다시 로그인화면으로 돌아옵니다.

뭐가문제ㅇ니가요?

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/login"
    app:startDestination="@id/logInFragment">
    <fragment
        android:id="@+id/logInFragment"
        android:name="com.example.lightweight.fragment.LogInFragment"
        android:label="fragment_log_in"
        tools:layout="@layout/fragment_log_in">
        <action
            android:id="@+id/action_logInFragment_to_registerFragment"
            app:destination="@id/registerFragment" />
        <action
            android:id="@+id/action_logInFragment_to_bottom_nav"
            app:destination="@id/bottom_nav" />
    </fragment>
    <fragment
        android:id="@+id/registerFragment"
        android:name="com.example.lightweight.fragment.RegisterFragment"
        android:label="fragment_register"
        tools:layout="@layout/fragment_register" >
        <action
            android:id="@+id/action_registerFragment_to_logInFragment"
            app:destination="@id/logInFragment" />
    </fragment>
    <include app:graph="@navigation/bottom_nav_graph" />

    <!-- Global Action-->
    <action
        android:id="@+id/action_global_logInFragment"
        app:destination="@id/logInFragment"
        app:popUpTo="@id/logInFragment"
        app:popUpToInclusive="true"/>
</navigation>

 

Fragment

class CalendarFragment : Fragment() {
    private lateinit var callback: OnBackPressedCallback

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_calendar, container, false)

    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        callback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                findNavController().navigate(R.id.action_global_logInFragment)
                Toast.makeText(context, "호출확인", Toast.LENGTH_SHORT).show()
            }
        }
        requireActivity().onBackPressedDispatcher.addCallback(this, callback)
    }

    override fun onDetach() {
        super.onDetach()
        callback.remove()
    }
}

 

 

codeslave (3,940 포인트) 님이 2022년 1월 18일 질문
codeslave님이 2022년 1월 18일 수정

2개의 답변

0 추천
네비게이션 컴포넌트는 네비게이션 그래프의 startingDestination으로 설정된 프레그먼트가 엔트리가 됩니다. 따라서 백스택에 startingDestination만 존재할 경우, 백버튼을 누르면 앱이 종료되게 됩니다. 한가지 아셔야 하는 점은 이 startingDestination을 강제로 pop을 시키면 네비게이션 그래프가 꼬일 수 있기 때문에 권장하지 않습니다. 대신  startingDestination은 setter가 존재하므로 동적으로 교체가 가능하도록은 되어 있습니다.

따라서 startingDestination 을 건드리지 않고 로그인을 구현하시려면, 로그인 후에 처음으로 이동하는 프레그먼트(홈이라고 하죠)를 startingDestination으로 설정하는 것이 덜 고치가 아픕니다. 그리고 로그인 여부의 체크는 메인액티비티에서 하시는 게 로직이 한군데서만 처리될 수 있기 때문에 좋을 듯 합니다.  네비게이션 컴포넌트 예제처럼, MainActivity에 MainViewModel을 두어서,
로그인이 안 된 상태라면 LoginFragment로 이동하게 하면 되겠죠. 그리고 LoginFragment는 뒤로 가기를 막고 뒤로가기를 누르면 앱을 종료하면 되겠죠. 아니면 MainViewModel에서 로그인을 체크하는 함수를 하나 두어서,  프래그먼트에서 MainViewModel을 Activity Scope으로 해서 공유한 다음, 해당 함수를 호출해 줄 수도 있구요.
다른 옵션으로는 NavigationController의 navigate함수를 님이 정의한 클래스로 위임하고 이 함수 내에서 로그인 상태를 먼저 체크할 수도 있습니다. 대신, 앱 내의 네비게이션은 이 클래스를 통해서 처리해야 겠죠.

앱이 크지 않으면, 첫번째 옵션으로 충분할 수도 있을 것 같고, 나중의 옵션은 앱이 커지면 고려해볼 수 있을 것 같은데 정답은 없어요. 앱의 상황을 잘 보고 판단하시기 바랍니다.
spark (224,800 포인트) 님이 2022년 1월 18일 답변
ViewModel + LiveData를 사용하시고 있다면 한가지 더 주의하실 부분은, ViewModel에 네비게이션 관련 로직을 가질 경우 LiveData를 잘 사용하셔야 합니다. LiveData는 화면에 액티브상태가 되면 자동으로 observer에게 기존에 있는 값을 전달하도록 되어 있습니다. 따라서 네비게이션 관련 데이터를 그냥 일반적인 방식으로 LiveData를 observe 하게 되면 다른 화면으로 나갔다 다시 돌아올 때 네비게이션 관련 데이터를 LiveData로부터 다시 받기 때문에, 화면이동이 더이상 되지않는 이상한 버그가 생길 수 있습니다.

LoginFramgnt: login button click
-> 1) LoginViewModel: login + liveData.postValue(success)
-> 2) LoginFramgnt: navigateToHomeFragment
-> 3) HomeFragment: press back button
-> 4) LoginFragment : 이 때, LoginViewModel에 있는 LiveData가 postValue(success) 실행. 2)번이 다시 실행 됨.

따라서 네비게이션 같은 "Action"계열의 이벤트, 즉 에러, 네비게이션 등과 같이 LiveData로부터 같은 데이터를 한번만 받아야 하는 경우, LiveData에서 전달된 값을 한번만 일고 다시 읽지 않도록 처리해주어야 합니다.
0 추천

원인은, Global Action의 popUpToInclusive인 것으로 보이네요. popUpToInclusive는 poUpTo 와 같이 사용되며, true이면 popUpTo로 지정된 프레그먼트까지, false 이면 바로 전까지의 백스택을 클리어해줍니다. 

네비게이션 그래프의 startingDestination은 항상 백스택의 마지막에 존재해야하기 때문에 pop시키시면 안됩니다. 로그인 후에도 그대로 남겨놓으셔야 합니다. 따라서 startingDestiantion으로 되어있는 loginFragment는 포함을 시키시면 안됩니다.

<!-- Global Action-->
    <action
        android:id="@+id/action_global_logInFragment"
        ...
        app:popUpToInclusive="false"/>

 이 경우, 한가지 신경쓰실 부분은 로그인 후에  홈에서 백버튼을 누를 때 LoginFragment로 가지않고 앱이 종료될 수 있는 코드를 추가해야 합니다. 

앞에서 설명드린 대로, startingDestination 을 로그인 후의 엔트리가 되는 프레그먼트로 바꾸실 수도 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/login"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.lightweight.fragment.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" />


<fragment
        android:id="@+id/loginFragment"
        android:name="com.example.lightweight.fragment.LogInFragment"
        android:label="fragment_log_in"
        tools:layout="@layout/fragment_log_in">
        ...
        
    </fragment>


<action
            android:id="@+id/action_global_logInFragment"
            app:destination="@id/loginFragment"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="false" />
</navigation>

이 때 주의 하실 점은 HomeFragment가 nested graph에 있으면 안되고 같은 xml 파일 안에 있어야 한다는 점입니다.

그리고 CalendarFragment의 onAttatch와 onDetach에 있는 코드를 LoginFragment로 옮겨 보세요.

 

추가로, 

++)))) 이것을 위해 LoginFragment를 Global Action으로 지정하고 바텀메뉴의 모든 프래그먼트에서 뒤로가기시 로그인 화면으로 돌아올 수 있게 하려했지만 뒤로가기를  눌러도 다시 로그인화면으로 돌아옵니다.

이 부분은 두번째 방법으로는 바로 되지는 않고, 리스너를 다시거나 해서 처리를 하셔야 할 것 같아요. 그리고 제가 보기에는 네비게이션이 약간 이상해 보입니다. 로그인 후에 유저가 Bottom Navigation 의 메뉴에 있는 화면에서 백을 하는 경우는 로그아웃이 아니라, 앱을 종료시키려고 하는 거거든요. 앱을 종료하려고 하는데 로그인 화면까지 이동해서 하게되면 이치에 맞치않아 보여요. 로그아웃은 별도의 로그아웃 버튼을 두시는게 맞을 듯 해요.

spark (224,800 포인트) 님이 2022년 1월 18일 답변
spark님이 2022년 1월 18일 수정
app:popUpToInclusive="true"로 뒀던 이유가 로그인후 홈화면에 갔다가 백버튼을 눌렀을때  LoginFragment까지 pop을 시키면 종료가 되지 않을까라는 예상에서 app:popUpToInclusive="true"로 뒀습니다. LoginFragment까지 pop시키면 백스택에 아무것도 없으니 앱이 종료될거라고 생각했거든요.. 로그아웃을 의도한건 아니었습니다...

아무튼 자잘한 질문 몇개 드리겠습니다.

1.
선생님이 말씀하신 activityViewModel 관련 이용 문서가 https://developer.android.com/guide/navigation/navigation-conditional?hl=ko#kotlin 이곳같은데, activityViewModel을 이용하는 이유가 무엇인가요?
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko#sharing 문서를 읽어보니 하나의 뷰모델을 사용하여 두 프래그먼트간의 상호작용(공유 등..)을 위한것 같은데 단순 뷰모델을 공유함으로써 로그인여부를 확인하기 위함인가요?

2.
이번답변에서 엔트리를 자주 언급하시고 문서에도 보면 백스택 엔트리해서 엔트리라는 단어가 자주나오는데 엔트리가 된다는것이 무엇인가요?

3.
https://developer.android.com/guide/navigation/navigation-conditional?hl=ko#kotlin 에서는 MainFragment가 따로 startDestination으로 설정되어있잖아요? 그런데 저같은 경우는 BottomNavigation중 첫번째 메뉴인 Calendar를 로그인후의 첫 Fragment로 설정하려합니다.
그렇다면 주소의 ProfilFragment가 저의경우에는 CalendarFragment가 되고 startDestination이 되며 LoginFragment와 상호작용(activityviewmodels 등)이 되어야하는것이 맞나요?
앱을 종료시키는 일은 안드로이 시스템의 몫입니다. 앱은 백스택 관리만 해주시면 됩니다. 따라서 GlobalAction 을 앱을 종료시키는 목적으로 사용하는건 다시 생각해볼 부분이네요. 그리고 이 부분은 Navigation Component의 제약사항과도 관련이 있습니다. 정식버전은 백스택을 하나만 가지고 있기 때문에, startingDestination만 백스택에 남았을 때 앱이 닫히게 됩니다. 최신 베타 버전같은 경우는 멀티스택이란 걸 지원해서, Navigtion menu별로 백스택을 관리할 수 있기 때문에, 님이 하려고 하는 부분이 기본으로 지원이 됩니다. 멀티스택이 필요하시면 구글을 찾아보시면, 해당 라이브러리 개발자가 work around(편법??)으로 extension function을 만들게 Github에 있을 겁니다.

1.
선생님이 말씀하신 activityViewModel 관련 이용 문서가 https://developer.android.com/guide/navigation/navigation-conditional?hl=ko#kotlin 이곳같은데, activityViewModel을 이용하는 이유가 무엇인가요?
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko#sharing 문서를 읽어보니 하나의 뷰모델을 사용하여 두 프래그먼트간의 상호작용(공유 등..)을 위한것 같은데 단순 뷰모델을 공유함으로써 로그인여부를 확인하기 위함인가요?

activityViewModel의 ViewModel의 scop(범위)가 Activity입니다. 즉, Single activity의 경우는  Applicaiton scope과 같아 지겠죠? 이말은 ViewModel 을 global하게 공유할 수 있다는 말입니다. 사용자 세션관리같은 경우가 공유가 필요한 예가 될 수 있겠죠.

2.
이번답변에서 엔트리를 자주 언급하시고 문서에도 보면 백스택 엔트리해서 엔트리라는 단어가 자주나오는데 엔트리가 된다는것이 무엇인가요?
Entry, 굳이 번역하면 입구라고 해야 되는데. 시작점이라고 보시면 될 듯합니다. 프로그래밍에서 그냥 일반적으로 사용하는 용어입니다.

3.
https://developer.android.com/guide/navigation/navigation-conditional?hl=ko#kotlin 에서는 MainFragment가 따로 startDestination으로 설정되어있잖아요? 그런데 저같은 경우는 BottomNavigation중 첫번째 메뉴인 Calendar를 로그인후의 첫 Fragment로 설정하려합니다.
그렇다면 주소의 ProfilFragment가 저의경우에는 CalendarFragment가 되고 startDestination이 되며 LoginFragment와 상호작용(activityviewmodels 등)이 되어야하는것이 맞나요?
네. CalendarFragment가 startDestination이 맞을 것 같네요. activityviewmodels은 필요하다고 판단되면 사용하시면 되구요.
...