Lesson 6: App navigation

Hanbinยท2021๋…„ 8์›” 12์ผ
0

Teach Android Development

๋ชฉ๋ก ๋ณด๊ธฐ
6/13
post-thumbnail

๐Ÿ’ก Teach Android Development

๊ตฌ๊ธ€์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ต์œก์ž๋ฃŒ๋ฅผ ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํฌ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

Android Development Resources for Educators

Multiple activities and intents

์•ฑ ๊ธฐ๋Šฅ์ด ์—ฌ๋Ÿฌ ํ™”๋ฉด์œผ๋กœ ๋ถ„๋ฆฌ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋‹จ์ผ item์˜ ์ƒ์„ธ ํ™”๋ฉด (์˜ˆ: ์‡ผํ•‘์•ฑ์˜ ์ œํ’ˆ)
  • ์ƒˆ๋กœ์šด item ๋งŒ๋“ค๊ธฐ (์˜ˆ: ์ƒˆ๋กœ์šด ์ด๋ฉ”์ผ)
  • ์•ฑ์˜ ์„ค์ • ํ‘œ์‹œ
  • ๋‹ค๋ฅธ ์•ฑ์˜ ์„œ๋น„์Šค ์•ก์„ธ์Šค (์˜ˆ: ์‚ฌ์ง„ ๊ฐค๋Ÿฌ๋ฆฌ, ๋ฌธ์„œ ํƒ์ƒ‰)

Intent

๋‹ค๋ฅธ Activity์™€ ๊ฐ™์ด ๋‹ค๋ฅธ ์•ฑ component์—์„œ ์ž‘์—…์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.

  • Intent์—๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋‘ ๊ฐ€์ง€ ๊ธฐ๋ณธ ์ •๋ณด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์ˆ˜ํ–‰ํ•  ์ž‘์—… (์˜ˆ: ACTION_VIEW, ACTION_EDIT, ACTION_MAIN)
    • ์ž‘์—…ํ•  ๋ฐ์ดํ„ฐ (์˜ˆ: ์—ฐ๋ฝ์ฒ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๊ฐœ์ธ ๊ธฐ๋ก)
  • ์ผ๋ฐ˜์ ์œผ๋กœ ๋‹ค๋ฅธ Activity๋กœ์˜ ์ „ํ™˜ ์š”์ณฅ์„ ์ง€์ •ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

Explicit intent

  • ํŠน์ • component๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญ์„ ์ดํ–‰
  • ์•ฑ์˜ Activity์— ๋‚ด๋ถ€์ ์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  • ํŠน์ • third-party app์ด๋‚˜ ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ๋‹ค๋ฅธ ์•ฑ์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

Explicit intent examples

// ์•ฑ ๋‚ด์˜ `Activity`๋กœ์˜ ์ด๋™
fun viewNoteDetail() {
   val intent = Intent(this, NoteDetailActivity::class.java)
   intent.putExtra(NOTE_ID, note.id)
   startActivity(intent)
}


// ํŠน์ • ์™ธ๋ถ€ ์•ฑ์œผ๋กœ ์ด๋™
fun openExternalApp() {
   val intent = Intent("com.example.workapp.FILE_OPEN")
   if (intent.resolveActivity(packageManager) != null) {
       startActivity(intent)
   }
}

Implicit intent

  • ์•ฑ์ด ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ๋ฐ˜์ ์ธ ํ–‰๋™์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ์•Œ๋ ค์ง„ component์— ๋Œ€ํ•œ data type ๋ฐ action ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.
  • ๊ธฐ์ค€๊ณผ ์ผ์น˜ํ•˜๋Š” ๋ชจ๋“  ์•ฑ์ด ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

Implicit intent example

// ํ•ด๋‹น Intent์˜ ๊ธฐ์ค€๊ณผ ์ผ์น˜ํ•˜๋Š” ๋ชจ๋“  ์•ฑ์ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
fun sendEmail() {
    val intent = Intent(Intent.ACTION_SEND)
    intent.type = "text/plain"
    intent.putExtra(Intent.EXTRA_EMAIL, emailAddresses)
    intent.putExtra(Intent.EXTRA_TEXT, "How are you?")
    
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }
}

