Techbase Academy
Android · Lesson 42

Android Views and XML Layouts

12 phasesClass Register (Views Edition)Android Studio
Phase 1 of 12
Lesson Introduction

Everything you have built so far uses Jetpack Compose. But Android had a user interface toolkit for more than a decade before Compose existed, called the View system, and a huge amount of real-world code still uses it. If you take a job, join an open-source project or maintain an older app, you will meet activity_main.xml, findViewById, RecyclerView and Fragment. You need to be able to read them, fix them and gradually replace them.

This lesson covers the Android View System material and the Build an Android app with Views work in Unit 8, Pathway 1 of the Android Basics with Compose programme (https://developer.android.com/courses/pathways/android-basics-compose-unit-8-pathway-1). The programme's final unit is deliberately about Views, because the next step for a working developer is a codebase that mixes both.

By the end you will be able to:

  • Explain the View hierarchy and compare it to Compose.
  • Write XML layouts with LinearLayout, FrameLayout and ConstraintLayout.
  • Use view binding instead of findViewById.
  • Show lists with RecyclerView, an adapter and a view holder.
  • Explain the difference between an Activity and a Fragment, and the fragment lifecycle.
  • Move between screens with the Jetpack Navigation component and a navigation graph.
  • Build a small Views app, and decide when to use Views versus Compose.

Big idea: Views are imperative: you build a tree of objects and then tell each object how to change (textView.text = "Hi"). Compose is declarative: you describe what the UI looks like for the current state. Both end up drawing the same pixels.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 12
Prerequisite Concepts
  • Activities and the lifecycle (lesson 23).
  • Resources: strings, drawables, R class (lessons 15 and 21).
  • Kotlin classes, interfaces, lambdas (lessons 10–12).
  • Navigation concepts (lesson 26).
  • ViewModel (lesson 24). It works the same way with Views.
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 12
Conceptual Understanding: The View System

Every visible thing in a View-based app is an instance of android.view.View: a TextView, a Button, an ImageView. A ViewGroup is a View that contains other Views (LinearLayout, ConstraintLayout, FrameLayout). Together they form a tree.

text
ConstraintLayout            <- ViewGroup (root)
 +-- TextView  (title)      <- View
 +-- EditText  (name)
 +-- Button    (save)
 +-- RecyclerView (list)    <- ViewGroup that recycles its children

You usually describe that tree in an XML file in res/layout, and Android inflates (parses and creates) it into objects at runtime. Then your Kotlin finds the objects and changes them.

Views + XML Jetpack Compose
Style Imperative, mutate objects Declarative, describe state
UI description XML files Kotlin functions
State Stored inside views and your code Stored in state objects
Lists RecyclerView + adapter LazyColumn
Theming XML themes/styles MaterialTheme
Reuse Custom Views, <include> Composable functions
Past/present Mature, huge ecosystem Recommended for new UI

Why still learn Views? Because many apps in production use them, because Google's own libraries (like WebView, MapView, ads, camera previews) are Views that you embed, and because incremental migration (lesson 43) means you read both languages.

Measure, layout, draw

Both systems use three steps: measure (how big do you want to be?), layout (where do you go?) and draw. In Views, a parent asks each child to measure itself, passing constraints, and nested LinearLayouts can measure children more than once, which is why deep nesting is slow. ConstraintLayout was built to keep the tree flat.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: The View System. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 12
Step-by-Step Build 1: Create a Views Project
  1. File → New → New Project.
  2. Choose the Empty Views Activity template (older Studio versions call it Empty Activity under the Phone and Tablet tab, with Empty Views Activity as a separate item; the labels change between versions, so read the template description).
  3. Name: ClassRegister. Package: com.example.classregister. Language Kotlin. Minimum SDK as in lesson 3.

Look at what Studio generated:

text
app/src/main/
  AndroidManifest.xml
  java/com/example/classregister/MainActivity.kt
  res/layout/activity_main.xml     <- the screen's XML
  res/values/{strings,colors,themes}.xml

MainActivity.kt does one thing: setContentView(R.layout.activity_main) instead of setContent { }.

MainActivity.kt
package com.example.classregister
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 }
}

