Navigation은 무엇이고 왜 쓰는가?
💬 안드로이드 아키텍처는 하나의 액티비티에 여러 개의 프래그먼트를 사용하도록 권장된다.
💬 기존 Fragment 당 인스턴스를 생성하고 Transaction Manager을 통해 일일이 화면 replace를 하던 비효율적인 방식에서 벗어나서, Navigation을 사용하면 Fragment, Dialog를 포함한 앱의 스토리보드를 볼 수 있으며, 코드 또한 직관적이고 간결해지는 장점을 갖게 된다.
환경 설정
build.gradle (RootProject)
buildscript {
ext {
nav_version = "2.2.0"
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
build.gradle (:app)
plugins {
id "androidx.navigation.safeargs.kotlin"
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.nav_version"
}
💬 Safeargs는 navigation destination들끼리의 안전하게 탐색 및 인수 전달을 사용하기 위해 설정하는 플러그인이다.
최신 버전
https://developer.android.com/guide/navigation/navigation-getting-started?hl=ko
예제 프로젝트 소개
프로젝트 기획
💬 우선, 전반적인 Android Navigation 활용 방법을 중점적으로 기획했다.
💬 대부분의 앱이 사용하고 있는 요소인 Navigation과 BottomNavigationView를 결합하여 구현했다.
💬 향후 코드 재활용을 위해 Fragment와 Dialog의 상호 전환에 대한 요소를 추가했다.
💬 Custom Dialog를 DialogFragment()과 BottomSheetDialogFragment()를 사용해서 각각 중앙과 하단에서 등장하도록 배치했다.
💬 자주 쓰이는 요소는 아니지만, DialogFragment()를 통한 AlertDialog도 구현했다.
💬 마지막 결과 화면에서 버튼을 클릭 시, BottomNavigationView의 세 번째 Item으로 이동하는데 Animation을 결합시켰다.
전환시킬 Fragment Sample 만들기
💬 Navigation에 추가할 Fragment 클래스와 layout을 쌍으로 여러 개 만들어둔다.
https://tmdfyd0807.tistory.com/61
Navigation 시작하기
💬 res 폴더 하단에 navigation이라는 새로운 resource 폴더를 만든다.
💬 해당 폴더에 nav_main.xml을 만들게 되면, navigation id가 nav_main으로 지정되며 파일이 생성된다.
activity_main.xml
<!--<?xml version="1.0" encoding="utf-8"?>-->
<!--<layout 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">-->
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:orientation="vertical"-->
<!-- tools:context=".MainActivity">-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" />
<!-- <com.google.android.material.bottomnavigation.BottomNavigationView-->
<!-- app:menu="@menu/menu_bottom_nav"-->
<!-- android:id="@+id/main_bottom_navigation"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:background="@color/white"-->
<!-- android:theme="@style/style_bottom_nav" />-->
<!-- </LinearLayout>-->
<!--</layout>-->
💬 Navigation 화면 전환을 띄워줄 Fragment 혹은 FragmentContainerView를 만든다.
▫ app:navGraph : NavHostFragment를 만들어놓은 navigation 그래프와 연결한다.
▫ app:defaultNavHost="true" : NavHostFragment가 시스템 뒤로가기 버튼 권한을 가져온다.
nav_main.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_main"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.ryong.navigationexample.FirstFragment"
android:label="FirstFragment"
tools:layout="@layout/fragment_first" />
<fragment
android:id="@+id/secondFragment"
android:name="com.ryong.navigationexample.SecondFragment"
android:label="SecondFragment"
tools:layout="@layout/fragment_second" />
<fragment
android:id="@+id/thirdFragment"
android:name="com.ryong.navigationexample.ThirdFragment"
android:label="ThirdFragment"
tools:layout="@layout/fragment_third" />
</navigation>
💬 만들어놓은 3개의 Fragment를 화면에 추가하고 스토리보드를 볼 수 있게끔 tools:layout을 지정해준다.
BottomNavigationView와 결합
💬 res 폴더 하단에 navigation이라는 새로운 menu 폴더와 함께 하단에 menu_bottom_nav라는 새로운 파일을 만든다.
menu_bottom_nav.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/firstFragment"
android:icon="@drawable/ic_folder"
android:title="첫번째메뉴" />
<item
android:id="@id/secondFragment"
android:icon="@drawable/ic_folder"
android:title="두번째메뉴" />
<item
android:id="@id/thirdFragment"
android:icon="@drawable/ic_folder"
android:title="세번째메뉴" />
</menu>
💬 Fragment와 BottomNavigationView의 메뉴를 결합시키려면 nav_main.xml의 fragment의 id와 동일하게 item id를 설정해야 한다.
activity_main.xml
<!--<?xml version="1.0" encoding="utf-8"?>-->
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:orientation="vertical"-->
<!-- tools:context=".MainActivity">-->
<!-- <androidx.fragment.app.FragmentContainerView-->
<!-- android:id="@+id/nav_host_fragment"-->
<!-- android:name="androidx.navigation.fragment.NavHostFragment"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="0dp"-->
<!-- android:layout_weight="1"-->
<!-- app:defaultNavHost="true"-->
<!-- app:navGraph="@navigation/nav_main" />-->
<com.google.android.material.bottomnavigation.BottomNavigationView
app:menu="@menu/menu_bottom_nav"
android:id="@+id/main_bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:theme="@style/style_bottom_nav" />
<!-- </LinearLayout>-->
<!--</layout>-->
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val navController = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)?.findNavController()
navController?.let {
binding.mainBottomNavigation.setupWithNavController(navController)
}
}
}
대상 (Destination)으로의 이동
💬 nav_main.xml에서 FirstFragment에서 SecondFragment로 이동을 원하는 두 화면을 결정했다고 가정하자.
💬 Design 탭에서 FirstFragment에서 SecondFragment 방향으로 화살표를 연결시켜주면, 아래와 같은 코드가 생성된다.
nav_main.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_main"-->
<!-- app:startDestination="@id/firstFragment">-->
<fragment
android:id="@+id/firstFragment"
android:name="com.ryong.navigationexample.FirstFragment"
android:label="FirstFragment"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment" />
</fragment>
<!--</navigation>-->
💬 action 종류
▫ To Destination : 현재 화면에서 다른 화면으로 이동
▫ To Self : 자기 자신으로 이동
▫ Return To Source : PopBackStatck 시 사용
▫ Global : 어떤 화면에서든 현재 화면으로 이동
💬 action 옵션
▫ 설명을 위해 네비게이션 시나리오가 [ 스플래쉬 ] -> [ 로그인 ] -> [ 메인 ]이라고 가정하자.
▫ PopUpToIncursive
▫ true : [ 메인 ] "Back Button" -> [ 스플래쉬 ]
▫ false : [ 메인 ] "Back Button" -> [ 로그인 ]
▫ PopUpTo : 설정한 Fragment까지 쌓여있던 모든 화면 스택들을 제거
▫ [ 메인 ]에 PopUpTo 값을 주면 [ 스플래쉬 ], [ 로그인 ] 화면들이 백스택에서 제거된다.
💬 이후, Rebuild Project를 진행해주면, 소스코드에서 화면 전환을 할 수 있는 클래스와 메서드를 사용할 수 있다.
Fragment.kt
//class FirstFragment : Fragment() {
//
// private lateinit var binding: FragmentFirstBinding
//
// override fun onCreateView(
// inflater: LayoutInflater, container: ViewGroup?,
// savedInstanceState: Bundle?,
// ): View {
// binding = FragmentFirstBinding.inflate(inflater, container, false)
//
binding.btnDialog.setOnClickListener {
val direction = FirstFragmentDirections.actionFirstFragmentToFirstDialog()
findNavController().navigate(direction)
}
//
// return binding.root
// }
//
//}
대상으로의 argument 전달
💬 화면 전환 시 Intent의 putExtra와 같이 손쉽게 데이터를 전달할 수 있다.
💬 데이터를 전달 받을 화면에서 argument를 설정한다.
nav_main.xml
<fragment
android:id="@+id/fourthFragment"
android:name="com.ryong.navigationexample.FourthFragment"
android:label="FourthFragment"
tools:layout="@layout/fragment_fourth">
<argument
android:name="name"
app:argType="string" />
</fragment>
💬 이후 데이터를 보낼 화면에 해당하는 클래스에서 action 메서드 파라미터에 지정한 argument에 대한 데이터를 넣어주면 된다.
binding.btnLeft.setOnClickListener {
val name = binding.etInput.text.toString()
val direction = FirstDialogDirections.actionFirstDialogToFourthFragment(name)
findNavController().navigate(direction)
}
💬 받는 곳에서는 args를 불러와서 args의 요소를 호출하면 된다.
FourthFragment
class FourthFragment : Fragment() {
private lateinit var binding: FragmentFourthBinding
private val args by navArgs<FourthFragmentArgs>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentFourthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvName.text = args.name
}
}
관련 자료
https://developer.android.com/guide/navigation/navigation-pass-data?hl=ko
이동 시 애니메이션 설정
💬 res 폴더 하단에 anim이라는 리소스 폴더를 만든다.
💬 다음으로, 애니메이션 설정 원리에 대해서 알아보자.
▫ 안드로이드는 위 그림과 같은 좌표계를 적용하고 있다.
▫ 이를 반영하여 화면 전환에 필요한 애니메이션을 만들면 아래와 같다.
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="@integer/slide"/>
</set>
▫ X -100%에서 X 0%로 이동하는 애니메이션이므로 화면의 왼쪽에서 등장하는 애니메이션이 된다.
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="@integer/slide"/>
</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="@integer/slide"/>
</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="@integer/slide"/>
</set>
💬 특정 방향으로의 action에 대해 animation을 설정할 수 있다.
nav_main.xml
<fragment
android:id="@+id/fourthFragment"
android:name="com.ryong.navigationexample.FourthFragment"
android:label="FourthFragment"
tools:layout="@layout/fragment_fourth">
<action
android:id="@+id/action_fourthFragment_to_thirdFragment"
app:destination="@id/thirdFragment"
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>
💬 animation 옵션
▫ enterAnim : 새로 들어오는 애니메이션의 동작
▫ exitAnim : 가려지거나 사라질 기존 화면의 애니메이션 동작
▫ popEnterAnim : 이전 화면으로 돌아갈 때, 이전 화면이 등장하는 애니메이션
▫ popExitAnim : 이전 화면으로 돌아갈 때, 기존에 있던 화면이 사라지는 애니메이션
DialogFragment()를 사용한 Custom Dialog 설정
FirstDialog
class FirstDialog : DialogFragment() {
private lateinit var binding: DialogFirstBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogFirstBinding.inflate(inflater, container, false)
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
return binding.root
}
}
💬 화면 상의 둥근 모서리 마무리를 위하여 바깥쪽 배경화면을 투명처리했다.
dialog_first.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstDialog">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:padding="20dp"
android:background="@drawable/round_background"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
...
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
💬 화면 상의 둥근 모서리 마무리를 위해 안쪽 layout에 둥근 모서리 배경을 주었다.
nav_main.xml
<dialog
android:id="@+id/firstDialog"
android:name="com.ryong.navigationexample.FirstDialog"
android:label="FirstDialog"
tools:layout="@layout/dialog_first" />
BottomSheetDialogFragment()를 이용한 Custom Dialog 설정
SecondDialog
class SecondDialog : BottomSheetDialogFragment() {
private lateinit var binding: DialogSecondBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogSecondBinding.inflate(inflater, container, false)
return binding.root
}
}
dialog_second.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondDialog">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:background="@drawable/round_background"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
...
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
nav_main.xml
<dialog
android:id="@+id/secondDialog"
android:name="com.ryong.navigationexample.SecondDialog"
android:label="SecondDialog"
tools:layout="@layout/dialog_second" />
DialogFragment()를 이용한 AlertDialog 설정
ThirdDialog
class ThirdDialog : DialogFragment() {
private val args by navArgs<ThirdDialogArgs>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.run {
AlertDialog.Builder(
this,
android.R.style.Theme_DeviceDefault_Light_Dialog_NoActionBar
).setTitle("확인")
.setMessage("메시지")
.setIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_check))
.setPositiveButton(Html.fromHtml(
"<font color='${ContextCompat.getColor(requireContext(), R.color.favorite_blue)}'>네</font>")
) { _, _ ->
val direction = ThirdDialogDirections.actionThirdDialogToFourthFragment(args.age)
findNavController().navigate(direction)
}
.setNegativeButton(Html.fromHtml(
"<font color='${ContextCompat.getColor(requireContext(), R.color.favorite_orange)}'>아니오</font>")
) { dialog, _ ->
dialog.cancel()
}
.create().apply {
window?.setGravity(Gravity.CENTER)
}
}!!
}
}
nav_main.xml
<dialog
android:id="@+id/thirdDialog"
android:name="com.ryong.navigationexample.ThirdDialog"
android:label="ThirdDialog" />
전체 코드
https://github.com/tmdfyd2020/Sunflower-tutorial/tree/main/NavigationExample
'Android > Sunflower' 카테고리의 다른 글
[ Android ] [ Kotlin ] Hilt (0) | 2021.12.09 |
---|---|
[ Android ] MVVM 패턴 (0) | 2021.09.27 |