Android Kotlin Fundamentals Course 코드랩 하면서 노트. Android Kotlin Fundamentals: 03.3 Start an external Activity 부터.

Useful Shortcuts

Safe Args

Safe Args generates NavDirection classes to prevent bugs during passing parameters between Fragments.

To pass the parameters between Fragments, Bundle is one of the way to handle the data (key-value store). It does not guarantee type-safety such as type mismatch error, missing key errors.

Dependency

// project Gradle file
dependencies {
  //...
  classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
// app Gradle file
apply plugin: 'androidx.navigation.safeargs'

Then, sync and rebuild the project. The plugin will generate NavDirection class, and it's in the generatedJava folder.

Use typed action ID

Use the type-safe name rather than R.id.<ID> action ID.

view.findNavController()
        .navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment())

Add and pass arguments

Open navigation.xml, select the fragment, then add argument from the Attributes tab.

Then, the build will fail because of the new arguments. Update the exisiting code with the parameters. The class will be <name>FragmentDirections.

view.findNavController()
    .navigate(GameFragmentDirections
      .actionGameFragmentToGameWonFragment(numQuestions, questionIndex))

In the next Fragment, it is available to access through <name>FragmentArgs.

val args = GameWonFragmentArgs.fromBundle(requireArguments())
Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()

Implicit intents

Android allows you tu use intents to navigate to activities that other apps provide. e.g. share the game result to share menu. Intent is a simple message object taht is used to communicate between Android components. Implicit intent doesn't require which app or activity will handle the task.

// in onCreateView
setHasOptionsMenu(true)

// Creating our Share Intent
private fun getShareIntent() : Intent {
  val args = GameWonFragmentArgs.fromBundle(requireArguments())
  val shareIntent = Intent(Intent.ACTION_SEND)
  shareIntent.setType("text/plain")
    .putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
  return shareIntent
}

Intent action would be Intent.ACTION_SEND, etc. Check the example.

// Starting an Activity with our new Intent
private fun shareSuccess() {
  startActivity(getShareIntent())
}

// Showing the Share Menu Item Dynamically
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  super.onCreateOptionsMenu(menu, inflater)

  // Note: winner_menu.xml is already prepped in the example
  inflater.inflate(R.menu.winner_menu, menu)
  if(getShareIntent().resolveActivity(requireActivity().packageManager) == null){
    menu.findItem(R.id.share).isVisible = false
  }
}

Pass resolveActivity through packageManager so that find out which app is eligible to handle this intent. If there is no app related with this, the menu will be hidden.

// Sharing from the Menu
override fun onOptionsItemSelected(item: MenuItem): Boolean {
  when(item.itemId){
    R.id.share -> shareSuccess()
  }
  return super.onOptionsItemSelected(item)
}

Lifecycle

Check Understand the Activity Lifecycle.

Check the use cases.

The general pattern is that when you set up or start something in a callback, you stop or remove that thing in the corresponding callback. This way, you avoid having anything running when it's no longer needed.

Activity Lifecycle

         [Resumed]      }  }-- activity has focus
          |     |       }  }
    onResume   onPause  }  
          |     |       }
         [Started]      }-- activity is visible
          |     |
    onStart    onStop
    onRestart   |
          |     |
         [Created]
          |     |
    onCreate   onDestroy
          |     |
[Initialized] [Destroyed]

Fragment Lifecycle

         [Resumed]      }  }-- fragment has focus
          |     |       }  }
    onResume   onPause  }
          |     |       }
         [Started]      }-- fragment is visible
          |     |
      onStart  onStop
onViewCreated  onDestoryView
 onCreateView   |
          |     |
         [Created]
          |     |
    onCreate   onDestroy
    onAttach   onDetach
          |     |
[Initialized] [Destroyed]

Check lifecycle using logging

Add log on onCreate() method in the MainActivity.

Log.i("MainActivity", "onCreate Called")
// i: info
// e: error
// w: warning

"MainActivity" tag helps to find the log in the Logcat.

Open Logcat pane in Android Studio and search I/MainActivity.

Create lifecycle methods

Use Override Methods menu, add onStart(). (CTRL+o)

override fun onStart() {
  super.onStart()

  Log.i("MainActivity", "onStart Called")
}

Check the log and test in vary (Press the Home button then back to the screen.)

Use Timber for logging

Add dependencies

Check latest version from the Timber project page.

// build.gradle (app level)
dependencies {
  // ...
  implementation 'com.jakewharton.timber:timber:4.7.1'
}

Then, sync it.

Initialize Timber

Create Application class for the logging library. Note: Do not put any activity code here unless the code is really needed.

package com.example.android.dessertclicker

import android.app.Application
import timber.log.Timber

class ClickerApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    Timber.plant(Timber.DebugTree())
  }
}

Open AndroidManifest.xml and add Application name there to connect custom application class.

<application
  android:name=".ClickerApplication"
  ... />

Add Timber log in the code.

Timber.i("onCreate called")

Lifecycle library

// change the class signature
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {

// add init block
init {
  lifecycle.addObserver(this)
}

// Add annotation on `startTimer()` and `stopTimer()`
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {
  // ...
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer() {
  // ...
}

Then, initialize the class with the lifecycle in activity.

dessertTimer = DessertTimer(this.lifecycle)

onSaveInstanceState()

Some app can be killed by Android during the background and respring it. Therefore the status need to be stored some places. It also needed to handle configuration change such as orientation changes of the device.

The bundle stores in RAM so keep the data small here(<100k) otherwise crash with TransactionTooLargeException.

Kill the app using adb.

adb shell am kill com.example.android.dessertclicker
                    [Resumed]
                     |     |
               onResume   onPause
                     |     |
                    [Started]
                     |     |
  onRestoreInstanceState  onStop
                 onStart  onSaveInstanceState
               onRestart   |
                     |     |
                    [Created]
                     |     |
               onCreate   onDestroy
                     |     |
           [Initialized] [Destroyed]
override fun onSaveInstanceState(outState: Bundle) {
  super.onSaveInstanceState(outState)

  Timber.i("onSaveInstanceState Called")
}

Add constants before the class definition.

const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

Save the information into the bundle.

outState.putInt(KEY_REVENUE, revenue)
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

Restore the data from the onCreate().

Note: If the activity is being re-created, the onRestoreInstanceState() callback is called after onStart(), also with the bundle. Most of the time, you restore the activity state in onCreate(). But because onRestoreInstanceState() is called after onStart(), if you ever need to restore some state after onCreate() is called, you can use onRestoreInstanceState().

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  // ...
  if (savedInstanceState != null) {
    revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
    dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
    dessertTimer.secondsCount = savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)

    // call anything if needed to update the initial states
    showCurrentDessert()
  }
}

다음 챕터: Android Kotlin Fundamentals: ViewModel

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.