본문 바로가기

안드로이드 Android/Android Study Jams

4. Navigation - Define navigation paths

github.com/google-developer-training/android-kotlin-fundamentals-apps/tree/master/AndroidTriviaFragment

 

google-developer-training/android-kotlin-fundamentals-apps

android-kotlin-fundamentals-apps. Contribute to google-developer-training/android-kotlin-fundamentals-apps development by creating an account on GitHub.

github.com

 

프로젝트에 내비게이션 컴포넌트 추가

최신 내비게이션 버전은 여기에서 찾을 수 있다.

 

build.gradle (Project)

ext {
        ...
        navigationVersion = "2.3.1"
        ...
    }

 

build.gradle (Module)

dependencies {
  ...
  implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
  implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
  ...
}

 

Android pane에서 res 폴더 우클릭 후 [New > Android Resource File]을 선택한다.

New Resource File 다이얼로그에서 Resource type은 Navigation, File name은 navigation으로 한다.

OK 버튼을 누르면 [res > navigation] 폴더에 navigation.xml이라는 새 파일이 생성된다.

navigation.xml 파일을 열고 Navigation Editor를 열기 위해 Design 탭을 누른다.

 

NavHostFragment 생성

navigation host fragment는 내비게이션 그래프에서 프래그먼트의 주인 역할을 한다. navigation host Fragment는 주로 NavHostFragment라고 이름 지어진다.

사용자가 내비게이션 그래프에서 정의된 목적지 간에 이동함에 따라 navigation host Fragment는 필요에 따라 프래그먼트를 안팎으로 바꾼다. Fragment는 또한 알맞은 Fragment back stack을 생성하고 관리한다.

 

TitleFragment가 NavHostFragment로 교체되도록 코드를 수정해보자.

1. [res > layout > activity_main.xml]을 열고 Code 탭을 연다.

2. 기존의 타이틀 Fragment의 name과 id를 바꾼다.

3. navigation host Fragment는 어떤 내비게이션 그래프를 사용하는 지 알아야 한다. app:navGraph 속성을 추가한다.

4. app:defaultNavHost 속성을 추가한다. 그러면 이 내비게이션 호스트는 기본 호스트가 되고 시스템 Back 버튼을 가로챌 것이다(아마 뒤로가기 버튼을 누르면 프래그먼트로 이동된다는 의미인 듯).

<!-- The NavHostFragment within the activity_main layout -->
            <fragment
                android:id="@+id/myNavHostFragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:navGraph="@navigation/navigation"
                app:defaultNavHost="true" />

 

내비게이션 그래프에 프래그먼트 추가

navigation.xml을 연다. Navigation Editor(Design 탭)에서 New Destination 버튼을 누르고 fragment_title을 선택한다.

New Destination 버튼을 사용해 GameFragment를 추가한다.

만약 미리보기에서 "Preview Unavailable" 메시지가 뜬다면 Code 탭을 클릭해 내비게이션 XML을 연다. gameFragment에 대한 fragment 요소가 다음과 같이 tools:layout="@layout/fragment_game"을 포함하는지 확인한다.

<!-- The game fragment within the navigation XML, complete with tools:layout. -->
<fragment
   android:id="@+id/gameFragment"
   android:name="com.example.android.navigation.GameFragment"
   android:label="GameFragment"
   tools:layout="@layout/fragment_game" />

레이아웃 편집기(Design 뷰 사용)에서 제목 Fragment와 겹치지 않도록 게임 Fragment를 오른쪽으로 드래그한다.

미리보기에서 제목 Fragment에 커서를 두면 Fragment 뷰의 오른쪽에 둥근 연결 점이 나타난다. 연결점을 gameFragment로 드래그한다. 두 프래그먼트를 연결하는 Action이 생성되었다.

Action의 속성을 보려면 두 프래그먼트를 연결하는 화살표를 클릭한다. Attribute 페인에 ID가 설정된 것을 확인한다.

 

제목 화면에서 Play 버튼이 사용자를 게임 화면으로 이동시키도록 해보자.

TitleFragment.kt 파일을 연다. onCreateView() 함수 안에서 다음 코드를 return 문 전에 추가한다.

