Mehdi Tmimi Mobile

Mobile Development Lab 1 Part 8 of 8

A second screen: Intents

Tap a card. Android must open another screen, and hand it the note.

The emulator at the end of Part 7: the notes screen with three cards, Exam: the lifecycle, Ideas for the app and Saturday, and the pill 3.
Where you start

Before you start

By the end you can

  • pass a String and an int to a second activity;
  • come back with Back or finish(); say what survives a rotation;
  • share a note with an implicit Intent; say who picks the app.
  • Android Studio Project
  • Layout Split
  • Java
  • Manifest
  • Emulator
  • Logcat
Behind, or a fresh project? Start from here about 4 min
  1. The project: your own from Part 7, or a fresh one: Part 0, steps 0.3 and 0.4 (about 5 minutes).

  2. Your package name, for the code of this page:

    Without JavaScript, the code says com.yourname.notes: replace yourname by the name you chose.

  3. Gradle: check these 3 lines. Add any that is missing, then click Sync Now. Keep your own version numbers.

    Gradle Scripts › libs.versions.tomlAdd the marked lines
    [versions]agp = "9.5.0"junit = "4.13.2"junitVersion = "1.3.0"espressoCore = "3.7.0"appcompat = "1.8.0"material = "1.14.0"constraintlayout = "2.2.2"
    [libraries]junit = { group = "junit", name = "junit", version.ref = "junit" }ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }material = { group = "com.google.android.material", name = "material", version.ref = "material" }constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
    [plugins]android-application = { id = "com.android.application", version.ref = "agp" }
    Gradle Scripts › build.gradle.kts (Module :app)Add the marked lines
    plugins {    alias(libs.plugins.android.application)}
    android { namespace = "com.yourname.notes" compileSdk { version = release(37) }
    defaultConfig { applicationId = "com.yourname.notes" minSdk = 24 targetSdk = 37 versionCode = 1 versionName = "1.0"
    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
    buildTypes { release { optimization { enable = true packageScope = setOf("androidx.**", "kotlin.**", "kotlinx.**") } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 }}
    dependencies { implementation(libs.appcompat) implementation(libs.material) implementation(libs.constraintlayout) testImplementation(libs.junit) androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.ext.junit)}
  4. Restarting this part? First delete NoteActivity.java and res/layout/activity_note.xml. If Android Studio shows Conflicts Detected, click Refactor Anyway.

  5. res and manifest: download the pack and open it in Explorer. Drag its app folder onto your project folder (C:\Users\<you>\AndroidStudioProjects\Notes), then choose Replace the files in the destination. Not Extract All: it makes a folder inside and replaces nothing. Then, in res › values › themes, delete themes.xml (night) if it is still there.

    lab1-part8-res.zip zip · 8.7 KB
  6. Java: open MainActivity.java, select all, then paste this file. Missing? First right-click kotlin+java › com.yourname.notes, the one without (test), then NewJava Class: type MainActivity, then press Enter.

    kotlin+java › com.yourname.notes › MainActivity.javaReplace the whole file
    package com.yourname.notes;
    import android.os.Bundle;import android.text.Editable;import android.text.TextWatcher;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.LinearLayout;import android.widget.TextView;import android.widget.Toast;
    import androidx.appcompat.app.AppCompatActivity;
    public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Lifecycle"; private static final int[] NOTE_BACKGROUNDS = { R.drawable.bg_note_butter, R.drawable.bg_note_mint, R.drawable.bg_note_lavender, R.drawable.bg_note_peach };
    private TextView textCount; private TextView textEmpty; private LinearLayout notesContainer; private int notesAdded = 0;
    private EditText editTitle; private EditText editBody; private TextView textError; private TextView textCounter;
    private final TextWatcher formWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
    @Override public void onTextChanged(CharSequence s, int start, int before, int count) { }
    @Override public void afterTextChanged(Editable s) { updateForm(); } };
    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG, "Main onCreate");
    textCount = findViewById(R.id.textCount); textEmpty = findViewById(R.id.textEmpty); notesContainer = findViewById(R.id.notesContainer);
    addNote(getString(R.string.seed_title_1), getString(R.string.seed_body_1)); addNote(getString(R.string.seed_title_2), getString(R.string.seed_body_2)); addNote(getString(R.string.seed_title_3), getString(R.string.seed_body_3)); updateCount();
    editTitle = findViewById(R.id.editTitle); editBody = findViewById(R.id.editBody); textError = findViewById(R.id.textError); textCounter = findViewById(R.id.textCounter);
    Button buttonAdd = findViewById(R.id.buttonAdd); buttonAdd.setOnClickListener(v -> addNoteFromComposer());
    editTitle.addTextChangedListener(formWatcher); editBody.addTextChangedListener(formWatcher); }
    private void updateCount() { int count = notesContainer.getChildCount(); textCount.setText(String.valueOf(count)); textEmpty.setVisibility(count == 0 ? View.VISIBLE : View.GONE); }
    private void addNote(String title, String body) { View card = LayoutInflater.from(this).inflate(R.layout.item_note, notesContainer, false); TextView cardTitle = card.findViewById(R.id.textNoteTitle); TextView cardBody = card.findViewById(R.id.textNoteBody); cardTitle.setText(title); cardBody.setText(body); cardBody.setVisibility(body.isEmpty() ? View.GONE : View.VISIBLE); int background = NOTE_BACKGROUNDS[notesAdded % NOTE_BACKGROUNDS.length]; card.setBackgroundResource(background); notesAdded++;
    card.setOnLongClickListener(v -> { notesContainer.removeView(card); updateCount(); Toast.makeText(this, R.string.note_deleted, Toast.LENGTH_SHORT).show(); return true; });
    notesContainer.addView(card, 0); }
    private void addNoteFromComposer() { String title = editTitle.getText().toString().trim(); String body = editBody.getText().toString().trim(); if (title.isEmpty()) { textError.setVisibility(View.VISIBLE); return; } textError.setVisibility(View.GONE); addNote(title, body); updateCount(); editTitle.setText(""); editBody.setText(""); Toast.makeText(this, R.string.note_added, Toast.LENGTH_SHORT).show(); }
    private void updateForm() { int length = editBody.length(); textCounter.setText(getString(R.string.counter, length)); int colour = length >= 120 ? R.color.error : R.color.muted; textCounter.setTextColor(getColor(colour)); if (editTitle.length() > 0) { textError.setVisibility(View.GONE); } }
    public void onClearClicked(View view) { editTitle.setText(""); editBody.setText(""); }
    @Override protected void onStart() { super.onStart(); Log.d(TAG, "Main onStart"); }
    @Override protected void onResume() { super.onResume(); Log.d(TAG, "Main onResume"); }
    @Override protected void onPause() { super.onPause(); Log.d(TAG, "Main onPause"); }
    @Override protected void onStop() { super.onStop(); Log.d(TAG, "Main onStop"); }
    @Override protected void onRestart() { super.onRestart(); Log.d(TAG, "Main onRestart"); }
    @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Main onDestroy"); }}
  7. ▶. You are ready when the phone shows three cards, and holding one deletes it.

