안드로이드 : diary – Fragment Navigation (2)

이번 포스트에서는 프래그먼트의 네비게이션을 조금 더 시각적으로 관리할 수 있는 방법에 대해서 알아볼거야.

지난 포스트(안드로이드 : diary – Fragment Navigation (1)) 에서 확인한 바로는 프래그먼트를 배치할 레이아웃으로 FragmentContainerView 를 사용했었지.

activity_main.xml
<?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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:name="com.woohahaapps.androiddiary.fragments.ListFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

그리고 FragmentManager 로부터 구한 FragmentTransaction 의 replace 메소드를 이용해서 프래그먼트를 전환했었어.

MainActivity.kt
...
        if (!loginState) {
            val fragmentTransaction = supportFragmentManager.beginTransaction()
            fragmentTransaction.replace(R.id.fragmentContainerView, LoginFragment())
            fragmentTransaction.commit()
        }
...
    fun moveFragmentToList() {
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.replace(R.id.fragmentContainerView, ListFragment())
        fragmentTransaction.commit()
    }
}

이번 포스트에서는 NavHostFragment 를 배치하고 navGraph 를 이용해서 프래그먼트간의 전환을 관리하는 방법에 대해서 알아볼거야.

우선 activity_main.xml 에서 MainActivity 의 레이아웃에 배치한 FragmentContainerView 의 name 속성값을 ListFragment 에서 androidx.navigation.fragment.NavHostFragment 로 수정해줄께.


수정하고나니 빨간색으로 표시가 되네? 그 이유는 의존성 라이브러리가 포함되지 않았기 때문에 인식되지 않는거야. 그렇다면 의존성 라이브러리를 추가해줘야겠군.

NavHostFragment 를 검색해보면 https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment 에서 androidx.navigation:navigation-fragment 에 정의되었다고 알려주네.


androidx.navigation:navigation-fragment 를 클릭해보면 https://developer.android.com/jetpack/androidx/releases/navigation?hl=ko 에서 아래와 같이 의존성 라이브러리에 대해서 설명을 해주고 있어.


androidx.navigation 은 Jetpack 라이브러리의 하나로 navigation-fragment-ktx 와 navigation-ui-ktx 라이브러리에 의존하고 있어.

위에 표시된대로 build.gradle.kts 파일에 2개의 의존성 라이브러리를 추가해볼께.

