기록장
[코틀린] Navigation Component를 이용한 BottomNavigation bar 구현 및 AAC ViewModel을 활용한 프래그먼트 간 데이터 공유 본문
[코틀린] Navigation Component를 이용한 BottomNavigation bar 구현 및 AAC ViewModel을 활용한 프래그먼트 간 데이터 공유
edit0 2021. 6. 5. 22:43이번 포스팅은 Navigation Component를 이용하여 하단 네비게이션 바를 구현해보고, 각 프래그먼트 간 데이터를 공유하는 방법도 알아보도록 하겠습니다.
네비게이션 컴포넌트는 액티비티 하위에 있는 프래그먼트들 사이의 호출 및 상호작용을 도와주는 컴포넌트입니다. 기존 프레그먼트를 사용하면서 구현해 온 것 들을 조금 더 유연하게 처리할 수 있도록 해주는 것 입니다.
- 기존 프래그먼트 간 전환에서 Callback을 만들어 프래그먼트 상위에 있는 액티비티에서 Callback을 구현해준 후 프래그먼트를 교체해 주는 형식으로 사용한다던지, 애니메이션을 직접 넣어주어야 한다던지, Bundle을 이용하여 데이터를 주고 받는다던지 등 의 작업들에 대한 솔루션을 제공해줍니다.
네비게이션을 사용하는데 있어서 중요한 3가지
- 네비게이션 그래프: 모든 탐색 관련 정보가 하나의 중심 위치에 모여 있는 XML 리소스입니다. 여기에는 대상이라고 부르는 앱 내의 모든 개별적 콘텐츠 영역과 사용자가 앱에서 갈 수 있는 모든 이용 가능한 경로가 포함됩니다. 액비티비 하위에 있는 프래그먼트들을 스토리보드 형식으로 한 눈에 볼 수 있습니다.
- NavHost: 네비게이션 그래프에서 프래그먼트들을 담고, 보여주는 역할을 하는 빈 컨테이너입니다. 구성요소에는 android:name="androidx.navigation.fragment.NavHostFragment" 와 app:navGraph="@navigation/FileName" 포함되어 있고, 네비게이션 그래프에 있는 프래그먼트들을 표현합니다.
- NavController: NavHost에서 앱 탐색을 관리하는 객체입니다. NavController는 사용자가 앱 내에서 이동할 때 NavHost에서 대상 콘텐츠의 전환을 오케스트레이션합니다.
안드로이드 문서에는 사용 시 이와 같은 장점들이 있다고 적혀있습니다.
프래그먼트 트랜잭션 처리.
기본적으로 '위로'와 '뒤로' 작업을 올바르게 처리.
애니메이션과 전환에 표준화된 리소스 제공.
딥 링크 구현 및 처리.
최소한의 추가 작업으로 탐색 UI 패턴(예: 탐색 창, 하단 탐색) 포함.
Safe Args - 대상 사이에서 데이터를 탐색하고 전달할 때 유형 안정성을 제공하는 그래프 플러그인.
ViewModel 지원 - 탐색 그래프에 대한 ViewModel을 확인해 그래프 대상 사이에 UI 관련 데이터를 공유.
이번 포스팅에서는 하단 네비게이션 구현 및 AAC의 ViewModel을 활용하여 데이터 공유하는 법을 알아보도록 하고,
추후 포스팅에서 up and back, 애니메이션, Safe Args에 대해 알아보도록 하겠습니다.
시작 전, 코드에 대한 결과물 입니다.
노란 색 부분은 액티비티 부분으로 현재 어떤 프래그먼트가 화면에 보여지고 있는지 텍스트로 표시해줍니다. 하단 바를 이동해도 카운트는 공유됩니다.
구조
빌드 그래들(모듈)을 설정해줍니다.
android {
...
dataBinding {
enabled true
}
}
dependencies {
...
//ViewModel, Lifecycle, LiveData
implementation 'androidx.activity:activity-ktx:1.1.0'
//Navigation
def nav_version = "2.3.5"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
하단 네비게이션을 구현하므로 res 폴더에 menu 폴더를 생성하여 추가할 아이템들을 설정해줍니다.
res -> menu -> bottom_nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/firstFragment"
android:title="첫번째"
android:icon="@drawable/ic_baseline_airplanemode_active_24"/>
<item
android:id="@+id/secondFragment"
android:title="두번째"
android:icon="@drawable/ic_baseline_access_alarm_24"/>
<item
android:id="@+id/thirdFragment"
android:title="세번째"
android:icon="@drawable/ic_baseline_power_settings_new_24"/>
</menu>
데이터 공유를 위해 ViewModel을 사용하는데 구조는 다음과 같습니다.
Activity의 MainViewModel을 Fragment 1, 2, 3이 공유하여 사용합니다.
뷰모델을 만들고, 프래그먼트 3개를 만들어 줍니다.
MainViewModel.kt
class MainViewModel : ViewModel() {
val TAG = "MainViewModel"
var count = MutableLiveData<Int>()
var mainText = MutableLiveData<String>()
init {
count.value = 0
mainText.value = "현재 프래그먼트"
}
fun plusButton(){
count.setValue(count.value!!.toInt() + 1)
Log.i(TAG, "플러스: ${count.value}")
}
fun minusButton(){
count.value = count.value!!.toInt() - 1
Log.i(TAG, "마이너스: ${count.value}")
}
}
ViewModel에서 비즈니스 로직을 만들어 줍니다.
firstFragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mainViewModel"
type="com.kotlin.k_navigation_viewmodel.viewmodel.MainViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:id="@+id/firstFragment"
tools:contexts="com.kotlin.k_navigation_viewmodel.fragment.FirstFragment">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30dp"
android:id="@+id/number"
android:text="@{String.valueOf(mainViewModel.count)}"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="+"
android:onClick="@{() -> mainViewModel.plusButton()}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="-"
android:onClick="@{() -> mainViewModel.minusButton()}"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30dp"
android:id="@+id/text"/>
</LinearLayout>
</layout>
FirstFragment.kt
class FirstFragment : Fragment() {
val TAG = "FirstFragment"
lateinit var binding: FirstfragmentBinding
lateinit var mainViewModel: MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.i(TAG,"onCreateView()")
var mainActivity = activity as MainActivity
//MainActivity의 객체를 통해 MainViewModel을 가지고 온다.
mainViewModel = ViewModelProvider(mainActivity).get(MainViewModel::class.java)
//firstfragment.xml을 인플레이트
binding = DataBindingUtil.inflate(inflater, R.layout.firstfragment,container,false)
//뷰모델 연결
binding.mainViewModel = mainViewModel
mainViewModel.mainText.value = TAG //액티비티 텍스트 바꿔주기(현재 프래그먼트)
binding.text.text = "1번"
//카운트 숫자를 관찰하여 업데이트
var count = Observer<Int>{
binding.number.text = it.toString()
}
mainViewModel.count.observe(viewLifecycleOwner, count)
return binding.root
}
override fun onDetach() {
super.onDetach()
Log.i(TAG,"onDetach()")
}
}
이와 같이 프래그먼트 2, 3을 만들어 줍니다. (2, 3도 1과 동일하게 MainViewModel을 활용합니다.)
여기까지 NavMenu와 ViewModel, 프래그먼트 3개가 완성되었습니다.
이제 NavMenu도 하단 네비게이션에 연결해주어야 하고, 프래그먼트를 전환시킬 코드도 작성해야 합니다.
res -> navigation -> nav_graph.xml
파일을 생성하면 다음과 같은 화면을 보실 수 있습니다.
프래그먼트들을 추가해 준 후 Split 화면으로 넘어가면, 다음과 같은 화면을 보실 수 있습니다.
파란색 네모칸의 아이디는 적혀있는대로 시작되는 프래그먼트를 지정해줍니다. 디자인 화면으로 보면 집 표시가 되어 있는 것을 보실 수 있습니다.
검은색 밑줄인 아이디는 위에 menu폴더에 생성한 각 아이템 아이디와 일치시켜줍니다.
이제 MainActivity에서 네비게이션 컨트롤러를 만들어주고 연결시켜주면 완성입니다.
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">
<data>
<variable
name="mainViewModel"
type="com.kotlin.k_navigation_viewmodel.viewmodel.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="50dp"
android:gravity="center"
android:background="#FFEB3B"
android:text="@{mainViewModel.mainText}"/>
<!--호스트 프레그먼트-->
<!--프레그먼트들이 보여질 컨테이너-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
<!--하단 바-->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu"/>
</LinearLayout>
</layout>
FragmentContainerView는 네임태그를 추가해 내부 라이브러리에 NavHostFragment를 사용하고 있고, 바로 위에서 추가해준 navGraph 내의 프래그먼트들을 표현하는 역할을 담당하고 있습니다.
defaultNavHost는 true든 false든 상관없습니다. 백프레스를 눌렀을 때 start 프래그먼트로 가느냐 아니면 액티비티 레벨로 뒤로가기가 되느냐에 차이입니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
val TAG = "MainActivity"
lateinit var MainBinding: ActivityMainBinding
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
MainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
MainBinding.setLifecycleOwner(this)
MainBinding.mainViewModel = mainViewModel
//네비게이션들을 담는 호스트
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
//네비게이션 컨트롤러
//내부 호스트 프래그먼트가 가지고 있는 네비게이션 컨트롤러를 가져온다.
val navController = navHostFragment.navController
//바텀 네비게이션바와 네비게이션을 묶는다.
NavigationUI.setupWithNavController(MainBinding.bottomNav, navController)
}
}
여기까지 네비게이션 컴포넌트를 이용한 하단 네비게이션 바를 구현해 보았고, AAC ViewModel을 활용한 데이터 공유 방법도 알아보았습니다.
다음 포스팅에는 스토리보드 형식으로 프래그먼트들을 활용하여 Safety Args, 애니메이션, 프래그먼트 간 이동 등 좀 더 네비게이션 컴포넌트를 활용할 수 있는 글을 작성해보도록 하겠습니다.
감사합니다.
'코틀린' 카테고리의 다른 글
[코틀린] Databinding, LiveData, ViewModel, Room 사용 예제 with MVVM (0) | 2021.05.01 |
---|---|
[코틀린] 코루틴 개념 익히기 URI (0) | 2021.04.21 |
[코틀린] Room (ViewModel, DataBinding, LiveData) 2 (0) | 2021.03.28 |
[코틀린] Room (SELECT, INSERT, UPDATE, DELETE) 1 (0) | 2021.03.27 |
[코틀린] LiveData, BindingAdapter 사용해보기 (with ViewModel and DataBinding) (0) | 2021.03.27 |