//The complete onClickListener with Navigation
binding.playButton.setOnClickListener { view : View ->
       view.findNavController().navigate(R.id.action_titleFragment_to_gameFragment)
}

 

조건부 내비게이션 추가

GameWonFragment와 GameOverFragment를 내비게이션 그래프에 추가

navigation.xml 파일을 연다.

New Destination 버튼을 누르고 fragment_game_over을 선택한다.

미리보기 영역에서 두 프래그먼트가 겹치지 않게 게임오버 Fragment를 게임 Fragment의 오른쪽으로 드래그한다.

게임오버 Fragment의 ID 속성을 gameOverFragment로 바꾸는 것을 명심한다.

New Destination 버튼을 누르고 fragment_game_won을 선택한다.

두 프래그먼트가 겹치지 않도록 게임승리 Fragment를 게임오버 Fragment의 밑으로 드래그한다. 게임승리 Fragment의 ID 속성을 gameWonFragment로 하는 것을 명심한다.

 

게임 Fragment를 게임결과 Fragment와 연결

Layout Editor의 미리보기 영역에서 게임 Fragment와 게임오버 Fragment를 연결한다. 같은 방법으로 게임 Fragment를 게임승리 Fragment로 연결하는 액션을 생성한다.

게임 Fragment와 게임승리 Fragment를 연결하는 선에 커서를 둔다. 자동으로 할당된 Action에 대한 ID에 주목하라.

 

한 Fragment에서 다음으로 넘어가는 코드 추가

GameFragment.kt 파일을 연다. onCreateView() 메서드는 플레이어가 졌는지 이겼는지 결정하는 if/else 조건을 정의한다.

binding.submitButton.setOnClickListener @Suppress("UNUSED_ANONYMOUS_PARAMETER")
        { 
              ...
                // answer matches, we have the correct answer.
                if (answers[answerIndex] == currentQuestion.answers[0]) {
                    questionIndex++
                    // Advance to the next question
                    if (questionIndex < numQuestions) {
                        currentQuestion = questions[questionIndex]
                        setQuestion()
                        binding.invalidateAll()
                    } else {
                        // We've won!  Navigate to the gameWonFragment.
                    }
                } else {
                    // Game over! A wrong answer sends us to the gameOverFragment.
                }
            }
        }

게임 승리를 위한 else 조건 안에 gameWonFragment로 이동시키는 다음 코드를 추가한다. Action 이름이 navigation.xml 파일에서 설정된 것과 정확히 일치하도록 확실히 한다.

// We've won!  Navigate to the gameWonFragment.
view.findNavController()
   .navigate(R.id.action_gameFragment_to_gameWonFragment)

게임 패배를 위한 else 조건 안에 gameOverFragment로 이동시키는 다음 코드를 추가한다.

// Game over! A wrong answer sends us to the gameOverFragment.
view.findNavController().
   navigate(R.id.action_gameFragment_to_gameOverFragment)

Android 시스템의 Back 버튼은 위 스크린샷에 표시한 부분과 같다. 만약 사용자가 게임승리 프래그먼트 혹은 게임패배 프래그먼트에서 Back 버튼을 누르면 앱은 질문 화면으로 이동된다. 이상적으로 Back 버튼은 앱의 제목 화면으로 되돌아가게 해야 한다.

 

Back 버튼의 목적지 변경

내비게이션 액션에 대한 pop behavior 설정

  • 액션의 popUpTo 속성은 이동되기 전에 주어진 목적지로 back stack이 튀어 나오게 한다.
  • 만약 popUpToInclusive 속성이 false이거나 설정되지 않으면 popUpTo는 명시된 목적지 이전까지 목적지를 제거하지만 명시된 목적지는 back stack으로 놔둔다.
  • 만약 popUpToInclusive 속성이 true로 설정되면 popUpTo 속성은 back stack으로부터 주어진 목적지까지 포함하여 모든 목적지를 제거한다.
  • popUpToInclusive가 ture이고 popUpTo가 앱의 시작 위치로 설정되면 액션은 back stack으로부터 모든 앱 목적지를 제거한다. Back 버튼은 사용자를 앱에서 완전히 나가게 한다.