build.gradle.kts
...
dependencies {
    implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
    implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

위 코드처럼 추가하면 밑줄이 표시되고 커서를 갖다대면 아래 그림에서처럼 제안이 표시가 돼.


Replace with new library catalog declaration for androidx-navigation-fragment-ktx 링크를 클릭하면 아래 그림처럼 라이브러리 형태로 코드가 변경이 돼.


아래의 androidx.navigation:navigation-ui-ktx:2.7.7 항목에 대해서도 마찬가지 방법으로 변경을 해줄께.


build.gradle.kts 파일을 수정한 후에는 Sync Now 를 해주어야 필요한 라이브러리가 동기화가 되지.


Sync 가 완료된 후에 activity_main.xml 파일을 열어보면 빨간색으로 표시되었던 androidx.navigation.fragment.NavHostFragment 가 초록색으로 변경된걸 볼 수 있을거야.


이 상태에서 프로그램을 실행시키는건 FragmentContainerView 의 초기 프래그먼트가 ListFragment 에서 NavHostFragment 로 변경되었다는것 말고는 바뀐게 없는 상태이기 때문에 결과는 변함이 없어.

이제 그래프를 사용해서 프래그먼트간의 전환 관계를 Visual 적으로 표시하는 방법에 대해서 알아볼께.

리소스에 navigation 이라는 이름의 리소스 디렉토리를 추가해볼께.




이 리소스 디렉토리에 Navigation Resource File 을 추가해주는데 이름은 nav_graph 로 해줄께.



추가한 nav_graph.xml 을 열어보면 NavHostFragments 가 발견되지 않았다고 표시되네.


이 nav graph 는 NavHostFragment 에 참조가 되어야 한데. 그럼 참조를 시켜줄께.

activity_main.xml 을 열어서 FragmentContainerView 의 속성에 app:defaultNavHost=”true” 와 app:navGraph=”@navigation/nav_graph” 를 추가해주자.

activity_main.xml
...
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"
        />
...

다시 nav_graph.xml 을 열어보면 아래 그림과 같이 바뀐것을 확인할 수가 있을거야.


이제 이 레이아웃에서 프래그먼트간의 전환을 Visual 하게 표시할 수 있는 준비가 되었어.

New Destination 버튼을 클릭하면 이미 생성해둔 3개의 Fragment 클래스가 대상 목록에 표시되는 것을 볼 수가 있을거야.


3개의 Fragment 를 각각 선택해서 추가해주면 아래 그림처럼 각 프래그먼트의 UI 레이아웃 미리보기가 표시되지.


editFragment 옆에 작은 집 그림이 표시된게 보일까? 이걸 가장 먼저 추가했기 때문에 초기 Fragment 로 지정이 된건데, 나는 listFragment 를 초기 Fragment 로 지정하겠다고 했으니, listFragment 를 선택한 상태에서 위쪽에 집 모양 아이콘을 클릭해서 초기 Fragment 를 변경해줄께.


이제 각 프래그먼트 미리보기 창을 선택해서 프래그먼트 간의 전환관계를 설정해볼께.


위 그림처럼 listFragment 를 선택하면 오른쪽 면에 동그라미가 표시되는데, 이걸 마우스로 선택해서 loginFragment 로 끌면 프래그먼트간의 전환 관계가 생성이 돼.


listFragment 를 표시하는 시점에 로그인되지 않은 상태라면 loginFragment 로 전환했다가, 로그인이 처리되고 나면 다시 listFragment 로 전환하는 과정을 설정한거야.


listFragment 에는 여러 개의 일기 데이터를 목록으로 표시해줄 생각인데, 그 중에 하나를 선택하면 editFragment 로 전환해서 수정할 수 있게 하거나, 새로운 일기를 쓸 수 있도록 editFragment 로 전환시켜주려고 해. 이 전환관계를 설정한 결과는 아래 그림과 같아.


이렇게 설정한 관계가 nav_graph.xml 에는 어떤 모습으로 추가되었는지를 볼께.


위 그림에서 오른쪽 Component Tree 에서 action_ 으로 시작되는 항목이 프래그먼트간의 전환을 추가한 항목이고, 왼쪽에서 그 내용을 확인할 수가 있어.

이제 프래그먼트간의 전환시에는 이 action 속성을 이용할 수 있게 된거야.

실제로 이 action 을 사용하려면 UI 가 준비되어야 하니까 각 프래그먼트 UI 를 테스트할 수 있는 정도로만 디자인해볼께.

우선 각 프래그먼트의 FrameLayout 을 컨트롤 배치에 용이하도록 androidx.constraintlayout.widget.ConstraintLayout 으로 변경해줬어.


fragment_list.xml 에서는 button 을 하나 추가해서 배치하고, id 는 button_new_diary 로, text 는 New Diary 로 설정했어.

fragment_list.xml
...
    <Button
        android:id="@+id/button_new_diary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New Diary"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
...

이 버튼을 클릭하면 EditFragment 로 이동해서 일기 내용을 작성할 수 있게 할거야.


fragment_edit.xml 에는 button 을 하나 추가해서 id 는 button_save, text 는 Save Diary 로 설정했어.

fragment_edit.xml
...
    <Button
        android:id="@+id/button_save"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Save Diary"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
...

이 버튼을 클릭하면 ListFragment 로 돌아가는 동작을 하게 될거야.


마지막으로 fragment_login.xml 에는 이미 배치되어 있던 버튼의 text 를 Login 으로 수정하고 위치를 수정해줬어.

fragment_login.xml
...
    <Button
        android:id="@+id/btn_login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Login"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
...


이렇게 nav_graph 방식에 의해서 프래그먼트 전환할 준비가 되었는데, 기존의 프래그먼트 전환 코드를 제거해서 프로그램이 실행되었을 때 어떻게 보이는지를 확인해볼께.

class MainActivity : AppCompatActivity() {

    private var loginState : Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        if (!loginState) {
//            val fragmentTransaction = supportFragmentManager.beginTransaction()
//            fragmentTransaction.replace(R.id.fragmentContainerView, LoginFragment())
//            fragmentTransaction.commit()
       }
    }

    fun setLoginState(state: Boolean) {
        loginState = state
    }

    fun moveFragmentToList() {
//        val fragmentTransaction = supportFragmentManager.beginTransaction()
//        fragmentTransaction.replace(R.id.fragmentContainerView, ListFragment())
//        fragmentTransaction.commit()
    }
}

