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

Safe Args 사용시 특정화면에서만 인수를 받는 방법있을까요

0 추천

https://ibb.co/8BZcZVG (현재 앱 상황입니다)

WriteRoutineFragment.kt

class WriteRoutineFragment : Fragment() {
    private var _binding : FragmentWriteRoutineBinding? = null
    private val binding get() = _binding!!
    private lateinit var adapter : RoutineAdapter
    private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        _binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)

        adapter = RoutineAdapter(::addDetail, ::deleteDetail)
        binding.rv.adapter = this.adapter
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        handleLoginForm()

        vm.items.observe(viewLifecycleOwner) { updatedItems ->
            adapter.setItems(updatedItems)
        }
    }
    private fun handleLoginForm() {
        val navController = findNavController()
        val navBackStackEntry = navController.getBackStackEntry(R.id.writeRoutine)

        // Create our observer and add it to the NavBackStackEntry's lifecycle
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME
                && navBackStackEntry.savedStateHandle.contains("selectedWorkout")) {
                val result = navBackStackEntry.savedStateHandle.get<String>("selectedWorkout");
                // Do something with the result
                vm.addRoutine(result!!)
            }
        }
        navBackStackEntry.lifecycle.addObserver(observer)

        // As addObserver() does not automatically remove the observer, we
        // call removeObserver() manually when the view lifecycle is destroyed
        viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_DESTROY) {
                navBackStackEntry.lifecycle.removeObserver(observer)
            }
        })
    }
}

 

TabFrragment(데이터 프래그먼트)

class WorkoutListTabPageFragment : Fragment() {
    private var _binding : FragmentWorkoutListTabPageBinding? = null
    private val binding get() = _binding!!
    private lateinit var adapter: WorkoutListAdapter
    private lateinit var part: String
    private val viewModel: WorkoutListViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        _binding = FragmentWorkoutListTabPageBinding.inflate(inflater, container, false)
        binding.apply {
            adapter = WorkoutListAdapter(::setNavResult)
            rv.adapter = adapter
        }

        viewModel.setList(part) // 탭 부위에 맞는 운동리스트를 보여주기 위해 해당 부위 정보 전달

        // setList()에서 part를 넣어 관찰해 탭도 swipe 할 때마다 업데이트
        viewModel.part.observe(viewLifecycleOwner) { _ ->
            adapter.addItems(viewModel.getList())
            adapter.notifyDataSetChanged()
        }

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // workout을 obeserve 하지 않으면 이 프래그먼트에 도착하고
        // 값을 선택하지도 않고서 다시 바로 되돌아 가버리는 현상이 발생.
        // previous~~ 코드가 이전화면으로 되돌리는것인지는 모르겠음
        // 그래서 강제로 viewmodel 내에 workout LiveData를 설정함
        // workout LiveData 자체는 아무 의미 없는 값이긴 하다.
        viewModel.workout.observe(viewLifecycleOwner, Observer { workout ->
            val navController = findNavController()
            navController.previousBackStackEntry?.savedStateHandle?.set("key", "TEST")
            navController.popBackStack()
        })
    }

    private fun setNavResult(result: String) {
        viewModel.setValue(result)
    }
}

 

어댑터

inner class ViewHolder(itemView: View, context: Context) : RecyclerView.ViewHolder(itemView) {
        private val tv: TextView = itemView.findViewById(R.id.workout)

        init {
            tv.setOnClickListener { view -> // 운동 선택
                setVal("ser")

            }
        }

        fun bind(item: String) {
            tv.text = item
        }
    }

 

뷰모델

class WorkoutListViewModel(application: Application) : AndroidViewModel(application){
    private var _part :MutableLiveData<String> = MutableLiveData()
    private var _list : MutableLiveData<List<String>> = MutableLiveData(arrayListOf())
    private val resources = application.resources
    private var _workout :MutableLiveData<String> = MutableLiveData()

    private val workoutListSource : WorkoutListSource by lazy { WorkoutListLocalSource(resources) }

    val list = _list
    val part = _part
    val workout = _workout

    fun setList(part : String) {
        _part.value = part
        when(_part.value) {
            "가슴" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.CHEST)
            "등" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.BACK)
            "하체" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.LEG)
            "어깨" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.SHOULDER)
            "이두" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.BICEPS)
            "삼두" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.TRICEPS)
            "복근" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.ABS)
        }
    }
    fun getList() : List<String> = list.value!!

    fun setValue(result: String) { // 테스트 위함
        _workout.value = result // workout의 값을 변화주기 위한 설정
    }
}

 