The class extends AppCompatActivity (from the AppCompat library), not ComponentActivity directly, because AppCompat provides backwards-compatible theming and Fragment support for Views. Note: AppCompatActivity extends ComponentActivity, so everything from earlier lessons (ViewModel, registerForActivityResult) works.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Create a Views Project. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 12
Step-by-Step Build 2: XML Layouts

The basic vocabulary

Open res/layout/activity_main.xml. A tiny layout:

activity_main.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="match_parent"
 android:orientation="vertical"
 android:padding="16dp">
 <TextView
 android:id="@+id/title"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="@string/class_register"
 android:textSize="24sp"
 android:textStyle="bold" />
 <EditText
 android:id="@+id/nameInput"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:hint="@string/student_name"
 android:inputType="textPersonName" />
 <Button
 android:id="@+id/addButton"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/add" />
</LinearLayout>

Read it like this:

  • xmlns:android=...: declares the namespace for android: attributes. Required on the root element.
  • android:layout_width and layout_height are mandatory on every View. Values: match_parent (fill the parent), wrap_content (just big enough), or a fixed size like 48dp.
  • android:id="@+id/title": the @+id/ syntax creates an id named title, available in code as R.id.title.
  • @string/class_register refers to a resource in res/values/strings.xml, never hard-code text (Studio warns you; this also enables translation).
  • dp for sizes and sp for text, as in Compose.

Add the three strings to res/values/strings.xml:

strings.xml
<resources>
 <string name="app_name">ClassRegister</string>
 <string name="class_register">Class Register</string>
 <string name="student_name">Student name</string>
 <string name="add">Add</string>
</resources>

LinearLayout, FrameLayout and ConstraintLayout

Layout Does Use for
LinearLayout Stacks children in a line (orientation vertical/horizontal), supports layout_weight Simple stacks, forms
FrameLayout Stacks children on top of each other One child, overlays, fragment containers
ConstraintLayout Positions each child with constraints to the parent or siblings Almost everything else; flat, flexible
RelativeLayout Older relative positioning Avoid in new code; replaced by ConstraintLayout

layout_weight is the equivalent of Modifier.weight in Compose:

xml
<LinearLayout
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="horizontal">
 <EditText
 android:id="@+id/nameInput"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:layout_weight="1"
 android:hint="@string/student_name" />
 <Button
 android:id="@+id/addButton"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/add" />
</LinearLayout>

Setting width to 0dp with a weight tells the layout "take whatever is left after the others have measured".

ConstraintLayout

Each child needs at least one horizontal and one vertical constraint, otherwise it jumps to the top-left corner at run time.

activity_main.xml
<?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"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:padding="16dp">
 <TextView
 android:id="@+id/title"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:text="@string/class_register"
 android:textSize="24sp"
 app:layout_constraintTop_toTopOf="parent"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintEnd_toEndOf="parent" />
 <EditText
 android:id="@+id/nameInput"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:hint="@string/student_name"
 android:inputType="textPersonName"
 app:layout_constraintTop_toBottomOf="@id/title"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintEnd_toStartOf="@id/addButton" />
 <Button
 android:id="@+id/addButton"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/add"
 app:layout_constraintBaseline_toBaselineOf="@id/nameInput"
 app:layout_constraintEnd_toEndOf="parent" />
 <androidx.recyclerview.widget.RecyclerView
 android:id="@+id/studentList"
 android:layout_width="0dp"
 android:layout_height="0dp"
 android:layout_marginTop="12dp"
 app:layout_constraintTop_toBottomOf="@id/nameInput"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Key points:

  • app: attributes belong to a library namespace (res-auto), not the framework.
  • 0dp in a ConstraintLayout means match constraints: fill the space between the two constraints.
  • The @id/... syntax (no plus) refers to an existing id.
  • layout_constraintBaseline_toBaselineOf aligns text baselines.

Layout Editor

Studio has a visual Layout Editor with Code, Split and Design buttons at the top right of an XML file. Dragging views from the Palette onto the design surface adds constraints for you. It is a fast way to learn the XML, but you should always read the generated code. The Attributes panel lets you set properties; the Component Tree shows the hierarchy.

