Android Kotlin Fundamentals Course 코드랩 하면서 노트.
Useful Shortcuts
- Project quick fix:
Alt+Enter
(Option+Enter
on a Mac)
- Reformat code: Choose Code > Reformat code, or
CTRL+ALT+L
(Command+Option+L
on a Mac)
Vector drawables
A part of Android Jetpack.
As default, android app will contain bitmap version of the drawables in lower than API 21. Vector drawables support in oldver vrsions of Android, back to API 7.
Add this line at defaultConfig
in build.gradle (Module:app)
.
vectorDrawables.useSupportLibrary = true
Click Sync Now button.
Open activity_main.xml
layout file and add this namespace at <LinearLayout>
tag.
xmlns:app="http://schemas.android.com/apk/res-auto"
Change android:src
in <ImageView>
to app:srcCompat
.
app:srcCompat="@drawable/hello_world"
Note: app
namespace is for attributes that come from custom code or libraries.
Use findViewById
once
Define member with lateint
keyword.
lateinit var diceImage : ImageView
Init the member in onCreate()
.
diceImage = findViewById(R.id.dice_image)
API Levels
compileSdkVersion
targetSdkVersion
: Usually same as compileSdkVersion
minSdkVersion
Architecture of the basic activity template
- Status bar: Hide the status bar
- App bar (action bar): Add the app bar
- App name
- Options menu overflow button:
onOptionsItemSelected()
in MainActivity.kt
, check res/menu/menu_main.xml
- CoordinatorLayout ViewGroup:
content_main
in activity_main.xml
- Content main
- Floating action button (FAB):
FloatingActionButton
in activity_main
Resources
Docs
Android team
- Right-click the component from the component tree and select Refactor > Extract Style.
- Fill in the form and save it.
- Use named style in activity xml file via
style
attribute
style="@style/NameStyle"
The style is defined in styles.xml
in res
.
Changing android:id
When android:id
of the component need to be changed, right-click and select Refector > Rename.
Density-independent pixels
Support different pixel densities
Unit conversion: px = dp * (dpi / 160)
Threshold must be needed for dp
conversion to pixel. Scale factor is in DisplayMetrics.density
.
Autofill
Optimize your app for autofill
Show/Hide the keyboard
// Hide
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
// Set focus
editText.requestFocus()
// Show
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, 0)
Attach onClick events
- in the XML, add
android:onClick
attr to the <View>
element
- in the code, use
setOnClickListener(View.OnClickListener)
function in the Activity
Constraints
In the activity page, a magnet icon is for Autoconnect feature.
Use Constraint Widget in Attributes section of the activity design page.
the type of the constraint
- Wrap Content: as much as the element
- Fixed: Specify a dimension
- Match Constraints: Expands until meet the constraints on each side
Chains
A chain is a group of views that are linked to each other with bidirectional constraints.
Chain styles
set the layout_constraintHorizontal_chainStyle
or the layout_constraintVertical_chainStyle
.
- Spread
- Spread Inside
- Packed
- Weighted:
layout_constraintHorizontal_weight
or layout_constraintVertical_weight
Baseline constraint
The baseline constraint aligns the baseline of a view's text with the baseline of another view's text. Right click on the component and select show baseline.
<Button
android:id="@+id/buttonB"
...
android:text="B"
app:layout_constraintBaseline_toBaselineOf="@+id/buttonA" />
Design-Time attributes
Only for the layout design. Design-time attributes are prefixed with the tools
namespace. e.g. tools:layout_editor_absoluteY
, tools:text
Data binding
Eliminate findViewById()
using binding objects. Benefits:
- Code is shorter and easier to read and maintain
- Resource efficiency
- Type safety
Setup
// build.gradle (Module: app)
android {
// ...
buildFeatures {
dataBinding true
}
// ...
}
// Then sync
Add <layout>
at the outermost tag around the layout xml file. Then, move all xml namespaces to the <layout>
. e.g.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
Usage
Add the type of binding in the activity class. For activity_main
, the class will be ActivityMainBinding
.
private lateinit var binding: ActivityMainBinding
It need to import something like import <your.project>.databinding.ActivityMainBinding
.
Then, use DataBindingUtil.setContentView()
and remove the previous setContentView()
.
// import androidx.databinding.DataBindingUtil
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
Now, the binding object holds all views in the layout.
binding.doneButton.setOnClickListener {}
binding.apply { // kotlinize the function
nicknameText.text = nicknameEdit.text.toString()
nicknameEdit.visibility = View.GONE
}
Data binding to display data
Create the data class in the package.
// MyName.kt
data class MyName(var name: String = "", var nickname: String = "")
Add data to the layout. Open activity xml file and add <data>
right under the <layout>
. Then, add <variable>
tag with name and package path. (package name + variable name)
<layout ...>
<data>
<variable
name="myName"
type="com.example.android.aboutme.MyName" />
</data>
<!-- ... -->
</layout>
@={}
is a directive to get the data that is referenced inside the curly braces. Replace the string like below:
<TextView android:text="@string/name" />
<TextView android:text="@={myName.name}" />
Create the data in the activity file.
// MainActivity.kt
private val myName: MyName = MyName("Edward Kim")
Then, binding the object with the view using binding
.
// in onCreate()
binding.myName = myName
Manipulate the binding object so that the updated data will reflect to the view.
android:text="@{myName.nickname}"
binding.apply {
myName?.nickname = nicknameEdit.text.toString()
invalidateAll() // UI is refreshed with the new value
// ...
}
Read more
Fragments
A Fragment represents a behavior or a protion of user interface in an activity. Modular section of an activity like a "sub-activity".
- A Fragment has its own lifecycle and receives its own input events
- You can add or remove a Fragment while the activity is running
- A Fragment is defined in a Kotlin class
- A Fragment's UI is defined in an XML layout file
"Inflate the Fragment's view" is equivalant to using setContentView()
for an Activity.
class TitleFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// create a binding object and inflate the Fragment's view
val binding = DataBindingUtil.inflate<FragmentTitleBinding>(inflater,
R.layout.fragment_title, container, false)
return binding.root
}
}
Add the new fragment to the main layout file.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/titleFragment"
android:name="com.example.android.navigation.TitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
</layout>
Navigation
Preparation
Add dependencies for the navigation library.
// build.gradle (project-level)
ext {
// ...
navigationVersion = "2.3.0"
// ...
}
// build.gradle (module-level)
dependencies {
// ...
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
// ...
}
Sync and rebuild.
Add a navigation graph to the project. right-click the res folder and select New > Android Resource File.
Set navigation
as a file name and Navigation
as a resource type, then okay.
Open res/navigation/navigation.xml
with the navigation editor.
Create the NavHostFragment
NavHostFragment
acts as a host for the fragments in a navigation graph.
Open layout xml file and add NavHostFragment
like below.
<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" />
Add fragments to the navigation graph
Open navigation.xml
and add new destinations. If the preview is not showing, check tools:layout
.
Then drag the dot (circular connection point) on the preview and drop to the destination. Check the ID of the action. (e.g. action_titleFragment_to_gameFragment
)
Add the button click event in the Fragment inside the onCreateView()
method.
binding.playButton.setOnClickListener { view: View ->
view.findNavController().navigate(R.id.action_titleFragment_to_gameFragment)
}
Conditional navigation
Add two fragments to the navigation graph.
Drag the circular connection point to the conditional fragments.
Then, add the code like:
// at onCreateView() in GameFragment.kt
// find the appropriate position...
view.findNavController().navigate(R.id.action_gameFragment_to_gameWonFragment)
Control the back stack by setting the "pop" behavior in actions on the navigation editor.
popUpTo
: back stack to a given destination before navigating.
popUpToInclusive
false
or not set: leaves the specified dest. in the back stack.
true
: remove all tracing stacks included specified dest.
popUpToInclusive=true
and popUpTo
at app's starting location: all the way out of the app.
(= action bar)
Designing Back and Up navigation
- The Up button navigates within the app, based on the hierarchical relationships between screens. The Up button never navigates the user out of the app.
- The Back button, shown as 2 in the screenshot below, appears in the system navigation bar or as a mechanical button on the device itself, no matter what app is open.
Back button is for the back stack, up button is for the screen hierachy.
Use NavigationUI
.
// onCreate() in MainActivity.kt
val navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this, navController)
// in MainActivity.kt
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return navController.navigateUp()
}
Add new destination to the navigation graph.
Add a options-menu to the project. right-click the res folder and select New > Android Resource File.
Set options_menu
as a file name and Menu
as a resource type, then okay.
Open options_menu.xml
.
Add Menu Item from Plaette to the component tree. Set the id of item to the same name as the target fragment e.g. aboutFragment
.
Add onClick handler.
// TitleFragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// ...
setHasOptionsMenu(true) // Add this line
return binding.root
}
// Add the options menu and inflate the menu resource file
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.options_menu, menu)
}
// Add onOptionsItemSelected() for the navigation
// It uses the id of the selected item
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return NavigationUI.
onNavDestinationSelected(item,requireView().findNavController())
|| super.onOptionsItemSelected(item)
}
Add the navigation drawer
Drawer appears when the user swipes edge to edge or tap the nav drawer button/hamburger icon. It is a part of the material components for Android.
// build.gradle (app-level)
dependencies {
// ...
implementation "com.google.android.material:material:$supportlibVersion"
// ...
}
// then sync!
Add a drawer to the project. right-click the res folder and select New > Android Resource File.
Set navdrawer_menu
as a file name and Menu
as a resource type, then okay.
Open navdarwer_menu
and add menu items. Note: Match the ID
for the menu item as for the destination Fragment.
Add <DrawerLayout>
and wrap the entire layout in activity xml file. Also, add NavigationView
before closing it.
<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>
<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" />
</androidx.drawerlayout.widget.DrawerLayout>
</layout>
For swaping, connect the drawer to the navigation controller:
// onCreate() in MainActivity.kt
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this, R.layout.activity_main)
NavigationUI.setupWithNavController(binding.navView, navController)
For drawer button, add the code bleow:
// in MainActivity.kt
private lateinit var drawerLayout: DrawerLayout
// onCreate()
drawerLayout = binding.drawerLayout
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
// add it as a third param
To make the Up button work with the drawer button:
// onSupportnavigateUp() in MainActivity.kt
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return NavigationUI.navigateUp(navController, drawerLayout)
}
다음 챕터: Android Kotlin Fundamentals: 03.3 Start an external Activity