Nav_graph

<fragment
        android:id="@+id/writeRoutine"
        android:name="com.example.lightweight.fragment.WriteRoutineFragment"
        android:label="fragment_write_routine"
        tools:layout="@layout/fragment_write_routine" >
        <action
            android:id="@+id/action_writeRoutineFragment_to_workoutListTabFragment"
            app:destination="@id/workoutListTabFragment" />

    </fragment>
    <fragment
        android:id="@+id/workoutListTabFragment"
        android:name="com.example.lightweight.fragment.WorkoutListTabFragment"
        android:label="WorkoutListTabFragment" >
<!--        <action-->
<!--            android:id="@+id/action_workoutListTab_to_writeRoutine"-->
<!--            app:destination="@id/writeRoutine"-->
<!--            app:popUpTo="@id/writeRoutine"-->
<!--            app:popUpToInclusive="true"/>-->
    </fragment>

 

codeslave (3,940 포인트) 님이 2021년 8월 22일 질문
codeslave님이 2021년 8월 27일 수정

4개의 답변

0 추천

질문 내용 중에서 정확하게 확인하고 싶은게 있는데,

Navigation Component 특성상 화면이동시 replace 되므로 저는 화면을 이동하고 돌아온 뒤에도 유지하기 위해서 by acitiviyviewmodels 를 이용해서 viewmodel을 사용하고 있습니다.

ViewModel은 다른 화면으로 갔다가 돌아오더라도 소멸되지 않습니다. 따라서 ViewModel에 안에 있는 데이터는 살아있어야 합니다. 그리고 SingleActivity를 사용하시고 계신다면 acitivytViewModels를 사용하는 건 추천드리고 싶지 않네요. 이유는 Activity scope에 ViewModel이 존재하는데, SingleActivity일 때는 앱이 살아있는 동안 ViewModel 또한 계속 메모리에 가지고 있는다는 말이 됩니다. 쓸데 없는 메모리 낭비라고 보여져요.

그런데 이것때문인지.. 데이터프래그먼트에서 받은 Args가.. 다른 메뉴로 갔다온 후에도

계속 이 데이터가 유지되고 있기때문에

다른 메뉴에서 작성 메뉴의 작성프래그먼트로 전환을 하는것만으로도 계속해서 아이템이 추가되고 있는

상황입니다. 

위의 문제는 라이프사이클 때문에 발생하는 것으로 보입니다. 다른 프레그먼트로로 이동했다 돌아오게 되면 onCreateView가 다시 호출될 거예요. 

public View onCreateView (LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState)

그리고 이 때는 saveInstanceState가 null이 아닐 겁니다. null이 아니라면 이미 아이템이 추가된 것이므로 스킵하면 되겠죠. 아니면 vm.addRoutine에 중복체크를 해서 이미 추가된 아이템이라면, 다시 추가를 하지 않도록 해주면 될 것 같은데요.

 

spark (224,800 포인트) 님이 2021년 8월 22일 답변
말씀대로 by viewmodels()로 다시 변경하고 saveInstanceState의 값을 디버깅으로 확인 해봤는데요 값은 계속 null로 뜹니다.
정확히는 다른 메뉴로 갔다가 돌아오면 null은 안뜨고, 이 작성프래그먼트에서 데이터 프래그먼트로 갔다가 오면 saveIntanceState는 계속 null인 상태이구요.
다른 메뉴로 갔다가 돌아오면 Safe Args에서 받은 값은 들어있지는 않지만 null은 아니라구 뜹니다..
사실 제가 실수로 본문 프래그먼트 코드에 by navArgs() 코드를 사용해서 Safe Args의 값을 받고 있긴합니다. (코드 수정)

또 추가로 nav_graph에서 popupTo로 백스택도 관리?하고 있어서 이 코드도 혹시나해서 업데이트 시켰습니다.
테스트를 해본 결과, Fragment의 saveInstance로는 원하시는 결과를 얻을 수가 없을 것 같습니다. onSaveInstancer는 액티비티의 라이프사이클에 따라 호출되기 때문에 예상대로 값이 세팅되지는 않습니다. 그리고 또한, 네비게이션 컴포넌트의 경우 네비게이션 그래프에 startDestination으로 지정된 프레그먼트는 destory시키지 않습니다.
ViewModel에서 중복체크하는 방법으로 처리하시는게 현실적인 옵션 같아요.
다른 가능한 방법으로는 다른 프레그먼트에서 backstackentryf를  통해 값을 전달하는 방법이 있습니다.