If a layout does not draw properly, check the Warnings panel, tooltips and Tools attributes (tools:text="Sample" shows sample text in the editor but never in the running app).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: XML Layouts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 12
Step-by-Step Build 3: View Binding

The old way to get a view was findViewById<TextView>(R.id.title): every call is a chance for a typo and a null crash. View binding generates a class for each layout file that has a typed, null-safe property for every view with an id.

Enable it in app/build.gradle.kts:

build.gradle.kts
android {
 // ...existing settings...
 buildFeatures {
 viewBinding = true
 }
}

Click Sync Now. For activity_main.xml Gradle now generates ActivityMainBinding (snake case file name → PascalCase + Binding). The ids become properties: nameInput, addButton, studentList, title.

MainActivity.kt
package com.example.classregister
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.classregister.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
 private lateinit var binding: ActivityMainBinding
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 binding = ActivityMainBinding.inflate(layoutInflater)
 setContentView(binding.root)
 binding.addButton.setOnClickListener {
 val name = binding.nameInput.text.toString().trim()
 if (name.isNotEmpty()) {
 binding.title.text = "Added: $name"
 binding.nameInput.text.clear()
 }
 }
 }
}

Explained line by line:

  • ActivityMainBinding.inflate(layoutInflater) creates the Views from the XML and wraps them.
  • binding.root is the top-level View that goes into setContentView.
  • binding.addButton.setOnClickListener { ... } registers a click handler. Compare with Compose's onClick = { ... }.
  • binding.title.text = ...: this is the imperative step: you change the view. In Compose you would change a state variable instead.
  • .text.clear() empties the EditText (its text is an Editable).

Run the app and test: type a name, press Add, and the title changes.

View binding vs findViewById vs data binding. View binding is a lightweight, type-safe replacement for findViewById. Data binding is a heavier older library that adds expressions in XML (@{user.name}); you may meet it in older code but it is not needed for new work. Kotlin synthetic imports (kotlinx.android.synthetic) are removed; if you see them in an old tutorial, switch to view binding.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: View Binding. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 12
Step-by-Step Build 4: RecyclerView

LinearLayout inside a ScrollView would create every row at once. RecyclerView is the View-system equivalent of LazyColumn: it keeps only enough row views to fill the screen (plus a few spare) and recycles them as you scroll, rebinding new data.

Three parts:

Part Job Compose analogy
RecyclerView Scrolling container LazyColumn
ViewHolder Holds the Views of one row The row's composable
Adapter Creates holders and binds data to them items { } lambda

Add the dependency (check for the current stable version of androidx.recyclerview:recyclerview in the official release notes; with a version catalog, add an entry like the one in lesson 3). Many Views templates already include it transitively through Material Components, but declare it explicitly if the import is unresolved.

The row layout

res/layout/item_student.xml:

item_student.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="horizontal"
 android:gravity="center_vertical"
 android:padding="12dp">
 <TextView
 android:id="@+id/nameText"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:layout_weight="1"
 android:textSize="18sp" />
 <CheckBox
 android:id="@+id/presentBox"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/present" />
</LinearLayout>

Add <string name="present">Present</string> to strings.xml. View binding generates ItemStudentBinding for this file too.

The adapter with ListAdapter and DiffUtil

The modern, efficient base class is ListAdapter. It computes differences between the old and new lists with a DiffUtil.ItemCallback and animates changes, so you never call notifyDataSetChanged().

StudentAdapter.kt
package com.example.classregister
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.classregister.databinding.ItemStudentBinding
data class Student(val id: Int, val name: String, val present: Boolean = false)
class StudentAdapter(
 private val onPresentChanged: (Student, Boolean) -> Unit
) : ListAdapter<Student, StudentAdapter.StudentViewHolder>(Diff) {
 class StudentViewHolder(val binding: ItemStudentBinding) : RecyclerView.ViewHolder(binding.root)
 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder {
 val binding = ItemStudentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 return StudentViewHolder(binding)
 }
 override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
 val student = getItem(position)
 holder.binding.nameText.text = student.name
 // Remove the old listener before changing "checked", otherwise recycled rows fire it.
 holder.binding.presentBox.setOnCheckedChangeListener(null)
 holder.binding.presentBox.isChecked = student.present
 holder.binding.presentBox.setOnCheckedChangeListener { _, checked ->
 onPresentChanged(student, checked)
 }
 }
 private object Diff : DiffUtil.ItemCallback<Student>() {
 override fun areItemsTheSame(old: Student, new: Student) = old.id == new.id
 override fun areContentsTheSame(old: Student, new: Student) = old == new
 }
}