Step 1 of 6

Create the note page, in three pieces

8 min

Goal Give the note page its layout, class and manifest line.

  • Resources res › values › strings.xml
  • Layout res › layout › activity_note.xml · Split
  • Java kotlin+java › com.yourname.notes › NoteActivity.java
  • Manifest manifests › AndroidManifest.xml
  1. Paste The page’s 3 texts, in strings.xml.

    1. In Project, open res › values › strings.xml.
    2. In the panel below, click Copy 3 lines.
    3. Click at the end of line 22, then press Enter.
    4. Press Ctrl+V: lines 23 to 25 appear.
    res › values › strings.xmlAdd the marked lines
    <resources>    <string name="app_name">Notes</string>    <string name="logo_description">Notes logo</string>    <string name="count_start">0</string>    <string name="title_hint">Title</string>    <string name="body_hint">Write something…</string>    <string name="error_no_title">Give your note a title first.</string>    <string name="counter_start">0 / 140</string>    <string name="counter">%1$d / 140</string>    <string name="clear">Clear</string>    <string name="add">Add</string>    <string name="your_notes">YOUR NOTES</string>    <string name="list_hint">Tap to open · hold to delete</string>    <string name="empty">No notes yet.\nWrite your first one above.</string>    <string name="note_added">Note added</string>    <string name="note_deleted">Note deleted</string>    <string name="seed_title_1">Saturday</string>    <string name="seed_body_1">Football at 10, then revise ConstraintLayout.</string>    <string name="seed_title_2">Ideas for the app</string>    <string name="seed_body_2">Pastel cards, a Share button, maybe a photo one day. Then save the notes, so that turning the phone loses nothing.</string>    <string name="seed_title_3">Exam: the lifecycle</string>    <string name="seed_body_3">onCreate, onStart, onResume… then onPause, onStop, onDestroy.</string>    <string name="note_kicker">NOTE</string>    <string name="back_to_notes">Back to notes</string>    <string name="share">Share</string></resources>
  2. Paste The layout activity_note.xml, a new file (step 2.1 made the first one).

    1. In Project, right-click res › layout.
    2. Choose NewLayout Resource File.
    3. A New Resource File window opens: in File name, type activity_note, then click OK.
    4. It opens in Design: click Code, at the top right of the editor.
    5. Click Copy the file, on the panel below.
    6. Click in the code, press Ctrl+A, then Ctrl+V.
    res › layout › activity_note.xmlReplace the whole file
    <?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"    android:fitsSystemWindows="true"    tools:context=".NoteActivity">
    <LinearLayout android:id="@+id/noteSheet" android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="16dp" android:background="@drawable/bg_note_butter" android:orientation="vertical" android:padding="24dp" app:layout_constraintBottom_toTopOf="@id/buttonShare" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent">
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:letterSpacing="0.1" android:text="@string/note_kicker" android:textColor="@color/muted" android:textSize="12sp" android:textStyle="bold" />
    <TextView android:id="@+id/textTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:fontFamily="sans-serif-black" android:textColor="@color/ink" android:textSize="24sp" tools:text="Ideas for the app" />
    <TextView android:id="@+id/textBody" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:lineSpacingMultiplier="1.25" android:textColor="@color/ink" android:textSize="18sp" tools:text="Pastel cards, a Share button, maybe a photo one day." /> </LinearLayout>
    <Button android:id="@+id/buttonShare" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" android:text="@string/share" app:cornerRadius="12dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" />
    <Button android:id="@+id/buttonBack" style="@style/Widget.Material3.Button.OutlinedButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:text="@string/back_to_notes" app:cornerRadius="12dp" app:layout_constraintBaseline_toBaselineOf="@id/buttonShare" app:layout_constraintEnd_toStartOf="@id/buttonShare" />
    </androidx.constraintlayout.widget.ConstraintLayout>

    NoteActivity reads 4 of the marked ids now; buttonShare from step 8.5. tools:text fills the preview only.

  3. Paste The class NoteActivity, which receives the note.

    1. In Project, right-click the plain kotlin+java › com.yourname.notes, without (test) or (androidTest).
    2. Choose NewJava Class.
    3. A New Java Class popup opens: type NoteActivity, then press Enter.
    4. The class opens: click Copy the file, on the panel below.
    5. Click in the class, press Ctrl+A, then Ctrl+V.
    kotlin+java › com.yourname.notes › NoteActivity.javaReplace the whole file
    package com.yourname.notes;
    import android.os.Bundle;import android.util.Log;import android.widget.TextView;
    import androidx.appcompat.app.AppCompatActivity;
    public class NoteActivity extends AppCompatActivity {
    public static final String EXTRA_TITLE = "com.yourname.notes.EXTRA_TITLE"; public static final String EXTRA_BODY = "com.yourname.notes.EXTRA_BODY"; public static final String EXTRA_BACKGROUND = "com.yourname.notes.EXTRA_BACKGROUND"; private static final String TAG = "Lifecycle";
    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_note); Log.d(TAG, "Note onCreate");
    String title = getIntent().getStringExtra(EXTRA_TITLE); String body = getIntent().getStringExtra(EXTRA_BODY); int background = getIntent().getIntExtra(EXTRA_BACKGROUND, R.drawable.bg_note_butter);
    TextView textTitle = findViewById(R.id.textTitle); TextView textBody = findViewById(R.id.textBody); textTitle.setText(title); textBody.setText(body); findViewById(R.id.noteSheet).setBackgroundResource(background);
    findViewById(R.id.buttonBack).setOnClickListener(v -> finish()); }
    @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Note onDestroy"); }}

    An Intent, the message that opens a screen, carries values under keys: its extras. This class reads 3, which step 8.2 sends. Back to notes calls finish(): the page closes.

  4. Paste The manifest line that declares the page.

    1. In Project, open manifests › AndroidManifest.xml.
    2. In the panel below, click Copy 3 lines.
    3. Click at the end of line 21, </activity>, then press Enter.
    4. Press Ctrl+V: lines 22 to 24 appear.
    manifests › AndroidManifest.xmlAdd the marked lines
    <?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools">
    <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Notes"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".NoteActivity" android:exported="false" /> </application>
    </manifest>

    exported="false": only your own app may open this screen.

  5. The note page’s preview.

    1. Click the activity_note.xml tab.
    2. Click Split, at the top right: the preview shows.
    activity_note.xml · preview Layout editor · centreFull size
    The preview of activity_note.xml: a butter sheet with NOTE, Ideas for the app and Pastel cards, a Share button, maybe a photo one day; at the bottom right, Back to notes (outlined) and Share (orange).