https://developer.android.com/guide/navigation/navigation-programmatic

데이터 Fragment를 종료할 때 backstackentry에 선택한 값을 세팅해 주고 작성 Frgment에는 값들이 존재할 경우에만 아이템을 추가해주면 어떨까하는 생각이 드네요.
감사합니다. 본문 코드에서 문제되는 부분이 args.workout?. 쪽이 문제인데,
workout 코드가 다른 메뉴로 갔다가 돌아와도 계속 유지가 되어서 메뉴 전환만으로도 아이템이 추가가 된다고 말씀드렸잖아요? 그렇다면 맨처음에 아이템을 추가한후에 addRoutine을 호출한 후에 let {} 안에서 workout 값을 null로 다시 설정하는 것도 방법중 하나일까요? null로 만든다면 다른 메뉴에 갔다가와도 null인 상태일것이니 args.workout?. 코드는 동작안할테구요..

------
아 by navArgs()는 var로 받을수 없어서 args.work의 값을 변경하는게 불가능하네요..


그런데 선생님 backstackentry에 선택한 값을 세팅해주라는게 무슨말씀인지 잘 이해가 가질 않습니다...
참고로 arguments를 통해 전달 받은 값을 null로 만드는 건 안전하지 못한 방법입니다. 왜냐하면 예를 들어, 디바이스가 rotate 되었을 경우에는 Fragment가 재 생성이 될 텐데, 이 때에는 arguments 의 값을 지워버렸기 때문에, 문제가 생길 수가 있어요.
private var workout: String? = arg.workout?.copy()

 처럼 데이터 클래스를 사용하여 복사본을 만들면 위에 말씀드린 문제없이 nullable처리가 가능해지겠네요.
0 추천

navArg을 사용하지 않고 직접 Bundle을 navigate 함수에 넘길 수도 있구요. 그리고 navArgs 소스코드를 확인해 보지 않아서 정확하지는 않지만 아마도, requireArguments() 에서 Bundle을 읽어오거나 아니면 BackStackEntry를 사용할 것 같은데요. 직접 Bundle에 접근한다면 nullable을 만들 수 있을 겁니다. 

하지만 이거 보다는 두번째 방법인 BackStackEntry에 값을 세팅해서 처리하는 방법이 더 유연해 보입니다. 이 방법을 통해 Fragment간에 값을 주고 받을 수 있어요. startActivityForResult와 유사하게 다른 쪽에서 값을 리턴하는 것을 기다리다가 값이 오면 처리하면 됩니다. 내부적으로는 LiveData 를 이용합니다. 위의 링크에 나온 문서를 읽어보시면 자세한 설명과 상황별 예제코드들이 나와 있습니다.

저같은 경우는 Extension 함수를 만들어서 아래처럼 사용하고 있어요. 아래 코드로 프레그먼트 간에 쉽게 값을 전달받을 수 있어요. (DialogFragment의 경우는 코드가 좀 달라집니다.)

fun <T> Fragment.setNavResult(key: String = EXTRA_RESULT_KEY, data: T) {
    findNavController().previousBackStackEntry?.also { stack ->
        stack.savedStateHandle.set(key, data)
    }
}


inline fun <T> Fragment.getNavResult(key: String = EXTRA_RESULT_KEY, crossinline onChanged: (T) -> Unit) {
    findNavController().currentBackStackEntry?.also { stack ->
        stack.savedStateHandle.getLiveData<T>(key)
            .observe(
                viewLifecycleOwner,
                Observer { result ->
                    onChanged(result)
                    stack.savedStateHandle.remove<T>(key)
                }
            )
    }
}


// 값을 전달하는 Fragment
val workout: String = ...
setNavResult("selectedWorkout", workout))

// 값을 받는 Fragment
override fun onViewCreated(...) {
   ...

    // LiveData를 observe하는 것이므로 onViewCreated가 적합한 위치임.
    getNavResult<String>("selectedWorkout") { workout: String ->
         vm.addRoutine(workout)
    }
}

이렇게 하면 데이터 프레그먼트에서 값을 선택한 경우만 getNavResult 블록이 실행되므로 중복이 생기지는 않을 것 같은데요.

