안드로이드 코틀린 : JetPack Navigation로 Fragment 전환 및 데이터 전송 기본 사용법
Lucy Archive
Lucy 2023
2021. 4. 13. 23:39

Android Kotlin : Navigation 으로 Fragment 화면 전환 및 데이터 전송

이 포스트는

Android JetPack Nagivation을 사용하여 탐색 그래프를 만들고, 탐색 그래프에서 생성된 action id로 Fragment 를 전환하는 방법과 Fragment간 데이터 주고 받는 방법

에 대해 작성하였습니다. 

 

Navigation 기본 사용법

Navigation, Navigation Graph 이란?

앱은 보통 여러개의 화면이 있습니다. 일반적으로, 앱에서 여러 화면을 구성하기 위해 한 개의 Activity 와 다수의 Fragment를 사용하여 화면을 구성하는 방법이 권장됩니다. 탐색 그래프(Navigation Graph)는 여러 Fragment의 화면 전환을 시각적으로 보여 줄 수 있는 도구입니다. 아래 그림과 같이 Navigation Graph를 사용하면 여러 Fragment의 화면 전환을 한눈에 알 수 있습니다. 탐색 그래프에 Fragment 전환을 표시하면 UI에서 화살표가 생성되고, XML 코드에서는 action 요소가 생성됩니다. 안드로이드 프로그래밍에서 이 id를 사용하여 쉽게 화면 전환이 가능합니다. 

참조 : https://developer.android.com/guide/navigation/navigation-getting-started?hl=ko

환경 설정

코틀린 언어를 사용하는 경우 build.gradle 파일의 dependencie 에 아래의 코드를 추가합니다. 포스트 작성일자 기준으로 최신버전은 2.3.5 입니다. 참고하세요.