navigation.xml을 연다. 내비게이션 그래프가 나타나지 않으면 레이아웃 에디터에서 Design 탭을 클릭한다.

gameFragment에서 gameOverFragment로 이동시키는 액션을 선택한다.

Attributes 페인에서 다음과 같이 설정한다.

이 속성은 내비게이션 컴포넌트에게 백스택으로부터 GameFragment까지 제거하라고 알려준다.(이것은 popUpTo 필드를 titleFragment로 설정하고 popUpToInclusive 체크박스를 해제하는 것과 같은 효과를 갖는다.)

gameFragment에서 gameWonFragment로 이동시키기 위한 액션을 선택한다. 위 스크린샷과 똑같이 설정한다.

 

더 많은 내비게이션 액션을 추가하고 onClick 핸들러 추가

navigation.xml 파일에서 gameOverFragment를 gameFragment로 연결하는 내비게이션 액션을 추가한다. Attributes 페인에서 다음과 같이 설정한다.

 navigation.xml 파일에서 gameWonFragment을 gameFragment로 연결하는 내비게이션 액션을 추가한다. 위 스크린샷과 똑같이 설정한다.

 

이제 Try Again과 Next Match 버튼에 기능을 추가한다.

GameOverFragment.kt 코틀린 파일을 연다. return 문 전에 onCreateView() 메서드의 끝에 다음 코드를 추가한다. 

// Add OnClick Handler for Try Again button
        binding.tryAgainButton.setOnClickListener{view: View->
        view.findNavController()
                .navigate(R.id.action_gameOverFragment_to_gameFragment)}

GameWonFragment.kt 코틀린 파일을 연다. return 문 전에 onCreateView() 메서드의 끝에 다음 코드를 추가한다.

// Add OnClick Handler for Next Match button
        binding.nextMatchButton.setOnClickListener{view: View->
            view.findNavController()
                    .navigate(R.id.action_gameWonFragment_to_gameFragment)}

 

앱바에 Up 버튼 추가

Up 버튼과 Back 버튼:

  • Up 버튼은 화면 간의 계층적 관계에 따라 앱 안에서 이동 시킨다. Up 버튼은 절대 사용자가 앱에서 나가게 하지 않는다.
  • Back 버튼은 사용자가 최근에 띄운(백스택) 화면들을 통해 뒤로 이동 시킨다.

developer.android.com/guide/navigation/navigation-design-graph

 

탐색 그래프 디자인  |  Android 개발자  |  Android Developers

탐색 구성요소는 탐색 그래프를 사용하여 앱의 탐색을 관리합니다. 탐색 그래프는 사용자가 한 대상에서 다른 대상으로 이동하기 위해 실행할 수 있는 작업 또는 논리적 연결과 함께 앱의 모든

developer.android.com

 

Up 버튼에 support 추가

내비게이션 컴포넌트는 NavigationUI 클래스를 포함한다. 이 클래스는 상단 앱바로 내비게이션을 관리하는 정적 메서드인 내비게이션 드로어와 하단 내비게이션을 가진다. 내비게이션 컨트롤러는 앱바가 Up 버튼의 동작을 구현하도록 일체화하므로 직접 구현할 필요가 없다.

 

NavigationUI  |  Android 개발자  |  Android Developers

NavigationUI public final class NavigationUI extends Object java.lang.Object    ↳ androidx.navigation.ui.NavigationUI Class which hooks up elements typically in the 'chrome' of your application such as global navigation patterns like a navigation draw

developer.android.com

MainActivity.kt 코틀린 파일을 연다. onCreate() 메서드에 내비게이션 컨트롤러 객체를 찾기 위한 코드를 추가한다.

val navController = this.findNavController(R.id.myNavHostFragment)

마찬가지로 onCreate() 메서드에 내비게이션 컨트롤러를 앱바와 연결하기 위한 코드를 추가한다.

NavigationUI.setupActionBarWithNavController(this,navController)

내비게이션 컨트롤러에서 navigateUp()을 호출하기 위해 onCreate() 메서드 후에 onSupportNavigateUp() 메서드를 오버라이드한다.

override fun onSupportNavigateUp(): Boolean {
        val navController = this.findNavController(R.id.myNavHostFragment)
        return navController.navigateUp()
    }

