Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

기록장

[코틀린] Navigation Component를 이용한 BottomNavigation bar 구현 및 AAC ViewModel을 활용한 프래그먼트 간 데이터 공유 본문

코틀린

[코틀린] Navigation Component를 이용한 BottomNavigation bar 구현 및 AAC ViewModel을 활용한 프래그먼트 간 데이터 공유

edit0 2021. 6. 5. 22:43

이번 포스팅은 Navigation Component를 이용하여 하단 네비게이션 바를 구현해보고, 각 프래그먼트 간 데이터를 공유하는 방법도 알아보도록 하겠습니다.

 

네비게이션 컴포넌트는 액티비티 하위에 있는 프래그먼트들 사이의 호출 및 상호작용을 도와주는 컴포넌트입니다. 기존 프레그먼트를 사용하면서 구현해 온 것 들을 조금 더 유연하게 처리할 수 있도록  해주는 것 입니다.

 - 기존 프래그먼트 간 전환에서 Callback을 만들어 프래그먼트 상위에 있는 액티비티에서 Callback을 구현해준 후 프래그먼트를 교체해 주는 형식으로 사용한다던지, 애니메이션을 직접 넣어주어야 한다던지, Bundle을 이용하여 데이터를 주고 받는다던지 등 의 작업들에 대한 솔루션을 제공해줍니다.

 

네비게이션을 사용하는데 있어서 중요한 3가지

 

https://developer.android.com/guide/navigation/navigation-getting-started

  • 네비게이션 그래프: 모든 탐색 관련 정보가 하나의 중심 위치에 모여 있는 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, 애니메이션, 프래그먼트 간 이동 등 좀 더 네비게이션 컴포넌트를 활용할 수 있는 글을 작성해보도록 하겠습니다.

 

감사합니다.