spark (224,800 포인트) 님이 2021년 8월 24일 답변
spark님이 2021년 8월 24일 수정
감사합니다. 시도해보겠습니다.
그런데 번외로 이 NavBackEntry라는거에 대해서 궁금한게 있는데요,
이게 문서를 번역해서 보니까 NavController.getBackStackEntry()에 destination ID를 전달하여  NavBackStackEntry에 대한 참조를 얻는다는데요
정확히는 currentBackStackEntry 던지 previousBackStackEntry 던지 NavBackStackEntry 라는 것을 반환하는게 아니라 백스택에 쌓여있는 현재 스택이던 이전 스택같은 스택 프래그먼트들(Destination)을 반환한다고 봐야하나요? 아니면 따로 NavBackStackEntry라는 인스턴스가 따로 존재하는건가요?
그림을 보아하니 NavBackStackEntry 인스턴스 자체를 반환해서 데이터를 저장하는 형식인것 같은데..

이전 NavBackStackEntry를 꺼내어 데이터를 설정하고 다시 스택에 집어넣고?
이전 화면에서 현재 NavBackStackEntry으로 데이터를 꺼내는 방식인것같은데

NavBackStackEntry가 무엇인지 잘몰라 헷갈리네요
안드로이드 백스택은 프레그먼트 자체를 저장하지 않습니다. 대신에 Transaction을 저장합니다. FragmentManager를 사용할 때 beginTransaction으로 시작하고 commit으로 끝냈던 기억이 있을 것입니다. 바로 beginTransaction부터 commit사이의 동작을 백스택에 보관하고 복구를 해주는 겁니다. 그리고 BackStackEntry에는 뷰상태 관리를 도와주기 위해 SavedStateHandle이라는 추가되었습니다. ViewModel에서도 내부적으로 사용하고 있구요. NavBackStack은 프레그먼트 백스택과는 다른 네비게이션 컴포넌트에서 사용하는 용도입니다. BackStackEntry라는 걸 저장하는데, 여기에는  LifecycleOwner, ViewModeStoreOwner, SaveStateRegisgtryOwner라는 객체들에 대한 참조를 가지고 있고, 네비게이션 정보인 NavDestination이란 걸 저장합니다. 즉 navigation.xml 에 정의하셨던 네비게이션 그래프를 기반으로 한 네비게이션 정보를 관리하는 스택이라고 보면 될 것 같습니다. 아무래도 이런 부분은 최근에 추가가 되었고 내부적으로 복잡하기 때문에, 바로 이해하기가 쉽지는 않아 보여요.
본문 코드 수정했습니다.
선생님 시도해봤지만 동작하질 않네요. 정확히는 동작은 합니다만 아이템이 추가가 안되는 상황입니다. 작성프래그먼트의 vm.addRoutine()에 브레이킹 포인트를 걸고 디버깅을 해봤으나 이까지 진행이 되질 않습니다. 호출이 전혀안되는것 같아요. 값이라도 제대로 넘어오는지 확인하려 했더니..
뭐가 문제일까요?

선생님 코드에서 확장함수를 쓰는 이유를 몰라서 그냥 내부 set하는 코드와 get하는 코드만 개발자 문서처럼 동일하게 떼어 썼는데, 왜 값을 못받는지 이해가 잘 안갑니다.

데이터 프래그먼트가 리사이클러뷰로 이루어져있기때문에 클릭한 데이터를 setVal로 넘겨주기 위해서 어댑터로 ::를 이용하여 보내어 어댑터내에서
클릭한 운동 데이터를 setVal의 인자로 넘기는 방식을 사용했습니다.
그런데 테스트하기위해서 실제로 어댑터에서 호출은 안했고 본문 코드처럼 onCreateView에서 값을 세팅하는 코드를 짰습니다. 값도 임의로 정하구요.

그런데 작성 프래그먼트에서 값을 받지를 못하네요..
짐작가는 부분있으신가요?
onViewCreated 에서
val navBackStackEntry = navController.getBackStackEntry(WriteRoutineFragment의 navigation id)
일단 이 부분이 빠져있네요. 그리고 라이프 사이클에 등록하고 해제하는 부분도 다 빠져있네요.   
WriteRoutineFragment에서 필요한 코드를 싹 빼고 코드를 작성하셨으니 동작이 제대로 할리가 없겠죠.
0 추천

문서대로 하셨다면 제대로 동작을 해야 합니다. 방금 샘플로 테스해 본 코드예요. 프레그먼트는 모두 naivagation graph에 등록이 되어있어야 해요. DialogFragment는 코드가 다르다는 것도 염두에 두시구요.