App bar, navigation drawer, and menus

App bar

menu items๋Š” XML menu resource์— ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. (res/menu ํด๋” ์•ˆ์— ์œ„์น˜)

<menu xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
   <item
       android:id="@+id/action_settings"
       android:orderInCategory="100"
       android:title="@string/action_settings"
       app:showAsAction="never" />
</menu>

More menu options

<menu>
   <group android:checkableBehavior="single">
       <item
           android:id="@+id/nav_home"
           android:icon="@drawable/ic_menu_camera"
           android:title="@string/menu_home" />
       <item
           android:id="@+id/nav_gallery"
           android:icon="@drawable/ic_menu_gallery"
           android:title="@string/menu_gallery" />
       <item
           android:id="@+id/nav_slideshow"
           android:icon="@drawable/ic_menu_slideshow"
           android:title="@string/menu_slideshow" />
   </group>
</menu>

Options menu example

<menu xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">

   <item android:id="@+id/action_intent"
       android:title="@string/action_intent" />

   <item
       android:id="@+id/action_settings"
       android:orderInCategory="100"
       android:title="@string/action_settings"
       app:showAsAction="never" />
</menu>

orderInCategory: ๊ทธ๋ฃน ๋‚ด ํ•ญ๋ชฉ์˜ ์ค‘์š”๋„ ์ˆœ์„œ
showAsAction = never: ์ด ํ•ญ๋ชฉ์„ App bar์— ๋ฐฐ์น˜ํ•˜์ง€ ์•Š๊ณ  ๋”๋ณด๊ธฐ ๋ฉ”๋‰ด ํ•ญ๋ชฉ์— ๋‚˜์—ดํ•ฉ๋‹ˆ๋‹ค.
์ž์„ธํ•œ ๋‚ด์šฉ

Inflate options menu

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.main, menu)
    return true
}

Handle menu options selected

