- Create a class hierarchy, that is a tree of classes where children inherit functionality from parent classes. Properties and functions are inherited by subclasses.
- Class hierarchy. An arrangement where classes are organized in a hierarchy of parents and children. Hierarchy diagrams are usually drawn with the parents shown above children.
- Child or subclass. Any class that is below another class in the hierarchy.
- Parent or superclass or base class. Any class with one or more child classes.
- Root or top-level class. The class at the top (or root) of the class hierarchy.
- Inheritance. When a child class includes (or inherits) all the properties and methods of its parent class. This allows you to share and reuse code, which makes programs easier to understand and maintain.
- Create an
abstract
class where some functionality is left to be implemented by its subclasses.
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
- An
abstract
class can therefore not be instantiated. - Create subclasses of an
abstract
class. - Use
override
keyword to override properties and functions in subclasses.
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
- Use the
super
keyword to reference functions and properties in the parent class.
override fun floorArea(): Double {
return super.floorArea() * floors
}
- Make a class
open
so that it can be subclassed.
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- Make a property
private
, so it can only be used inside the class. - Use the
with
construct to make multiple calls on the same object instance.
with(roundHut) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
- Import functionality from the
kotlin.math
library
import kotlin.math.PI
import kotlin.math.sqrt
fun floorArea(): Double {
return PI * radius * radius
}
fun calculateMaxCarpetSize(): Double {
val diameter = 2 * radius
return sqrt(diameter * diameter / 2)
}
- XML (Extensible Markup Language) is a way of organizing text, made of tags, elements, and attributes.
- Use XML to define the layout of an Android app.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Use
EditText
to let the user input or edit text.
<EditText
android:id="@+id/plain_text_input"
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:inputType="text"/>
- An
EditText
can have a hint to tell the user what is expected in that field.
android:hint="Cost of Service"
- Specify the
android:inputType
attribute to limit what type of text the user can input into anEditText
field.
android:inputType="numberDecimal"
- Make a list of exclusive options with
RadioButtons
, grouped with aRadioGroup
.
<RadioGroup
android:id="@+id/tip_options"
android:checkedButton="@id/option_twenty_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/service_question"
app:layout_constraintStart_toStartOf="parent"
android:orientation="vertical">
<RadioButton
android:id="@+id/option_twenty_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Amazing (20%)" />
<RadioButton
android:id="@+id/option_eighteen_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Good (18%)" />
</RadioGroup>
- A
RadioGroup
can be vertical or horizontal, and you can specify whichRadioButton
should be selected initially.
android:orientation="vertical"
android:checkedButton="@id/option_twenty_percent"
- Use a
Switch
to let the user toggle between two options.
<Switch
android:id="@+id/round_up_switch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="true"
android:text="Round up tip?"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tip_options" />
- You can add a label to a
Switch
without using a separateTextView
.
android:text="Round up tip?"
- Each child of a
ConstraintLayout
needs to have vertical and horizontal constraints. - Use "start" and "end" constraints to handle both Left to Right (LTR) and Right to Left (RTL) languages.
- Names of the constraint attributes follow the form
layout_constraint<Source>_to<Target>Of
.layout_constraintLeft_toLeftOf
layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf
layout_constraintRight_toRightOf
layout_constraintTop_toTopOf
layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf
layout_constraintBottom_toBottomOf
layout_constraintBaseline_toBaselineOf
layout_constraintStart_toEndOf
layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf
layout_constraintEnd_toEndOf
- To make a
View
as wide as theConstraintLayout
it's in, constrain the start and end to the start and end of the parent, and set the width to 0dp.
<Button
android:id="@+id/calculate_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Calculate"
app:layout_constraintTop_toBottomOf="@id/round_up_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
- View binding lets you more easily write code that interacts with the UI elements in your app.
- Open the app's
build.gradle
file ( Gradle Scripts > build.gradle (Module: Tip_Time.app) ) - In the android section, add the following lines:
buildFeatures {
viewBinding = true
}
- Declare a top-level variable in the class for the binding object. It's defined at this level because it will be used across multiple methods in MainActivity class.
lateinit var binding: ActivityMainBinding
- The
lateinit
keyword is something new. It's a promise that your code will initialize the variable before using it. If you don't, your app will crash. - Initializes the
binding
object which you'll use to access Views in theactivity_main.xml
layout.
binding = ActivityMainBinding.inflate(layoutInflater)
- Set the content view of the activity. Instead of passing the resource ID of the layout,
R.layout.activity_main
, this specifies the root of the hierarchy of views in your app,binding.root
.
setContentView(binding.root)
- The full code in
MainActivity
now should be like:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
- Now when you need a reference to a
View
in your app, you can get it from thebinding
object instead of callingfindViewById()
. Thebinding
object automatically defines references for every View in your app that has an ID. Using view binding is so much more concise that often you won't even need to create a variable to hold the reference for a View, just use it directly from the binding object.
// Old way with findViewById()
val myButton: Button = findViewById(R.id.my_button)
myButton.text = "A button"
// Better way with view binding
val myButton: Button = binding.myButton
myButton.text = "A button"
// Best way with view binding and no extra variable
binding.myButton.text = "A button"
- The
Double
data type in Kotlin can store a decimal number. Kotlin provides a method for converting aString
to aDouble
, calledtoDouble()
.
val cost = binding.costOfService.text.toString().toDouble()
- Calling
toDouble()
on a string that is empty or a string that doesn't represent a valid decimal number doesn't work. Fortunately Kotlin also provides a method calledtoDoubleOrNull()
which handles these problems. It returns a decimal number if it can, or it returnsnull
if there's a problem.
val cost = stringInTextField.toDoubleOrNull()
- Use the
checkedRadioButtonId
attribute of aRadioGroup
to find whichRadioButton
is selected.
val selectedId = binding.tipOptions.checkedRadioButtonId
val tipPercentage = when (selectedId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
- For a
Switch
element, you can check theisChecked
attribute to see if the switch is "on".
val roundUp = binding.roundUpSwitch.isChecked
- To round a number you can use
kotlin.math.ceil()
method.
val tip = kotlin.math.ceil(tip)
- Use
NumberFormat.getCurrencyInstance()
to get a formatter to use for formatting numbers as currency.
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
- You can use string parameters like
%s
to create dynamic strings that can still be easily translated into other languages.
<string name="tip_amount">Tip Amount: %s</string>
- You can set the string parameters by calling
getString(R.string.tip_amount, formattedTip)
and assign that to thetext
attribute of the tip resultTextView
.
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
- When developing your app (and viewing the preview), it's useful to have a placeholder for that
TextView
.
<!-- android:text="@string/tip_amount" -->
tools:text="Tip Amount: $10"
- Testing is important!
- You can use Logcat in Android Studio to troubleshoot problems like the app crashing.
- A stack trace shows a list of methods that were called. This can be useful if the code generates an exception.
2020-06-24 10:09:41.564 24423-24423/com.example.tiptime E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.tiptime, PID: 24423
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
at com.example.tiptime.MainActivity$onCreate$1.onClick(MainActivity.kt:17)
- Exceptions indicate a problem that code didn't expect.
Null
means "no value".- Not all code can handle
null
values, so be careful using it. - Use Analyze > Inspect Code for suggestions to improve your code.
- Use the Material Color Tool to select colors for your app theme.
- Alternatively, you can use the Material palette generator to help select a color palette.
- Declare color resources in the
colors.xml
file to make it easier to reuse them.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
- Use the colors in your theme in
themes.xml
:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/green</item>
<item name="colorPrimaryVariant">@color/green_dark</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/blue</item>
<item name="colorSecondaryVariant">@color/blue_dark</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
- Dark theme can reduce power usage and make your app easier to read in low light. (
themes.xml (night)
)
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Application theme for dark theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/green_light</item>
<item name="colorPrimaryVariant">@color/green</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/blue_light</item>
<item name="colorSecondaryVariant">@color/blue_light</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
- Place app icon files in the
mipmap
resource directories. - Provide different versions of an app icon bitmap image in each density bucket (
mdpi
,hdpi
,xhdpi
,xxhdpi
,xxxhdpi
) for backwards compatibility with older versions of Android.mdpi
- resources for medium-density screens (~160 dpi)hdpi
- resources for high-density screens (~240 dpi)xhdpi
- resources for extra-high-density screens (~320 dpi)xxhdpi
- resources for extra-extra-high-density screens (~480dpi)xxxhdpi
- resources for extra-extra-extra-high-density screens (~640dpi)nodpi
- resources that are not meant to be scaled, regardless of the screen's pixel density
anydpi
- resources that scale to any density- Add resource qualifiers onto resource directories to specify resources that should be used on devices with a certain configuration (e.g.
v26
). - Vector drawables are Android's implementation of vector graphics. They are defined in XML as a set of points, lines, and curves along with associated color information. Vector drawables can be scaled for any density without loss of quality.
- Adaptive icons were introduced to the Android platform in API 26. They are made up of a foreground and background layer that follow specific requirements, so that your app icon looks high-quality on a range of devices with different OEM masks.
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
- Use Image Asset Studio in Android Studio to create legacy and adaptive icons for your app.
- Use Material Design Components where possible to adhere to Material Design guidelines and allow for more customization.
- In
app/build.gradle
Add:
dependencies {
...
implementation 'com.google.android.material:material:<version>'
}
- To create a text field as shown above, use a
TextInputLayout
with an enclosedTextInputEditText
from the MDC library. The Material text field can be easily customized to:- Display input text or a label that's always visible
- Display an icon in the text field
- Display helper or error messages
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:hint="@string/cost_of_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
- To use
SwitchMaterial
, you must explicitly specifySwitchMaterial
in your layout and use the fully qualified path name. In theactivity_main.xml
layout, change the XML tag fromSwitch
to:
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
android:layout_width="0dp"
android:layout_height="wrap_content" ... />
- it's important to note that support for vector drawables on the Android platform wasn't added until Android 5.0 (API level 21). in
app/build.gradle
add:
android {
defaultConfig {
...
vectorDrawables.useSupportLibrary = true
}
...
}
- Add icons to give users visual cues about how parts of your app will function.
<ImageView
android:id="@+id/icon_service_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/service_question"
app:layout_constraintBottom_toBottomOf="@id/service_question" />
<TextView
android:id="@+id/service_question"
...
android:layout_marginTop="16dp"
app:layout_constraintStart_toEndOf="@id/icon_service_question" />
- Use
ConstraintLayout
to position elements in your layout. - A style is a collection of view attributes values for a single type of widget. For example, a
TextView
style can specify font color, font size, and background color, to name a few. By extracting these attributes into a style, you can easily apply the style to multiple views in the layout and maintain it in a single place (styles.xml
).
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TextView style -->
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
<!-- Radio Button style -->
<style name="Widget.TipTime.CompoundButton.RadioButton" parent="Widget.MaterialComponents.CompoundButton.RadioButton">
<item name="android:paddingStart">8dp</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
<!-- Switch style -->
<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
</resources>
- Test your app for edges cases (e.g. rotating your app in landscape mode) and make improvements where applicable.
- You can add a
ScrollView
around theConstraintLayout
to prevent UI items from getting truncated in landscape mode.
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:context=".MainActivity">
...
</ConstraintLayout>
</ScrollView>
- Comment your code to help other people who are reading your code understand what your approach was.
- Reformat your code and clean up your code to make it as concise as possible.
It's a bit cumbersome to manually hide the keyboard each time to better access the calculate button. Instead, make the keyboard automatically hide itself when the Enter key is pressed.
The handleKeyEvent()
is a private helper function that hides the onscreen keyboard if the keyCode
input parameter is equal to KeyEvent.
KEYCODE_ENTER
. The InputMethodManager controls if a soft keyboard is shown, hidden, and allows the user to choose which soft keyboard is displayed. The method returns true if the key event was handled, and returns false otherwise.
private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Hide the keyboard
val inputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
return true
}
return false
}
The onKey()
method takes in 3 input arguments: the view, the code for the key that was pressed, and a key event (which you won't use, so you can call it "_
"). When the onKey()
method is called, you should call your handleKeyEvent()
method and pass along the view and key code arguments. The syntax for writing this out is: view, keyCode, _ -> handleKeyEvent(view, keyCode).
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(binding.root)
binding.calculateButton.setOnClickListener { calculateTip() }
binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
}
}
You can change the tint of the icons based on the primary color of the theme, so that the icons appear differently in light vs. dark theme.
One of the advantages of VectorDrawables
versus bitmap images is the ability to scale and tint them. Below we have the XML representing the bell icon. There are two specific color attributes to take notice of: android:tint
and android:fillColor
.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
<!-- android:tint="?attr/colorControlNormal" -->
android:tint="?attr/colorPrimary">
<path
android:fillColor="@android:color/white"
android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>
Kotlin provides functionality to help you manage and manipulate collections of data more easily through the Kotlin Standard Library. A collection can be defined as a number of objects of the same data type. There are different basic collection types in Kotlin: lists, sets, and maps. This codelab focused specifically on lists, and you'll learn more about sets and maps in future codelabs.
- A list is an ordered collection of elements of a specific type, such as a list of
Strings.
val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
- The index is the integer position that reflects the position of the element (e.g.
myList[2]
).
println("Second element: ${numbers[1]}")
- In a list, the first element is at index 0 (e.g.
myList[0]
), and the last element is atmyList.size-1
(e.g.myList[myList.size-1]
ormyList.last()
).
println("Last index: ${numbers.size - 1}")
println("Last element: ${numbers[numbers.size - 1]}")
- Kotlin also supports
first()
andlast()
operations on a list.
println("First: ${numbers.first()}")
println("Last: ${numbers.last()}")
- Another useful list operation is the
contains()
method to find out if a given element is in the list.
println("Contains 4? ${numbers.contains(4)}")
- There are two types of lists:
List
andMutableList.
- A
List
is read-only and cannot be modified once it has been initialized. However, you can apply operations such assorted()
andreversed()
which return a new list without changing the original.
println("Sorted list: ${colors.sorted()}")
println("Reversed list: ${colors.reversed()}")
- A
MutableList
can be modified after creation such as adding, removing, or modifying elements.
val entrees = mutableListOf<String>()
val entrees: MutableList<String> = mutableListOf()
println("Add noodles: ${entrees.add("noodles")}")
println("Remove spaghetti: ${entrees.remove("spaghetti")}")
println("Remove first element: ${entrees.removeAt(0)}")
entrees.clear()
- You can add a list of items to a mutable list using
addAll()
.
val moreItems = listOf("ravioli", "lasagna", "fettuccine")
println("Add list: ${entrees.addAll(moreItems)}")
- Kotlin gives you a way to check if a list is empty using
isEmpty()
function.
println("Empty? ${entrees.isEmpty()}")
- Use a
while
loop to execute a block of code until the expression evaluates to false and you exit the loop.
while (expression) {
// While the expression is true, execute this code block
}
val guestsPerFamily = listOf(2, 4, 1, 3)
var totalGuests = 0
var index = 0
while (index < guestsPerFamily.size) {
totalGuests += guestsPerFamily[index]
index++
}
println("Total Guest Count: $totalGuests")
- Use a
for
loop to iterate over all items of a list:
for (item in myList) {
// Execute this code block for each element of the list
}
val names = listOf("Jessica", "Henry", "Alicia", "Jose")
for (name in names) {
println("$name - Number of characters: ${name.length}")
}
Note: Here are some other variations of what you can do with for loops, including using them with ranges with specific steps (instead of incrementing by 1 each time).
for (item in list) print(item) // Iterate over items in a list
for (item in 'b'..'g') print(item) // Range of characters in an alphabet
for (item in 1..5) print(item) // Range of numbers
for (item in 5 downTo 1) print(item) // Going backward
for (item in 3..6 step 2) print(item) // Prints: 35
- The
vararg
modifier allows you to pass in a variable number of arguments to a function or constructor.
class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
override fun toString(): String {
return name + " " + toppings.joinToString()
}
}
- To specify a different separator other than a comma, pass in the desired separator string as an argument to the
joinToString()
method. Example:joinToString(" ")
to separate each item with a space.
- Organizing your code logically helps you and other developers understand, maintain, and extend it. In the same way that you can organize paperwork into files and folders, you can organize your code into files and packages.
RecyclerView
widget helps you display a list of data.-
- item - One data item of the list to display. Represents one
Affirmation
object in your app.
- item - One data item of the list to display. Represents one
- Adapter - Takes data and prepares it for
RecyclerView
to display. - ViewHolders - A pool of views for
RecyclerView
to use and reuse to display affirmations.
-
ConstraintLayout
is ideal and flexible when you want to position multiple child views in a layout. Since your layout only has a single child view,RecyclerView
, you can switch to a simplerViewGroup
calledFrameLayout
that should be used for holding a single child view.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager" />
</FrameLayout>
RecyclerView
uses the adapter pattern to adapt and display the data.ViewHolder
creates and holds the views forRecyclerView
.
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.item_title)
}
- Each item in the
RecyclerView
has its own layout, which you define in a separate layout file. For example,list_item.xml
.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
RecyclerView
comes with built inLayoutManagers
.RecyclerView
delegates how items are laid out toLayoutManagers
.
app:layoutManager="LinearLayoutManager"
- To be able to scroll through a vertical list of items that is longer than the screen, you need to add a vertical scrollbar.
android:scrollbars="vertical"
package com.example.affirmations.model
data class Affirmation(val stringResourceId: Int)
Data displayed in your app may come from different sources (e.g. within your app project or from an external source that requires connecting to the internet to download data). As a result, data may not be in the exact format that you need. The rest of the app should not concern itself with where the data originates from or in what format it is originally. You can and should hide away this data preparation in a separate Datasource
class that prepares the data for the app.
For example we used strings as data sources:
<resources>
<string name="app_name">Affirmations</string>
<string name="affirmation1">I am strong.</string>
<string name="affirmation2">I believe in myself.</string>
...
<string name="affirmation10">I will be present in all the moments that this day brings.</string>
</resources>
The Datasource
class code should be like this:
package com.example.affirmations.data
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation
class Datasource {
fun loadAffirmations(): List<Affirmation> {
return listOf<Affirmation>(
Affirmation(R.string.affirmation1),
Affirmation(R.string.affirmation2),
...
Affirmation(R.string.affirmation10)
)
}
}
- Create a new class for the adapter, for example,
ItemAdapter
. - Create a custom
ViewHolder
class that represents a single list item view. Extend fromRecyclerView.ViewHolder
class. - Modify the
ItemAdapter
class to extend from theRecyclerView
.Adapter
class with the customViewHolder
class. - Implement these methods within the adapter:
getItemsCount()
,onCreateViewHolder()
, andonBindViewHolder()
.
package com.example.affirmations.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation
/**
* Adapter for the [RecyclerView] in [MainActivity]. Displays [Affirmation] data object.
*/
class ItemAdapter(
private val context: Context,
private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder.
// Each data item is just an Affirmation object.
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.item_title)
}
/**
* Create new views (invoked by the layout manager)
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
// create a new view
val adapterLayout = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ItemViewHolder(adapterLayout)
}
/**
* Replace the contents of a view (invoked by the layout manager)
*/
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = dataset[position]
holder.textView.text = context.resources.getString(item.stringResourceId)
}
/**
* Return the size of your dataset (invoked by the layout manager)
*/
override fun getItemCount() = dataset.size
}
- Create an instance of
Datasource
, and call theloadAffirmations()
method on it. Store the returned list of affirmations in aval
namedmyDataset
.
val myDataset = Datasource().loadAffirmations()
- Create a variable called
recyclerView
and usefindViewById()
to find a reference to theRecyclerView
within the layout.
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
- To tell the
recyclerView
to use theItemAdapter
class you created, create a newItemAdapter
instance.ItemAdapter
expects two parameters: the context (this
) of this activity, and the affirmations inmyDataset
. - Assign the
ItemAdapter
object to theadapter
property of therecyclerView
.
recyclerView.adapter = ItemAdapter(this, myDataset)
- Since the layout size of your
RecyclerView
is fixed in the activity layout, you can set thesetHasFixedSize
parameter of theRecyclerView
totrue
. This setting is only needed to improve performance. Use this setting if you know that changes in content do not change the layout size of theRecyclerView
.
recyclerView.setHasFixedSize(true)
- When you are done, the code for
MainActivity
should be similar to the following.
package com.example.affirmations
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.data.Datasource
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize data.
val myDataset = Datasource().loadAffirmations()
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = ItemAdapter(this, myDataset)
// Use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
recyclerView.setHasFixedSize(true)
}
}
- To display additional content in a
RecyclerView
, modify the underlying data model class and data source. Then update the list item layout and adapter to set that data onto the views.Affirmation.kt
package com.example.affirmations.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class Affirmation(
@StringRes val stringResourceId: Int,
@DrawableRes val imageResourceId: Int
)
Datasource.kt
package com.example.affirmations.data
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation
class Datasource() {
fun loadAffirmations(): List<Affirmation> {
return listOf<Affirmation>(
Affirmation(R.string.affirmation1, R.drawable.image1),
Affirmation(R.string.affirmation2, R.drawable.image2),
...
Affirmation(R.string.affirmation10, R.drawable.image10)
)
}
}
list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="194dp"
android:id="@+id/item_image"
android:importantForAccessibility="no"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAppearance="?attr/textAppearanceHeadline6" />
</LinearLayout>
ItemAdapter.kt
class ItemViewHolder(private val view: View): RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(R.id.item_title)
val imageView: ImageView = view.findViewById(R.id.item_image)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = dataset[position]
holder.textView.text = context.resources.getString(item.stringResourceId)
holder.imageView.setImageResource(item.imageResourceId)
}
- Use resource annotations to help ensure that the right type of resource ID is passed into a class constructor.
@StringRes val stringResourceId: Int,
@DrawableRes val imageResourceId: Int
- Use the Material Components for Android library to have your app more easily follow the recommended Material Design guidelines.
- Use
MaterialCardView
to display content in a Material card.
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/item_image"
android:layout_width="match_parent"
android:layout_height="194dp"
android:importantForAccessibility="no"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAppearance="?attr/textAppearanceHeadline6" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
- Small visual tweaks to your app in terms of color and spacing can make the app look more polished and consistent.
- Common Theme Attributes allows you to use predefined text appearance values.