navigation.xml
<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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.krpot.android.navigationdemo.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/nav_login"
        android:name="com.krpot.android.navigationdemo.ui.login.LoginFragment"
        android:label="@string/title_login"
        tools:layout="@layout/fragment_login" />
</navigation>

class HomeFragment : Fragment(R.layout.fragment_home) {

    private lateinit var homeViewModel: HomeViewModel
    private var _binding: FragmentHomeBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupViews()
        handleLoginForm()
    }

    private fun setupViews() {
        _binding = FragmentHomeBinding.bind(requireView())

        binding.apply {
            logoutBtn.setOnClickListener {
                findNavController().navigate(R.id.nav_login)
            }
        }

    }

    private fun handleLoginForm() {
        val navController = findNavController()
        // After a configuration change or process death, the currentBackStackEntry
        // points to the dialog destination, so you must use getBackStackEntry()
        // with the specific ID of your destination to ensure we always
        // get the right NavBackStackEntry
       // R.id.navigation_home는 다른 프레그먼트를 호출하는  navigation.xml에 등록된 id.
        val navBackStackEntry = navController.getBackStackEntry(R.id.navigation_home)

        // Create our observer and add it to the NavBackStackEntry's lifecycle
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME
                && navBackStackEntry.savedStateHandle.contains(LoginFragment.EXTRA_RESULT_KEY)
            ) {
                val loginResult =
                    navBackStackEntry.savedStateHandle.get<LoginInfo>(LoginFragment.EXTRA_RESULT_KEY);
                showLoginResult(loginResult!!)
            }
        }
        navBackStackEntry.lifecycle.addObserver(observer)

        // As addObserver() does not automatically remove the observer, we
        // call removeObserver() manually when the view lifecycle is destroyed
        viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_DESTROY) {
                navBackStackEntry.lifecycle.removeObserver(observer)
            }
        })
    }

    private fun showLoginResult(loginResult: LoginInfo) {
        Toast.makeText(requireContext(), "login successful!!!($loginResult)", Toast.LENGTH_SHORT).show()
    }
}


class LoginFragment : Fragment(R.layout.fragment_login) {

    companion object {
        const val EXTRA_RESULT_KEY = "Extra.Result"
    }

    private val viewModel: LoginViewModel by lazy { ViewModelProvider(this).get(LoginViewModel::class.java) }
    private var _binding: FragmentLoginBinding? = null
    private val binding get() = _binding!!

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupViews()
    }

    private fun setupViews() {
        _binding = FragmentLoginBinding.bind(requireView())

        binding.apply {
            loginBtn.setOnClickListener {
                viewModel.submitLogin(userNameTxt.text.toString(), passwordTxt.text.toString())
            }
        }

        viewModel.loginInfo.observe(viewLifecycleOwner, Observer { loginInfo ->
            val navController = findNavController()
            navController.previousBackStackEntry?.savedStateHandle?.set(EXTRA_RESULT_KEY, loginInfo)
            navController.popBackStack()
        })
    }
}

class LoginViewModel : ViewModel() {

    private val _loginInfo = MutableLiveData<LoginInfo>()
    val loginInfo: LiveData<LoginInfo> = _loginInfo

    fun submitLogin(userName: String, password: String) {
        val loginInfo = LoginInfo(userName, password)

        viewModelScope.launch {
            val loginResult = handleLogin(loginInfo)
            _loginInfo.postValue(loginResult)
        }
    }

    private suspend fun handleLogin(loginInfo: LoginInfo): LoginInfo {
        delay(1000L)
        return loginInfo
    }
}

data class LoginInfo(
    val userName: String,
    val password: String
) : Serializable

 

spark (224,800 포인트) 님이 2021년 8월 26일 답변
감사합니다. 선생님이 올려주신 코드를 개발자 문서에서는 봤는데.. Dialog 일경우에 사용하는 코드인줄알고 추가를 안했었네요..
아무튼 추가를해서 다시 테스트를 진행했고 여러가지 상황이 발생했고
이와 관련해서 질문이 몇가지 있습니다.