Done I see the preview, and no red code.

Stuck? 3 symptoms · the files
  • @string/note_kicker is red, and the preview shows an error The 3 strings are missing. Do action a again.
  • In the manifest, .NoteActivity stays red NoteActivity.java is not in the plain package. Drag it onto the plain com.yourname.notes.
  • No preview beside the code Click Split, at the top right of the editor.

The four panels above hold the files at the end of this step: click Whole file, then Copy the file.

Step 2 of 6

Open the note page with an Intent

7 min

Goal Make a tap on a card open its note page.

  • Java kotlin+java › com.yourname.notes › MainActivity.java
  • Emulator
  1. Type A tap on a card calls openNote().

    1. Open kotlin+java › com.yourname.notes › MainActivity.java.
    2. In addNote(), click in the empty line above card.setOnLongClickListener, then press Enter.
    3. Completion lists open as you type: keep typing.
    4. Type the marked line. openNote stays red until action b writes it.
    kotlin+java › com.yourname.notes › MainActivity.javaAdd the marked lines
    package com.yourname.notes;
    import android.content.Intent;import android.os.Bundle;import android.text.Editable;import android.text.TextWatcher;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.LinearLayout;import android.widget.TextView;import android.widget.Toast;
    import androidx.appcompat.app.AppCompatActivity;
    public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Lifecycle"; private static final int[] NOTE_BACKGROUNDS = { R.drawable.bg_note_butter, R.drawable.bg_note_mint, R.drawable.bg_note_lavender, R.drawable.bg_note_peach };
    private TextView textCount; private TextView textEmpty; private LinearLayout notesContainer; private int notesAdded = 0;
    private EditText editTitle; private EditText editBody; private TextView textError; private TextView textCounter;
    private final TextWatcher formWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
    @Override public void onTextChanged(CharSequence s, int start, int before, int count) { }
    @Override public void afterTextChanged(Editable s) { updateForm(); } };
    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG, "Main onCreate");
    textCount = findViewById(R.id.textCount); textEmpty = findViewById(R.id.textEmpty); notesContainer = findViewById(R.id.notesContainer);
    addNote(getString(R.string.seed_title_1), getString(R.string.seed_body_1)); addNote(getString(R.string.seed_title_2), getString(R.string.seed_body_2)); addNote(getString(R.string.seed_title_3), getString(R.string.seed_body_3)); updateCount();
    editTitle = findViewById(R.id.editTitle); editBody = findViewById(R.id.editBody); textError = findViewById(R.id.textError); textCounter = findViewById(R.id.textCounter);
    Button buttonAdd = findViewById(R.id.buttonAdd); buttonAdd.setOnClickListener(v -> addNoteFromComposer());
    editTitle.addTextChangedListener(formWatcher); editBody.addTextChangedListener(formWatcher); }
    private void updateCount() { int count = notesContainer.getChildCount(); textCount.setText(String.valueOf(count)); textEmpty.setVisibility(count == 0 ? View.VISIBLE : View.GONE); }
    private void addNote(String title, String body) { View card = LayoutInflater.from(this).inflate(R.layout.item_note, notesContainer, false); TextView cardTitle = card.findViewById(R.id.textNoteTitle); TextView cardBody = card.findViewById(R.id.textNoteBody); cardTitle.setText(title); cardBody.setText(body); cardBody.setVisibility(body.isEmpty() ? View.GONE : View.VISIBLE); int background = NOTE_BACKGROUNDS[notesAdded % NOTE_BACKGROUNDS.length]; card.setBackgroundResource(background); notesAdded++;
    card.setOnClickListener(v -> openNote(title, body, background)); card.setOnLongClickListener(v -> { notesContainer.removeView(card); updateCount(); Toast.makeText(this, R.string.note_deleted, Toast.LENGTH_SHORT).show(); return true; });
    notesContainer.addView(card, 0); }
    private void addNoteFromComposer() { String title = editTitle.getText().toString().trim(); String body = editBody.getText().toString().trim(); if (title.isEmpty()) { textError.setVisibility(View.VISIBLE); return; } textError.setVisibility(View.GONE); addNote(title, body); updateCount(); editTitle.setText(""); editBody.setText(""); Toast.makeText(this, R.string.note_added, Toast.LENGTH_SHORT).show(); }
    private void updateForm() { int length = editBody.length(); textCounter.setText(getString(R.string.counter, length)); int colour = length >= 120 ? R.color.error : R.color.muted; textCounter.setTextColor(getColor(colour)); if (editTitle.length() > 0) { textError.setVisibility(View.GONE); } }
    public void onClearClicked(View view) { editTitle.setText(""); editBody.setText(""); }
    private void openNote(String title, String body, int background) { Intent intent = new Intent(this, NoteActivity.class); intent.putExtra(NoteActivity.EXTRA_TITLE, title); intent.putExtra(NoteActivity.EXTRA_BODY, body); intent.putExtra(NoteActivity.EXTRA_BACKGROUND, background); startActivity(intent); }
    @Override protected void onStart() { super.onStart(); Log.d(TAG, "Main onStart"); }
    @Override protected void onResume() { super.onResume(); Log.d(TAG, "Main onResume"); }
    @Override protected void onPause() { super.onPause(); Log.d(TAG, "Main onPause"); }
    @Override protected void onStop() { super.onStop(); Log.d(TAG, "Main onStop"); }
    @Override protected void onRestart() { super.onRestart(); Log.d(TAG, "Main onRestart"); }
    @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Main onDestroy"); }}

    Each card’s lambda keeps that card’s own title, body and background.

  2. Type The method openNote(), which sends the note in an Intent.

    1. Click right after the } that ends onClearClicked(), at the end of its line.
    2. Press Enter twice: the caret is now where private void openNote(… begins.
    3. Completion lists open as you type: keep typing.
    4. Type from private void openNote(… down to startActivity(intent);. The method’s closing } arrives by itself: do not type it.
    5. If Intent is red, click it, then press Alt+Enter.
    6. If a list opens, choose Import class: the import arrives on line 3.
    kotlin+java › com.yourname.notes › MainActivity.javaAdd the marked lines
    package com.yourname.notes;
    import android.content.Intent;import android.os.Bundle;import android.text.Editable;import android.text.TextWatcher;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.LinearLayout;import android.widget.TextView;import android.widget.Toast;
    import androidx.appcompat.app.AppCompatActivity;
    public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Lifecycle"; private static final int[] NOTE_BACKGROUNDS = { R.drawable.bg_note_butter, R.drawable.bg_note_mint, R.drawable.bg_note_lavender, R.drawable.bg_note_peach };
    private TextView textCount; private TextView textEmpty; private LinearLayout notesContainer; private int notesAdded = 0;
    private EditText editTitle; private EditText editBody; private TextView textError; private TextView textCounter;
    private final TextWatcher formWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
    @Override public void onTextChanged(CharSequence s, int start, int before, int count) { }
    @Override public void afterTextChanged(Editable s) { updateForm(); } };
    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG, "Main onCreate");
    textCount = findViewById(R.id.textCount); textEmpty = findViewById(R.id.textEmpty); notesContainer = findViewById(R.id.notesContainer);
    addNote(getString(R.string.seed_title_1), getString(R.string.seed_body_1)); addNote(getString(R.string.seed_title_2), getString(R.string.seed_body_2)); addNote(getString(R.string.seed_title_3), getString(R.string.seed_body_3)); updateCount();
    editTitle = findViewById(R.id.editTitle); editBody = findViewById(R.id.editBody); textError = findViewById(R.id.textError); textCounter = findViewById(R.id.textCounter);
    Button buttonAdd = findViewById(R.id.buttonAdd); buttonAdd.setOnClickListener(v -> addNoteFromComposer());
    editTitle.addTextChangedListener(formWatcher); editBody.addTextChangedListener(formWatcher); }
    private void updateCount() { int count = notesContainer.getChildCount(); textCount.setText(String.valueOf(count)); textEmpty.setVisibility(count == 0 ? View.VISIBLE : View.GONE); }
    private void addNote(String title, String body) { View card = LayoutInflater.from(this).inflate(R.layout.item_note, notesContainer, false); TextView cardTitle = card.findViewById(R.id.textNoteTitle); TextView cardBody = card.findViewById(R.id.textNoteBody); cardTitle.setText(title); cardBody.setText(body); cardBody.setVisibility(body.isEmpty() ? View.GONE : View.VISIBLE); int background = NOTE_BACKGROUNDS[notesAdded % NOTE_BACKGROUNDS.length]; card.setBackgroundResource(background); notesAdded++;
    card.setOnClickListener(v -> openNote(title, body, background)); card.setOnLongClickListener(v -> { notesContainer.removeView(card); updateCount(); Toast.makeText(this, R.string.note_deleted, Toast.LENGTH_SHORT).show(); return true; });
    notesContainer.addView(card, 0); }
    private void addNoteFromComposer() { String title = editTitle.getText().toString().trim(); String body = editBody.getText().toString().trim(); if (title.isEmpty()) { textError.setVisibility(View.VISIBLE); return; } textError.setVisibility(View.GONE); addNote(title, body); updateCount(); editTitle.setText(""); editBody.setText(""); Toast.makeText(this, R.string.note_added, Toast.LENGTH_SHORT).show(); }
    private void updateForm() { int length = editBody.length(); textCounter.setText(getString(R.string.counter, length)); int colour = length >= 120 ? R.color.error : R.color.muted; textCounter.setTextColor(getColor(colour)); if (editTitle.length() > 0) { textError.setVisibility(View.GONE); } }
    public void onClearClicked(View view) { editTitle.setText(""); editBody.setText(""); }
    private void openNote(String title, String body, int background) { Intent intent = new Intent(this, NoteActivity.class); intent.putExtra(NoteActivity.EXTRA_TITLE, title); intent.putExtra(NoteActivity.EXTRA_BODY, body); intent.putExtra(NoteActivity.EXTRA_BACKGROUND, background); startActivity(intent); }
    @Override protected void onStart() { super.onStart(); Log.d(TAG, "Main onStart"); }
    @Override protected void onResume() { super.onResume(); Log.d(TAG, "Main onResume"); }
    @Override protected void onPause() { super.onPause(); Log.d(TAG, "Main onPause"); }
    @Override protected void onStop() { super.onStop(); Log.d(TAG, "Main onStop"); }
    @Override protected void onRestart() { super.onRestart(); Log.d(TAG, "Main onRestart"); }
    @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Main onDestroy"); }}

    Whole file shows line 3 in grey: Android Studio writes it.

  3. The note page, on the phone.

    1. Click ▶ Run 'app' (↻ Rerun if the app is running).
    2. On the phone, tap Ideas for the app.