이 코드가 없으면 앱바의 뒤로가기 버튼을 눌러도 동작하지 않는다.

앱을 실행해 보면 상단 왼쪽 구석에 "fragment_title"을 볼 수 있는데 navigation.xml에서 TitleFragment의 label을 다음과 같이 편집하고 실행해 본다.

<fragment
        android:id="@+id/titleFragment"
        android:name="com.example.android.navigation.TitleFragment"
        android:label="@string/app_name"
        ... >

 

옵션 메뉴 추가

내비게이션 그래프에 AboutFragment 추가

navigation.xml 파일을 열고 Design 탭을 클릭한다.

New Destination 버튼을 클릭하고 fragment_about을 선택한다.

레이아웃 에디터에서 about Fragment를 왼쪽으로 드래그한다. (Fragment의 ID가 올바른 지 확인할 것)

 

옵션메뉴 리소스 추가

Android Studio Project 페인에서 res 폴더를 우클릭하고 [New > Android Resource File]을 선택한다.

New Resource File 다이얼로그에서 다음과 같이 설정하고 OK를 클릭한다.

[res > menu] 폴더로부터 options_menu.xml 파일을 열고 Layout Editor가 보이도록 Design 탭을 클릭한다.

Palette 페인으로부터 Menu Item을 드래그하고 디자인 에디터 페인의 아무 곳에 둔다.

메뉴 아이템을 클릭하고 Attributes 페인에서 다음과 같이 설정한다.

Tip: 방금 추가한 메뉴 아이템의 ID를 내비게이션 그래프에 추가한 AboutFragment의 ID와 똑같이 하라. 이것은 onClick 핸들러를 위한 코드를 훨씬 더 간단하게 해준다.

 

onClick 핸들러 추가

TitleFragment.kt 코틀린 파일을 연다. onCreateView() 메서드 안에서 return 문 전에 setHasOptionsMenu() 메서드를 호출하고 true를 넘겨준다.

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                         savedInstanceState: Bundle?): View? {
   ...
   setHasOptionsMenu(true)
   return binding.root
}

onCreateView() 메서드 후에 onCreateOptionsMenu() 메서드를 오버라이드한다. 메서드에서 옵션 메뉴를 추가하고 메뉴 리소스 파일을 inflate한다.

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.options_menu, menu)
}

메뉴 아이템이 터치될 때 알맞은 동작을 하기 위해 onOptionsItemSelected() 메서드를 오버라이드한다. 이 경우에는 선택된 메뉴 아이템과 같은 id를 가진 Fragment로 이동시키는 동작이다.

override fun onOptionsItemSelected(item: MenuItem): Boolean {
     return NavigationUI.
            onNavDestinationSelected(item,requireView().findNavController())
            || super.onOptionsItemSelected(item)
}

 

내비게이션 드로어 추가

내비게이션 드로어는 Material Components for Android 라이브러리 혹은 Material 라이브러리에 포함된다. Google의 Material Design 가이드라인에 해당하는 패턴을 구현하기 위해 Material 라이브러리를 사용한다.

 

프로젝트에 Material 라이브러리 추가

build.gradle (Module)

dependencies {
    ...
    implementation "com.google.android.material:material:$version"
    ...
}
Note: 프로젝트의 [File > ProjectStructure > Variables] 설정에서 변수 $version을 최신 버전에 맞춰 생성하거나 업데이트해야 할 수 있다.
여기에서 최신 버전을 확인할 수 있다.

 

목적지 프래그먼트들이 ID를 갖고 있는지 확인

fragment_rules.xml 레이아웃 파일을 열고 Design 탭을 클릭해 디자인 에디터에서 미리보기로 어떻게 생겼는지 본다.

Navigation Editor에서 navigation.xml 파일을 연다. rules Fragment를 추가하고 ID를 rulesFragment로 설정한다.

 

드로어 메뉴와 드로어 레이아웃 생성

드로어를 위한 메뉴를 생성한다. Project 페인에서 res 폴더를 우클릭하고 New Resource File을 선택한다. 다음과 같이 설정하고 OK를 클릭한다.