1.테스트를 진행할때 선생님은 LoginFragment에서 viewmodel.logininfo를 observe하시고 그 안에서 previous코드를 사용하셨는데..
저는 그냥 사용했습니다..애초에 값을 그냥 선택만 하기때문에 필요없다고 생각했습니다.
아무튼 이 결과 TabFragment(데이터프래그먼트)에 진입을 하자마자 데이터를 선택하지도 않았는데도 바로 이전화면으로 되돌아가는 상황이 발생했습니다.
popBackStack()은 스택에서 스택을 pop시키는 것이니 화면전환과 관련이 없는 것같고.. previousBacstackEntry 코드가 화면전환의 역할도 하는건가요?
저는 그냥 이전 destination 프래그먼트에 값을 세팅만하는 역할이라고 생각했었는데 말이죠.. 데이터를 선택하기도전에 이전화면으로 돌아가는 현상을 막기 위해서..
본문 코드와 같이 workout을 observe 시켰습니다. 그랬더니 정상적으로 동작은 하는데 workout은 정말 화면이 돌아가는 것을 막기 위한 목적이라 의미없는 값인데 이게 맞는건가요? 그리고 previoust~~코드가 화면전환의 역할도 하는지..

2.또 스택이 계속 쌓이는 문제를 해결하기위해 popBackStack을 사용하셨는데 전 여태 nav_graph에서 action태그의 popupTo와 popupToInclusive를 사용했습니다. popBackStack 코드하나만으로도 충분한가요? 아직까지 저는 문제가 없습니다만 궁금해서요.,

3.본문에 추가한 링크의 앱상황과 같이 정상적인 흐름(탭 프래그먼트에서 데이터를 받아와 아이템을 추가하는것)과 달리 Calenar 메뉴로 전환후 돌아온 것만으로도 계속해서 아이템이 추가되는데..왜이럴까요..ㅠ
popupTo와 popupToInclusive 스택에 존재하는 navigation 을 클리할 때 사용합니다. 예를 들어 A -> B -> C 이렇게 호출이 되었는데 C에서 바로 A로 돌아갈 때가 대표적인 경우죠.
그리고 NavigationComponent를 LiveData와 같이 사용하실 때는 좀 주의하셔야 해요. LiveData의 특성상 observer(보통 Fragment겠죠)의 라이프사이클에 따라 observer는 비활성화가 되고(예를 들면, 다른 프레그먼트로 이동), 다시 활성화 될 때(다른 프레그먼트에서 돌아옴)는 자동으로 가지고 있던 데이터를 observer 에게 전달해 줍니다. 그래서 LiveData 를 아무 처리없이 에러처리나 네비게이션 같은 일회성 액션에 사용하면 문제가 될 수 있습니다. 이 경우는 SingleLiveEvent 같은 많이 사용하는 해결방법 들이 존재합니다. 구글에 검색해보세요. 원리는 LiveData에서 한번  observe가 된 데이터는 다시 내보내지 않는 겁니다. 님의 경우가 여기에 해당하는지는 확실하지는 않지만요.
0 추천

previousBackStackEntry나 savedStateHandle 에는 그런 기능이 없어요. 프레그먼트간 이동은 NavController 의 역할입니다.

님의 코드는 명확하게 TabFragment로 이동하자 마자 결과를 리턴하도록 코드가 되어있어요. 코드를 잘 확인해 보세요.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
 
        // workout을 obeserve 하지 않으면 이 프래그먼트에 도착하고
        // 값을 선택하지도 않고서 다시 바로 되돌아 가버리는 현상이 발생.
        // previous~~ 코드가 이전화면으로 되돌리는것인지는 모르겠음
        // 그래서 강제로 viewmodel 내에 workout LiveData를 설정함
        // workout LiveData 자체는 아무 의미 없는 값이긴 하다.
        //viewModel.workout.observe(viewLifecycleOwner, Observer //{ workout ->
            val navController = findNavController()navController.previousBackStackEntry?.savedStateHandle?.set("key", "TEST")
            navController.popBackStack()
        //})
    }

 

TabFragment가 생성되고 나서 말씀하신 대로라면 위처럼 실행이 될 텐데 당연히  navController.popBackStack()을 호출하고 있으니 당연히 이전 화면으로 돌아가겠죠. 그래서 제 코드에는 뷰모델의 LiveData 를 통해서 처리가 되도록 한거고 뷰모델을 거치지 않고 싶으시면, 해당 코드를 setNavResult로 옮기세요.

private fun setNavResult(result: String) {
     val navController = findNavController()navController.previousBackStackEntry?.savedStateHandle?.set("key", "TEST")
     navController.popBackStack()
}

함수를 사용하는 방법의 문제로 보여요.

spark (224,800 포인트) 님이 2021년 8월 27일 답변
...