The emulator: the note page of Ideas for the app, on a mint sheet with its whole text, and Back to notes and Share at the bottom.

You should see The note page, mint like its card, its text whole.

Done I see the note page in its card’s colour.

Why new Intent(this, NoteActivity.class) names the screen to open: an explicit Intent. Its keys are NoteActivity’s constants, whose values start with your package: Android’s rule for your own extras.

Stuck? 4 symptoms · the file
  • local variables referenced from a lambda expression must be final or effectively final You changed title or body inside addNote(). Leave them as they arrive.
  • The app closes when you tap a card The manifest lacks NoteActivity’s lines. Do step 8.1, action d, again.
  • Intent stays red Its import is missing. Click Intent, then press Alt+Enter.
  • error: cannot find symbol · symbol: class NoteActivity NoteActivity.java is not in the plain package. Drag it onto the plain com.yourname.notes.

The files at the end of this step

kotlin+java › com.yourname.notes › MainActivity.javaReplace the whole file
package com.yourname.notes;
import android.content.Intent;import android.os.Bundle;import android.text.Editable;import android.text.TextWatcher;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.LinearLayout;import android.widget.TextView;import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "Lifecycle"; private static final int[] NOTE_BACKGROUNDS = { R.drawable.bg_note_butter, R.drawable.bg_note_mint, R.drawable.bg_note_lavender, R.drawable.bg_note_peach };
private TextView textCount; private TextView textEmpty; private LinearLayout notesContainer; private int notesAdded = 0;
private EditText editTitle; private EditText editBody; private TextView textError; private TextView textCounter;
private final TextWatcher formWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override public void onTextChanged(CharSequence s, int start, int before, int count) { }
@Override public void afterTextChanged(Editable s) { updateForm(); } };
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(TAG, "Main onCreate");
textCount = findViewById(R.id.textCount); textEmpty = findViewById(R.id.textEmpty); notesContainer = findViewById(R.id.notesContainer);
addNote(getString(R.string.seed_title_1), getString(R.string.seed_body_1)); addNote(getString(R.string.seed_title_2), getString(R.string.seed_body_2)); addNote(getString(R.string.seed_title_3), getString(R.string.seed_body_3)); updateCount();
editTitle = findViewById(R.id.editTitle); editBody = findViewById(R.id.editBody); textError = findViewById(R.id.textError); textCounter = findViewById(R.id.textCounter);
Button buttonAdd = findViewById(R.id.buttonAdd); buttonAdd.setOnClickListener(v -> addNoteFromComposer());
editTitle.addTextChangedListener(formWatcher); editBody.addTextChangedListener(formWatcher); }
private void updateCount() { int count = notesContainer.getChildCount(); textCount.setText(String.valueOf(count)); textEmpty.setVisibility(count == 0 ? View.VISIBLE : View.GONE); }
private void addNote(String title, String body) { View card = LayoutInflater.from(this).inflate(R.layout.item_note, notesContainer, false); TextView cardTitle = card.findViewById(R.id.textNoteTitle); TextView cardBody = card.findViewById(R.id.textNoteBody); cardTitle.setText(title); cardBody.setText(body); cardBody.setVisibility(body.isEmpty() ? View.GONE : View.VISIBLE); int background = NOTE_BACKGROUNDS[notesAdded % NOTE_BACKGROUNDS.length]; card.setBackgroundResource(background); notesAdded++;
card.setOnClickListener(v -> openNote(title, body, background)); card.setOnLongClickListener(v -> { notesContainer.removeView(card); updateCount(); Toast.makeText(this, R.string.note_deleted, Toast.LENGTH_SHORT).show(); return true; });
notesContainer.addView(card, 0); }
private void addNoteFromComposer() { String title = editTitle.getText().toString().trim(); String body = editBody.getText().toString().trim(); if (title.isEmpty()) { textError.setVisibility(View.VISIBLE); return; } textError.setVisibility(View.GONE); addNote(title, body); updateCount(); editTitle.setText(""); editBody.setText(""); Toast.makeText(this, R.string.note_added, Toast.LENGTH_SHORT).show(); }
private void updateForm() { int length = editBody.length(); textCounter.setText(getString(R.string.counter, length)); int colour = length >= 120 ? R.color.error : R.color.muted; textCounter.setTextColor(getColor(colour)); if (editTitle.length() > 0) { textError.setVisibility(View.GONE); } }
public void onClearClicked(View view) { editTitle.setText(""); editBody.setText(""); }
private void openNote(String title, String body, int background) { Intent intent = new Intent(this, NoteActivity.class); intent.putExtra(NoteActivity.EXTRA_TITLE, title); intent.putExtra(NoteActivity.EXTRA_BODY, body); intent.putExtra(NoteActivity.EXTRA_BACKGROUND, background); startActivity(intent); }
@Override protected void onStart() { super.onStart(); Log.d(TAG, "Main onStart"); }
@Override protected void onResume() { super.onResume(); Log.d(TAG, "Main onResume"); }
@Override protected void onPause() { super.onPause(); Log.d(TAG, "Main onPause"); }
@Override protected void onStop() { super.onStop(); Log.d(TAG, "Main onStop"); }
@Override protected void onRestart() { super.onRestart(); Log.d(TAG, "Main onRestart"); }
@Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Main onDestroy"); }}

