Welcome! Until now your programs have used loose variables and functions. That works for tiny programs, but real apps model real things: a student, a bank account, a bus route, a shopping basket. A phone app may handle thousands of such things at once. Kotlin gives you a tool to describe them neatly: the class.
Think of a class as the architect's plan for a building. The plan says "every house has a door, three windows and a roof". The actual houses built from the plan are called objects (or instances). One plan, many houses. In this lesson you will learn to write the plans, build the houses, and organise related plans into families.
By the end of this lesson you will be able to:
- Define a class with properties and functions, and create objects from it.
- Write primary and secondary constructors, default values and
initblocks. - Use custom getters and setters, and control access with
private,protected,internalandpublic. - Build class hierarchies with
open,overrideandsuper. - Declare
abstractclasses andinterfaces, and know when to choose which. - Use
data classfor classes that just hold information. - Use
enum classfor fixed sets of choices, and meetsealed classfor closed hierarchies. - Use
objectandcompanion objectfor single shared instances.
How this fits the bigger picture: This lesson matches the Use classes and objects in Kotlin codelab in Unit 2 of Google's official Android Basics with Compose course. Everything on Android, from an
Activityto a ViewModel to a Room database, is a class, so this lesson is the foundation for the rest of the course.
1. Variables, val and var (lesson 07)
val cannot be reassigned, var can. Both are used for properties inside classes too.
2. Functions (lesson 08)
A function inside a class is called a method (Kotlin people often still just say function). It works exactly like the functions you have written, except it belongs to an object and can use the object's data.
3. Conditionals (lesson 09)
You will use if and when inside methods and getters.
4. Nullability (lesson 10)
Properties can be nullable (String?). The ?. and ?: operators work on properties just as they do on variables.
The plan and the houses
CLASS (the plan) OBJECTS (the houses built from it)
class Student Student("Ada", 70)
------------- Student("Bola", 55)
name : String ----> Student("Chidi", 90)
score : Int
describe() Each has its OWN name and score,
but they all share the same plan.
A class bundles two things together:
| Part | Kotlin word | Meaning |
|---|---|---|
| Data the object remembers | property | name, score, balance |
| Things the object can do | method (member function) | describe(), deposit() |
Why is bundling useful? Compare keeping three students in loose variables (name1, score1, name2, score2, ...) with one Student plan you reuse. The class approach scales to a thousand students, groups related things so you cannot mix them up, and lets you add behaviour (such as isPassing()) in one place.
The four big ideas (in plain words)
- Encapsulation: an object keeps its data safe and only lets the outside world touch it in controlled ways, like a bank that lets you deposit but not simply type a bigger number in your balance.
- Inheritance: a new class can be built on an existing one and reuse its features, like a Dog being a kind of Animal.
- Abstraction: you can describe what something must do (an interface) without saying how, like "anything payable must be able to produce a fee".
- Polymorphism: code written for the general type works with every specific type, like a conductor collecting fares from every passenger however they pay.
You will see all four in action below.
Run these in the Kotlin Playground (play.kotlinlang.org) as in lesson 06.
Example 1: Your first class
class Phone {
var brand = "Tecno"
var batteryPercent = 100
fun call(number: String) {
println("Calling $number on $brand")
}
}
fun main() {
val myPhone = Phone()
myPhone.call("0803 000 1234")
myPhone.batteryPercent = 80
println("Battery: ${myPhone.batteryPercent}%")
}
Expected output:
Calling 0803 000 1234 on Tecno
Battery: 80%
Explanation:
class Phone { ... }is the plan. It has two properties and one method.Phone()builds an object. Notice there is nonewkeyword in Kotlin.myPhone.call(...)runs a method, andmyPhone.batteryPercent = 80changes a property. Theval myPhonelabel cannot be pointed at a different phone, but the phone object's ownvarproperties can still change.
If you print a plain object with println(myPhone), you get something like Phone@1b6d3586: the class name and a code that varies from run to run. That is rarely useful, and later we will see how data class fixes it.
Example 2: Primary constructors
Most classes need their values supplied when they are created. Kotlin lets you write the constructor right in the class header. Putting val or var in front of a parameter turns it into a property at the same time:
class Student(val name: String, var score: Int = 0) {
fun describe(): String = "$name has $score points"
}
fun main() {
val ada = Student("Ada", 70)
val bola = Student("Bola")
bola.score += 15
println(ada.describe())
println(bola.describe())
}
Expected output:
Ada has 70 points
Bola has 15 points
Explanation:
val namemakes a read-only property: you can readada.namebutada.name = "X"will not compile.var scoremakes a changeable property with a default value of 0, soStudent("Bola")is allowed.bola.score += 15takes 0 and adds 15.- Named arguments from lesson 08 work with constructors too:
Student(name = "Chidi", score = 90).
A parameter without val or var (such as class A(x: Int)) is only a constructor input. It is not stored as a property, though it can be used in init blocks and property initialisers.
Example 3: init blocks and secondary constructors
An init block runs right after the primary constructor, as the object is being built. A secondary constructor is an extra way to create the object. It must call the primary constructor with this(...).
class Counter(val start: Int) {
var value = start
init {
println("Counter created at $start")
}
constructor() : this(0) {
println("Default counter")
}
}
fun main() {
val c1 = Counter(5)
val c2 = Counter()
println(c2.value)
}
Expected output:
Counter created at 5
Counter created at 0
Default counter
0
Explanation: Counter(5) runs the primary constructor, then the init block, giving the first line. Counter() calls the secondary constructor, which immediately delegates to this(0), so the primary constructor and init run first (line two), and only then does the secondary constructor's own body print "Default counter". Finally c2.value is 0. In everyday Kotlin, default parameter values usually replace secondary constructors, so you will not write many.
Example 4: Getters, setters and private set
A property is more than a variable: it comes with a hidden getter and setter. You can customise them.
class Account(val owner: String) {
var balance: Int = 0
private set
val isRich: Boolean
get() = balance > 100000
var nickname: String = ""
set(value) {
field = value.trim()
}
fun deposit(amount: Int) {
if (amount > 0) {
balance += amount
}
}
}
fun main() {
val acc = Account("Ada")
acc.deposit(50000)
acc.deposit(-5)
println("Balance: ${acc.balance}, rich: ${acc.isRich}")
acc.deposit(60000)
println("Balance: ${acc.balance}, rich: ${acc.isRich}")
acc.nickname = " Tee "
println("[${acc.nickname}]")
}
Expected output:
Balance: 50000, rich: false
Balance: 110000, rich: true
[Tee]
Explanation:
private setmeans code outside the class can readbalancebut cannot assign to it. Onlydepositcan change it. That is encapsulation: the bank's rule "no negative deposits" cannot be bypassed.isRichhas a custom getter and no stored value at all. It is recalculated every time it is read, so it can never be out of date.- In the
nicknamesetter,fieldis the hidden storage behind the property. Writingfield = ...stores the value. Writingnickname = ...inside the setter would call the setter again forever.valueis the name for the incoming value. - The
-5deposit is silently ignored by theif, so the first line shows 50000.
Visibility modifiers
You decide who may see each property or function.
| Modifier | Who can use it |
|---|---|
public (default) |
Everyone, everywhere |
private |
Only code inside the same class (or the same file, for top-level items) |
protected |
The class and its subclasses |
internal |
Anything in the same module (for example, your app module) |
Rule of thumb: make things as private as you can, and open them up only when something else needs them. A small public surface is easier to understand and change.
class Safe(private val code: String) {
fun open(attempt: String): Boolean = attempt == code
}
fun main() {
val safe = Safe("1234")
println(safe.open("0000"))
println(safe.open("1234"))
// println(safe.code) // error: cannot access 'code': it is private
}
Expected output:
false
true
Example 5: open, override and super
In Kotlin classes are closed by default: nobody can inherit from them unless you write open. The same goes for methods you want subclasses to change.
open class Animal(val name: String) {
open fun sound(): String = "..."
fun introduce(): String = "$name says ${sound()}"
}
open class Dog(name: String) : Animal(name) {
override fun sound(): String = "Woof"
}
class Puppy(name: String) : Dog(name) {
override fun sound(): String = super.sound() + "!"
}
class Fish(name: String) : Animal(name)
fun main() {
println(Dog("Rex").introduce())
println(Puppy("Bit").introduce())
println(Fish("Nemo").introduce())
}
Expected output:
Rex says Woof
Bit says Woof!
Nemo says ...
Explanation:
class Dog(name: String) : Animal(name)reads "Dog is an Animal, and passesnameup to Animal's constructor".override fun sound()replaces the parent's version.super.sound()calls the parent's version, soPuppysays "Woof" plus an exclamation mark.Fishdoes not overridesound, so it inherits the default"...".introduce()is notopen, so it cannot be overridden; it always calls whicheversound()belongs to the actual object. That is polymorphism: the same line of code inintroducebehaves differently for aDogand aFish.
Example 6: Abstract classes
Sometimes the parent class is too general to exist on its own. "A shape" has no area until you say which shape. An abstract class can hold shared code and also declare functions that subclasses must supply. You cannot create an object of an abstract class directly.
abstract class Shape(val label: String) {
abstract fun area(): Double
fun show() {
println("$label area = ${area()}")
}
}
class Rectangle(private val w: Double, private val h: Double) : Shape("Rectangle") {
override fun area(): Double = w * h
}
class Triangle(private val base: Double, private val height: Double) : Shape("Triangle") {
override fun area(): Double = 0.5 * base * height
}
fun main() {
val shapes = listOf(Rectangle(4.0, 2.5), Triangle(6.0, 3.0))
for (shape in shapes) {
shape.show()
}
// val s = Shape("Blob") // error: cannot create an instance of an abstract class
}
Expected output:
Rectangle area = 10.0
Triangle area = 9.0
Explanation: Shape forces every subclass to write area(). Members marked abstract are automatically open, so you do not write open as well. 4.0 times 2.5 is 10.0, and half of 6.0 times 3.0 is 9.0. The loop treats both objects as a Shape, which is polymorphism again.
Example 7: Interfaces
An interface is a contract: "anything that claims to be Payable can do these things". A class can extend only one parent class, but it can implement many interfaces. Interfaces cannot store state (no backing fields), but they can contain default implementations.
interface Payable {
val fee: Int
fun pay(): String = "Paid ₦$fee"
}
class Tuition : Payable {
override val fee = 45000
}
class Uniform : Payable {
override val fee = 8500
override fun pay(): String = "Paid ₦$fee for uniform"
}
fun main() {
val bills = listOf<Payable>(Tuition(), Uniform())
for (bill in bills) {
println(bill.pay())
}
}
Expected output:
Paid ₦45000
Paid ₦8500 for uniform
Explanation: Payable demands a fee property and gives pay() a default body. Tuition supplies only the fee and inherits pay(). Uniform supplies the fee and replaces pay(). Writing listOf<Payable>(...) states that the list holds anything payable. (The angle brackets are generics; lesson 13 covers them.)
| Abstract class | Interface | |
|---|---|---|
| Can hold state (stored properties) | Yes | No |
| Constructor | Yes | No |
| A class can extend / implement | Only one | Many |
| Best for | "Is a kind of..." with shared code | "Can do..." capabilities |
Example 8: Data classes
Many classes exist only to carry information: a book, a user, a product. Write data before class and Kotlin generates useful members for you: toString(), equals(), hashCode(), copy() and destructuring.
data class Book(val title: String, val pages: Int)
fun main() {
val a = Book("Learn Kotlin", 120)
val b = Book("Learn Kotlin", 120)
println(a)
println(a == b)
println(a === b)
val longer = a.copy(pages = 300)
println(longer)
val (title, pages) = a
println("$title has $pages pages")
}
Expected output:
Book(title=Learn Kotlin, pages=120)
true
false
Book(title=Learn Kotlin, pages=300)
Learn Kotlin has 120 pages
Explanation:
println(a)prints a readable description instead ofBook@....==compares contents, so two books with the same title and pages are equal.===compares identity (are they the very same object in memory?), which is false here because we built two objects.copy(pages = 300)makes a new book, changing only what you name. Because data classes are usually built fromvalproperties,copyis how you "change" something without mutating the original. You will use this pattern constantly when handling state in Compose.val (title, pages) = ais destructuring: it pulls out the properties in declaration order.
A data class needs at least one parameter in its primary constructor, and every parameter must be val or var.
Example 9: Enum classes
An enum class is for a fixed set of options: days, directions, order status. Each option is a constant object.
enum class Day(val shortName: String, val isWeekend: Boolean) {
SATURDAY("Sat", true),
SUNDAY("Sun", true),
MONDAY("Mon", false)
}
fun mood(day: Day): String = when (day) {
Day.SATURDAY -> "Party time"
Day.SUNDAY -> "Rest and church"
Day.MONDAY -> "Back to work"
}
fun main() {
for (day in Day.entries) {
println("${day.name} (${day.shortName}) weekend=${day.isWeekend} position=${day.ordinal}")
}
println(mood(Day.MONDAY))
}
Expected output:
SATURDAY (Sat) weekend=true position=0
SUNDAY (Sun) weekend=true position=1
MONDAY (Mon) weekend=false position=2
Back to work
Explanation: Each constant is created with its constructor values. name is the constant's own name, ordinal is its position starting from zero, and Day.entries lists all constants in order (older code uses Day.values()). When you use when on an enum and cover every constant, the compiler knows the when is exhaustive, so no else is needed, and if you later add a new day the compiler tells you every when that must be updated. That safety net is the main reason to prefer an enum over loose strings like "sat".
Example 10: Sealed classes (an introduction)
An enum constant has one fixed shape. What if each option needs different data? A loading screen has no data, a success carries the result, and a failure carries an error message. A sealed class is a closed family of related classes: all subclasses are known when the code is compiled.
sealed class LoadState {
object Loading : LoadState()
class Success(val data: String) : LoadState()
class Failure(val reason: String) : LoadState()
}
fun message(state: LoadState): String = when (state) {
is LoadState.Loading -> "Loading..."
is LoadState.Success -> "Got: ${state.data}"
is LoadState.Failure -> "Error: ${state.reason}"
}
fun main() {
println(message(LoadState.Loading))
println(message(LoadState.Success("12 products")))
println(message(LoadState.Failure("No network")))
}
Expected output:
Loading...
Got: 12 products
Error: No network
Explanation: is LoadState.Success -> checks the type, and inside that branch state is smart-cast (lesson 10) so you can read state.data. Because the hierarchy is sealed, when is exhaustive without an else. You will use exactly this pattern for screen state in lessons 24 and 29. Sealed classes are for "one of several different shapes"; enums are for "one of several identical-shaped constants".
Example 11: object and companion object
Sometimes you need exactly one of something: app settings, a logger. An object declaration creates a class and its single instance in one step. A companion object attaches shared members to a class itself instead of to each instance (like static in Java).
object AppConfig {
const val NAME = "Techbase"
var visits = 0
}
class Ticket(val seat: Int) {
companion object {
const val MAX_SEATS = 14
fun first(): Ticket = Ticket(1)
}
}
fun main() {
AppConfig.visits++
AppConfig.visits++
println("${AppConfig.NAME} visits: ${AppConfig.visits}")
val t = Ticket.first()
println("Seat ${t.seat} of ${Ticket.MAX_SEATS}")
}
Expected output:
Techbase visits: 2
Seat 1 of 14
Explanation: You never write AppConfig(); you use the name directly, and everyone shares the same single object. const val is for compile-time constants (plain text or numbers) and is written in UPPER_SNAKE_CASE by convention. Ticket.first() and Ticket.MAX_SEATS are reached through the class name. Lesson 13 digs deeper into both forms of object.
Exercise 1: A Rectangle with computed properties
Write a class Rect(val width: Int, val height: Int) with two read-only computed properties, area and perimeter, written with custom getters. Create Rect(3, 4) and print its area and perimeter.
class Rect(val width: Int, val height: Int) {
val area: Int
get() = width * height
val perimeter: Int
get() = 2 * (width + height)
}
fun main() {
val r = Rect(3, 4)
println("Area: ${r.area}")
println("Perimeter: ${r.perimeter}")
}
Expected output:
Area: 12
Perimeter: 14
Explanation: 3 times 4 is 12, and twice (3 plus 4) is 14. Neither property has stored data, so they always match the current width and height.
Exercise 2: Fix the inheritance
This program does not compile. Find the two missing keywords and fix it so that it prints Driving.
class Vehicle {
fun move(): String = "Moving"
}
class Car : Vehicle() {
override fun move(): String = "Driving"
}
fun main() {
println(Car().move())
}
Classes and functions are closed by default. Vehicle must be open so it can be inherited, and move must be open so it can be overridden.
open class Vehicle {
open fun move(): String = "Moving"
}
class Car : Vehicle() {
override fun move(): String = "Driving"
}
fun main() {
println(Car().move())
}
Expected output:
Driving
Exercise 3: Traffic lights
Create an enum class Light(val action: String) with RED("Stop"), YELLOW("Get ready") and GREEN("Go"). Write fun next(light: Light): Light using when so that RED goes to GREEN, GREEN to YELLOW and YELLOW to RED. Print one line per light in the form RED: Stop, next is GREEN.
enum class Light(val action: String) {
RED("Stop"),
YELLOW("Get ready"),
GREEN("Go")
}
fun next(light: Light): Light = when (light) {
Light.RED -> Light.GREEN
Light.GREEN -> Light.YELLOW
Light.YELLOW -> Light.RED
}
fun main() {
for (light in Light.entries) {
println("${light.name}: ${light.action}, next is ${next(light).name}")
}
}
Expected output:
RED: Stop, next is GREEN
YELLOW: Get ready, next is RED
GREEN: Go, next is YELLOW
Explanation: entries follows declaration order (RED, YELLOW, GREEN). The when covers every constant, so no else is needed.
Create a data class Product(val name: String, val price: Int). Make one product, print it, make a copy with a different price using copy, print both, and check whether they are equal with ==. Predict each line before running.
Mistake #1: Forgetting open. The wrong code: class Animal then class Dog : Animal(). The error: "This type is final, so it cannot be inherited from". The fix: write open class Animal, and open fun for any method a subclass should override.
Mistake #2: Forgetting override. The wrong code: declaring fun sound() in a subclass when the parent has open fun sound(). The problem: the compiler insists you say your intention, to protect you from accidental name clashes. The fix: write override fun sound().
Mistake #3: Trying to create an abstract class or an interface directly. The wrong code: val s = Shape("x"). The fix: create one of its concrete subclasses, such as Rectangle(...).
Mistake #4: Expecting val to freeze the whole object. The wrong code idea: val acc = Account("Ada") and then being surprised that acc.deposit(100) works. The explanation: val stops you pointing acc at a different object. It does not stop the object's own var properties from changing. To make an object truly unchangeable, give every property val.
Mistake #5: A setter that calls itself. The wrong code:
var name: String = ""
set(value) {
name = value.trim() // calls the setter again, forever
}
The fix: assign to field, not to the property name: field = value.trim().
Mistake #6: Using == on ordinary classes and expecting content comparison. The problem: for a normal class == compares identity unless you write equals. Two Student("Ada", 70) objects are not equal. The fix: make the class a data class when "same contents" should mean "equal".
Mistake #7: Writing data class with no parameters, or with plain parameters. The fix: every primary-constructor parameter of a data class must be marked val or var, and there must be at least one.
Mistake #8: Making everything public. The problem: any code can break your object's rules. The fix: use private by default, and private set for properties that others may read but only the class may change.
Question 1. What is the difference between a class and an object?
A class is the plan or template that describes properties and behaviour. An object (instance) is one actual thing built from that plan, with its own values for the properties. One class can produce many objects.
Question 2. What does the private set in var balance: Int = 0 followed by private set achieve?
Code outside the class can read balance but cannot assign to it. Only code inside the class (such as a deposit method) may change it. This is encapsulation: the class controls how its data changes.
Question 3. Why does the following fail to compile, and how do you fix it?
class Bird {
fun fly() = "Flap"
}
class Eagle : Bird()
Kotlin classes are final by default, so Bird cannot be inherited. Mark it open class Bird. (If Eagle also needed to change fly(), fly would need to be open as well.)
Question 4. Name two things a data class gives you for free, and one rule its constructor must follow.
Any two of: a readable toString(), content-based equals() and hashCode(), the copy() function, and destructuring declarations. The primary constructor must have at least one parameter, and every parameter must be marked val or var.
Question 5. When would you choose a sealed class over an enum class?
Choose a sealed class when the alternatives need to carry different data (for example Loading, Success(data) and Failure(reason)). Choose an enum when the alternatives are a fixed set of constants that all have the same shape. Both give you an exhaustive when without else.
Go deeper: official resources
- Android Basics with Compose, Unit 2, Pathway 1: the official pathway containing Use classes and objects in Kotlin.
- Kotlin docs: Classes: constructors,
init, properties and more. - Kotlin docs: Inheritance:
open,override,abstract. - Kotlin docs: Data classes and Sealed classes.
- Kotlin Playground: run every example in this lesson.
Project goal: Model a small school with a class hierarchy, an interface, an enum, a data class and a companion object, and print a tidy report.
Requirements:
- Create
enum class Role(val title: String)with at leastTEACHER,BURSARandPRINCIPAL. - Create
interface Payablewithval monthlyPay: Int. - Create
abstract class Person(val name: String)with anabstract fun describe(): String. - Create
class Staffthat extendsPersonand implementsPayable, with arole, and adescribe()that returns text such asTeacher Mrs Ade earns ₦120000. - Create
class Student(val name: String, var score: Int)whose companion object keeps acountof how many students have been created (hint: use aninitblock). Only the class itself may changecount(private set). - Create
data class Score(val subject: String, val mark: Int), then print one score and acopyof it with a changed mark. - In
main, create two staff members and two students, print every staff description, the total monthly pay, the student count and the scores.
Stretch goal: Add a method isPassing() to Student (a pass is 50 or more) and print each student's name with PASS or FAIL.
enum class Role(val title: String) {
TEACHER("Teacher"),
BURSAR("Bursar"),
PRINCIPAL("Principal")
}
interface Payable {
val monthlyPay: Int
}
abstract class Person(val name: String) {
abstract fun describe(): String
}
class Staff(
name: String,
val role: Role,
override val monthlyPay: Int
) : Person(name), Payable {
override fun describe(): String = "${role.title} $name earns ₦$monthlyPay"
}
class Student(val name: String, var score: Int) {
init {
count++
}
fun isPassing(): Boolean = score >= 50
companion object {
var count = 0
private set
}
}
data class Score(val subject: String, val mark: Int)
fun main() {
val staff = listOf(
Staff("Mrs Ade", Role.TEACHER, 120000),
Staff("Mr Bello", Role.BURSAR, 150000)
)
val ada = Student("Ada", 72)
val bola = Student("Bola", 41)
var total = 0
for (member in staff) {
println(member.describe())
total += member.monthlyPay
}
println("Total monthly pay: ₦$total")
println("Students enrolled: ${Student.count}")
for (s in listOf(ada, bola)) {
println("${s.name}: ${if (s.isPassing()) "PASS" else "FAIL"}")
}
val maths = Score("Maths", 65)
println(maths)
println(maths.copy(mark = 70))
}
Expected output:
Teacher Mrs Ade earns ₦120000
Bursar Mr Bello earns ₦150000
Total monthly pay: ₦270000
Students enrolled: 2
Ada: PASS
Bola: FAIL
Score(subject=Maths, mark=65)
Score(subject=Maths, mark=70)
Why it works: Staff inherits from Person (so it must implement describe) and implements Payable (so it must supply monthlyPay, which the constructor does with override val). The total is 120000 plus 150000, which is 270000. Each Student creation runs init, which bumps the shared count in the companion object, so after two students it is 2. Ada's 72 is at least 50 and Bola's 41 is not. The data class prints in the generated Score(subject=..., mark=...) format, and copy changes only the mark.
Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.
Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 11 project”.
On github.com click New repository, name it android-lesson-11-classes-and-objects-in-kotlin, set it to Public, leave Add a README unticked, and click Create repository.
Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.
Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉
Lesson 11 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: Classes, Objects and Why We Need Them
- Worked Examples: Writing Classes
- Worked Examples: Inheritance, Abstract Classes and Interfaces
- Worked Examples: Data, Enums, Sealed Classes and Objects
- Guided Practice Exercises
- Common Beginner Mistakes (and How to Fix Them)
- Knowledge Check