override fun onOptionsItemSelected(item: MenuItem): Boolean {

    when (item.itemId) {
        R.id.action_intent -> {
            val intent = Intent(Intent.ACTION_WEB_SEARCH)
            intent.putExtra(SearchManager.QUERY, "pizza")
            if (intent.resolveActivity(packageManager) != null) {
                startActivity(intent)
            }
        }
        else -> Toast.makeText(this, item.title, Toast.LENGTH_LONG).show()
    ...

Fragments

  • Activity์—์„œ UI์˜ ์ผ๋ถ€๋ถ„ ๋˜๋Š” ๋™์ž‘์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
  • Activity๊ฐ€ ํ˜ธ์ŠคํŒ…๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒ๋ช…์ฃผ๊ธฐ๋Š” ํ˜ธ์ŠคํŠธ Activity์˜ ์ˆ˜๋ช…์ฃผ๊ธฐ์™€ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.
  • ๋Ÿฐํƒ€์ž„์— ์ถ”๊ฐ€ ๋˜๋Š” ์ œ๊ฑฐ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Note about fragments

AndroidX ๋ฒ„์ „์˜ Fragment ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
(androidx.fragment.app.Fragment)
์ง€์› ์ค‘๋‹จ๋œ ํ”Œ๋žซํผ ๋ฒ„์ „์˜ Fragment ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
(android.app.Fragment)

  • ์•ฑ์„ ํ†ตํ•ด navigation ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ†ตํ•ฉ ํŽธ์ง‘๊ธฐ๋ฅผ ํฌํ•จํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„๊ตฌ ๋ชจ์Œ์ž…๋‹ˆ๋‹ค.
  • ๊ทธ๋ž˜ํ”„๋‹น ์—ฌ๋Ÿฌ Fragment ๋Œ€์ƒ์ด ์žˆ๊ณ , ํ•˜๋‚˜์˜ Activity๋กœ ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ์„ธ๊ฐ€์ง€ ์ฃผ์š” ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
    • Navigation graph
    • Navigation Host (NavHost)
    • Navigation Controller (NavController)

Add dependencies

build.gradle์˜ dependencies ๋ถ€๋ถ„์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Activity์˜ XML์— ์„ ์–ธํ•ฉ๋‹ˆ๋‹ค.

<fragment
    android:id="@+id/nav_host"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph_name"/>

res/navigation ๋””๋ ‰ํ† ๋ฆฌ์— ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค.

  • navigation ๋Œ€์ƒ๊ณผ action์ด ํฌํ•จ๋œ XML ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.
  • ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€์ƒ(Fragment/Activity)์„ ๋‚˜์—ดํ•ฉ๋‹ˆ๋‹ค.
  • ๋Œ€์ƒ ์‚ฌ์ด๋ฅผ ์ˆœํšŒํ•  ๊ด€๋ จ action์„ ๋‚˜์—ดํ•ฉ๋‹ˆ๋‹ค.
  • enter, exit ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋‚˜์—ดํ•ฉ๋‹ˆ๋‹ค. (optionally)

Creating a Fragment

  • Fragment ํด๋ž˜์Šค๋ฅผ extend ํ•ฉ๋‹ˆ๋‹ค.
  • onCreateView()๋ฅผ override ํ•ฉ๋‹ˆ๋‹ค.
  • XML์— ์ •์˜ํ•œ Fragment์˜ ๋ ˆ์ด์•„์›ƒ์„ inflate ํ•ฉ๋‹ˆ๋‹ค.
class DetailFragment : Fragment() {

   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
           savedInstanceState: Bundle?): View? {
       return inflater.inflate(R.layout.detail_fragment, container, false)
   }
}

Specifying Fragment destinations

  • ํ”„๋ž˜๊ทธ๋จผํŠธ ๋Œ€์ƒ์€ navtgation graph์•ˆ์— action ํ…Œ๊ทธ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
  • Action์€ XML์— ์ง์ ‘ ์ •์˜ํ•˜๊ฑฐ๋‚˜, Navigation Editor๋ฅผ ํ†ตํ•ด ์†Œ์Šค์—์„œ ๋ชฉ์ ์ง€๊นŒ์ง€ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ž๋™ ์ƒ์„ฑ๋˜๋Š” action ID๋Š” action_<sourceFragment>_to_<destinationFragment> ํ˜•์‹์„ ์ทจํ•ฉ๋‹ˆ๋‹ค.

Example fragment destination

<fragment
    android:id="@+id/welcomeFragment"
    android:name="com.example.android.navigation.WelcomeFragment"
    android:label="fragment_welcome"
    tools:layout="@layout/fragment_welcome" >

    <action
        android:id="@+id/action_welcomeFragment_to_detailFragment"
        app:destination="@id/detailFragment" />

</fragment>

NavController๋Š” navigation host์—์„œ UI ํƒ์ƒ‰์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • ๋Œ€์ƒ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜๋ฉด action์˜ ์ด๋ฆ„์€ ์ •ํ•ด์ง€์ง€๋งŒ ์‹คํ–‰๋˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.
  • ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ฅด๋ ค๋ฉด NavController๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Example NavController

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       ...
       val navController = findNavController(R.id.myNavHostFragment)
   }
   fun navigateToDetail() {
       navController.navigate(R.id.action_welcomeFragment_to_detailFragment)
   }
}

More custom navigation behavior

Passing data between destinations

Safe Args ์‚ฌ์šฉ

  • argument๊ฐ€ ์œ ํšจํ•œ type์„ ๊ฐ€์ง€๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋Œ€์ƒ์˜ ๋ชจ๋“  action์— ๋Œ€ํ•œ ๋ฉ”์†Œ๋“œ๊ฐ€ ํฌํ•จ๋œ <SourceDestination>Directions class๊ฐ€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์ •ํ•ด์ง„ ๋ชจ๋“  action์— ๋Œ€ํ•œ argument๋ฅผ ์„ค์ •ํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ๋Œ€์ƒ์˜ argument์— ๋Œ€ํ•œ ์—‘์„ธ์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” <TargetDestination>Args ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