Understand these three methods, the beating heart of every adapter:

  1. onCreateViewHolder runs only when a new row view is needed: inflate the XML (false means "do not attach to the parent yet").
  2. onBindViewHolder runs every time a row is shown with new data: fill the Views. Because rows are recycled, a holder that showed "Ada" may now show "Bola". Always set every View's state, including checkboxes, or old values leak in.
  3. DiffUtil uses areItemsTheSame (same identity? like a Compose key) and areContentsTheSame (any change to redraw?). That is the same idea as keys in lesson 39.

Wire it up in the Activity with a ViewModel

StudentViewModel.kt
package com.example.classregister
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class StudentViewModel : ViewModel() {
 private val _students = MutableStateFlow<List<Student>>(emptyList())
 val students: StateFlow<List<Student>> = _students.asStateFlow()
 private var nextId = 1
 fun add(name: String) {
 _students.update { it + Student(nextId++, name) }
 }
 fun setPresent(id: Int, present: Boolean) {
 _students.update { list -> list.map { if (it.id == id) it.copy(present = present) else it } }
 }
}
MainActivity.kt
package com.example.classregister
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.classregister.databinding.ActivityMainBinding
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
 private lateinit var binding: ActivityMainBinding
 private val viewModel: StudentViewModel by viewModels()
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 binding = ActivityMainBinding.inflate(layoutInflater)
 setContentView(binding.root)
 val adapter = StudentAdapter { student, present ->
 viewModel.setPresent(student.id, present)
 }
 binding.studentList.layoutManager = LinearLayoutManager(this)
 binding.studentList.adapter = adapter
 binding.addButton.setOnClickListener {
 val name = binding.nameInput.text.toString().trim()
 if (name.isNotEmpty()) {
 viewModel.add(name)
 binding.nameInput.text.clear()
 }
 }
 lifecycleScope.launch {
 repeatOnLifecycle(Lifecycle.State.STARTED) {
 viewModel.students.collect { adapter.submitList(it) }
 }
 }
 }
}

This is the View-system version of collectAsStateWithLifecycle: repeatOnLifecycle(STARTED) collects only while the screen is visible. adapter.submitList(...) hands the new list to DiffUtil.

Layout managers decide how rows are arranged: LinearLayoutManager (list), GridLayoutManager(context, 2) (grid), StaggeredGridLayoutManager (uneven grid).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: RecyclerView. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 12
Step-by-Step Build 5: Fragments

A Fragment is a reusable portion of UI with its own lifecycle, hosted inside an Activity. Originally fragments were created for tablets (a list on the left and detail on the right); today they are mostly used as screens in a single-activity app, with the Navigation component moving between them.

Activity versus Fragment

Activity Fragment
Declared in the manifest Yes No
Can exist alone Yes Needs a host Activity
Owns a window Yes Lives in the Activity's window
Has a lifecycle Yes Yes (plus a separate view lifecycle)
Typical role today The entry point; one per app One per screen

The fragment lifecycle (the part that surprises people)

A fragment has a lifecycle for the fragment object and a separate lifecycle for its view. The view can be destroyed and recreated (when the fragment is on the back stack) while the fragment object lives on.

text
onAttach -> onCreate -> onCreateView -> onViewCreated -> onStart -> onResume
   ...running...
onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
        ^                  ^
        |                  +-- view destroyed: release the binding here!
        +-- fragment may remain alive on the back stack

That is why fragment code that uses view binding follows a strict pattern:

ClassFragment.kt
package com.example.classregister
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.classregister.databinding.FragmentClassBinding
import kotlinx.coroutines.launch
class ClassFragment : Fragment() {
 private var _binding: FragmentClassBinding? = null
 private val binding get() = _binding!!
 private val viewModel: StudentViewModel by activityViewModels()
 override fun onCreateView(
 inflater: LayoutInflater,
 container: ViewGroup?,
 savedInstanceState: Bundle?
 ): View {
 _binding = FragmentClassBinding.inflate(inflater, container, false)
 return binding.root
 }
 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 super.onViewCreated(view, savedInstanceState)
 val adapter = StudentAdapter { s, present -> viewModel.setPresent(s.id, present) }
 binding.studentList.layoutManager = LinearLayoutManager(requireContext())
 binding.studentList.adapter = adapter
 viewLifecycleOwner.lifecycleScope.launch {
 viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
 viewModel.students.collect { adapter.submitList(it) }
 }
 }
 }
 override fun onDestroyView() {
 super.onDestroyView()
 _binding = null
 }
}

The three rules:

  1. Inflate in onCreateView, use views from onViewCreated onwards.
  2. Null the binding in onDestroyView, otherwise the fragment holds the destroyed view tree: a memory leak.
  3. Collect flows with viewLifecycleOwner, not this, otherwise you collect into views that no longer exist.

activityViewModels() shares one ViewModel between fragments of the same Activity, which is a convenient way to pass data; viewModels() gives each fragment its own.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: Fragments. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 12
Step-by-Step Build 6: The Navigation Component

You met Navigation Compose in lesson 26. The Navigation component for fragments is the original, with the same ideas: a navigation graph (an XML file listing destinations and actions), a NavHost (a container showing the current destination) and a NavController.

Dependencies (use the latest stable of androidx.navigation:navigation-fragment-ktx and navigation-ui-ktx; check the Navigation release notes).

Create a graph: right-click res → New → Android Resource File, type Navigation, name nav_graph. A simple graph with a list screen and a detail screen:

nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 android:id="@+id/nav_graph"
 app:startDestination="@id/classFragment">
 <fragment
 android:id="@+id/classFragment"
 android:name="com.example.classregister.ClassFragment"
 android:label="Class">
 <action
 android:id="@+id/action_class_to_detail"
 app:destination="@id/detailFragment" />
 </fragment>
 <fragment
 android:id="@+id/detailFragment"
 android:name="com.example.classregister.DetailFragment"
 android:label="Detail">
 <argument
 android:name="studentId"
 app:argType="integer" />
 </fragment>
</navigation>

The Activity's layout hosts the graph in a FragmentContainerView:

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 android:id="@+id/nav_host"
 android:name="androidx.navigation.fragment.NavHostFragment"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 app:defaultNavHost="true"
 app:navGraph="@navigation/nav_graph" />

