Android Layouts
In this lab you build a Customer List app with two screens:
- MainActivity shows an Add Customer button above a scrollable list of every customer added so far. Its whole layout is built in Java code.
- AddCustomerActivity is a form with Id, Name, Phone, a Gender spinner and an Add Customer button. Its layout is written in XML.
Tapping Add Customer on the main screen opens the form with an Intent. Submitting the form adds the customer and returns to the list, which now shows the new entry.
Objectives
- Understand Views, ViewGroups, layouts and widgets, and how they relate.
- Declare a layout statically in XML.
- Build a layout dynamically at runtime and add widgets to it from code.
- Switch between two Activities with an Intent.
- Handle user actions with event listeners.
Background
Views and ViewGroups
Every Android screen is a tree of View objects.
- A widget is a single UI element:
TextView,EditText,Button,Spinner… Widgets extendViewdirectly. - A layout is a container that positions its children:
LinearLayout,ConstraintLayout,FrameLayout,ScrollView… Layouts extendViewGroup, which itself extendsView. Because aViewGroupis aView, layouts can be nested inside other layouts.
The add-customer form you build in this lab has this tree:
text
LinearLayout (vertical) ← root, id = main
├── TextView "Customer Info"
├── LinearLayout (horizontal)
│ ├── TextView "Id"
│ └── EditText editText_Id
├── LinearLayout (horizontal)
│ ├── TextView "Name"
│ └── EditText editText_Name
├── LinearLayout (horizontal)
│ ├── TextView "Phone"
│ └── EditText editText_Phone
├── LinearLayout (horizontal)
│ ├── TextView "Gender"
│ └── Spinner spinner_Gender
└── Button button_Add_CustomerCommon layouts
| Layout | Arranges its children… |
|---|---|
LinearLayout | in a single row (horizontal) or column (vertical) |
ConstraintLayout | by constraints relative to each other and to the parent. It is the template default and best for complex screens. |
FrameLayout | stacked on top of each other, usually holding one child |
ScrollView | in a scrollable area. It has exactly one child, usually a LinearLayout. |
Keep the tree shallow
Each nested layout costs time to measure and draw. Prefer a wide tree over a deep one. This lab nests LinearLayouts because they are easy to understand. Real apps often use one ConstraintLayout instead.
XML or code?
You can declare a layout statically in an XML file (res/layout/…), dynamically in Java at runtime, or mix both. XML is easier to read and preview. Code is needed when the content isn't known in advance, like a list whose length depends on user input. This lab does one screen each way.
Intents and the back stack
An Intent is a message asking Android to do something. An explicit Intent names the Activity to open:
java
startActivity(new Intent(MainActivity.this, AddCustomerActivity.class));Android keeps open Activities on a back stack. Starting AddCustomerActivity puts it on top of MainActivity. Calling finish() (or pressing Back) removes it, and MainActivity comes back. When it does, Android calls its onResume() method. We use that callback to refresh the list.
Different from the PDF manual
The old manual started a new MainActivity from the form and finished the old one. That breaks the Back button and needlessly recreates the screen. This guide simply calls finish() and rebuilds the list in onResume().
Step 1: Create the project
Create a new project as described in Setup Guide → Create a lab project:
| Field | Value |
|---|---|
| Template | Empty Views Activity |
| Name | Exp2CustomerList |
| Package name | edu.birzeit.encs5150.exp2 |
| Language | Java |
| Minimum SDK | API 24 |
Step 2: The Customer class
This plain Java class holds one customer's data. The static list stores all customers so both Activities can reach it.
- In the Project window, right-click the package
edu.birzeit.encs5150.exp2→ New → Java Class. - Name it
Customerand press Enter. - Replace the file's contents with:
java
package edu.birzeit.encs5150.exp2;
import java.util.ArrayList;
public class Customer {
// Shared by both activities. It lives in memory only, so it is
// emptied when Android kills the app (Exp 4 stores it in SQLite).
public static final ArrayList<Customer> customersArrayList = new ArrayList<>();
private long customerId;
private String name;
private String phone;
private String gender;
public Customer() {
}
public Customer(long customerId, String name, String phone, String gender) {
this.customerId = customerId;
this.name = name;
this.phone = phone;
this.gender = gender;
}
public long getCustomerId() {
return customerId;
}
public void setCustomerId(long customerId) {
this.customerId = customerId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "ID: " + customerId
+ "\nName: " + name
+ "\nPhone: " + phone
+ "\nGender: " + gender;
}
}Let Android Studio write it for you
After typing only the four fields, press Alt+Insert (⌘+N on macOS) inside the class and choose Constructor, Getter and Setter, or toString(). This saves a lot of typing.
Step 3: Create AddCustomerActivity
Let the wizard create the Java class, the layout file and the manifest entry in one go:
- Right-click the package
edu.birzeit.encs5150.exp2→ New → Activity → Empty Views Activity. - Set Activity Name to
AddCustomerActivity. The layout name fills in asactivity_add_customer. - Leave Launcher Activity unchecked, check that Source Language is Java, and click Finish.
Open AndroidManifest.xml. You should see a new line:
xml
<activity
android:name=".AddCustomerActivity"
android:exported="false" />exported="false" means only your own app can open this screen, which is what we want.
Step 4: The form layout (XML)
Open res/layout/activity_add_customer.xml, switch to Code view, and replace the whole file with:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".AddCustomerActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Customer Info"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<!-- Id row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Id" />
<EditText
android:id="@+id/editText_Id"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Insert Id"
android:importantForAutofill="no"
android:inputType="number"
android:maxLength="9" />
</LinearLayout>
<!-- Name row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Name" />
<EditText
android:id="@+id/editText_Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Insert Name"
android:importantForAutofill="no"
android:inputType="textPersonName" />
</LinearLayout>
<!-- Phone row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Phone" />
<EditText
android:id="@+id/editText_Phone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Insert Phone Number"
android:importantForAutofill="no"
android:inputType="phone" />
</LinearLayout>
<!-- Gender row: the options are added in Java -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="Gender" />
<Spinner
android:id="@+id/spinner_Gender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<Button
android:id="@+id/button_Add_Customer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Add Customer" />
</LinearLayout>Switch to Split view to check that it matches the tree in the Background section.
Things to notice:
- Each row is a horizontal
LinearLayout. The label has a fixed width of80dpso all fields line up. The field takeslayout_width="0dp"pluslayout_weight="1", which means "fill whatever width is left". inputTypechooses the keyboard:numbershows a digits-only keypad andphonea dial pad.maxLength="9"keeps the Id small enough to parse as along.- The spinner has no items yet. They are added in Java in the next step.
- The root keeps
android:id="@+id/main". The wizard's Java code uses it to keep the form clear of the status bar.
Step 5: The form code
Open AddCustomerActivity.java and replace the whole file with the code below. The first lines of onCreate are exactly what the wizard generated, and everything after them is new.
java
package edu.birzeit.encs5150.exp2;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class AddCustomerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_add_customer);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// Fill the gender spinner from a String array through an ArrayAdapter.
String[] options = {"Male", "Female"};
Spinner genderSpinner = findViewById(R.id.spinner_Gender);
ArrayAdapter<String> genderAdapter =
new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, options);
genderAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
genderSpinner.setAdapter(genderAdapter);
EditText idEditText = findViewById(R.id.editText_Id);
EditText nameEditText = findViewById(R.id.editText_Name);
EditText phoneEditText = findViewById(R.id.editText_Phone);
Button addCustomerButton = findViewById(R.id.button_Add_Customer);
addCustomerButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String idText = idEditText.getText().toString().trim();
String name = nameEditText.getText().toString().trim();
String phone = phoneEditText.getText().toString().trim();
// Mark every empty field, then stop if any was empty.
boolean valid = true;
if (idText.isEmpty()) {
idEditText.setError("Id is required");
valid = false;
}
if (name.isEmpty()) {
nameEditText.setError("Name is required");
valid = false;
}
if (phone.isEmpty()) {
phoneEditText.setError("Phone is required");
valid = false;
}
if (!valid) {
return;
}
// inputType="number" and maxLength="9" guarantee idText is a small number.
long id = Long.parseLong(idText);
String gender = genderSpinner.getSelectedItem().toString();
Customer.customersArrayList.add(new Customer(id, name, phone, gender));
// Close this screen. MainActivity is still underneath and reappears.
finish();
}
});
}
}How it works:
- Spinner: an
ArrayAdapterturns theString[]into one row per option.simple_spinner_itemis the built-in look of the closed spinner, andsimple_spinner_dropdown_itemthat of the open list. - Listener:
onClickruns when Add Customer is tapped. - Validation: every empty field is marked with
setError, which shows a red icon and a message on the field. If anything is missing,returnstops before saving. The user can't add a customer with missing data. - Save and close: the customer is added to
Customer.customersArrayListandfinish()closes the form. There is no new Intent back toMainActivity, because it is still on the back stack underneath.
Step 6: The main screen (built in code)
MainActivity doesn't use an XML layout at all. It creates every view with new, nests them, and hands the root to setContentView:
text
LinearLayout (vertical) ← rootLinearLayout
├── Button "Add Customer" ← addButton
└── ScrollView
└── LinearLayout (vertical) ← customersLinearLayout, one TextView per customerOpen MainActivity.java and replace the whole file with:
java
package edu.birzeit.encs5150.exp2;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
// A field, because onResume() refills it every time the screen comes back.
private LinearLayout customersLinearLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. Create the views.
LinearLayout rootLinearLayout = new LinearLayout(this);
rootLinearLayout.setOrientation(LinearLayout.VERTICAL);
Button addButton = new Button(this);
addButton.setText("Add Customer");
addButton.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
ScrollView scrollView = new ScrollView(this);
customersLinearLayout = new LinearLayout(this);
customersLinearLayout.setOrientation(LinearLayout.VERTICAL);
// 2. Build the hierarchy: root -> [button, scrollView -> customers list].
rootLinearLayout.addView(addButton);
scrollView.addView(customersLinearLayout);
rootLinearLayout.addView(scrollView);
// 3. Show it. No XML layout is used for this screen.
setContentView(rootLinearLayout);
// Keep the content clear of the status and navigation bars, plus a 16dp margin.
int margin = (int) (16 * getResources().getDisplayMetrics().density);
ViewCompat.setOnApplyWindowInsetsListener(rootLinearLayout, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left + margin, systemBars.top + margin,
systemBars.right + margin, systemBars.bottom + margin);
return insets;
});
// 4. Open AddCustomerActivity with an explicit Intent.
addButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, AddCustomerActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onResume() {
super.onResume();
// Called on first launch and every time we return from AddCustomerActivity.
customersLinearLayout.removeAllViews();
if (Customer.customersArrayList.isEmpty()) {
TextView emptyTextView = new TextView(this);
emptyTextView.setText("No customers yet. Tap Add Customer.");
customersLinearLayout.addView(emptyTextView);
return;
}
for (Customer customer : Customer.customersArrayList) {
TextView customerTextView = new TextView(this);
// AppCompat's styles live in androidx.appcompat.R, not in your app's R.
customerTextView.setTextAppearance(androidx.appcompat.R.style.TextAppearance_AppCompat_Medium);
customerTextView.setText(customer.toString());
customerTextView.setPadding(0, 0, 0, 24);
customersLinearLayout.addView(customerTextView);
}
}
}Remove the template lines
The template's onCreate contains these lines. They must not be in your file:
java
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { … });R.id.main only exists inside activity_main.xml, which this screen no longer loads. findViewById would return null and the app would crash on launch. The code above attaches the same kind of insets listener to rootLinearLayout instead. That listener is still needed because Android 15 and later always draw apps behind the status bar.
You may delete res/layout/activity_main.xml, since nothing uses it anymore. Leaving it there does no harm.
How it works:
onCreatebuilds the view tree once: it creates the views, sets their properties (setOrientation,setText,setLayoutParams), and nests them withaddView. AScrollViewcan only hold one child, so the customers go into a verticalLinearLayoutinside it.LayoutParamsis the code equivalent oflayout_width/layout_heightin XML.- The button's listener starts
AddCustomerActivitywith an explicit Intent.MainActivityis not finished, so it stays underneath on the back stack. onResumeruns on first launch and every time the user comes back from the form. It clears the list withremoveAllViews()and adds oneTextViewper customer. If the list were filled inonCreate, new customers would never appear.
Run and check
- The app opens on the main screen with an Add Customer button and "No customers yet."
- Tapping Add Customer opens the form. Back returns to the main screen without adding anything.
- Tapping Add Customer on the empty form shows an error on Id, Name and Phone and stays on the form.
- The Id field accepts digits only, and the Phone field shows a dial pad.
- Filling in all fields and tapping Add Customer returns to the list, which shows the new customer.
- After adding several customers, the list scrolls and the button stays at the top.
- Pressing Back on the main screen exits the app in one press. With the manual's old design, you had to press Back once for every customer added.
Common problems
| Symptom | Cause and fix |
|---|---|
Crash on launch: NullPointerException … setOnApplyWindowInsetsListener in MainActivity | The template's findViewById(R.id.main) line is still in MainActivity. Remove it (see Step 6). |
Crash on launch: NullPointerException in AddCustomerActivity | The root of activity_add_customer.xml lost android:id="@+id/main", or a view ID doesn't match the Java code. |
ActivityNotFoundException: Unable to find explicit activity class | AddCustomerActivity is missing from AndroidManifest.xml. Create Activities with the wizard, or add the <activity> line by hand. |
cannot find symbol: variable TextAppearance_AppCompat_… | Library styles aren't in your app's R. Write androidx.appcompat.R.style.TextAppearance_AppCompat_Medium, not R.style.… (the old manual's line no longer compiles). |
| New customers don't appear in the list | The list is filled in onCreate instead of onResume. |
| Customers appear twice | removeAllViews() is missing at the start of onResume. |
| Button is hidden under the status bar | The insets listener on rootLinearLayout is missing. |
IllegalStateException: ScrollView can host only one direct child | Add views to customersLinearLayout, not to scrollView. |
| All customers disappear after a while | Expected: the list lives in memory and is cleared when Android kills the app. Exp 4 stores data in SQLite. |
Optional: WebView
A WebView is a widget that displays a web page inside your app. To try it, add one to a layout:
xml
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />Load a page from code:
java
WebView webView = findViewById(R.id.webView);
webView.loadUrl("https://ritaj.birzeit.edu");Then allow Internet access in AndroidManifest.xml, above the <application> element:
xml
<uses-permission android:name="android.permission.INTERNET" />Things to try
- Reject an Id that already exists in the list, with
setError. - Show a short confirmation such as "Customer added" before the form closes. Look up
Toast. - Move the Gender options into
res/values/strings.xmlas a<string-array>and load them withandroid:entriesinstead of Java. - Rebuild the add-customer form with a single
ConstraintLayoutand compare the two trees in the Layout Inspector.