Step 3 of 6

Come back, and read Logcat

5 min

Goal Come back; read the round trip in Logcat.

  • Emulator
  • Logcat package:mine tag:Lifecycle

Predict first

Open a note, then come back: what happens to the notes screen?

See the answer

It stops (Main onStop) and waits under the note page: the back stack. finish() destroys the note page (Note onDestroy), and the notes screen comes back as you left it.

  1. The list as you leave it: one more note, one title typed but not added.

    1. On the phone, tap Back to notes: the list of notes comes back.
    2. Type Groceries in Title, then tap Add.
    3. Type Call Yasmine in Title, without tapping Add.
    4. Close the keyboard with its down arrow, at the bottom left of the phone.
  2. One clean round trip, in Logcat.

    1. In Logcat, at the bottom of the window, the query must read package:mine tag:Lifecycle: type it if not.
    2. Click the trash icon, Clear Logcat, at the top of Logcat’s left column.
    3. On the phone, tap the Groceries card: its note page opens.
    4. Tap Back to notes.
You should see10-31 19:00:23.570  7176  7176 D Lifecycle: Main onPause
10-31 19:00:23.595  7176  7176 D Lifecycle: Note onCreate
10-31 19:00:24.338  7176  7176 D Lifecycle: Main onStop
10-31 19:00:30.111  7176  7176 D Lifecycle: Main onRestart
10-31 19:00:30.112  7176  7176 D Lifecycle: Main onStart
10-31 19:00:30.112  7176  7176 D Lifecycle: Main onResume
10-31 19:00:30.698  7176  7176 D Lifecycle: Note onDestroy