프래그먼트 전환관련 코드를 모두 주석처리한 후에 프로그램을 실행시켰는데도 nav_graph.xml 에서 홈으로 설정한 ListFragment 가 정상적으로 표시가 되고 있는 것을 확인할 수가 있어.

이제 새로운 프래그먼트 전환 방법을 구현해볼께.

우선 프로그램이 실행되었을 때 로그인 상태가 아닐때 LoginFragment 로 전환하는 코드는 아래와 같이 작성할 수가 있어.

MainActivity.kt
...
        if (!loginState) {
//            val fragmentTransaction = supportFragmentManager.beginTransaction()
//            fragmentTransaction.replace(R.id.fragmentContainerView, LoginFragment())
//            fragmentTransaction.commit()

            val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
            val navController = navHostFragment.navController
            navController.navigate(R.id.action_listFragment_to_loginFragment)
        }
...

navController 를 구하는 과정의 코드와 navController 의 navigate 함수의 파라미터를 어떻게 사용하고 있는지를 잘 살펴봐야 해.

R.id.action_listFragment_to_loginFragment 는 nav_graph 에서 프래그먼트간의 전환관계를 설정해서 생성된 전환 액션의 id 야.

이번에는 fragment_login.xml 에 배치한 Login 버튼에 대해서 클릭 이벤트를 처리해볼께.

LoginFragment.kt
...
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        val v: View = inflater.inflate(R.layout.fragment_login, container, false)

        val btnLogin : Button? = v.findViewById(R.id.btn_login)
        btnLogin?.setOnClickListener{
            mainActivity.setLoginState(true)
            //mainActivity.moveFragmentToList()

            findNavController().navigate(R.id.action_loginFragment_to_listFragment)
        }

        return v
    }
...

기존 코드 mainActivity.moveFragmentToList() 를 주석처리했고, findNavController().navigate(R.id.action_loginFragment_to_listFragment) 라인을 새로 추가했어.

역시 프래그먼트 전환을 위해서 추가한 액션 id 를 지정해서 navigate 하고 있어.

프로그램을 실행시켜보면 Login 버튼이 보이는 상태로 시작되는데, Login 버튼을 클릭하면 ListFragment 화면으로 이동할거야.

그런데 이 상태에서 Back 버튼을 누르면 다시 Login 화면이 보이게 돼. 즉, 프래그먼트 전환에 의해서 이동한 화면들이 거꾸로 하나씩 보이게 되는거지.


앞서 확인해봤던 Navigation (1) 에서(replace() 사용시)와는 다르게 프래그래먼트 전환에 사용된 프래그먼트가 그대로 중첩되는 방식으로 전환되고 있다는 사실에 주의해야 해.

이제 ListFragment 의 New Diary 버튼과 EditFragment 의 Save Diary 버튼에도 클릭 이벤트 처리 함수를 작성해볼께.

ListFragment.kt
...
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val v: View = inflater.inflate(R.layout.fragment_list, container, false)

        val buttonNewDiary : Button = v.findViewById(R.id.button_new_diary)
        buttonNewDiary.setOnClickListener {
            findNavController().navigate(R.id.action_listFragment_to_editFragment)
        }

        return v
    }
...
EditFragment.kt
...
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val v: View = inflater.inflate(R.layout.fragment_edit, container, false)

        val buttonSaveDiary: Button = v.findViewById(R.id.button_save)
        buttonSaveDiary.setOnClickListener {
            findNavController().navigate(R.id.action_editFragment_to_listFragment)
        }
        return v
    }
...

이렇게 Fragment 간의 전환 방법을 알아보았어. 다음 포스트에서는 diary 웹 프로그램에서 작성한 RestAPI 통신 방법을 알아볼께.

Leave a Comment