피드백은 언제나 환영합니다 😊
해당 포스팅에 사용된 샘플 코드는 여기를 참고해주세요 :)
네비게이션은 화면 이동과 관련된 액션을 쉽게 구현하도록 도와주는 Jetpack
라이브러리 중 하나이다.
기존 Fragment
간 전환 코드를 작성할 때는 Fragment Manager
를 활용할 때가 많았는데, Fragment
백스택에 대한 관리, 전달할 파라미터 등 많은 요소를 코드로 직접 구현해주어야 했기 때문에 컴포넌트의 코드 양이 길어지고 난잡해지는 경우가 많았다.
이런 상황 속에서 Jetpack Navigation
이 등장했고, 기존과 다르게 화면 이동은 물론, 스택 관리, 데이터 전송까지 손쉽게 구현할 수 있게 됐다. 안드로이드 측에서 이야기하는 Navigation
의 장점은 아래와 같다.
Fragment
트랜잭션 처리Safe args
와 함께 안전한 데이터 전달 지원Bottom Navigation
, Navigation Drawer
와 같은 요소들을 손쉽게 구현할 수 있도록 지원ViewModel
을 이용, 그래프 내의 컴포넌트 간에 UI 관련 데이터를 공유할 수 있도록 지원Navigation
이 생긴 개요와 장점에 대해 간략히 살펴보았으니, 이제 Navigation
의 구성 요소를 살펴보도록 하자.
Navigation
은 크게 3개의 구성 요소로 이루어진다.
Navigation Graph
Navigation
과 관련해 모든 정보를 가지고 있는 구성 요소로, 어떤 목적지들이 있는지, 이동 중 어떤 액션을 취할 것인지, 어떤 데이터를 넘겨줄 것인지에 대한 정보가 담겨져 있다.XML
리소스를 작성할 수도, 코드로 직접 생성할 수도 있으나 보통은 위의 사진과 같이 비쥬얼 에디터를 제공하기 때문에 XML
로 주로 작성한다.NavHost
Navigation Graph
에 담겨져 있는 목적지, 즉 화면을 표현하는 빈 컨테이너 공간이다.findNavController().navigate(action)
NavController
NavHost
가 개별적으로 가지고 있는 구성 요소로, 이름에 맞게 NavHost
에 어떤 화면을 띄울 것인지 컨트롤하는 역할을 수행한다.Navigation
을 사용할 때 간단한 플로우는 아래와 같다.
NavController
에게 요청한다.NavController
는 Navigation Graph
내에서 적절한 목적지를 찾아 NavHost
에 표현한다.dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
}
app
레벨의 그래들에 의존성을 추가해준다. 글을 쓰는 시점 (22.08.17)
에서는 2.5.1이 최신 버전이다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">
<TextView
android:id="@+id/first_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/first_fragment"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/first_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/first_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
이런 식으로, 텍스트와 버튼이 혼합된 간단한 Fragment
2개를 생성한다.
그래프를 작성하기 위해 res
폴더 내부에 navigation
폴더를 생성하고, 네비게이션 리소스 파일을 생성한다.
위의 GIF와 동일하게, 아이콘을 클릭하여 destination
, 즉 목적지를 생성해준다. 현재는 Fragment
2개를 목적지로 생성하였다. 이 상황에서 XML
내부의 코드는 아래와 같다.
<?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/nav_graph"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="co.kr.hoyaho.samplenavi.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" />
<fragment
android:id="@+id/secondFragment"
android:name="co.kr.hoyaho.samplenavi.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
</navigation>
주의깊게 봐야할 것은 app:startDestination
부분인데, 초기에 보여줄 화면을 지정하는 것이다. 초기에 목적지를 생성하면 자동으로 생성되는 코드라 깊게 걱정하진 않아도 되겠지만, 혹시나 코드에 구성되어 있지 않으면 에러가 발생하니 한번 더 살펴볼 것을 권장한다.
아! 그리고 android:name
에도 해당 화면의 경로를 지정해주어야 한다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph">
</androidx.fragment.app.FragmentContainerView>
</androidx.constraintlayout.widget.ConstraintLayout>
이제 Root Activity
를 지정하고, 해당 Activity
에 NavHost
를 생성해주면 된다. XML
코드 내에 중요 키워드가 몇 가지 있는데, 해당 키워드가 어떤 역할을 하는지 알아보도록 하자.
android:name
: NavHostFragment
라는 것을 명시, 즉 화면을 담을 빈 컨테이너 역할을 수행할 것임을 알린다.app:defaultNavHost
: 위의 NavGraph
에서 명시한 시작점을 백스택에 추가한다. 쉽게 이야기하면, 다른 화면에서 뒤로가기 버튼을 누르면 시작점으로 돌아오게 만든다. (StartDestination
-> B
-> 백 버튼
-> StartDestination
)app:navGraph
: 현재 NavHost
와 위에서 지정한 NavGraph
를 연결해주는 역할을 수행한다.class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.firstButton.setOnClickListener {
findNavController().navigate(R.id.secondFragment) // Navigate to another fragment
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
버튼을 클릭했을 때 두 번째 Fragment
로 이동하는 코드이다.
findNavController().navigate()
코드를 통해 NavController
를 탐색하고, 곧바로 이동한다. 정말 손쉽게 화면 이동이 이루어지는 것을 확인할 수 있다.
위에서도 이야기했듯이 Navigation
은 이동 중 애니메이션도 쉽게 적용할 수 있도록 도와준다. 스무스하게 오른쪽, 왼쪽으로 이동하는 애니메이션을 적용해보자.
<!-- slide_in_left.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
<!-- slide_in_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
<!-- slide_out_left.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%" android:toXDelta="-100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
<!-- slide_out_right.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%" android:toXDelta="100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
</set>
스무스한 이동 애니메이션을 위해 위의 XML
리소스들을 res/anim
폴더에 작성 및 저장한다.
<?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/nav_graph"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="co.kr.hoyaho.samplenavi.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/next_action"
app:destination="@id/secondFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="co.kr.hoyaho.samplenavi.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" >
<action
android:id="@+id/before_action"
app:destination="@id/firstFragment"
app:enterAnim="@anim/slide_in_left"
app:exitAnim="@anim/slide_out_right"
app:popEnterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_left" />
</fragment>
</navigation>
다시 NavGraph
리소스로 돌아와, <action>
태그를 작성해주도록 한다.
Action
역시 이름을 보면 알겠지만, 이동 간에 취할 행동을 지정해주는 것이다. 이동할 목적지를 지정해주고, 애니메이션을 넣어주도록 하자.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.firstButton.setOnClickListener {
findNavController().navigate(R.id.next_action, null) // 목적지가 아닌 액션을 매개변수로 전달
}
}
Fragment
에서는 navigate()
의 매개변수를 바꿔준다. 이 전에는 목적지를 명시해주었다면, 이번에는 액션을 명시해준다.
적용하면 위와 같이 애니메이션이 적용된 것을 확인할 수 있다.
데이터 전달을 위해 그래들에서 의존성을 추가해준다.
plugins {
id 'androidx.navigation.safeargs' // java
id 'androidx.navigation.safeargs.kotlin' // kotlin
}
여기서 끝이 아니고, 또 app
레벨의 그래들에서도 플러그인 세팅을 진행해준다.
plugins {
id 'androidx.navigation.safeargs.kotlin' version '2.5.1' apply false
}
이제, 위에서 이야기했던 SafeArgs
를 사용할 준비가 완료되었다.
<fragment
android:id="@+id/secondFragment"
android:name="co.kr.hoyaho.samplenavi.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" >
<argument
android:name="arg_number"
app:argType="integer"
android:defaultValue="5"/>
<action
android:id="@+id/before_action"
app:destination="@id/firstFragment"
app:enterAnim="@anim/slide_in_left"
app:exitAnim="@anim/slide_out_right"
app:popEnterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_left" />
</fragment>
다시 NavGraph
리소스로 돌아가서, 데이터를 전달받을 목적지에 <argument>
태그를 추가하고, 전달 받을 데이터의 형식을 지정한다.
// FirstFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.firstButton.setOnClickListener {
findNavController().navigate(FirstFragmentDirections.nextAction(argNumber = 30))
}
}
인자를 전달할 첫 번째 Fragment
의 코드이다. 자세히 보면 navigate
의 매개변수가 Directions
클래스로 수정된 것을 확인할 수 있다.
Directions
클래스는 위의 의존성 추가 과정을 거치면 생성되는 클래스인데, NavGraph
에서 <action>
태그가 있는 화면들에 한해서 생성된다. 이 Directions
클래스를 통해 손쉽게 해당 화면의 <action>
태그에 접근할 수 있다.
그렇게 다음 Fragment
에 인자값으로 30을 넘겨준 모습이다.
// SecondFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args: SecondFragmentArgs by navArgs()
binding.secondNumberText.text = args.argNumber.toString()
binding.secondButton.setOnClickListener {
findNavController().navigate(R.id.before_action, null)
}
}
인자를 전달받는 Fragment
에서도 SecondFragmentArgs
라는 새로운 클래스가 생성되었는데, 마찬가지로 의존성 추가 과정을 거쳐 생성된 클래스이다. <argument>
태그에 접근할 수 있으며, 받아온 인자값을 사용할 수 있게 해준다.
성공적으로 인자값을 받아온 것을 확인할 수 있다.
기존에 Fragment
에서 데이터를 전달할 때는, Bundle
을 사용했다.
arguments = Bundle().apply {
putString(ItemId, id)
}
그러나, 이런 방식으로 데이터를 넘길 때 몇 가지 치명적 단점이 존재한다.
Key-Value
의 형태로 구성되어 있어 수신자가 Key
값을 잘못 입력하면 데이터를 전달받지 못한다.String
형태로 보냈지만, 수신자가 Int
형으로 받는다면 대응할 방법이 없다.안드로이드 측에서는 이러한 문제를 해결하기 위해 Navigation
에서 Safe arg
를 도입한 것이다.
참고 및 출처
안드로이드 공식 문서
Navigation 훑어보기
Using Safe Args With the Android Navigation Component
유익한 아티클 감사합니다