Your times and process numbers will differ.

The emulator, back on the notes screen: Groceries on top of the three cards, the pill 4, and Call Yasmine still typed in Title.

You should see The list as you left it.

Done I see Note onDestroy in Logcat.

Stuck? 2 symptoms
  • Logcat shows other apps’ lines, or none of yours Type package:mine tag:Lifecycle in Logcat’s query field, as in Part 5.
  • No Note onCreate line Logcat was cleared after you tapped the card. Clear it, then tap the card again.

No file changes in this step: steps 8.1 and 8.2 hold the files.

Step 4 of 6

Break it: hide the note page from the manifest

4 min

Goal See Android refuse an undeclared activity.

  • Manifest manifests › AndroidManifest.xml
  • Emulator
  • Logcat package:mine

Break it

Can Android open an activity the manifest forgot?

  1. The page’s 3 lines in the manifest, made a comment.

    1. Open manifests › AndroidManifest.xml.
    2. Click anywhere in line 22, <activity.
    3. Press Ctrl+/ three times, once a line: lines 22 to 24 become comments, <!-- … -->.
    manifests › AndroidManifest.xmlRead only
    <?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools">
    <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Notes"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".NoteActivity" android:exported="false" /> </application>
    </manifest>
  2. The app asks for the forgotten page.

    1. In Logcat’s query field, delete tag:Lifecycle: keep only package:mine.
    2. Click ▶ Run 'app' (↻ Rerun if the app is running).
    3. On the phone, tap a card.
  3. Put them back.

    1. In the manifest, click in line 22 again.
    2. Press Ctrl+/ three times: the comments go.
    3. Click ▶ Run 'app' (↻ Rerun if the app is running).
    4. On the phone, tap a card: its page opens again.
What happened

The app closes. In Logcat, in red:

You should see10-31 18:59:41.415  6726  6726 E AndroidRuntime: FATAL EXCEPTION: main
10-31 18:59:41.415  6726  6726 E AndroidRuntime: Process: com.yourname.notes, PID: 6726
10-31 18:59:41.415  6726  6726 E AndroidRuntime: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.yourname.notes/com.yourname.notes.NoteActivity}; have you declared this activity in your AndroidManifest.xml, or does your intent not match its declared <intent-filter>?

Your times and process numbers will differ.

startActivity() asks Android for NoteActivity, and Android opens only the activities a manifest declares.

Done I see ActivityNotFoundException, then a card opens again.

Stuck? 2 symptoms · the file
  • The app closes, but Logcat shows no red line The query still says tag:Lifecycle, which hides Android’s lines. Keep package:mine only.
  • A card still closes the app after action c The lines are still a comment (<!--). Paste the manifest’s lines of step 8.1 again.

The manifest at the end of this step is the one of step 8.1, action d.

Step 5 of 6

Share the note with an implicit Intent

6 min

Goal Make Share hand the note to an app Android finds.

  • Java kotlin+java › com.yourname.notes › NoteActivity.java
  • Emulator

Explicit · step 8.2: you name the screen

  1. MainActivity builds new Intent(this, NoteActivity.class)
  2. 3 extras the title, the text, the background
  3. Android opens NoteActivity on top of the list; Back or finish() removes it

Implicit · step 8.5: you name the action, Android chooses

  1. NoteActivity builds new Intent(Intent.ACTION_SEND)
  2. 1 extra the note’s text, of type text/plain
  3. Android lists the apps that accept it; the user picks one
Two kinds of Intent. Sharing goes through Android, as chapter 02 showed.
  1. Type The Share button calls share().

    1. Open kotlin+java › com.yourname.notes › NoteActivity.java.
    2. In onCreate(), click at the end of the buttonBack line, then press Enter.
    3. Completion lists open as you type: keep typing.
    4. Type the marked line. share stays red until action b writes it.
    kotlin+java › com.yourname.notes › NoteActivity.javaAdd the marked lines
    package com.yourname.notes;
    import android.content.Intent;import android.os.Bundle;import android.util.Log;import android.widget.TextView;
    import androidx.appcompat.app.AppCompatActivity;
    public class NoteActivity extends AppCompatActivity {
    public static final String EXTRA_TITLE = "com.yourname.notes.EXTRA_TITLE"; public static final String EXTRA_BODY = "com.yourname.notes.EXTRA_BODY"; public static final String EXTRA_BACKGROUND = "com.yourname.notes.EXTRA_BACKGROUND"; private static final String TAG = "Lifecycle";
    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_note); Log.d(TAG, "Note onCreate");
    String title = getIntent().getStringExtra(EXTRA_TITLE); String body = getIntent().getStringExtra(EXTRA_BODY); int background = getIntent().getIntExtra(EXTRA_BACKGROUND, R.drawable.bg_note_butter);
    TextView textTitle = findViewById(R.id.textTitle); TextView textBody = findViewById(R.id.textBody); textTitle.setText(title); textBody.setText(body); findViewById(R.id.noteSheet).setBackgroundResource(background);
    findViewById(R.id.buttonBack).setOnClickListener(v -> finish()); findViewById(R.id.buttonShare).setOnClickListener(v -> share(title, body)); }
    private void share(String title, String body) { Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); send.putExtra(Intent.EXTRA_TEXT, title + "\n" + body); startActivity(Intent.createChooser(send, null)); }
    @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Note onDestroy"); }}
  2. Type The method share(), which hands the note to Android.

    1. Click right after the } that ends onCreate(), at the end of its line.
    2. Press Enter twice: the caret is now where private void share(… begins.
    3. Completion lists open as you type: keep typing.
    4. Type from private void share(… down to startActivity(Intent.createChooser(send, null));. The method’s closing } arrives by itself: do not type it.
    5. If Intent is red, click it, then press Alt+Enter.
    6. If a list opens, choose Import class: the import arrives on line 3.
    kotlin+java › com.yourname.notes › NoteActivity.javaAdd the marked lines
    package com.yourname.notes;
    import android.content.Intent;import android.os.Bundle;import android.util.Log;import android.widget.TextView;
    import androidx.appcompat.app.AppCompatActivity;
    public class NoteActivity extends AppCompatActivity {
    public static final String EXTRA_TITLE = "com.yourname.notes.EXTRA_TITLE"; public static final String EXTRA_BODY = "com.yourname.notes.EXTRA_BODY"; public static final String EXTRA_BACKGROUND = "com.yourname.notes.EXTRA_BACKGROUND"; private static final String TAG = "Lifecycle";
    @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_note); Log.d(TAG, "Note onCreate");
    String title = getIntent().getStringExtra(EXTRA_TITLE); String body = getIntent().getStringExtra(EXTRA_BODY); int background = getIntent().getIntExtra(EXTRA_BACKGROUND, R.drawable.bg_note_butter);
    TextView textTitle = findViewById(R.id.textTitle); TextView textBody = findViewById(R.id.textBody); textTitle.setText(title); textBody.setText(body); findViewById(R.id.noteSheet).setBackgroundResource(background);
    findViewById(R.id.buttonBack).setOnClickListener(v -> finish()); findViewById(R.id.buttonShare).setOnClickListener(v -> share(title, body)); }
    private void share(String title, String body) { Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); send.putExtra(Intent.EXTRA_TEXT, title + "\n" + body); startActivity(Intent.createChooser(send, null)); }
    @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Note onDestroy"); }}

    Whole file shows line 3 in grey: Android Studio writes it. createChooser() asks Android for the share sheet.

  3. The share sheet, on the phone.

    1. Click ▶ Run 'app' (↻ Rerun if the app is running).
    2. On the phone, tap a card: its note page opens.
    3. Tap Share.