navdrawer_menu.xml을 열고 Design 탭을 클릭한다. Palette 페인에서 Component Tree 페인으로 드래그해서 메뉴 아이템 두 개를 추가한다.

첫 번째와 두 번째 메뉴 아이템의 속성을 각각 다음과 같이 설정한다. 메뉴 아이템에서의 ID(navdrawer_menu.xml에서 설정한 값)는 Fragment에서의 ID(navigation.xml에서 설정한 값)와 같아야 한다.

Note: 메뉴 아이템과 목적지 Fragment에 대해서 같은 ID를 사용하면 onClick 리스너를 구현하는 데 아무 코드도 쓸 필요 가 없다!

activity_main.xml 레이아웃 파일을 연다. 모든 드로어 기능을 자유롭게 사용하려면 뷰들은 DrawerLayout 안에 두면 된다. <DrawerLayout>으로 <LinearLayout> 전체를 감싼다.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
   <androidx.drawerlayout.widget.DrawerLayout
       android:id="@+id/drawerLayout"
       android:layout_width="match_parent"
       android:layout_height="match_parent">

		<LinearLayout
		. . . 
		</LinearLayout>
   </androidx.drawerlayout.widget.DrawerLayout>
</layout>

이제 방금 정의한 navdrawer_menu를 사용하는 드로어인 NavigationView를 추가한다. DrawerLayout에서 </LinearLayout> 요소 후에 다음 코드를 추가한다.

<com.google.android.material.navigation.NavigationView
   android:id="@+id/navView"
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:layout_gravity="start"
   app:headerLayout="@layout/nav_header"
   app:menu="@menu/navdrawer_menu" />

 

내비게이션 드로어 디스플레이

MainActivity.kt 코틀린 파일을 연다. onCreate()에서 사용자가 내비게이션 드로어를 디스플레이할 수 있도록 하는 코드를 추가한다. setupWithNavController()를 호출하면 된다. onCreate()의 하단에 다음 코드를 추가한다.

NavigationUI.setupWithNavController(binding.navView, navController)

앱을 실행해 본다. 내비게이션 드로어를 디스플레이하기 위해 왼쪽 모서리에서 스와이프하고 드로어에서 메뉴 아이템이 오른쪽으로 나오는 것을 확인한다.

 

드로어 버튼을 통해 내비게이션 드로어 디스플레이

MainActivity.kt 코틀린 파일에서 드로어 레이아웃을 보여주기 위해 lateinit drawerLayout 멤버 변수를 추가한다.

private lateinit var drawerLayout: DrawerLayout

onCreate() 메서드 안에서 binding 변수가 초기화된 후 drawerLayout을 초기화한다.

val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,
                R.layout.activity_main)

drawerLayout = binding.drawerLayout

drawerLayout을 setupActionBarWithNavController() 메서드에 세 번째 매개변수로 추가한다.

NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)

onSupportNavigateUp() 메서드를 NavigationUI.navigateUp을 리턴하도록 수정한다. 내비게이션 컨트롤러와 드로어 레이아웃을 navigateUp()으로 전달한다.

override fun onSupportNavigateUp(): Boolean {
   val navController = this.findNavController(R.id.myNavHostFragment)
   return NavigationUI.navigateUp(navController, drawerLayout)
}

앱을 실행해 보자. 홈 화면으로 가서 내비게이션 드로어가 나타나도록 nav drawer 버튼을 누른다. Rules나 About 옵션을 클릭할 때 올바른 장소로 이동 시키는지 확인한다.

 

Homework

Build and run an app

github.com/Hyesung82/android-kotlin-fundamentals-starter-apps/tree/master/AndroidTrivia-Starter

 

Hyesung82/android-kotlin-fundamentals-starter-apps

android-kotlin-fundamentals-starter-apps. Contribute to Hyesung82/android-kotlin-fundamentals-starter-apps development by creating an account on GitHub.

github.com

 

Answer these questions

Q1. 4 / Q2. 3 / Q3. 1, 3 / Q4. 1, 2, 4 / Q5. 1 / Q6. 3 / Q7. 1, 3 / Q8. 4 / Q9. 1, 2 / Q10. 2 / Q11. 1, 2 / Q12. 1 / Q13. B / Q14. 2, 3