dependencies {

	...

	//Navigation : Kotlin
	def nav_version = "2.3.5"
	implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
	implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

탐색 그래프 만들기

리소스 폴더 res에서 Nagivation 리소스 타입의 리소스 파일을 추가합니다. 아래 그림에서는 nav_graph 라는 파일을 생성하였습니다. 

Navigation 리소스 파일을 생성하면, res >navigation 폴더에 nav_graph.xml 파일이 생성됩니다. 해당 파일을 열면 아래와 같이 탐색 그래프를 볼 수 있고, 이곳에서 새로운 Fragment를 추가, 방향 설정 및 홈지정 등 다양한 작업이 가능합니다. 

화면 전환 및 데이터 전송

Fragment에서 Fragmet로 화면 전환하기 위해 NavController 객체의 navigate() 메서드를 사용합니다. NavController 객체는 아래의 메서드로 불러올 수 있습니다.

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController()
  • Navigation.findNavController() : 파라미터로 View, Fragment, Activity가 올 수 있음

아래 코드는 Fragment 전환 사용 예시입니다. 아래 코드에서 it은 View객체로 Button을 지칭합니다. navigate() 메서드의 인자는 네비게이션 그래프에 생성된 <action> 요소의 id를 입력하면 됩니다. 

button.setOnClickListener {
	it.findNavController().navigate(R.id.action_permissions_to_mainFragment)
}

데이터를 전달하기 위해 Bundle() 타입을 사용할 수 있습니다. 아래는 예시입니다.

binding.button.setOnClickListener {
    val bundle = Bundle()
    bundle.putString("name", "Hong Gil-Dong")
    Navigation.findNavController(view).navigate(R.id.action_permissions_to_mainFragment, bundle)
}

수신 Fragment에서는 argument를 사용하여 수신된 데이터를 처리 할 수 있습니다. 

binding.textView.text = arguments?.getString("name")

종료된 Fragment로 부터 결과 데이터 받기

Nav Graph에 FragmentA -> FragmentB 로 action이 설정되어 있는 경우 FragmentB 화면에서 뒤로가기 버튼을 누르면 FragmentA로 돌아가게 됩니다. 이 때 FragmentA 에서 FragmentB의 결과 데이터를 보고 싶은 경우는 아래와 같이 코드를 작성 할 수 있습니다. 

결과값을 저장할 FragmentB는 아래의 코드로 BackStackEntry에 Map형식(Key, Data)으로 데이터를 저장 할 수 있습니다.

val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("return", returnData)

FragmentA는 아래의 코드로 BackStackEntry에 저장된 데이터를 observe 할 수 있습니다. 

val navController = findNavController()
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("return")?.observe(viewLifecycleOwner){
    textView.text = it
}

※ 위에서 소개한 방법 이 외에 Safe Args를 사용한 화면 전환 방법이 있고, Safe Args와 다른 다양한 방법으로 데이터를 전송하는 방법이 있습니다. 이 부분은 추후에 포스팅할 예정입니다. 

Fragment 종료하기 - 4/15일 추가

Fragment를 종료하기 위해 아래의 코드를 사용할 수 있습니다. 

val navController = findNavController()
navController.popBackStack()

 

사용 예시

프로젝트 생성

아래와 같이 프로젝트를 생성 합니다. 본 포스트의 코드는 Android Studio(4.1.2), Kotlin(1.4.31) 버전에서 작성하였습니다.

  • 템플릿 : Empty Activity
  • 프로젝트명 : Navigation
  • 언어 : Kotlin

 

ViewBinding 설정

Layout에 있는 View의 Id를 이용하여 코드와 바인딩 하기 위해 App > Gradle Scripts > build.gradle에서 ViewBinding 설정 코드를 추가합니다. 코드 추가 완료되면 Sync Now를 클릭하여 변경 사항을 적용 합니다.

android { 
	... 
    viewBinding { 
    	enabled = true 
    } 
}

Gragle 수정 사항 적용

VIewBinding 설정 및 Activity 및 Fragment의 사용법은 아래의 링크를 참고하세요.

안드로이드 View Binding 사용하기

 

안드로이드 View Binding 사용하기 - kotlin-android-extensions 지원 중단

안드로이드 View Binding 방법 정리 안드로이드 코드에서 레이아웃 View에 접근하기 위해 사용된 kotlin-android-extensions 의 지원이 중단예정으로, 이를 대체하여 사용 할 수 있는 ViewBinding 사용법에 대해

juahnpop.tistory.com

 

Navigation 환결 설정

build.gradle(module:Navigation.app) 파일의 dependency에 아래의 코드를 추가하고, SyncNow를 눌러 Gradle 변경 사항을 적용해주세요.

dependencies {

	...

	//Navigation : Kotlin
	def nav_version = "2.3.5"
	implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
	implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

최종 build.gradle(module:Navigation.app) 파일의 코드는 아래와 같습니다. 참고하세요.

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.blacklog.navigation"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    viewBinding {
        enabled = true
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    //Navigation
    def nav_version = "2.3.5"
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

 

Navigation Graph 생성 후 그래프 만들기

앞서 설명한 대로 Navigation 타입의 nav_graph.xml 안드로이드 리소스 파일을 생성합니다. 아래 그림과 같이 FragmentA, FragmetB를 만들어 전환 방향을 지정합니다.

 

UI 만들기

activity_main.xml 에서 기본으로 생성된 TextView를 삭제하고 아래와 같이 FragmentContainerView 코드를 작성합니다.  Palette 에서 NavHostFragment 를 추가하셔도 됩니다. NavHostFragment를 추가하고 생성된 nav_graph를 선택하면 됩니다. 

<?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/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

fragment_a.xml 코드는 아래와 같이 작성하였습니다. FragmentB로 전송할 데이터를 입력하는 EditText와 FragmentB가 Fragment BackStackEntry에 저장한 데이터를 보여주기 위한 TextView가 존재합니다. 

<?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=".FragmentA">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment A"
        android:textSize="30sp"
        android:layout_margin="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txtTx"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="204dp"
        android:text="TX Message"
        android:textColor="@color/black"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:textSize="20sp"
        android:hint="Input TX Message"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txtTx" />

    <TextView
        android:id="@+id/txtRx"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="450dp"
        android:text="RX Message : "
        android:textSize="20sp"
        android:textColor="@color/black"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txtRecieve"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="@+id/txtRx"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/txtRx"
        app:layout_constraintTop_toTopOf="@+id/txtRx" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Go FragmentB"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

fragment_b.xml 코드는 아래와 같이 작성하였습니다. FragmentA로 부터 수신한 데이터를 보여주는 TextView와 FragmentA BackStackEntry에 저장할 데이터를 입력하기 위한 EditText가 존재합니다.

<?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=".FragmentA">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment B"
        android:textSize="30sp"
        android:layout_margin="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txtTx"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="204dp"
        android:text="TX Message"
        android:textColor="@color/black"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:textSize="20sp"
        android:hint="Input TX Message"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txtTx" />

    <TextView
        android:id="@+id/txtRx"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="450dp"
        android:text="RX Message : "
        android:textSize="20sp"
        android:textColor="@color/black"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txtRecieve"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="Nothing..."
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="@+id/txtRx"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/txtRx"
        app:layout_constraintTop_toTopOf="@+id/txtRx" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Back"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

위 코드로 생성된 UI는 아래와 같습니다.

FragmentA 와 FragmentB의 UI

FragmentA.kt 코드 작성

FragmentA.kt 는 아래와 같이 코드를 작성하였습니다. Navigation 관련된 코드는 아래와 같습니다.

  • 24~27번 줄 : FragmentB로 부터 수신한 데이터 처리
  • 29~33번 줄 : FragmentB를 호출하면서 데이터를 보내기
package com.blacklog.navigation

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import com.blacklog.navigation.databinding.FragmentABinding

class FragmentA : Fragment() {

    private var _binding: FragmentABinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = FragmentABinding.inflate(inflater, container, false)

        findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("return")?.observe(viewLifecycleOwner){
            binding.txtRecieve.text = it
            Log.d("Navigation_debug", it)
        }

        binding.button.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("data", binding.editText.text.toString())
            this.findNavController().navigate(R.id.action_fragmentA_to_fragmentB, bundle)
        }

        return binding.root
    }
}