The emulator: a sheet over the note page, titled Sharing text, with the note’s title and text and a copy button, then a row of apps.

You should see A sheet, “Sharing text”, showing your note.

Done I see my note’s text in the sheet.

Stuck? 3 symptoms · the file
  • Share does nothing The buttonShare line is missing: line 34.
  • Intent stays red Its import is missing. Click Intent, then press Alt+Enter.
  • The sheet looks different from the picture Each phone lists its own apps. What matters is your note’s text at the top.

The files at the end of this step

kotlin+java › com.yourname.notes › NoteActivity.javaReplace the whole file
package com.yourname.notes;
import android.content.Intent;import android.os.Bundle;import android.util.Log;import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class NoteActivity extends AppCompatActivity {
public static final String EXTRA_TITLE = "com.yourname.notes.EXTRA_TITLE"; public static final String EXTRA_BODY = "com.yourname.notes.EXTRA_BODY"; public static final String EXTRA_BACKGROUND = "com.yourname.notes.EXTRA_BACKGROUND"; private static final String TAG = "Lifecycle";
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_note); Log.d(TAG, "Note onCreate");
String title = getIntent().getStringExtra(EXTRA_TITLE); String body = getIntent().getStringExtra(EXTRA_BODY); int background = getIntent().getIntExtra(EXTRA_BACKGROUND, R.drawable.bg_note_butter);
TextView textTitle = findViewById(R.id.textTitle); TextView textBody = findViewById(R.id.textBody); textTitle.setText(title); textBody.setText(body); findViewById(R.id.noteSheet).setBackgroundResource(background);
findViewById(R.id.buttonBack).setOnClickListener(v -> finish()); findViewById(R.id.buttonShare).setOnClickListener(v -> share(title, body)); }
private void share(String title, String body) { Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); send.putExtra(Intent.EXTRA_TEXT, title + "\n" + body); startActivity(Intent.createChooser(send, null)); }
@Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "Note onDestroy"); }}

Step 6 of 6

Turn the phone: what survives?

5 min

Goal Turn the phone, and see what survives.

  • Emulator Running Devices
  1. The list with five notes.

    1. In Running Devices, click Back until the list of notes shows.
    2. On the phone, type Groceries in Title, then tap Add.
    3. Type Call Yasmine in Title.
    4. Type About the project, before Friday. in Write something…, then tap Add.
  2. A title half typed.

    1. Type Half in Title, without tapping Add.
    2. Close the keyboard with its down arrow, at the bottom left of the phone.
  3. The phone turned, then upright again.

    1. In Running Devices, click Rotate Left: the phone turns on its side.
    2. Click Rotate Right: the phone stands upright again.
Before turning: five cards, Call Yasmine and Groceries on top of the three seeds, the pill 5, and Half typed in Title.
Before
Rotate
After turning back: the three seed cards only, the pill 3, and Half still in Title.
After

You should see After: the 3 seeds, pill 3, Half still in Title.

After the rotation, at real size: the pill 3 in the header, and Half in Title.
The pill and Title, at real size

Why Turning recreated the screen: onCreate() added the seeds again. A text field with an id keeps its text (chapter 03).

Stuck? 1 symptom
  • The screen does not turn Auto-rotate is off. Drag down twice from the top of the phone’s screen, then tap the rotation tile (two arrows around a phone) so it turns blue.

No file changes in this step: steps 8.1, 8.2 and 8.5 hold the files.

Now you

Turn the note page

Open a note and turn the phone. Does the note stay? Why?

See the answer

Yes. Android recreates the note page, and the new NoteActivity receives the same Intent: onCreate() reads the same extras again.

If it doesn’t work

  • The title is empty on the note page A key differs between the two classes: getStringExtra() then returns null. Use the constants, never a typed string.
  • error: class NoteActivity is public, should be declared in a file named NoteActivity.java The class name or its file name differs. Both must be NoteActivity.
  • Two “Notes” apps on the phone You ran lab1-final.zip, which has its own package name. Uninstall that one.

End of the part

Recap

An Intent names NoteActivity and carries the note; finish() brings the list back. Your notes live only in views: saving them comes next.

  • pass a String and an int to a second activity;
  • come back with Back or finish(); say what survives a rotation;
  • share a note with an implicit Intent; say who picks the app.
Your app at the end of Lab 1: the note page of Ideas for the app, mint, with Back to notes and Share.
Your app now

End of Lab 1

Nine parts, one app: what you can do now

  1. Part 01–2 h, home

    Before class: install and prepare

    I can install Android Studio, start a virtual phone and create my project.

  2. Part 118 min

    Find your way in the project

    I can read the five Gradle values and add a library.

  3. Part 226 min

    Your first screen, piece by piece

    I can write the three pieces of a screen and run it.

  4. Part 342–72 min

    Place the views with ConstraintLayout

    I can place views that keep their shape on any phone.

  5. Part 435 min

    Make it beautiful: resources

    I can define texts, colours, shapes and the theme once, in res/.

  6. Part 528 min

    From XML to Java: the activity and its life

    I can follow my activity’s life in Logcat.

  7. Part 637 min

    Java takes over the screen

    I can change views from Java and add note cards.

  8. Part 743 min

    The screen answers: events

    I can react to taps, long presses and typing.

  9. Part 835 min

    A second screen: Intents

    I can open a second screen with an Intent, and share a note.

Homework: one “Now you”, and one extension.

lab1-final.zip zip · 105 KB

The finished project, to compare with yours.

← Lab 1, all parts