Setting up Safe Args

ํ”„๋กœ์ ํŠธ ์ˆ˜์ค€์˜ build.grdle

buildscript {
   repositories {
       google()
   }
   dependencies {
       classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
   }
}

์•ฑ ์ˆ˜์ค€์˜ build.gradle

apply plugin: "androidx.navigation.safeargs.kotlin"

Sending data to a Fragment

  • ๋Œ€์ƒ fragment์ด ์˜ˆ์ƒํ•˜๋Š” argument๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • source ๋Œ€์ƒ์—์„œ ์—ฐ๊ฒฐํ•˜๋Š” action์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • <Source>FragmentDirections์˜ action ๋ฉ”์„œ๋“œ์— argument๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • Navigation Controller๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด๋‹น action์„ ๋”ฐ๋ผ ํƒ์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
  • ๋Œ€์ƒ fragment์—์„œ argument๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.

Destination arguments

nav_graph.xml์—์„œ argument๋กœ ๋ฐ›์„ data๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

<fragment
    android:id="@+id/multiplyFragment"
    android:name="com.example.arithmetic.MultiplyFragment"
    android:label="MultiplyFragment" >
    <argument
        android:name="number1"
        app:argType="float"
        android:defaultValue="1.0" />
    <argument
        android:name="number2"
        app:argType="float"
        android:defaultValue="1.0" />
 </fragment>

Create action from source to destination

nav_graph.xml์—์„œ source ๋Œ€์ƒ์—์„œ ๋‹ค๋ฅธ ๋Œ€์ƒ์œผ๋กœ ๋ณด๋‚ผ action์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

<fragment
    android:id="@+id/fragment_input"
    android:name="com.example.arithmetic.InputFragment">

    <action
        android:id="@+id/action_to_multiplyFragment"
        app:destination="@id/multiplyFragment" />

</fragment>

InputFragment.kt์—์„œ ์ง€์ •ํ•œ action์„ button ํด๋ฆญ ์‹œ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.button.setOnClickListener {
       val n1 = binding.number1.text.toString().toFloatOrNull() ?: 0.0
       val n2 = binding.number2.text.toString().toFloatOrNull() ?: 0.0

       val action = InputFragmentDirections.actionToMultiplyFragment(n1, n2)
       view.findNavController().navigate(action)
   }
}

Retrieving Fragment arguments

์ง€์ •ํ•œ argument๋ฅผ notnullํ•˜๊ฒŒ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

class MultiplyFragment : Fragment() {
   val args: MultiplyFragmentArgs by navArgs()
   lateinit var binding: FragmentMultiplyBinding
   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
       val number1 = args.number1
       val number2 = args.number2
       val result = number1 * number2
       binding.output.text = "${number1} * ${number2} = ${result}"
   }
}

Attach destinations to menu items

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    val navController = findNavController(R.id.nav_host_fragment)
    return item.onNavDestinationSelected(navController) ||
            super.onOptionsItemSelected(item)
}

DrawerLayout for navigation drawer

<androidx.drawerlayout.widget.DrawerLayout    
    android:id="@+id/drawer_layout" ...>

    <fragment
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:id="@+id/nav_host_fragment" ... />

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        app:menu="@menu/activity_main_drawer" ... />

</androidx.drawerlayout.widget.DrawerLayout>

Finish setting up navigation drawer

DrawerLayout์— navigation graph์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

val appBarConfiguration = AppBarConfig(navController.graph, drawer)

NavController๋ฅผ ์‚ฌ์šฉํ•ด NavigationView๋ฅผ ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค.

val navView = findViewById<NavigationView>(R.id.nav_view)
navView.setupWithNavController(navController)

menu์— navtigation ์—ฐ๊ฒฐ ์ฐธ๊ณ  ๋งํฌ

Understanding the back stack

Fragments and the back stack

0๊ฐœ์˜ ๋Œ“๊ธ€