FragmentB 코드 작성

FragmentB.kt 는 아래와 같이 코드를 작성하였습니다. Navigation 관련된 주요 코드는 아래와 같습니다.

  • 24번 줄 : FragmentA로 부터 수신한 데이터 처리
  • 30번 줄 : 뒤로 버튼을 누르면 실행되는 콜백 함수
  • 37~42번 줄 : UI의 버튼 또는 네비게이션바의 뒤로 버튼을 누르면 호출되는 함수로, 이전 Fragment의 BackStackEntry에 데이터를 저장하고 뒤로 이동
package com.blacklog.navigation

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.navigation.fragment.findNavController
import com.blacklog.navigation.databinding.FragmentBBinding

class FragmentB : Fragment() {

    private var _binding: FragmentBBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = FragmentBBinding.inflate(inflater, container, false)

        binding.txtRecieve.text = arguments?.getString("data")

        binding.button.setOnClickListener {
            goBack()
        }

        requireActivity().onBackPressedDispatcher.addCallback {
            goBack()
        }

        return binding.root
    }

    private fun goBack(){
        val returnMessage = binding.editText.text.toString()
        findNavController().apply {
            previousBackStackEntry?.savedStateHandle?.set("return", returnMessage)
            popBackStack()
        }
    }
}

실행 결과

위 코드를 실행한 결과는 아래와 같습니다. FragmentA의 EditText의 문자열은 FragmentB의 TextView에, FragmentB의 EditText의 문자열은 FragmentA의 TextView로 보여지는 것을 확인 하였습니다. 

실행 결과

끝까지 읽어 주셔서 감사합니다.^^

관련포스트

🤞 안드로이드(코틀린) 앱 제작하기 관련글 목록 보기

🤞 안드로이드(코틀린) 내비게이션 관련글 목록 보기

🤞 안드로이드(코틀린) 프래그먼트 관련글 목록 보기