(With this setup the list's views move into fragment_class.xml, which is the old activity_main.xml content. The Activity just hosts the container.)

Navigate from code. The Safe Args Gradle plugin generates type-safe Directions classes; without it you can use the raw action id and a Bundle. A version without any extra plugin:

kotlin
import android.os.Bundle
import androidx.navigation.fragment.findNavController
// inside ClassFragment, e.g. in a click handler
val args = Bundle().apply { putInt("studentId", student.id) }
findNavController().navigate(R.id.action_class_to_detail, args)
// inside DetailFragment
val studentId = requireArguments().getInt("studentId")

With the Safe Args plugin you instead write ClassFragmentDirections.actionClassToDetail(student.id) and read navArgs<DetailFragmentArgs>(), which is checked at compile time. Both approaches work; follow the Safe Args setup in the Navigation documentation if you want it.

The back stack works like Compose's: navigate pushes, the back button pops. findNavController().popBackStack() goes back from code. To connect the system bar's title and back arrow to the graph, call setupActionBarWithNavController(navController) from navigation-ui-ktx in the Activity.

This is the navigating between fragments part of the Unit 8 material; Compose's version of the same ideas is in lesson 26.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 6: The Navigation Component. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 12
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Read the XML

A colleague gives you this snippet. Describe what it displays, then fix the bug that makes the button appear in the top-left corner of the screen.

xml
<androidx.constraintlayout.widget.ConstraintLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 <TextView
 android:id="@+id/greeting"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Hello"
 app:layout_constraintTop_toTopOf="parent"
 app:layout_constraintStart_toStartOf="parent" />
 <Button
 android:id="@+id/ok"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="OK" />
</androidx.constraintlayout.widget.ConstraintLayout>

It shows the text "Hello" at the top-left and an "OK" button. The button has no constraints, so ConstraintLayout places it at the top-left corner, over the text. Give it a horizontal and vertical constraint:

xml
<Button
 android:id="@+id/ok"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="OK"
 app:layout_constraintTop_toBottomOf="@id/greeting"
 app:layout_constraintStart_toStartOf="parent" />

Also, the hard-coded "Hello" and "OK" should be string resources.

🎯 Your Challenge

Exercise 2: Delete a student

Extend the Class Register: add a delete icon to each row. Tapping it removes the student from the list through the ViewModel. Explain why ListAdapter makes this easy and what animation you get for free.

Add an ImageButton (@+id/deleteButton) to item_student.xml with a contentDescription, pass a second lambda onDelete: (Student) -> Unit to the adapter, and call it in onBindViewHolder:

kotlin
holder.binding.deleteButton.setOnClickListener { onDelete(student) }

In the ViewModel:

kotlin
fun remove(id: Int) {
 _students.update { list -> list.filterNot { it.id == id } }
}

ListAdapter receives the new list, DiffUtil sees that one item id is gone and calls notifyItemRemoved internally, so the row animates out and the others slide up. No manual index handling, and no notifyDataSetChanged() which would redraw everything without an animation.

🎯 Your Challenge

Exercise 3: Convert Views to Compose in your head

Write the Compose equivalent of the item_student.xml row (name on the left taking the remaining space, a "Present" checkbox on the right).

kotlin
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun StudentRow(student: Student, onPresentChange: (Boolean) -> Unit) {
 Row(
 Modifier.fillMaxWidth().padding(12.dp),
 verticalAlignment = Alignment.CenterVertically
 ) {
 Text(student.name, Modifier.weight(1f), fontSize = 18.sp)
 Text("Present")
 Checkbox(checked = student.present, onCheckedChange = onPresentChange)
 }
}

layout_weight="1" with 0dp became Modifier.weight(1f); the ViewHolder, the adapter and the "remove the listener first" workaround all disappear because the row is just a function of its data.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 12
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Forgetting layout width and height. Symptom: the inflate crashes with "You must supply a layout_width attribute". Fix: every View needs both.

Mistake #2: ConstraintLayout child with no constraints. Symptom: piles up top-left. Fix: one horizontal and one vertical constraint.

Mistake #3: Hard-coded strings and sizes. Symptom: lint warnings, no translation. Fix: @string/ and @dimen/ resources, dp and sp.

Mistake #4: Using findViewById in new code. Symptom: null crashes, casting. Fix: view binding.

Mistake #5: Not nulling the fragment binding. Symptom: memory leak after navigating away. Fix: set _binding = null in onDestroyView.

Mistake #6: Using viewLifecycleOwner incorrectly. Symptom: crashes touching a null binding. Fix: collect flows and observe LiveData with viewLifecycleOwner.

Mistake #7: notifyDataSetChanged() for everything. Symptom: no animations, wasted work. Fix: ListAdapter with DiffUtil.

Mistake #8: Not resetting recycled row state. Symptom: checkboxes tick on the wrong rows when scrolling. Fix: always set every view's state in onBindViewHolder.

Mistake #9: Forgetting RecyclerView.layoutManager. Symptom: blank list. Fix: set a LayoutManager, either in code or app:layoutManager in XML.

Mistake #10: Putting a RecyclerView inside a ScrollView. Symptom: clipped or all rows created at once. Fix: one scrolling container; put header content in the adapter as another item type.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 12
Knowledge Check

Question 1. What is the difference between a View and a ViewGroup?

A View is a single UI element such as a TextView or Button. A ViewGroup is a View that contains other Views and decides how to lay them out, like LinearLayout or ConstraintLayout.

Question 2. What does view binding give you over findViewById?

A generated class per layout with type-safe, null-safe properties for each view with an id, so there are no casts, no wrong-id crashes and no lookups by hand.

Question 3. What are the three parts of a RecyclerView list and what does each do?

The RecyclerView (scrolling container with a layout manager), the ViewHolder (holds the views of one row) and the Adapter (creates holders and binds data to them).

Question 4. Why must you set _binding = null in a fragment's onDestroyView?

A fragment's view can be destroyed while the fragment object survives (for example on the back stack). Keeping a reference to the binding holds the old view tree in memory and can cause leaks or crashes.

Question 5. What three things make up the Navigation component for fragments?

A navigation graph (destinations and actions), a NavHost (a container such as NavHostFragment in a FragmentContainerView) and a NavController that performs navigation.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Class Register (Views Edition)

Project goal: Build the Class Register app with the classic View system: add students, mark them present, and open a detail screen.

Requirements:

  1. A Views project with view binding on and a single MainActivity hosting a NavHostFragment.
  2. ClassFragment: an EditText + Add button and a RecyclerView (ListAdapter + DiffUtil).
  3. A shared StudentViewModel with a StateFlow<List<Student>>, collected with repeatOnLifecycle.
  4. Each row: name, present CheckBox, and tapping the name opens DetailFragment.
  5. DetailFragment shows the name and attendance status, with a back button.
  6. All text in string resources, all sizes in dp/sp.

Stretch goal: show the count of present students in the title, and write one small Compose StudentRow preview of what the same row would look like in Compose, so you can compare the two approaches.

res/layout/fragment_detail.xml:

fragment_detail.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="match_parent"
 android:orientation="vertical"
 android:padding="24dp">
 <TextView
 android:id="@+id/detailName"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:textSize="28sp"
 android:textStyle="bold" />
 <TextView
 android:id="@+id/detailStatus"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_marginTop="8dp"
 android:textSize="18sp" />
</LinearLayout>
DetailFragment.kt
package com.example.classregister
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.example.classregister.databinding.FragmentDetailBinding
import kotlinx.coroutines.launch
class DetailFragment : Fragment() {
 private var _binding: FragmentDetailBinding? = null
 private val binding get() = _binding!!
 private val viewModel: StudentViewModel by activityViewModels()
 override fun onCreateView(
 inflater: LayoutInflater,
 container: ViewGroup?,
 savedInstanceState: Bundle?
 ): View {
 _binding = FragmentDetailBinding.inflate(inflater, container, false)
 return binding.root
 }
 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 super.onViewCreated(view, savedInstanceState)
 val id = requireArguments().getInt("studentId")
 viewLifecycleOwner.lifecycleScope.launch {
 viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
 viewModel.students.collect { list ->
 val student = list.firstOrNull { it.id == id }
 if (student != null) {
 binding.detailName.text = student.name
 binding.detailStatus.text =
 if (student.present) getString(R.string.present) else getString(R.string.absent)
 }
 }
 }
 }
 }
 override fun onDestroyView() {
 super.onDestroyView()
 _binding = null
 }
}

