Android Kotlin Fundamentals Course 코드랩 하면서 노트. Android Kotlin Fundamentals: 03.3 Start an external Activity 부터.
Useful Shortcuts
- Override Methods: Choose Code > Override Methods, or
CTRL+o
- Comment: Choose Code > Comment with Line Comment or
CTRL+/
(Command+/
on a Mac)
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]
onAttach()
: Called when the fragment is associated with its owner activity.onCreate()
: Similarly toonCreate()
for the activity,onCreate()
for the fragment is called to do initial fragment creation (other than layout).onCreateView()
: Called to inflate the fragment's layout.onViewCreated()
: Called immediately afteronCreateView()
has returned, but before any saved state has been restored into the view.onStart()
: Called when the fragment becomes visible; parallel to the activity'sonStart()
.onResume()
: Called when the fragment gains the user focus; parallel to the activity'sonResume()
.
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
- Generates log tag by class name
- Show the log only in dev
- integration with crash reporting libs
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
- Lifecycle owners:
Activity
andFragment
. Implement theLifecycleOwner
interface Lifecycle
class: holds actual state of a lifecycle owner and triggers events- Lifecycle observers: observe the lifecycle state and perform tasks. Implement the
LifecycleObserver
interface
// 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]
onSaveInstanceState()
: Save any data that need if the app destoryed.onRestoreInstanceState()
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()
}
}