Add <string name="absent">Absent</string> to strings.xml. In ClassFragment, extend the adapter with a second lambda onOpen: (Student) -> Unit, set nameText.setOnClickListener { onOpen(student) } in onBindViewHolder, and pass:

kotlin
val adapter = StudentAdapter(
 onPresentChanged = { s, present -> viewModel.setPresent(s.id, present) },
 onOpen = { s ->
 findNavController().navigate(
 R.id.action_class_to_detail,
 Bundle().apply { putInt("studentId", s.id) }
 )
 }
)

(Import android.os.Bundle and androidx.navigation.fragment.findNavController.) Expected behaviour: adding names appends rows with animation, ticking the checkbox updates the ViewModel, tapping a name opens the detail screen showing "Present" or "Absent", and Back returns to the list with the list state intact because the ViewModel is shared through activityViewModels().

1

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.

2

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 42 project”.

3

On github.com click New repository, name it android-lesson-42-android-views-and-xml-layouts, set it to Public, leave Add a README unticked, and click Create repository.

4

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.

5

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 42 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: The View System
  • Step-by-Step Build 1: Create a Views Project
  • Step-by-Step Build 2: XML Layouts
  • Step-by-Step Build 3: View Binding
  • Step-by-Step Build 4: RecyclerView
  • Step-by-Step Build 5: Fragments
  • Step-by-Step Build 6: The Navigation Component
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close