Android Programming
Android Programming
of Contents
Introduction 1.1
Unit 1. Get started 1.2
1.1 P: Install Android Studio, Hello World, Logging 1.2.1
1.2 P: Make Your First Interactive UI 1.2.2
1.25 P: Working with TextView Elements 1.2.3
1.3 P: Create a Recycler View 1.2.4
1.4 P: Learning Resources 1.2.5
2.1 P: Create and Start Activities 1.2.6
2.2 P: Activity Lifecycle and State 1.2.7
2.3 P: Activities and Implicit Intents 1.2.8
3.1 P: Create an AsyncTask 1.2.9
3.2 P: Connect to the Internet 1.2.10
3.3 P: Using an AsyncTaskLoader 1.2.11
4.1P: Using the Debugger 1.2.12
4.2P: Testing your App 1.2.13
4.3P: Using Support Libraries 1.2.14
Unit 2. User experience 1.3
6.1 P: Use Keyboards, Input Controls, Alerts, and Pickers 1.3.1
6.2 P Use an Options Menu and Radio Buttons 1.3.2
6.3 PC: Tab Navigation 1.3.3
7.1 P: Themes, Custom Styles, Drawables 1.3.4
7.2 P: Material Design: Cards and the FAB 1.3.5
7.3 PC: Transitions and Animations 1.3.6
7.4 P: Supporting Landscape and Multiple Screen Sizes 1.3.7
8.1 P: Use Espresso to test your UI 1.3.8
Unit 3. All about data 1.4
9.0 P: Shared Preferences 1.4.1
9.1 P: SQLite Data Storage 1.4.2
9.2 P: Searching an SQLite Database 1.4.3
10.1 P: Implement a Minimalist Content Provider 1.4.4
10.2 P: Add a Content Provider to WordListSQL 1.4.5
10.3 P: Sharing content with other apps 1.4.6
11.1 P: Load and display data fetched from a content provider 1.4.7
Unit 4. Working in the background 1.5
12.1 P: Broadcast Receivers 1.5.1
12.3 P: Notifications 1.5.2
13.1 P: Alarm Manager 1.5.3
13.2 P: Job Scheduler 1.5.4
14.1 PC: Creating Widgets 1.5.5
Appendix Utilities 1.6
Android Developer Fundamentals
The general software development process for object-oriented applications using an IDE
(Integrated Development Environment).
At least 1-3 years of experience with object-oriented programming and the Java
programming language. (These practicals will not explain object-oriented programming or
the Java language.)
2. What you will NEED
A Mac, Windows, or Linux computer. See the bottom of the Android Studio download page
for up-to-date system requirements.
Internet access or an alternative way of loading the latest Android Studio and Java
installations onto your computer.
3. What you will LEARN
How to install and use the Android IDE.
The development process for building Android apps.
How to create an Android project from a basic app template.
4. What you will DO
Install the Android Studio development environment.
Create a an emulator (virtual device) to run your app on your computer.
Create and run the Hello World app on the virtual and physical devices.
Explore the project layout.
Generate and view log statements from your app.
Explore the AndroidManifest.xml file.
5. App Overview
After you successfully install the Android Studio IDE, you will create a new Android project for
the Hello World app from a template. This simple app displays the string Hello World on the
screen of the Android virtual or physical device. Heres what it should look like once you
successfully build it:
Note: Android Studio is continually being improved. For the latest information on system
requirements and installation instructions, refer to the documentation at [developer.android.com]
(http://developer.android.com/sdk/index.html).
To get up and running with Android Studio:
You may need to install the Java Development Kit - Java 7 or better.
Install Android Studio
Android Studio is available for Windows, Mac, and Linux computers. The installation is similar
for all platforms. Any differences will be noted.
Windows:
Mac:
1. Open Terminal.
2. Confirm you have JDK by typing which java. ...
3. Check you have the needed version of Java, by typing java -version.
4. Set JAVA_HOME using this command in Terminal: export
JAVA_HOME=$(/usr/libexec/java_home)
5. echo $JAVA_HOME on Terminal to confirm the path.
Linux:
https://docs.oracle.com/cd/E19182-01/820-7851/inst_cli_jdk_javahome_t/
Important: Do not move on with Android Studio install until after you have installed the
JDK. Without a working copy of Java, the rest of the process will not work. If you can't get
the download to work, look for error messages, and search online to find a solution.
Basic Troubleshooting:
There is no UI, Control Panel, or Startup icon associated with the JDK.
Verify that you have indeed installed the JDK by going to the directory where you installed
it. If you don't know where that is, look at your PATH variable and/or search your computer
for the "jdk" directory or the "java" or "javac" executable. </div>
Why: This task confirms your installation is correct, and familiarizes you with the Android Studio
workflow.
Your Android Studio window should look similar to the following diagram. You can look at the
hierarchy of the files of your app in multiple ways.
1. Click on the Hello World folder to expand the hierarchy of files (1),
2. Click on Project (2).
3. Click on the Android menu (3).
4. Explore the different view options for your project.
Note: This book uses the Android view of the project files, unless specified otherwise.
8. Task 3: Explore the project structure
In this practical, you will explore how the project files are organized in Android Studio.
Why: Being familiar with the project structure makes it easier to find and change files for later
exercises.
These steps assume that your Hello World project starts out as shown in the diagram above.
This folder contains AndroidManifest.xml. This file describes all the components of your
Android app and is read by the Android run-time system when your program is executed .
2. Expand the java folder. All your Java language files are organized in this folder. The java
folder contains three subfolders:
com.example.hello.helloworld (or whatever your domain was called): All the files
for a package are in a folder named after the package. For your Hello World
application, there is one package and it only contains MainActivity.java (the file
extension may be omitted in the Project view).
com.example.hello.helloworld(androidTest): This folder is for your instrumented
tests and starts out with a skeleton ApplicationTest.java file.
com.example.hello.helloworld(test): This folder is for your unit tests and starts out
with an automatically created skeleton unit test file.
3. Expand the res folder. This folder contains all the resources for your app, including images,
layout files, strings, icons, and styling. It includes these subfolders:
drawable. Store all your apps images in this folder.
layout. Every activity has at least one layout file that describes the UI in XML. For
Hello World, this folder contains activity_main.xml.
mipmap. Store your icons in this folder. There is a sub-folder for each supported
screen density. Android uses the screen density, that is, the number of pixels per inch
to determine the required image resolution. Android groups all actual screen densities
into generalized densities, such as medium (mdpi), high (hdpi), or extra-extra-extra-
high (xxxhdpi). The ic_launcher.png folder contains the default launcher icons for all the
densities supported by your app.
values. Instead of hardcoding values into your XML files, it is best practice to define
them in their respective values file. This makes it easier to change and be consistent
across your app.
4. Expand the values subolder within the res folder. It includes these subfolders:
colors.xml. Shows the default colors for your theme, and you can add your own
colors or change them.
dimens.xml. Store sizes of views and objects for different resolutions.
strings.xml. Create resources for all your strings. This makes it easy to translate
them to other languages.
styles.xml. All the CSS for your app goes here. Styles help you have a consistent
look for all UI elements in your app.
1. Expand the Gradle Scripts folder. This folder contains all the files needed by the build
system.
2. Look for the build.gradle(Module:app) file. When you are adding app-specific
dependencies, such as using additional libraries, they go into this file.
9. Task 4: Create a virtual device (emulator)
In this task, you will use the Android Virtual Device (AVD) manager to create a virtual device or
emulator that simulates the configuration for a particular type of Android device.
Using the AVD Manager, you define the hardware characteristics of a device and its API level,
and save it as a virtual device configuration.
When you start the Android emulator, it reads a specified configuration and creates an
emulated device on your computer that behaves exactly like a physical version of that device.
Why: With virtual devices, you can test your app on different devices (tablets, phones) with
different API levels to make sure it looks good and works for most users. You do not depend
on having a physical device available for app development.
1. In Android Studio, select Tools > Android > AVD Manager, or click the AVD Manager
icon in the toolbar.
2. Click the +Create Virtual Device. (If you have created a virtual device before, the
window shows all your existing devices and the button is at the bottom.)
The Select Hardware screen appears showing a list of preconfigured hardware devices.
For each device, the table shows its diagonal display size (Size), screen resolution in
pixels (Resolution), and pixel density (Density).
For the Nexus 5 device, the pixel density is xxhdpi, which means your app uses the icons in
the xxhdpi folder of the mipmap folder. Likewise, you app will use layouts and drawables
from folders defined for that density as well.
There are many more versions available than shown in the Recommended tab. Look at
the x86 Images and Other Images tabs to see them.
5. If a Download link is visible next to a system image version, it is not installed yet, and you
need to download. If necessary, click the link to start the download, and Finish when it's
done.
6. On System Image screen, choose a system image and click Next.
7. Verify your configuration, and click Finish. (If the Your Android Devices AVD Manager
window stays open, you can close it.)
10. Task 5. Run your app on an emulator
In this task, you will finally run your Hello World app.
The emulator starts and boots just like a physical device. Depending on the speed of your
computer, this may take a while. Your app builds, and once the emulator is ready, Android
Studio will upload the app to the emulator and run it.
You should see the Hello World app as shown in the following screenshot.
Note: If you are testing on an emulator, it is good practice to start it up once at the very
beginning of your session, and not to close it until you are done so that it doesn't have to go
through the boot process again.
Challenge: You can fully customize your virtual devices.
You may notice that not all combinations of devices and system versions work when you run
your app. This is because not all system images can run on all hardware devices.
11. Task 6. Add log statements to your app
In this practical, you will add log statements to your app, which are displayed in the logging
window of the Android Monitor.
Why: Log messages are a powerful debugging tool that you can use to check on values,
execution paths, and report exceptions.
1. Click the Android Monitor button at the bottom of Android Studio to open the Android
Monitor.
By default, this opens to the logcat tab, which displays information about your app as it is
running. If you add log statements to your app, they are printed here as well.
You can also monitor the Memory, CPU, GPU, and Network performance of your app from
the other tabs of the Android Monitor. This can be helpful for debugging and performance
tuning your code.
2. The default log level is Verbose. In the drop-down menu, change the log level to Debug.
4. If the Android Monitor is not already open, click the Android Monitor tab at the bottom of
Android Studio to open it. (See screenshot.)
5. Make sure that the Log level in the Android Monitor logcat is set to Debug or Verbose
(default).
6. Run your app.
Solution Code:
package com.example.hello.helloworld;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("MainActivity", "Hello World");
}
}
Challenge: A common use of the Log class is to log Java exceptions when they occur in your
program. There are some useful methods in the Log class that you can use for this purpose.
Use the Log class documentation to find out what methods you can use to include an exception
with a log message. Then, write code in the MainActivity.java fileto trigger and log an exception.
12. Task 7: Explore the AndroidManifest.xml
file
Every app includes an Android Manifest file ( AndroidManifest.xml ).The manifest file contains
essential information about your app and presents this information to the Android runtime
system. Android must have this information before it can run any of your app's code.
In this practical you will find and read the AndroidManifest.xml file for the Hello World app.
Why: As your apps add more functionality and the user experience becomes more engaging
and interactive, the AndroidManifest.xml file contains more and more information. In later
lessons, you will modify this file to add features and feature permissions.
Annotated code:
Challenge:
There are many other elements that can be set in the Android Manifest. Explore the Android
Manifest documentation and learn about additional elements in the Android Manifest.
13. Task 8. Explore the build.gradle file
Android Studio uses a build system called Gradle. Gradle does incremental builds, which allows
for shorter edit-test cycles.
Gradle site
Configure your build developer documentation
Search the internet for "gradle tutorial".
Why: When you add new libraries to your Android project, you may also have to update your
build.gradle file. It's useful to know where it is and its basic structure.
Solution:
Resources: To learn more about Gradle, start with the Gradle Wikipedia page.
Challenge:
For a deeper look into Gradle check out the Build System Overview and Configuring
Gradle Builds documentation.
There are tools to help you shrink your code, remove unnecessary libraries/resource and
even obfuscate your program to prevent unwanted reverse-engineering.
Android Studio itself provides some useful features. Learn more about a valuable open-
source tool called ProGuard.
14. Task 9. [Optional] Run your app on a device
In this final task, you will run your app on a physical mobile device such as a phone or tablet.
Why: Your users will run your app on physical devices. You should always test your apps on
both virtual and physical devices.
On Android 4.2 and higher, the Developer options screen is hidden by default. To show
Developer options and enable USB Debugging:
1. On your device, open Settings > About phone and tap Build number seven times.
2. Return to the previous screen (Settings). Developer options appears at the bottom of
the list. Click Developer options.
3. Choose USB Debugging.
Now you can connect your device and run the app from Android Studio.
14.2. Troubleshooting
If you Android Studio does not recognize your device, try the following:
Installed Android Studio and deployed the Hello World app in the Android emulator and
[optionally] on a mobile device.
acquired a basic understanding of the structure of an Android app.
added log statements that give you a basic tool for debugging.
obtained a basic understanding of the development workflow in Android Studio.
17. Resources
Developer Documentation:
Code: https://devrel-review.git.corp.google.com/#/c/20766/4
Make Your First Interactive UI
The user interface displayed on the screen of a mobile Android device consists of a hierarchy
of "views". Views are Android's basic user interface building blocks. For example, views can be
components that:
You specify the views in XML layout files. You can explore the view hierarchy of your app using
Hierarchy Viewer.
The Java code that displays and drives the user interface is contained in a class that extends
Activity and contains methods to inflate views, that is, take the XML layout of views and display
it on the screen. For example, the MainActivity in Hello World inflates a text view and prints
Hello World. In more complex apps, an activity might implement click and other event handlers,
request data from a database or the internet, or draw graphical content.
Android makes it straightforward to clearly separate UI elements and data from each other,
and use the activity to bring them back together. This separation is an implementation of an
MVP (Model-View-Presenter) architecture.
You will work with Activities and Views throughout this book.
Create your app's user interface (UI) using the Android Studio Layout Editor and XML.
Experiment with different UI elements.
Programmatically access UI elements.
Use string resources.
Add on-click functionality to a button to programmatically change the UI.
1. What you should already KNOW
For this practical you should be familiar with:
Select Run > Run app or click the Run icon in the toolbar to build and execute the app
on the emulator from Practicals 1.1 or your device. The Run icon looks like this:
6. Task 2: Add views to "Hello Toast" in the
Layout Editor
In this task, you will create and configure a user interface for the Hello Toast app by arranging
view UI components on the screen.
Why: Every app should start with the user experience, even if the first implementation is very
simple.
Here is a rough sketch of the UI you will build in this exercise. Simple UI sketches can be very
useful for deciding which views to use and how to arrange them, especially when your layouts
1. In the app > res > layout folder, open the activiy_main.xml file (1).
Your Android Studio Screen should look similar to the screenshot below. If you see the
XML code for the UI layout, click the Design tab below the Component Tree (8).
2. Using the annotated screenshot below as your guideline, explore the Layout Editor.
3. Find the different ways in which the "Hello World" string's UI element, a TextView, is
represented.
In the Palette of UI elements (2) you can create a text view by dragging it into the
design pane.
Visually, in the Design pane (6).
In the Component Tree (7), as a component in a hierarchy of UI elements called the
View Hierarchy. That is, views are organized into a tree hierarchy of parents and
children, where children inherit properties of their parents.
In the Properties pane (4), as a list of its properties, where "Hello Toast" is the value
of the text property of the TextView (5).
4. Use the selectors above the virtual device (3) to do the following:
5. Switch between the Design and Text tabs (8). Some UI changes can only be made in
code, and some are quicker to accomplish in the virtual device.
6. When you are done, undo your changes (for UI changes, use Edit > Undo or the keyboard
shortcut for your operating system).
See the Android Studio User Guide for the full Android Studio documentation.
Note: If you get an error about a missing App Theme, try File > Invalidate Caches / Restart
or choose a theme that does not generate the error. You can find additional help in [this
stackoverflow post](http://stackoverflow.com/questions/13439486/missing-styles-is-the-correct-
theme-chosen-for-this-layout/35818631).
By default, the Blank Template uses a RelativeLayout view group. This layout offers a lot of
flexibility in positioning views in the view groups.
A vertical linear layout is one of the most common layouts. It is simple, fast, and always a good
starting point. Change the view group to a vertical linear layout as follows:
1. In the Component Tree pane (7 in the previous screenshot), find the top or root view
directly below the Device Screen, which should be RelativeLayout.
2. Click the Text tab (8) to switch to the code view of the layout.
3. In the second line of the code, find RelativeLayout and change it to LinearLayout.
4. Make sure the closing tag at the end of the code has changed to </LinearLayout>. if it
hasn't changed automatically, change it yourself.
5. The android:layout_height is defined as part of the template. The default layout
orientation a horizontal row. To change the layout to be vertical, add the following code
below android:layout_height. android:orientation="vertical"
6. From the menu bar, select: **Code > Reformat Code
**You may see "no lines changed: code is already properly formatted".
Solution Code:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>
6.3. 2.3 Add views to the Linear Layout in the Layout Editor
In this task you will delete the current TextView (for practice), and add a new TextView and two
buttons to the LinearLayout as shown in the UI sketch for this task. Refer to the UI diagram
above, if necessary.
Add UI Elements
1. Make sure you click the Design tab (8) to show the virtual device layout.
2. Click the TextView whose text value is "Hello World" in the virtual device layout or the
Component Tree pane (7).
3. Press the Delete key to remove that TextView.
4. From the Palette pane (2), drag and drop a Button element, a Plain TextView, and another
Button element, in that order, one below the other into the virtual device layout.
1. Move the elements in the device layout. Resize elements in the Component Tree by
changing the android:layout_width and android:layout_height until each takes up a third
of the screen and they roughly match the UI sketch.
2. To identify each view uniquely within an activity, each view needs a unique id. And to be of
any use, the buttons need labels and the text views need to show some text. Double-click
each element in the Layout Manager to see its properties and change the text and id
strings as follows:
Element Text ID
Top button Toast button_toast
Text view 0 show_count
Bottom button Count button_count
Solution Layout:
There should be three Views on your screen. They may not match the sizes on the image
below, but as long as you have three Views in a vertical layout, you are doing fine!
Challenge: Use the Hierarchy Viewer tool to explore the view hierarchy of your app.
7. Task 3: Edit the "Hello Toast" layout in XML
In this practical, you will edit the XML code for the Hello Toast app UI layout. You will also edit
the properties of the views you have created. You can find the properties common to all views
in the View class documentation.
Why: While the Layout Editor is a powerful tool, some changes are easier to make directly in
the XML source code. It is a personal preference to use either the graphical LayoutEditor or
edit the XML file directly.
Note that your code may not be an exact match, depending on what changes you made in the
Layout Editor. Use the sample solutions as guidelines.
layout_width
layout_height
orientation
The match_parent attribute expands the view to fill its parent by width or height. When the
LinearLayout is the root view, it expands to the size of the device screen.
The wrap_content attribute shrinks the view dimensions just big enough to enclose its
content. (If there is no content, the view becomes invisible.)
Use a fixed number of dp (device independent pixels) to specify a fixed size, adjusted for
the screen size of the device. For example, 16dp means 16 device independent pixels.
Property Value
layout_width match_parent (to fill the screen)
layout_height match_parent (to fill the screen)
orientation vertical
Why: Having the strings in a separate file makes it easier to manage them, especially if you
use these strings more than once. Also, string resources are mandatory for translating and
localizing your app as you will create one string resource file for each language.
This creates a string resource in the values/res/string.xml file, and the string in your code is
replaced with a reference to the resource,
@string/button_label_toast
1. Extract and name the remaining strings from the views as follows:
View String Resource name
Button Hello Toast! button_label_toast
TextView 0 count_initial_value
Button Count button_label_count
2. In the Project view, navigate to values/strings.xml to find your strings. Now, you can edit
all your strings in one place.
Why: This makes it easier to manage dimensions, especially if you need to adjust your layout
for different device resolutions. It also makes it easy to have consistent sizing, and change the
size of multiple objects by changing one property.
Do the following:
If you want to use the graphical Layout Editor, click on the Design tab, select each
element in the Component Tree pane and change the layout:width property in the
Properties pane. If you want to directly edit the XML file, click on the Text tab, change the
android:layout_width for the first Button, the TextView, and the last Button.
2. Click the Text tab to show the XML code, if you haven't already done so.
3. Place the cursor on one of the "300dp".
4. Press Alt-Enter (Option-Enter on the Mac).
5. Click Extract dimension resource.
6. Set the Resource name to ** my_view_width </strong>, and click OK. (If you make a
mistake, you can undo the change with Ctrl-Z).
7. Copy the new resource name @dimen/my_view_width and paste it to replace the "300dp"
in each element.
8. In the Project view, navigate to values/dimens.xml to find your dimensions. The
dimens.xml file applies to all devices. The dimens.xml file for w820dp applies only to
devices that are wider than 820dp.
9. Change the layout_height of the TextView to 300dp.
10. Repeat the process (steps 3-7) to extract the height dimension for the text view.
11. Set the Resource name to counter_height and click OK.
7.4. 3.4 Set colors and backgrounds
Styles and colors are additional properties that can be extracted into resources. All views can
have backgrounds that can be colors or images.
Why: Extracting styles and colors makes it easy to use them consistently throughout the app,
and straightforward to change across all UI elements.
1. Change the text size of the show_count TextView. "sp" stands for scale-independent pixel,
and like dp, is a unit that scales with the size of the device.
android:textSize="200sp"
2. Extract the text size of the TextView as a dimension resource named count_text_size.
3. Change the text weight of the show_count TextView to bold.
android:textStyle="bold"
4. Change the text color of the text view in the show_count text view to the primary color of
the theme.
The colorPrimary color is one of the predefined theme base colors and is used for the app
bar. In a production app, you could, for example, customize this to fit your brand. Using the
base colors for other UI elements creates a uniform UI. See Using the Material Theme.
You will learn more about app themes and material design in a later practical.
android:textColor="@color/colorPrimary"
5. Change the color of both buttons to be the primary color of the theme.
android:background="@color/colorPrimary"
android:textColor="@android:color/white"
Note this code accesses a resource provide by the android package. See Accessing
Resources.
7. Add a background color to the TextView.
android:background="#FFFF00"
8. In the Layout Editor (Text tab), place your mouse cursor over this text view color and press
Alt-Enter (Option-Enter on the Mac).
9. Select Choose color, which brings up the color picker, and choose a color you like.
10. Open values/colors.xml. Notice that colorPrimary that you used earlier is defined here.
11. Using the colors in values/colors.xml as an example, add a resource named
myBackgroundColor for your background color, and then use it to set the background of
the text view.
The android:layout_gravity attribute specifies how a view is aligned within its parent
View.
The android:gravity attribute specifies the alignment of the content of a View within the
View itself.
For all three views, add the layout_gravity property to center the views horizontally on the
screen.
android:layout_gravity="center_horizontal"
<resources>
<string name="app_name">Hello Toast</string>
<string name="button_label_count">Count</string>
<string name="button_label_toast">Toast</string>
<string name="count_initial_value">0</string>
<string name="toast_button_toast">Hello Toast!</string>
</resources>
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="my_view_width">300dp</dimen>
<dimen name="count_text_size">200sp</dimen>
<dimen name="counter_height">300dp</dimen>
</resources>
<Button
android:id="@+id/button_toast"
android:layout_width="@dimen/my_view_width"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/button_label_toast"
android:background="@color/colorPrimary"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/show_count"
android:layout_width="@dimen/my_view_width"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="@string/count_initial_value"
android:textSize="@dimen/count_text_size"
android:textStyle="bold"
android:textColor="@color/colorPrimary"
android:background="@color/myBackgroundColor" />
<Button
android:id="@+id/button_count"
android:layout_width="@dimen/my_view_width"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/button_label_count"
android:background="@color/colorPrimary"
android:textColor="@android:color/white" />
</LinearLayout>
Note: Youll notice that when the device is rotated, the counter button becomes hidden. This is
because the height of the TextView was hardcoded to a fixed amount of dps, greater than the
height of the screen when rotated. To resolve this issue, see the Challenge questions below.
Challenges:
What are the android:weightSum and android:layout_weight attributes for? Find out
using the developer.android.com documentation.
Change the Hello Toast app to behave properly when the screen is rotated using the
android:weightSum and android:layout_weight attributes.
Create a new project with 5 views. Have one view use the top-half of the screen, and the
other 4 views share the bottom half of the screen. Use only a LinearLayout and weights to
accomplish this.
Use an image as the background of the Hello Toast app. Add an image to the drawable
folder, then set it as the background of the root view. For a deep dive into drawables, see
the Drawable Resources documentation.
Resources:
All Views are subclasses of the View class and therefore inherit many properties of the
View superclass.
You can find information on all Button properties in the Button class documentation, and all
the TextView properties in the TextView class documentation.
You can find information on all the LinearLayout properties in the LinearLayout class
documentation.
The Android resources documentation will describe other types of resources.
Android color constants: Android standard R.color resources
More information about dp and sp units can be found at Supporting Different Densities
8. Task 4: Add onClick handlers for the buttons
In this task, you will add methods to your MainActivity that execute when the user clicks on
each button.
To connect a user action in a view to application code, you need to do two things:
Write a method that performs a specific action when a user; for example: when a user
clicks an on-screen button.
Associate this method to the view, so the method executes when the user interacts with
the view.
1. Open res/layout/activity_main.xml.
2. Add the following property to the button_toast button.
android:onClick="showToast"
android:onClick="countUp"
4. Inside of activity_main.xml, place your mouse cursor over each of these method names.
5. Press Alt-Enter (Option-Enter on the Mac), and select Create onClick event handler.
6. Chose the MainActivity and click OK.
This creates placeholder method stubs for the onClick methods in MainActivity.java.
Note: You can also add click handlers to views programmatically, which you will do in a later
practical. Whether you add click handlers in XML or programmatically is largely a personal
choice; though, there are situations where you can only do it programmatically.
Solution MainActivity.java:
package hellotoast.android.example.com.hellotoast;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
To create an instance of a Toast, you call makeText() on the Toast class, supplying a context
(see below), the message to display, and the duration of display. You display the toast calling
show(). This is a boilerplate pattern, so you can reuse the code you are going to write.
2. The length of a toast string can be either short or long, and you specify which one by using
a Toast constant.
Toast.LENGTH_LONG
Toast.LENGTH_SHORT
The actual lengths are about 3.5s for the long toast and 2s for the short toast. The values
are specified in the Android source code. See this Stackoverflow post details.
3. Create an instance of the Toast class with the context, message, and duration.
The context is the application context we got earlier.
The message is the string you want to display
The duration is one of the
4. Extract the "Hello Toast" string into a string resource and call it ** toast_message </strong>.
toast.show();
9. Run your app and verify the toast shows when the Toast button is tapped.
Solution:
/*
* When the TOAST button is clicked, show a toast.
*
* @param view The view that triggers this onClick handler.
* Since a toast always shows on the top, view is not used.
* */
public void showToast(View view) {
// Interface to global information about an application environment
Context context = getApplicationContext();
int duration = Toast.LENGTH_LONG; // LENGTH_SHORT for a short toast
8.3. 4.3 Increase the count in the text view when the Count
button is clicked
To display the current count in the text view:
1. In MainActivity.java, add a class variable count to track the count and start it at 0.
2. In the countUp() method, increase the value of the count variable each time the button is
clicked.
3. Get a reference to the text view using the id you set in the layout file.
Views, like strings and dimensions, are resources that can have an id. The findViewById) call
takes the id of a view as its parameter and returns the view. Because the method returns a
View, you have to cast the result to the view you expect.
1. Set the text in the text view to the value of the count variable.
showCount.setText("" + count);
2. Run your app to verify that the count increases when the Count button is pressed.
Solution:
coutUp Method:
public void countUp(View view) {
count++;
TextView showCount = (TextView) findViewById(R.id.show_count);
showCount.setText("" + count);
}
Resources:
Create a coffee ordering app. Add buttons to change the number of coffees ordered.
Calculate and display the total price.
Create a scoring app for your favorite team sport. Make the background an image that
represents that sport. Create buttons to count the scores for each team.
10. Conclusion
In this chapter, you:
App Overview
Summary
Resources
1.25 P: Working with TextView Elements
The TextView class is a subclass of the View class that displays text on the screen. You can
control how the text appears with TextView attributes in the XML layout file. This practical
shows how to work with multiple TextView elements, including one that the user can scroll its
contents vertically.
If the information you want to show in your app is larger than the device's display, you can
create a scrolling view that the user can scroll vertically by swiping up or down, or horizontally
by swiping right or left.
You would typically use a scrolling view for news stories, articles, or any lengthy text that
doesnt completely fit on the display. You can also use a scrolling view to enable users to enter
multiple lines of text, or to combine View elements (such as a text field and a button) within a
scrolling view.
The ScrollView class provides the layout for the scrolling view. ScrollView is a subclass of
FrameLayout, which means that you should place only one View as a child within it, where the
child View contains the entire contents to scroll. And this child View may itself be a layout
manager with a complex hierarchy of objects, such as a LinearLayout. Note that complex
layouts may suffer performance issues with child Views such as images. A good choice for a
View within a ScrollView is a LinearLayout that is arranged in a vertical orientation, presenting
top-level items that the user can scroll through.
With a ScrollView, all of your views are in memory and in the View hierarchy even if they aren't
displayed on screen. This makes ScrollView ideal for scrolling pages of free-form text
smoothly, because the text is already in memory. However, ScrollView can use up a lot of
memory, which can affect the performance of the rest of your app. To display long lists of items
that users can add to, delete from, or edit, you may want to consider using a RecyclerView,
which is described in a separate practical.
1. What you should already KNOW
You should be familiar with:
You will make all these changes in the XML code and in the strings.xml file. You will edit the
XML code for the layout in the Text pane, which you show by clicking the Text tab, rather than
clicking the Design tab for the Design pane. Some changes to UI elements and attributes are
easier to make directly in the Text pane using XML source code.
2. In the app > res > layout folder, open the activity_main.xml file, and click the Text tab to
see the XML code if it is not already selected.
At the top, or root, of the view hierarchy is a ViewGroup called RelativeLayout. LIke other
ViewGroups, RelativeLayout is a view that contains other views. In addition, it also allows
you to position its child Views relative to each other or relative to the parent RelativeLayout
itself. The default Hello World TextView element that is created for you by the Empty
Layout template is a child View within the RelativeLayout view group. For more information
about using a RelativeLayout, see Relative Layout.
3. Add two more TextView elements above the Hello World TextView. As you enter
<TextView to start a TextView, Android Studio automatically adds the ending />, which is
shorthand for </TextView>. Add the following attributes to the TextViews:
TextView #1 Attribute Value
layout_width "match_parent"
layout_height "wrap_content"
android:id "@+id/article_heading"
android:background "@color/colorPrimary"
android:textColor "@android:color/white"
android:paddingTop "10dp"
android:paddingBottom "10dp"
android:paddingLeft "10dp"
android:paddingRight "10dp"
android:textAppearance "@android:style/TextAppearance.Large"
android:textStyle "bold"
android:text "Article Title"
Note: The attributes for styling the text and background are summarized in the [TextView
class documentation]
(https://developer.android.com/reference/android/widget/TextView.html).
4. Extract string resources for the android:text attribute values in each new TextView to
create entries for them in strings.xml.
Place the cursor on the hard-coded string, press Alt-Enter (Option-Enter on the Mac), and
select Extract string resources. Then edit the Resource name for the string value.
Extract as follows:
Old Text New String Name
Article Title article_title
Article Subtitle article_subtitle
Note: String resources are described in detail in the [String Resources documentation]
(https://developer.android.com/guide/topics/resources/string-resource.html).
5. Add the following TextView attributes to the Hello World TextView element, and change
the android:text attribute:
TextView Attribute Value
android:id "@+id/article"
android:lineSpacingExtra "5sp"
android:layout_below "@id/article_subheading"
android:text Change to Article text
i. As you enter or paste text in the strings.xml file, the text lines dont wrap around to
the next line they extend beyond the right margin. This is the correct behavior
each new line of text starting at the left margin represents an entire paragraph.
ii. Use \n to represent the end of a line, and another \n to represent a blank line.
Why? You need to add end-of-line characters to keep paragraphs from running into
each other.
Tip: If you want to see the text wrapped in strings.xml, you can press Return to enter
hard line endings, or format the text first in a text editor with hard line endings.
iii. If you have an apostrophe (') in your text, you must escape it by preceding it with a
backslash (\'). If you have a double-quote in your text, you must also escape it (\").
You must also escape any other non-ASCII characters.See the Formatting and
Styling section of String Resources for more details.
iv. Use the HTML and </b> tags around words that should be in bold.
v. Use the HTML and </i> tags around words that should be in italics. Note, however,
that if you use curled apostrophes within an italic phrase, you should replace them with
straight apostrophes.
vi. You can combine bold and italics by combining the tags, as in ... words...</i></b>.
Other HTML tags are ignored.
vii. Enclose The entire text within </string> in the strings.xml file.
viii. Include a web link to test, such as www.google.com (the example below uses
www.rockument.com). Dont use an HTML tag any HTML tags except the bold
and italic tags will be ignored and presented as text, which is not what you want.
4. Run the app.
The article appears, but notice you cant scroll it to see all of the text. Note also that tapping a
web link does not currently do anything.
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/article_heading"
android:background="@color/colorPrimary"
android:textColor="@android:color/holo_orange_light"
android:textColorHighlight="@color/colorAccent"
android:padding="10dp"
android:textAppearance="@android:style/TextAppearance.Large"
android:textStyle="bold"
android:text="@string/article_title"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/article_subheading"
android:layout_below="@id/article_heading"
android:padding="10dp"
android:textAppearance="@android:style/TextAppearance"
android:text="@string/article_subtitle"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/article"
android:layout_below="@id/article_subheading"
android:lineSpacingExtra="5sp"
android:text="@string/article_text"/>
</RelativeLayout>
6. Task 2: Add active Web links and a
ScrollView
In the previous task you created the Scrolling Text app with TextViews for an article title,
subtitle, and lengthy article text. You also included a web link, but the link is not yet active. You
will add the code to make it active.
Also, the TextView by itself cant enable users to scroll the article text to see all of it. You will
add a new view group called ScrollView to the XML layout that will make the TextView
scrollable.
6.1. 2.1 Add the autoLink attribute for active web links
Add the android:autoLink="web" attribute to the article TextView. The XML code for this
TextView should now look like this:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/article"
android:lineSpacingExtra="5sp"
android:autoLink="web"
android:text="@string/article_text"/>
1. Add a ScrollView between the article_subheading TextView and the article TextView. As
you enter <ScrollView , Android Studio automatically adds </ScrollView> at the end, and
presents the android:layout_width and android:layout_height attributes. Choose
wrap_content for both attributes. The code should now look like this:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/article_subheading"
android:layout_below="@id/article_heading"
android:padding="10dp"
android:textAppearance="@android:style/TextAppearance"
android:text="@string/article_subtitle"/>
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/article_subheading"></ScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/article"
android:layout_below="@id/article_subheading"
android:lineSpacingExtra="5sp"
android:autoLink="web"
android:text="@string/article_text"/>
Now move the ending </ScrollView> code after the article TextView so that the article
TextView attributes are inside the ScrollView XML element.
1. Remove the following attribute from the article TextView, because the ScrollView itself will
be placed below the article_subheading element, and this attribute for TextView would
conflict with the ScrollView:
android:layout_below="@id/article_subheading"
The layout should now look like this:
1. Choose Code > Reformat Code to reformat the XML code so that the article TextView
now appears indented inside the <Scrollview code.
2. Run the app.
Swipe up and down to scroll the article. The scroll bar appears in the right margin as you
scroll.
Tap the web link to go to the web page. The android:autoLink attribute turns any
recognizable URL in the TextView (such as www.rockument.com) into a web link.
3. Rotate your device or emulator while running the app. Notice how the scrolling view widens
to use the full display and still scrolls properly.
4. Run the app on a tablet or tablet emulator. Notice how the scrolling view widens to use the
full display and still scrolls properly.
In the above figure, the following appear:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/article_heading"
android:background="@color/colorPrimary"
android:textColor="@android:color/white"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:textAppearance="@android:style/TextAppearance.Large"
android:textStyle="bold"
android:text="@string/article_title"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/article_subheading"
android:layout_below="@id/article_heading"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:textAppearance="@android:style/TextAppearance"
android:text="@string/article_subtitle"/>
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/article_subheading">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/article"
android:lineSpacingExtra="5sp"
android:autoLink="web"
android:text="@string/article_text"/>
</ScrollView>
</RelativeLayout>
7. Task 3: Scroll multiple TextView elements
As noted before, the ScrollView view group can contain only one View (such as the article
TextView you created for the article); however, that View can be another view group that
contains Views, such as LinearLayout. You can nest a view group such as LinearLayout within
the ScrollView view group, thereby scrolling everything that is inside the LinearLayout.
For example, if you want the subheading of the article to scroll along with the article, add a
LinearLayout within the ScrollView, and move the subheading, along with the article, into the
LinearLayout. The LinearLayout view group becomes the single child View in the ScrollView,
and the user can scroll the entire view group: the subheading and the article.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"></LinearLayout>
You use match_parent to match the width of the parent view group, and wrap_content to
make the view group only big enough to enclose its contents and padding.
1. Move the ending </LinearLayout> code after the article TextView and before the closing
</ScrollView> so that the LinearLayout includes the article TextView and is completely
inside the ScrollView.
2. Add the android:orientation="vertical" attribute to the LinearLayout in order to set the
orientation of the LinearLayout to vertical. The LinearLayout within the ScrollView should
now look like this (choose Code > Reformat Code to indent the view groups correctly):
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/article_subheading">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/article"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:lineSpacingExtra="5sp"
android:text="@string/article_text" />
</LinearLayout>
</ScrollView>
3. Move the article_subheading TextView to a position inside the LinearLayout above the
article TextView.
4. Remove the android:layout_below="@id/article_heading" attribute from the
article_subheading TextView. Since this TextView is now within the LinearLayout, this
attribute would conflict with the LinearLayout attributes.
5. Change the ScrollView layout attribute from
android:layout_below="@id/article_subheading" to
android:layout_below="@id/article_heading". Now that the subheading is part of the
LinearLayout, the ScrollView must be placed below the heading, not the subheading.
6. Run the app.
Swipe up and down to scroll the article, and notice that the subheading now scrolls along with
the article while the heading stays in place.
7.2. Solution code
<TextView
android:id="@+id/article_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="10dp"
android:text="@string/article_title"
android:textAppearance="@android:style/TextAppearance.Large"
android:textColor="@android:color/white"
android:textStyle="bold" />
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/article_heading">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/article_subheading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="10dp"
android:text="@string/article_subtitle"
android:textAppearance="@android:style/TextAppearance" />
<TextView
android:id="@+id/article"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:lineSpacingExtra="5sp"
android:text="@string/article_text" />
</LinearLayout>
</ScrollView>
</RelativeLayout>
TextView
ScrollView
String Resources
View
Relative Layout
Other:
Displaying and manipulating a scrollable list of similar data items, as you did in the scrolling
view practical, is a common feature of apps. For example, contacts, playlists, photos,
dictionaries, shopping lists, an index of documents, or a listing of saved games are all
examples.
2. Build and run the app on an emulator or hardware device. You should see the "WordList"
title and "Hello World" in a blank view.
1. In Android Studio, in your new project, make sure you are in the Project pane (1) and in
the Android view (2).
2. In the hierarchy of files, find the Gradle Scripts folder (3).
3. Expand Gradle Scripts, if necessary, and open the build.gradle (Module: app) file (4).
(Note: build.gradle (Project: WordList) is NOT the right file to change.)
4. Towards the end of the build.gradle (Module: app) file, find the dependencies section.
5. Add these two dependencies as the last two lines (inside the curly braces):
compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
time error.)
6. If prompted, sync your app now.
7. Build and run your app. You should see the same WorldList app displaying "Hello World".
(If you get gradle errors, sync your project. You do not need to install additional plugins.)
Solution:
This is an example of the dependencies section of the build.gradle file. Note that your file may
be slightly different, e.g., the version number.
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:recyclerview-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
}
6. Task 2. Create a dataset
Before you can display anything, you need data to display. In a more sophisticated app, your
data could come from internal storage (a file, SQLite database, saved preferences), from
another app (Contacts, Photos), or from the internet (cloud storage, Google Sheets, or any
data source with an API). For this exercise, you will simulate data by creating it in the
MainActivity's onCreate method.
Why: Storing and retrieving data is a topic of its own covered in the data storage chapter. You
will have an opportunity to extend WordList to use real data in that later lesson.
Note: You must use a LinkedList for this practical. Refer to the solution code, if you need
help.
The app UI has not changed, but you should see a list of log messages in log cat, such as:
android.example.com.wordlist D/WordList: Word 1
Solution:
Class variables:
To display your data in a recycler view, you need the following things:
The diagram below shows the relationship between the data, the adapter, the view holder, and
In main_activity.xml, replace the code created by the Empty Activity with code for a
CoordinatorLayout, and then add a RecyclerView:
1. Open activity_main.xml.
2. Select all the code in activity_main.xml and replace it with this code:
<android.support.v7.widget.RecyclerView>
</android.support.v7.widget.RecyclerView>
6. Build and run your app and make sure there are no errors displayed in logcat. You will only
see a blank screen, because you haven't put any items into the RecyclerView yet.
Solution:
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>
Resources:
CoordinatorLayout
RecyclerView
Learn more about the Android Support Library.
1. Right-click the app/res/layout folder and choose New > Layout resource file.
2. Name the file wordlist_item.xml and click OK.
3. In Text mode, change the LinearLayout that was created with the file to match with these
attributes:
Attribute Value
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:orientation "vertical"
android:padding "6dp"
You can use styles to allow elements to share display attributes. An easy way to create a style
is to extract the style of a UI element that you already created.
1. Anywhere in the text view Right-click > Refactor > Extract > Style.
2. In the Extract Android Style dialog,
Name your style word_title.
Leave all boxes checked.
Check the Launch 'Use Style Where Possible' box.
Click OK.
3. When prompted, apply the style to the Whole Project.
4. Find and examine the word_title style in values/styles.xml .
5. Go back to wordlist_item.xml. The text view now references the style instead of using
individual styling properties.
6. Run your app. Since you have removed the default "Hello World" text view, you should see
the "Word List" title and a blank view.
Solution:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
<TextView
android:id="@+id/word"
style="@style/word_title" />
</LinearLayout>
To connect data with views, the adapter needs to know about the views into which it will place
the data. The adapter contains a view holder (from the ViewHolder class) that describes an
item view and its place within the RecyclerView. The following diagram shows the relationship
between data, adapter, view holder, and the recycler view.
In this task you will build an adapter with a view holder that bridges the gap between the data in
your word list and the recycler view that displays it.
1. Right-click java/com.android.example.wordlist and select New > Java Class. (Not the
(test) or (androidTest) directories.)
2. Name the class WordListAdapter.
3. Give WordListAdapter the following signature:
This brings up a dialog box that asks you to select the methods you want to implement. Select
all three methods and click OK.
This creates empty placeholders for all the methods that you must implement. Note how
onCreateViewHolder and onBindViewHolder both reference the view holder, which hasn't been
implemented yet.
2. You will see an error about a missing default constructor. You can see details about the
errors by hovering your mouse cursor over the red-underlined source code or over any red
horizontal line on the right margin of the open-files pane.
3. Add class variables to the WordViewHolder inner class for the text view and the adapter:
4. In the inner class WordViewHolder, add a constructor that initializes the view holder's text
view from the XML resources and sets its adapter:
5. Build and run your app. You should see the familiar blank view. Take note of the
E/RecyclerView: No adapter attached; skipping layout warning in logcat.
2. You can now fill in the getItemCount() method to return the size of mWordList .
@Override
public int getItemCount() {
return mWordList.size();
}
Next, WordListAdapter needs a constructor that initializes the word list from that data.
In the WordListAdapter constructor we are going to add something new, called a layout
inflater. A LayoutInflator reads a layout XML description and converts it into the
corresponding views. There is a bit of magic here, as you don't have to use the inflater
directly. For this exercise, you just create it, and it is already correctly configured to do its
work.
@Override
public WordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View mItemView = mInflater.inflate(R.layout.wordlist_item, parent, false);
return new WordViewHolder(mItemView, this);
}
@Override
public void onBindViewHolder(WordViewHolder holder, int position) {
String mCurrent = mWordList.get(position);
holder.wordItemView.setText(mCurrent);
}
7. Build and run your app. You will still see the E/RecyclerView: No adapter attached warning.
You will fix that in the next task.
1. Open MainActivity.java
2. Add class variables to MainActivity for the recycler view and the adapter.
3. In the onCreate method of MainActivity, add the following code that creates the recycler
view and connects it with an adapter and the data. Read the code comments!
Note you must insert this code after the mWordList initialization.
// Create recycler view.
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
// Create an adapter and supply the data to be displayed.
mAdapter = new WordListAdapter(this, mWordList);
// Connect the adapter with the recycler view.
mRecyclerView.setAdapter(mAdapter);
// Give the recycler view a default layout manager.
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
You should see your list of words displayed, and you can scroll the list.
8. Task 4. Make the list interactive
Looking at lists of items is interesting, but it's a lot more fun and useful if your user can interact
with them.
To see how the recycler view can respond to user input, you will programmatically attach a click
handler to each item. When the item is tapped, the click handler is executed, and that item's
text will change.
3. Click on the class header and on the red light bulb to implement stubs for the required
methods, which in this case is just the onClick() method.
4. Add the following code to the body of the onClick() method.
5. Connect the onClickListener with the view by adding this code to the WordViewHolder
constructor (below the "this.mAdapter = adapter" line):
itemView.setOnClickListener(this);
6. Build and run your code. Click on items to see their text change.
Solution. Final code for WordListAdapter.java:
Why: You have already seen that you can change the content of list items. The list of items that
a RecyclerView displays can be modified dynamically-- it's not just a static list of items.
For this practical, you will generate a new word to insert into the list. For a more useful
application, you would get data from your users. You will learn how to do this in a later lesson.
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true"
android:src="@drawable/ic_add_24dp" />
Note: Because this is a vector drawing, it is stored as an XML file. Vector drawings are
automatically scaled, so you do not need to keep around a bitmap for each screen resolution.
Learn more: Android Vector Asset Studio.
Build and run your app. Test your app by doing the following;
1. Scroll the list of words.
2. Click on items.
3. Add items by clicking on the FAB.
4. What happens if you rotate the screen? You will learn in a later lesson how to
preserve the state of an app when the screen is rotated.
10. Coding challenge
Creating a click listener for each item in the list is easy, but can hurt the performance of your
app if you have a lot of data. Research how you could implement this more efficiently. This is an
advanced challenge. Start by thinking about it conceptually, and then search for an
implementation example.
Note: All coding challenges are optional and not prerequisite for the material in the next
chapter.
.
11. Summary
RecyclerView is a resource-efficient way to display a scrollable list of items.
To use RecyclerView, you associate the data to the Adapter/ViewHolder that you create
and to the layout manager of your choice.
Click listeners can be created to detect mouse clicks in a RecyclerView.
Android support libraries contain backward-compatible versions of the Android framework.
Android support libraries contain a range of useful utilities for your apps.
Build dependencies are added to the build.gradle (Module app) file.
Layouts can be specified as a resource file.
A LayoutInflater reads a layout resource file and creates the View objects from that file.
A Floating Action Button (FAB) can dynamically modify the items in a RecyclerView.
12. Resources
Developer Documentation:
Explore some of the many resources available to Android developers of all levels.
Add a home screen icon to your Word List app that will launch it when tapped.
1. What you should already KNOW
For this practical you should be familiar with:
http://developer.android.com/index.html
Solution:
Note that the panel on the top-left is scrollable; scroll to see additional customizations.
1. Change the Name of the icon to ic_launcher_text, if you don't want to overwrite the default
Android ic_launcher icon that comes with your project.
2. In the Asset Type row, click b.
3. Type "Toast Me!" into the text box.
4. Experiment with adjusting the font.
5. Scroll down and change font and background colors.
6. Click Next.
7. The Confirm Icon Path window shows how an icon with your specified text will be created
for each resolution and the default storage location and path in your app.
8. Click Finish.
9. Got the the res/mipmap folder. If now contains your new icon, a default version at the top
level, and size-adjusted versions for different resolutions.
10. To use the new icon, open the Android Manifest. Change the android icon line to from
referencing ic_launcher to ic_launcher_text.
android:icon="@mipmap/ic_launcher_text"
The Android Monitor page discusses how to monitor your app's performance. Android Studio
and your mobile device offer tools to measure your app's memory use, GPU, CPU, and
Networkd performance. App crashes are often related to memory leaks, that is, your app
allocates memory and does not release it, eventually using up all the available memory. You can
use the Memory Monitor that comes with Android Studio to observe how your app uses
memory.
1. In Android Studio, at the bottom of the window, click the Android Monitor tab. By default
this opens on logcat.
2. Click the Monitors tab next to logcat. Scroll or make the window larger to see all for
monitors: Memory, CPU, Networking, and GPU.
3. Run your app and interact with it. The monitors update to reflect the app's use of
resources. Note that you may have to manually enable the GPU monitor by clicking the
Pause button.
App performance is a big topic on its own and you will learn about it in a later lesson.
6. Task 2. Use project templates
Android Studio provides templates for common and recommended app and activity designs.
Using templates saves time, and helps you follow design best practices.
Each template incorporates an skeleton activity and user interface. You've already used the
Empty Activity template. The Basic Activity template has more features and incorporates
recommended app features, such as the options menu.
Being familiar with this code will help you extend and customize this template for your own
needs.
Application name
This is derived from your package
3 name, but can be anything you AndroidManifest.xml
choose.
MainActivity.java
onOptionsItemSelected() implements what
Options menu happens when a menu item is selected.
Menu items for the activity, as well as
4 global options, such as "Search". Your res > menu > menu_main.xml
app menu items go into this menu.
Resource that specifies the menu items for
the options menu.
acvivity_main.xml
Notice that there are no views specified in this
CoordinatorLayout layout; rather, it includes another layout with
CoordinatorLayout is a feature-rich
5 layout that provides mechanisms for
views to interact. Your app's user
where the views are specified. This
interface goes inside this view group.
separates system views from the views
unique to your app.
TextView
In the example, use to display "Hello content_main.xml
6 World". Replace this with the views for All your app's views are defined in this file.
your app.
activity_main.xml
7 Floating Action button (FAB) MainActivity.java > onCreate has a stub that
sets an onClick listener on the FAB.
6.2. 2.2. Create projects using different templates
For the practicals so far, you've used the Empty Activity and Basic Activity templates:
In later lessons, the templates you use will vary, depending on the task. In this practical, you
have an opportunity to explore project templates.
1. Create and run projects using other templates. There is no right or wrong answer here.
2. Experiment and explore the different templates and become familiar with the possibilities.
1. Choose Tools > Android > SDK Manager. This opens Settings at Android SDK.
2. Click on the SDK Platforms tab. You can install additional versions of the Android system
from here.
3. Click on SDK Update Sites. Android Studio checks the listed and checked sites regularly
for updates.
4. Click on the SDK Tools tab. Here you can install additional SDK Tools that are not installed
by default, as well as an offline version of the Android developer documentation. This gives
you access to documentation even when you are not connected to the internet.
5. Check "Documentation for Android SDK", click Apply, and follow the prompts.
6. Navigate to the Android/sdk directory and open the docs folder.
7. Find index.html and open it.
8. Task 4. Many more resources
The Android Developer YouTube channel is a great source of tutorials and tips.
The Android team posts news and and tips on the Official Android blog.
Stack Overflow is a community of millions of programmers helping each other. If you run
into a problem, chances are, someone else has already posted an answer on this forum.
And last but not least, type your questions into Google search, and the Google search
engine will collect relevant results from all of these resources. For example, "What is the
most popular Android OS version in India?"
9. Conclusion
In this chapter, you learned about the many resources available to developers. As you move
beyond the fundamentals, they will help you build anything you want.
10. Resources
Developer Documentation:
Videos
In the second version the user enters a message on the first activity, clicks send, and that
message appears on the second activity. The app uses intents to pass this data from the main
to the second activity.
The final version of the app enables the user to type a reply on the second activity, click Reply,
and the reply is displayed on the main activity. This version of the app also uses intents to pass
data back from the second activity to the main activity.
5. Task 1. Create the TwoActivities project
Set up the initial project with a main (primary) activity, define the layout, and define a skeleton
method for the onClick button event.
Call your application "Two Activities" and change the company domain to
"android.example.com." Choose the same Minimum SDK that you did in the previous projects.
1. Open res/values/strings.xml .
2. Add a string resource for the button text:
<string name="button_main">Send</string>
3. Preview the layout. The layout for the first activity should look like this:
Solution Code:
<Button
android:id="@+id/button_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:onClick="launchSecondActivity"
android:text="@string/button_main" />
</RelativeLayout>
1. Open java/com.example.android.twoactivities/MainActivity .
2. Create a new method called launchSecondActivity() , with this signature:
4. Run your app. When you click the "Send" button you will see the log message in the
Android Monitor logcat.
Solution Code:
package com.example.android.twoactivities;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
Although every activity in your app is independent of any other activity, you can define an
activity as a parent of another in the Android manifest. This relationship enables Android to add
navigation hints such as left-facing arrows in the title bar for each activity.
Activities communicate with each other (both in the same app and across different apps)
through the use of intents. An explicit intent is one in which you know the target of the intent,
that is, you already know the specific activity you want to communicate with.
In this task you'll add a second activity to our app, with its own layout. You'll modify the Android
manifest to define the main activity as the parent of the second activity. Then you'll modify the
onClick event method in the main activity to include an intent that launches the second activity
Android Studio adds both a new activity layout (activity_second) and a new Java file
(SecondActivity) to your project for the new activity. It also updates the Android manifest to
include the new activity.
The label attribute adds the title of the activity to the action bar.
The parentActivityName attribute indicates that the main activity is the parent of the second
activity. This parent activity relationship is used for "up" navigation within your app. By defining
this attribute, the action bar for the second activity will appear with a left-facing arrow to enable
the user to navigate "up" to the main activity.
These two attributes accomplish the same thing as the android:parentActivityName attribute --
they define a relationship between two activities for the purpose of up navigation. These
attributes are required for older versions of Android. android:parentActivityName is only
available for API 16 and higher.
1. Open res/values/strings.xml.
2. Add a string resource for the activity label:
Solution Code:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<activity
android:name=".SecondActivity"
android:label="@string/activity2_name"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.android.twoactivities.MainActivity" />
</activity>
</application>
</manifest>
1. Open res/values/strings.xml and add a string resource for the text in the TextView:
2. Preview the layout. The layout for the second activity should look like this:
Solution Code:
<TextView
android:id="@+id/text_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/text_header"
android:textSize="18sp"
android:textStyle="bold" />
</RelativeLayout>
3. Call the startActivity() method with the new intent as the argument.
startActivity(intent);
4. Run the app. When you click the Send button the second activity opens. To get back to the
main activity, click Back, or use the left arrow at the top of the second activity to return
there.
Challenge: What happens if you remove the android:parentActivityName and the <meta-
data> elements from the manifest? Make this change and run your app.
7. Task 3. Send data from the main activity to
the second activity
In the last task you added a simple explicit intent to the main activity that launched the second
activity. You can also use intents to send data from one activity to another.
In this task you'll modify the explicit intent to send data (in this case, a user-entered string) from
the main activity to the second activity through intent extras, and to display that data.
1. Open res/values/strings.xml and add a string resource for the hint in the EditText:
The new layout for the main activity looks like this:
Solution Code:
<Button
android:id="@+id/button_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:onClick="launchSecondActivity"
android:text="@string/button_main" />
<EditText
android:id="@+id/editText_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toLeftOf="@+id/button_main"
android:layout_toStartOf="@+id/button_main"
android:hint="@string/editText_main" />
</RelativeLayout>
3. In the launchSecondActivity() method, get an EditText object from the new EditText you
just created in the layout file:
5. Test to make sure the editText variable is not null. If the test passes, create a string
message with the URI:
if (editText != null) {
message = editText.getText().toString();
}
intent.putExtra(EXTRA_MESSAGE, message);
Solution Code:
package com.example.android.twoactivities;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
intent.putExtra(EXTRA_MESSAGE, message);
startActivity(intent);
}
}
7.3. 3.3 Add a TextView for the message to the layout of the
second activity
1. Open res/layout/activity_second.xml .
2. Add a second TextView. Give the TextView these attributes:
Attribute Value
android:id "@+id/text_message"
android:layout_width wrap_content
android:layout_height wrap_content
android:layout_below "@+id/text_header"
android:layout_marginLeft "12dp"
android:layout_marginStart "12dp"
android:textSize "18sp"
The new layout for the second activity looks the same as it did in the previous task, because
the new TextView does not (yet) contain any text and thus does not appear on the screen.
Solution Code:
<TextView
android:id="@+id/text_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/text_header"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/text_header"
android:layout_marginLeft="12dp"
android:layout_marginBottom="12dp"
android:textSize="18sp"/>
</RelativeLayout>
3. Get the string extra from that intent with the MainActivity.EXTRA_MESSAGE static variable as
the key:
String message =
intent.getStringExtra(MainActivity.EXTRA_MESSAGE);
5. Test to make sure the textView variable is not null. If the test passes, set the text of that
TextView to the string from the intent extra:
if (textView != null) {
textView.setText(message);
}
When you type a message in the main activity and click Send, the second activity is launched
and displays that message.
Solution Code:
package com.example.android.twoactivities;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
1. Open res/values/strings.xml and add string resources for the button text and the hint in
the EditText:
<string name="button_second">Reply</string>
<string name="editText_second">Enter Your Reply Here</string>
The new layout for the second activity looks like this:
Solution Code:
<TextView
android:id="@+id/text_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="Message Received"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/text_header"
android:layout_marginLeft="12dp"
android:textSize="18sp" />
<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:onClick="returnReply"
android:text="@string/button_second" />
<EditText
android:id="@+id/editText_second"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toLeftOf="@+id/button_second"
android:layout_toStartOf="@id/button_second"
android:hint="@string/editText_second" />
</RelativeLayout>
6. Test to make sure the editText variable is not null. If the test passes, get the text contents
of that EditText as a string:
if (editText != null) {
reply = editText.getText().toString();
}
Note: do not reuse the same intent from the original request. Always create a new intent for
the response.
1. Add the string from the EditText to the new intent as an extra:
replyIntent.putExtra(REPLY, reply);
2. Set the result to RESULT_OK to indicate the response was successful, and call finish()
to close the activity.
setResult(RESULT_OK,replyIntent);
finish();
Solution Code:
package com.example.android.twoactivities;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
android:visibility="invisible"
2. Open res/values/strings.xml and add a string resource for the reply header:
The layout for the main activity looks the same as it did in the previous task, because
although you have added two new textviews to the layout, their visibility is invisible, and
thus they do not appear on the screen.
Solution Code:
<TextView
android:id="@+id/text_header_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/text_header_reply"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="invisible" />
<TextView
android:id="@+id/text_message_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/text_header_reply"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:textSize="18sp"
android:visibility="invisible" />
<Button
android:id="@+id/button_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:onClick="launchSecondActivity"
android:text="@string/button_main" />
<EditText
android:id="@+id/editText_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toLeftOf="@+id/button_main"
android:layout_toStartOf="@+id/button_main"
android:hint="@string/editText_main" />
</RelativeLayout>
8.4. 4.4 Modify the main activity to handle and display the
response
1. Open java/com.example.android.twoactivities/MainActivity .
2. Add a static at the top of the class to define the key for the response:
startActivityForResult(intent, TEXT_REQUEST);
5. Call super.onActivityResult() :
6. Add code to test for both TEXT_REQUEST (to process the right intent result, in case there
are multiple ones) and the RESULT_CODE (to make sure the request was successful):
if (requestCode == TEXT_REQUEST) {
if (resultCode == RESULT_OK) {
}
}
Inside both if blocks, get the intent extra from the data variable (the response intent):
7. Get the TextView for the message header from the layout.
8. Test to make sure the textViewHead variable is not null. If the test passes, set its visibility
to true:
if (textViewHead != null) {
textViewHead.setVisibility(View.VISIBLE);
}
10. Test to make sure the textViewReply variable is not null. If the test passes, set its text to
the reply message, and set its visibility to true:
if (textViewReply != null) {
textViewReply.setText(reply);
textViewReply.setVisibility(View.VISIBLE);
}
Now, when you send a message to the second activity and get a reply back, the main activity
Solution Code:
package com.example.android.twoactivities;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
intent.putExtra(EXTRA_MESSAGE, message);
startActivityForResult(intent, TEXT_REQUEST);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == TEXT_REQUEST) {
if (resultCode == RESULT_OK) {
String reply = data.getStringExtra(SecondActivity.EXTRA_REPLY);
}
}
}
}
9. Coding challenge
Modify the RecyclerView app from the last chapter so that clicking on a list item starts a
second activity.
In response to these changes to the activity state, the system calls specific lifecycle callback
methods in your activity. If you override and implement those methods you can perform
different actions as the activity runs.
Changes to the activity state can also occur in response to device configuration changes such
as rotating the device from portrait to landscape. This can result in the activity being recreated
from scratch and the loss of state information in that activity. In the second part of this section
we'll experiment with these configuration changes, make note of the information that is retained
when the activity restarts, and find out how to preserve the state of your activities in response
to lifecycle events or device configuration changes.
If you choose to keep and modify the original project, skip to Task 2.
If you copy TwoActivities to a new project, use these steps to update the files and package
names in the new project.
Each stage in the lifecycle has a corresponding callback method (onCreate(), onStart(),
onPause(), and so on). You've already seen one of these methods: onCreate(). By overriding
any of the lifecycle callback methods, you can change how your activity behaves in response to
different user or system actions.
In this task you willimplement all of the activity lifecycle callback methods to print messages to
logcat. This lets you see when the activity lifecycle changes and how that lifecycle affects your
app as it runs.
Log.d(TAG_ACTIVITY, "-------");
Log.d(TAG_ACTIVITY, "onCreate");
4. Create a new method for the onStart() callback that looks like this:
@Override
public void onStart(){
super.onStart();
Log.d(TAG_ACTIVITY, "onStart");
}
The new onStart() method does two things: it calls super.onStart(), and logs that the
method was called.
TIP: Select Code > Override Methods in Android Studio. A dialog appears with all the
possible methods you can override in your class. You can sort the list alphabetically, and group
it by superclass. Choosing one of the callback methods from the list inserts a complete
template for that method, including the call to the superclass.
1. Use the onStart() method as a template to implement the other lifecycle callbacks:
2. onPause()
3. onStop()
4. onResume()
5. onRestart()
6. onDestroy()
All the callback methods have the same signatures (except for the name). If you copy and
paste onStart() to create these other callback methods don't forget to update the contents to
call the right method in the superclass, and to log the correct method.
Log.d(TAG_ACTIVITY, "-------");
Log.d(TAG_ACTIVITY, "onCreate");
}
@Override
public void onStart(){
super.onStart();
Log.d(TAG_ACTIVITY, "onStart");
}
@Override
public void onPause(){
super.onPause();
Log.d(TAG_ACTIVITY, "onPause");
}
@Override
public void onStop(){
super.onStop();
Log.d(TAG_ACTIVITY, "onStop");
}
@Override
public void onResume(){
super.onResume();
Log.d(TAG_ACTIVITY, "onResume");
}
@Override
public void onRestart(){
super.onRestart();
Log.d(TAG_ACTIVITY, "onRestart");
}
@Override
public void onDestroy(){
super.onDestroy();
Log.d(TAG_ACTIVITY, "onDestroy");
}
1. Open java/com.example.android.twoactivities/SecondActivity.
2. Add a static at the top of the class to tag the log statements:
3. Repeat steps 3-5 in the previous task to add the lifecycle callbacks and log statements to
the second activity. (you can also just copy and paste the callback methods from
MainActivity)
4. Add a log statement to the returnReply() method (just before finish()):
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_Second);
Log.d(TAG_ACTIVITY, "-------");
Log.d(TAG_ACTIVITY, "onCreate");
}
5. Experiment using your app and note the lifecycle events that occur in response to different
actions. In particular, try these things:
Use the back button to go back from the second activity to the first.
Use the left arrow to go back from the second activity to the first.
Rotate the device on both the main and second activity at different times in your app
and observe what happens in the log and on the screen. TIP: If you're running your
app in an emulator, you can simulate rotation with Ctrl-F11 or Ctrl-Fn-F11.
Return to the home screen, wait a few seconds, and restart your app.
7. Task 3. Save and restore the activity
instance state
Depending on system resources and user behavior, the activities in your app may be destroyed
and recreated from scratch far more frequently than you think they will be. Rotating the device
is one example of a device configuration change where this happens. All configuration changes
result in the currently running activity being destroyed and recreated as if it were new. If you
don't account for this behavior in your activity, your activity may revert to the default
appearance or the user may lose data or progress in your app.
To keep from losing data in your activities when they are unexpectedly recreated for any
reason, implement the onSaveInstanceState() method. The system calls this method on your
activity (usually between onPause() and onStop()) when there is a possibility the activity may
be destroyed and recreated.
Note that the data you save in the instance state is specific to this specific running instance of
this activity. If you need to save user data in a more persistent way, use shared preferences
(which you'll learn about in a later chapter ) or a database (described in Chapter 7, "Storing
Data.")
1. Open java/com.example.android.twoactivities/MainActivity.
2. Add this skeleton implementation of onSaveInstanceState() to the activity, or use Code >
Override Methods to find and insert a skeleton override.
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
3. Get the two text views for the reply header and reply message:
4. Check to see if the header is currently visible, and if so put that visibility state into the
bundle.
if (textViewHead.getVisibility() == View.VISIBLE) {
outState.putBoolean("reply_visible", true);
}
You only need to save the state of these text views if they are currently visible (that is, a
reply message has been returned from the second activity). If the header is visible there is
reply data to be saved. We put the visibility state (true or false) into the bundle with the
putBoolean() method and the key "reply_visible".
5. Inside that same check, add the reply text into the bundle.
if (textViewHead.getVisibility() == View.VISIBLE) {
outState.putBoolean("reply_visible", true);
outState.putString("reply_text", textViewReply.getText().toString());
}
If the header is visible you can assume that the reply message itself is also visible. You
don't need to test for or save the current visibility state of the reply message. Only the
actual text of the message goes into the state bundle with the key "reply_text".
Note that we only saved the state of those views that can potentially change after the activity is
created. The other views in your app (the EditText, the Button) can be recreated from the
default layout at any time. Note also the system will save the state of some views (such as the
contents of the EditText) as long as they have an ID in the layout file.
if (textViewHead.getVisibility() == View.VISIBLE) {
outState.putBoolean("reply_visible", true);
outState.putString("reply_text", textViewReply.getText().toString());
}
}
1. In the onCreate() method, add a test to make sure the bundle is not null.
if (savedInstanceState != null) {
}
When your activity is created, the system passes the state bundle to onCreate() as the
only argument. The first time onCreate() is called, this bundle is null -- the first time your
app starts, there is no state. Subsequent calls to onCreate() have a bundle populated with
the data you stored in onSaveInstanceState().
2. Inside the bundle test, get the current visibility (true or false) out of the bundle with the key
"reply_visible"
if (savedInstanceState != null) {
Boolean isVisible = savedInstanceState.getBoolean("reply_visible");
}
4. Inside that test, get the two text views for the reply header and reply message:
if (isVisible) {
TextView textViewHead = (TextView)findViewById(R.id.text_header_reply);
TextView textViewReply = (TextView)findViewById(R.id.text_message_reply);
}
5. Also inside that test, get the text reply message from the bundle with the key "reply_text",
and set the text view to show that string.
textViewReply.setText(savedInstanceState.getString("reply_text"));
textViewHead.setVisibility(View.VISIBLE);
textViewReply.setVisibility(View.VISIBLE);
7. Run the app. Try rotating the device or the emulator to ensure that the reply message (if
there is one) remains on the screen even after the activity is recreated.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG_ACTIVITY, "-------");
Log.d(TAG_ACTIVITY, "onCreate");
Use intents to pass information between the two activities. Make sure that the current state of
the playlist activity is saved when you rotate the device.
Extra Credit: Create a Song class to hold the song data, and pass Song objects between the
activities instead of simple strings. Hint: your Song class should implement Parcelable.
Learned about the activity lifecycle and the callback methods that correspond to events in
that lifecycle
Modified the TwoActivities app to explore the activities lifecycle and see which events
happen when as your app runs
Learned about activity instance state and how to save and restore that state in your
activity.
10. Resources
Activity (API Guide)
Activity (API Reference)
Managing the Activity Lifecycle
Recreating an Activity
Handling Runtime Changes
Coding challenge
Conclusion
Resources
Start Activities with Implicit Intents
In this section you'll learn more about intents and how you can use them to launch activities. In
section 2.1 you learned about explicit intents -- launching a specific activity in your app or a
different app by indicating the specific package and class name of that activity.
Implicit intents allow you to launch another activity if you know the action but not the specific
app that will handle that action. For example, if you want your app to take a photo, or send
email, or display a location on a map, those are all actions that another activity can perform.
When your app sends an implicit intent the Android system matches your intent with an app that
can handle that action. If there are multiple apps installed that can handle those actions, the
user is presented with an app chooser that lets them pick which app they want to use for that
request.
Call your application "Implicit Intents" and change the company domain to
"android.example.com."
<string name="edittext_uri">http://developer.android.com</string>
<string name="button_uri">Open Website</string>
2. Change the default RelativeLayout to a Linear Layout. Add the android:orientation attribute
and give it the value "vertical."
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.android.implicitintents.MainActivity"
android:orientation="vertical">
3. Add an EditText and a Button to the layout for the Open Website function. Use these
attribute values:
Attribute (EditText) Value (EditText)
android:id "@+id/editText"
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:text "@string/edittext_uri"
Attribute (Button) Value (Button)
android:id "@+id/button"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:layout_marginBottom "24dp"
android:text "@string/button_uri"
android:onClick "openWebsite"
4. Add a second EditText and a Button for the Open Website function. Use the same
attributes to those in the previous step, but modify these attributes:
Attribute (EditText) Value (EditText)
android:id "@+id/editText2"
android:text "@string/edittext_loc"
Attribute (Button) Value (Button)
android:id "@+id/button2"
android:text "@string/button_loc"
android:onClick "openLocation"
5. Add a third EditText and a Button for the Share This function. Make these changes:
Attribute (EditText) Value (EditText)
android:id "@+id/editText3"
android:text "@string/edittext_share"
Attribute (Button) Value (Button)
android:id "@+id/button2"
android:text "@string/button_share"
android:onClick "shareText"
Solution Code:
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="false"
android:layout_below="@id/editText"
android:layout_marginBottom="24dp"
android:onClick="openWebsite"
android:text="@string/button_uri" />
<EditText
android:id="@+id/editText2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/button"
android:text="@string/edittext_loc" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/editText2"
android:layout_marginBottom="24dp"
android:onClick="openLocation"
android:text="@string/button_loc" />
<EditText
android:id="@+id/editText3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/button2"
android:text="@string/edittext_share" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/editText3"
android:layout_marginBottom="24dp"
android:onClick="shareText"
android:text="@string/button_share" />
</RelativeLayout>
6. Task 2. Implement open website
In this task you'll add the onClick action for the first button in the layout ("Open Website.") This
action uses an implicit intent to send the given URI to an activity that can manage that resource
(such as a web browser).
Note that in this example app we are assuming that the web page URI in the text is an actual
valid URI. Improperly formed or mistyped URIs do not resolve to an action.
6. Use the resolveActivity() and the package manager to find an activity that can handle your
implicit intent. Test to make sure the intent resolved successfully.
if (intent.resolveActivity(getPackageManager()) != null) {
}
7. Inside the test, call startActivity() to send the intent.
startActivity(intent);
8. Add an else block to the test to print a log message if the intent could not be resolved.
} else {
Log.d("ImplicitIntents", "Can't handle this!");
}
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
} else {
Log.d("ImplicitIntents", "Can't handle this!");
}
}
8. Task 4. Implement share this text
Share actions are an easy way for users to share items in your app with social networks and
other apps. Although you could build a share action in your own app using implicit intents,
Android provides the ShareCompat.IntentBuilder helper class to make implementing sharing
easy. Use ShareCompat.IntentBuilder to build the sharing intent and launch a chooser to let the
user choose the destination app for sharing.
In this final task we'll implement sharing a bit of text in a text edit with the
ShareCompat.IntentBuilder class.
ShareCompat.IntentBuilder
.from(this)
.setType(mimeType)
.setChooserTitle("Share this text with: ")
.setText(txt)
.startChooser();
This format, with all the builder's setter methods strung together in one statement, is an easy
shorthand way to create and launch the intent. You can add any of the additional methods to
this list.
ShareCompat.IntentBuilder
.from(this)
.setType(mimeType)
.setChooserTitle("Share this text with: ")
.setText(txt)
.startChooser();
}
9. Task 5. Receive Implicit Intents
Up to this point in the chapter we've created apps that use both explicit and implicit intents to
launch some other app's activity. In this task we'll look at the problem from the other way
around: allowing an activity in your app to respond to intents from some other app.
Activities in your app can always be launched from inside or outside your app with explicit
intents. To allow an activity to receive implicit intents, you define an intent filter in your manifest
to indicate which implicit intents your activity is interested in handling. When your app is
installed, the system registers the intents your activities can handle. When an app sends an
implicit intent, if your activity's intent filter matches that intent, your app appears in the list of
available apps for a given task.
In this task you'll create a very simple app that receives implicit intents to open a web page.
When launched with an intent, that app displays the requested URI.
These lines define an intent filter for the activity, that is, the kind of intents that the activity
is interested in. In this case the receiver app registers its interest in browsable URIs that
use the http protocol and the host developer.android.com.
Solution Code
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
1. Open MainActivity .
2. In the onCreate() method, get the intent that used to start the activity:
4. Check to make sure the uri variable is not null. If that check passes, create a string
message from that URI object:
if (uri != null) {
String uri_string = "URI: " + uri.toString();
}
5. Inside that same if block, get the text view for the message:
6. Check to make sure the text view object is not null, and if that check passes set the text of
that textview to the URI (this code also goes inside the first if block):
if (textView != null) {
textView.setText(uri_string);
}
Running the app on its own shows a blank activity with no text. This is because the activity
was started from the system launcher, and not with an intent from another app.
8. Run the ImplicitIntents app, and click Open Website with the default URI.
An app chooser appears asking if you want to use the default browser or the
ImplicitIntentsReceiver app. Choose "Just Once" for the receiver app. The
ImplicitIntentsReceiver app launches and the message shows the URI from the original
request.
9. Tap the back button and enter a different URI. Click Open Website.
The receiver app has an intent filter that matches only exact URI protocol (http) and host
(developer.android.com). Any other URI opens in the default web browser.
10. Coding challenge
In the last section you created a playlist builder with two activities: one to hold the playlist, and
a song selector. In the playlist activity add functionality that sends an implicit intent to play a
song in a music player.
In this practical, you will learn how to add a background task to your Android app using an
AsyncTask. An AsyncTask is a Java class An AsyncTask is one way to move this work onto a
separate thread, allowing the UI thread to remain responsive.
android:text="Start Task"
Button android:onClick ="startTask"
3. The onClick attribute for the button will be highlighted in yellow, since the startTask()
method is not yet implemented in the MainActivity. Place your cursor in the highlighted text,
press Alt + Enter and choose Create 'startTask(View) in 'MainActivity' to create the
method stub in MainActivity.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ready_to_start"
android:id = "@+id/textView1"
android:textSize="24sp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_task"
android:id="@+id/button"
android:layout_marginTop="56dp"
android:onClick="startTask" />
</LinearLayout>
When an Android application starts, it creates the main (or UI) thread. A thread controls
what executes and in what order. Threads are a way for a program to divide itself into one
or more simultaneously running tasks. See Java 101: Understanding Java Threads.
Asynchronous tasks and any other long-running tasks should not run on the main (or UI)
thread, because the UI thread's primary responsibility is to draw the UI smoothly and
respond to user input instantly. When the UI thread is doing other things (such as finding a
picture on the internet or drawing on a canvas), it cannot refresh the screen or respond to
user taps, which the users experiences as a stuttering and unresponsive app. To avoid this,
any work that does not involve drawing the UI or responding to the user should be moved
from the UI thread to another thread.
In this exercise you will use an AsyncTask to define work to run asynchronously in the
background (that is, to not run on the main thread).
When you create an AsyncTask, you can configure it using these parameters:
Params -- the data type of the parameters sent to the task upon executing the
doInBackground() override method.
Progress -- the data type of the progress units published using the onProgressUpdated()
override method.
Result -- the data type of the result delivered by the onPostExecute() override method.
The work that the AsyncTask performs is defined in the doInBackground() method.
6. Task 2. Create the AsyncTask subclass
In order to use AsyncTask, you need to subclass it. In this example the AsyncTask will execute
a very simple background task: it just sleeps for a random amount of time. In a real app, the
background task could perform all sorts of work, from querying a database, to connecting to
the internet, to calculating the next Go move to beat the champion.
An AsyncTask has the following useful methods for performing work off of the main thread:
onPreExecute(): This method runs on the UI thread, and used for setting up your task (like
showing a progress bar).
doInBackground(): This where your task is accomplished on a separate thread.
onProgressUpdate(): Also invoked on the UI thread and used for updating progress in the
UI (such as filling up a progress bar)
onPostExecute(): Again on the UI thread, used for updating the results to the UI once the
When you create an AsyncTask, you need to give it information about the work to perform,
whether and how to report its progress, and in what form the return the result.
For example, an AsyncTask with the following class declaration would take a String as a
parameter in doInBackground() (yo use in a query, for example), an Integer for
onProgressUpdate() (percentage of job complete), and a Bitmap for the the result in
onPostExecute() (the query result):
1. Create a new Java class called SimpleAsyncTask that extends AsyncTask and takes three
generic type parameters: Void for the params, since this AsyncTask does not require any
inputs, Void for the progress type, since the progress is not published, and a String as the
result type, since you must update the TextView with a string when the AsyncTask has
completed exectin.
Note: The class declaration will be underlined in red, since the required method
doInBackground() has not yet been implemented </div>
The AsyncTask will need to update the TextView once it has completed sleeping. The
constructor will therefore need to include the TextView, so that it can be updated in
onPostExecute().
@Override
protected String doInBackground(Void... voids) {
return null;
}
// Make the task take long enough that we have // time to rotate the phone while it is
running int s = n * 200;
// Sleep for the random amount of time try { Thread.sleep(s); } catch (InterruptedException
e) { e.printStackTrace(); }
// Return a String result return "Awake at last after sleeping for " + s + " milliseconds!"; }
```
1. Implement onPostExecute to take a String argument (this is what you defined in the third
parameter of AsyncTask and what your doInBackground() method returned) and display
that string in the TextView:
1. In the MainActivity class, add code to the startTask() method to create an instance of
SimpleAsyncTask, passing the TextView to the constructor.
2. Call execute() on that SimpleAsyncTask instance.
3. Update the TextView to show the text "Napping"
package android.example.com.simpleasynctask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize mTextView
mTextView = (TextView) findViewById(R.id.textView1);
Note: If the background task completes too quickly before you can rotate the phone while it is
napping, try again. Alternatively, update the code and make it sleep for longer. </div>
Youll notice that when the device is rotated, the TextView resets to its initial content and the
AsyncTask never finishes. There are two things going on here:
Rotating the device restarts the app, calling onDestroy() and then onCreate(), restarting
the activity lifecycle. If you rotate the device while the background task is still running, it
gets disconnected from the activity, and cannot reconnect to it. The task will continue
running in the background, but will not be able to update the TextView that was passed to
it, because that TextView has been destroyed. The task will continue running to completion
in the background, consuming system resources, but never showing the results in the UI,
which gets reset in onCreate().
Even without the AsyncTask, the rotation resets all of the UI elements to their default state,
which for the TextView implies a particular string that you set in the activity_main.xml file.
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize mTextView
mTextView = (TextView) findViewById(R.id.textView1);
AsyncTask provides another very useful override method: onProgressUpdate(), which allows
you to update the UI while the AsyncTask is running. Use this method to update the UI with the
current sleep time. Look to the AsyncTask documentation) to see how onProgressUpdate() is
properly implemented. Remember that in the class definition of your AsyncTask, you will need
to specify the data type to be used in the onProgressUpdate() method.
9. Conclusion
In this exercise, you used an AsyncTask to accomplish a task off of the UI thread, in order to
preserve a fluid user experience. The drawback of an AsyncTask is that when the activity is
destroyed, the task has no way of reconnecting to the recreated activity and the results will
never be delivered. If the task must display results in the UI even when the device's
configuration changes, for example, the device is rotated, you can use an AsyncTaskLoader
instead of an AsyncTask. You will learn about AsyncTaskLoader in the next lesson.
10. Resources
10.0.1. Android Developer Documentation
Guides
Reference
AsyncTask
10.0.3. Videos
Threading Performance 101 by Performance Guru Colt McAnlis. Learn more about the
main thread and why it's bad to run long-running tasks on the main thread.
Good AsyncTask Hunting by Colt McAnlis. Learn more about AsyncTasks.
The Request field is an example of a URI, or a Uniform Resource Identifier. A URI is a string
that locates a particular resource. URLs are a type of URI for locating web pages. For the
Books API, the request is a URL that contains your search as a parameter (following the q
parameter). Notice the API key field after the query field. Usually, to access a public API, you
must obtain an API key and include it in your Request. However, this specific API does not
require a key, so you can omit that portion of the request URI in your app.
1. Find the value for the "title" key. Notice that this result has a single key and value.
2. Find the value for the "authors" key. Notice that this one contains an array of values.
3. In your application, you will only return the title and authors of the first item with both of
these defined in the response.
6. Task 2. Create the "Who Wrote It?" App
Now that you are familiar with the Books API method that you will be using, it is time to set up
the layout of your application.
android:layout_width match_parent
android:layout_height wrap_content
android:id @+id/bookInput
EditText
android:inputType text
android:hint @string/input_hint
android:layout_width wrap_content
android:layout_height wrap_content
android:id @+id/searchButton
Button
android:text @string/button_text
android:onClick searchBooks
android:layout_width wrap_content
android:layout_height wrap_content
TextView android:id @+id/titleText
android:textAppearance @style/TextAppearance.AppCompat.Headline
android:layout_width wrap_content
android:layout_height wrap_content
TextView android:id @+id/authorText
android:textAppearance @style/TextAppearance.AppCompat.Headline
1. Create member variables for the EditText and both the author and title TextViews.
2. Initialize these variables by id in onCreate.
3. In the searchBooks method, get the text from the EditText widget and convert to a String,
assigning it to a string variable.
1. Create a new Java class called FetchBook and have it extend AsyncTask.
An AsyncTask requires three arguments: The input parameters, the progress indicator and the
result type. The generic type parameters for the task will be <String,Void,String> since the
AsyncTask takes a string as a parameter (the query), has no progress update, and returns a
string as a result (the JSON response).
1. Implement the required method (doInBackground()) by placing your cursor on the red
underlined text, pressing Alt + Enter and selecting Implement methods. Choose
doInBackground() and press OK. Make sure the parameters and return types are the
correct type (It takes a String array and returns a String).
2. Click the Code menu and choose Override methods (or pressing Ctrl + O). Select the
onPostExecute() method. The onPostExecute() method should take a String as a
parameter and return void.
3. To display the results in the TextViews, you must have access to those TextViews inside
the AsyncTask. Create member variables in the FetchBook AsyncTask for the two
TextViews that show the results, and assign them in a constructor. You will use this
constructor in your MainActivity to pass along the TextViews to your AsyncTask.
4. Create a LOG_TAG variable to be used throughout your AsyncTask for logging.
@Override
protected String doInBackground(String... params) {
return null;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
}
}
1. In the doInBackground() method of the FetchBook class, get the query string from the
params variable and assign it to a String variable called queryString. The parameter of the
doInBackground() method is a String array generated by the system and created from the
arguments passed into the execute() method when it is triggered. In this case, you will only
pass in a single string (the query), so it will be stored in the first slot of the params array.
2. Create the following two variables that will be needed further along:
3. Create a variable to contain the raw response from the query (the JSON string you
examined earlier) and return it at the end of the method:
4. Create a skeleton try/catch/finally block. This is where you will make your HTTP request.
All of the following code will go in the try block. The catch block is used to handle any
cases where whatever is in the try block fails, and the finally block is for closing the
connections after you've finished taking the data.
try{}
catch(Exception e){}
finally{}
return bookJSONString;
5. Looking back to the request URI from the Books API, notice that all of the requests begin
with the same base URI, followed by query parameters that specify what kind of resource
you are looking for. It is best practice to separate all of these query parameters into
constants, and combine them using a URI builder so that they can be reused for different
URI's. For this application, we limit the number and type of results in order to increase the
query speed, and only look for books that are printed. final String BOOK_BASE_URL =
"https://www.googleapis.com/books/v1/volumes?"; // Base URI for the Books API
//Build up your query URI, limiting results to 5 items and printed books
Uri builtURI = Uri.parse(BOOK_BASE_URL).buildUpon()
.appendQueryParameter(QUERY_PARAM, queryString)
.appendQueryParameter(MAX_RESULTS, "5")
.appendQueryParameter(PRINT_TYPE, "books")
.build();
1. In the try block of the doInBackground() method, open the URL connection and make the
request:
2. Read the response using an InputStream and a StringBuffer, then convert it to a String:
3. Close the try block and log the exception in the catch block.
catch (IOException e) {
e.printStackTrace();
return null;
}
4. Close both the urlConnection and the reader variables in the finally block:
finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Note: Each time the connection fails, this code returns null. This means that onPostExecute()
will have to check for a null string and let the user know that connection failed. This error
handling strategy is overly simple, as the user has no idea why the connection failed. A better
solution would be to handle each point of failure differently so that the user can get the
appropriate feedback. </div>
1. Log the bookJSONString variable before returning it. That's it for the doInBackground()
method.
Log.d(LOG_TAG, bookJSONString);
2. Now that your AsyncTask is set up, you need to launch it from the MainActivity using the
execute() method. Add the following code to your searchBooks method in MainActivity.java
to launch the AsyncTask:
new FetchBook(mTitleText,mAuthorText).execute(mQueryString);
3. Run your app. Execute a search. Your app will crash. Look at your Logs, what is causing
the error? You should see the following line:
This error indicates that you have not included the permission to access the internet in your
AndroidManifest.xml file.
As previously mentioned, there is chance that the doInBackground() method does not return the
expected JSON string (the try catch fails and throws an exception, the network times out or a
number of other possible errors). In that case, the Java JSON methods will fail to parse the
data and another exception will be thrown. This is why the parsing must also be done in a try
block, and the case where incorrect or incomplete data is return must be handled in the catch
block.
To parse the JSON data and handle possible exceptions, do the following:
JSONObject jsonObject = new JSONObject(s); //Convert the response into a JSON object
JSONArray itemsArray = jsonObject.getJSONArray("items"); // Get the JSON Array of
book items
3. Iterate through the itemsArray, checking each book for title and author information. If both
are not null, exit the loop and update the UI; otherwise, continue looking through the list.
This way, only entries with both a title and authors will result in a result being displayed.
// Try to get the author and title from the current item, catch if either field is
empty and move on
try {
title = volumeInfo.getString("title");
authors = volumeInfo.getString("authors");
} catch (Exception e){
e.printStackTrace();
}
//If both a title an authors exist, update the textviews and return
if (title != null && authors != null){
mTitleText.setText(title);
mAuthorText.setText(authors);
return;
}
}
4. If no results with valid titles and authors are found (if the loop finishes executing), set the
title textview to read "No Results Found", and clear the authors textview.
5. In the catch block, print the error to the log, set the title TextView to "No Results Found",
and clear the author textview.
Solution Code:
// Try to get the author and title from the current item, catch if either
field is empty and move on
try {
title = volumeInfo.getString("title");
authors = volumeInfo.getString("authors");
} catch (Exception e){
e.printStackTrace();
}
//If both a title an authors exist, update the textviews and return
if (title != null && authors != null){
mTitleText.setText(title);
mAuthorText.setText(authors);
return;
}
}
When the user clicks "Search Books", the keyboard does not disappear, and there is no
indication to the user that the query is being executed.
If there is no network connection, or the search field is empty, the app still tries to query
the API and fails without properly updating the UI.
If you rotate the screen during a query, the AsyncTask becomes disconnected from the
Activity, and it is not able to update the UI with the results.
1. Add the following code to the searchBooks() method to hide the keyboard when the button
is pushed:
InputMethodManager inputManager =
(InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),InputMethodM
anager.HIDE_NOT_ALWAYS);
2. Add a line of code below the call to execute the FetchBook task that changes the title
textview to read "Loading" and clears the author textview.
3. Extract your String resources.
1. Add the following permission to your Android Manifest to enable access to the network
connection state:
2. Modify your searchBooks() method to check both the connection and if there is anything in
the search field before executing the FetchBook task.
3. Update the UI in the case that there is no connection or no search query, to prompt the
user to fix the error.
Solution Code:
//If the network is active and the search field is not empty, starts a FetchBook
AsyncTask
if (networkInfo != null && networkInfo.isConnected() && queryString.length()!=0) {
new FetchBook(mTitleText, mAuthorText).execute(queryString);
mAuthorText.setText("");
mTitleText.setText(R.string.loading);
}
// Otherwise updates the TextView to tell the user there is no connection or no
search term
else {
if (queryString.length() == 0) {
mAuthorText.setText("");
mTitleText.setText("Please enter a search term");
} else {
mAuthorText.setText("");
mTitleText.setText("Please check your network connection and try again.");
}
}
}
For now, go through your code and extract your resources to finalize this version of Who Wrote
It!
8. Coding challenge
Explore the the specific API you are using in greater detail and find a search parameter that
restricts the results to books that are downloadable in the epub format. Add this parameter to
your request and view the results.
Reference
AsyncTask
You can use an AsyncTaskLoader, instead of an AsyncTask, to run a task asynchronously in the
background. The loader will take care of reconnecting the task to the appropriate activity as
necessary.
When you use an AsyncTask, you implement the onPostExecute() method in the AsyncTask to
display the results on the screen. However, when you use an AsyncTaskLoader, you define
callback methods in an Activity to display the results.
Loaders provide a lot of additional functionality beyond just running tasks and reconnecting to
the Activity. For example, you can attach a loader to a data source and have it automatically
update the UI elements when the underlying data changes. Loaders can also be programmed
to resume loading if interrupted.
So when should you use an AsyncTask if an AsyncTask Loader is so much more robust? The
answer is that it depends on the context. If the background task is likely to finish before any
configuration changes occur, and it is not crucial that is updates the UI, an AsyncTask may be
sufficient. The Loader framework can be inflexible, and actually uses an AsyncTask behind the
scenes to work its magic. A good rule of thumb is to check whether a screen rotation can
happen in the time that your task is running, and if so, use a loader instead of an AsyncTask.
In this chapter, you will learn how to use a AsyncTaskLoader instead of an AsyncTask to run
your Books API query. You will learn more about the other uses of loaders in a later lesson.
//Connects to the network and makes the Books API request on a Background thread
@Override
public String loadInBackground() {
//Sets up variables to be closed
HttpURLConnection urlConnection = null;
BufferedReader reader = null;
String bookJSONString = null;
//Gets the InputStream and reads the response string into a StringBuffer
InputStream inputStream = urlConnection.getInputStream();
StringBuffer buffer = new StringBuffer();
if (inputStream == null) {
// Nothing to do.
return null;
}
reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
/* Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
but it does make debugging a *lot* easier if you print out the completed buffer
for debugging. */
buffer.append(line + "\n");
}
if (buffer.length() == 0) {
// Stream was empty. No point in parsing.
// return null;
return null;
}
bookJSONString = buffer.toString();
// Catches errors
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
//Returns the raw response
return bookJSONString;
}
2. Implement all of the required methods. Be sure to check that you are importing the
LoaderManager class from the support library, to match the loader that you also imported
from the support library.
3. Instead of executing the FetchBook AsyncTask in response to the button press, replace it
with the following code to start up your loader, passing in the query string in a Bundle:
Examine the Override methods from the LoaderCallbacks class. These are where you
instantiate your Loader (onCreateLoader), update your UI with the results (onLoadFinished),
and clean up any remaining resources (onLoaderReset). You will only be using the first two
methods, since your current data model is a simple string that does not need extra care when
the loader is reset.
2. Update onLoadFinished() to process your result: the raw JSON String response from the
BooksAPI.
i. Copy the code from onPostExecute() in your FetchBook class to onLoadFinished() in
your MainActivity.
ii. Replace the argument to the JSONObject constructor with the passed in data String.
iii. Create a LOG_TAG for the MainActivity.
3. Run your app.
4. You should have the same functionality as before, but now in a Loader! One thing still does
not work: when the phone is rotated, the data is lost. That is because the initLoader()
method is needed in onCreate() of MainActivity to reconnect to the loader.
5. Add the following code in onCreate to reconnect to the Loader if it already exists:
if(getSupportLoaderManager().getLoader(0)!=null){
getSupportLoaderManager().initLoader(0,null,this);
}
Note: This pattern is a little counter intuitive. You are only initializing the loader if it already
exists, and not the other way around, since you only want to reattach the loader to the activity if
a query has already been executed. In otherwords, in the initial state of the app no data is
loaded and therefore there is none to preserve. </div>
1. Run your app again and rotate the device. The LoaderManager now holds on to your data
across device configurations!
2. Remove the FetchBook class as it is no longer used.
6. Coding challenge
The response from the Books API contains as many results as you set with the maxResults
parameter, but in this implementation you are only returning the first valid Book result. Modify
your app so that the Data is displayed in a RecyclerView that has maxResults amount of
entries.
Loaders
Managing Network State
Reference
AsyncTaskLoader
Contents:
Coding challenge
Conclusion
Resources
Using the Debugger
In previous practicals you used the Log class to print information to the system log (logcat)
when your app runs. Adding logging statements to your app is one way to find errors and
improve your app's operation. Another way is to use the debugger built into Android Studio.
In this practical you'll learn how to debug your app in an emulator and on the device, set and
view breakpoints, step through your code, and examine variables.
Creating an Android Studio project, and working with EditText and Button views.
Building and running your app in Android Studio, in both an emulator and on a device.
Adding log statements and viewing the system log (logcat) in Android Monitor.
2. What you will LEARN
How to run your app in debug mode in an emulator or on a device.
How to step through the execution of your app.
How to set and organize breakpoints.
How to examine and set watches for variables in the debugger.
3. What you will DO
Build the SimpleCalc app.
Set and view breakpoints in the code for SimpleCalc.
Step through your code as it runs.
Examine variables and evaluate expressions.
4. App Overview
The SimpleCalc app has two edit texts and four buttons. When you enter two numbers and click
a button, the app performs the calculation for that button and displays the result.
5. Task 1. Create the SimpleCalc Project and
App
For this practical you won't build the SimpleCalc app yourself. The complete project is available
at SimpleCalc.zip. In this task you will load the project into Android Studio and explore some of
the app's key features.
The SimpleCalc project builds. Open the project view if it is not already open.
input. The keyboard that appears on screen will only contain numbers.
5.3. 1.3 Explore the Activity Code
1. Open MainActivity (java/com.example.android.simplecalc/MainActivity).
2. Examine the code and note these things:
The calcButton method is the onClick handler for all four calculation buttons.
The calcButton method gets the input values, determines which button was clicked,
and then calls one of four other methods to perform the requested operation.
The calcAdd, calcSub, calcMul, and calcDiv methods perform the actual calculations
and return the results back to calcButton.
The calcButton method updates the view to display the result.
3. Run the app. Try these things:
Enter both integer and floating-point values for the calculation.
Enter floating-point values with large decimal fractions (for example, 1.6753456)
Divide a number by zero.
Leave one or both of the EditText views empty, and try any calculation.
4. Examine the stack trace in Android Studio when the app crashes.
If the stack trace is not visible, click the Android Monitor button at the bottom of the
Android Studio, and then click logcat.
If one or both the EditText views in SimpleCalc is empty, the app will crash. The system log
displays the state of the execution stack at the time the app crashed, which can include
important information about why the crash happened. Examine the stack trace and try to
figure out why the crash in SimpleCalc occurred (but don't fix it yet.)
6. Task 2. Run SimpleCalc in the Debugger
In this task you'll get an introduction to the debugger in Android Studio, and learn how to run
your app in debug mode. You'll also learn about breakpoints, which indicate the points in your
code where you want to stop execution and use the debugger, and how to step through the
execution of your code.
If your app is already running, you will be asked if you want to restart your app in debug
mode. Click Restart app.
2. Choose a device or emulator on which to run your app, and click OK.
Android Studio builds and runs your app on the emulator or on the device. Debugging is the
same in either case. While Android Studio is initializing the debugger, you may see a
message that says "Waiting for debugger" before you can use your app.
If the Debug window does not automatically appear in Android Studio, click the Debug tab
at the bottom of the screen, and then the Debugger tab. The debug window is mostly blank
at this time.
3. Open the MainActivity.java file and click in the first line of the calcButton method (the
declaration of the num1 variable).
4. Click in the left gutter of the editor window at that line. A red dot appears at that line,
indicating a breakpoint.
You can also use Run > Toggle Line Breakpoint or Control-F8 (Command-F8 on OS X)
to set a breakpoint at a line.
5. In the SimpleCalc app, enter numbers in the EditText views and click one of the calculate
buttons.
The execution of your app stops when it reaches the breakpoint you set in the calcButton
method, and the debugger shows the current state of your app at that breakpoint.
9. Watches panel: displays the values for any variable watches you have set.
10. Resume your app's execution with Run > Resume Program or click the Resume icon
on the left side of the debugger window.
The SimpleCalc app continues running, and you can interact with the app until the next time
code execution arrives at the breakpoint.
1. Debug your app, with the breakpoint you set in the last task.
2. In the SimpleCalc app, enter numbers in the EditText views and click one of the calculate
buttons.
You app's execution stops at the breakpoint, and the debugger shows the current state of
the app. The current line is highlighted in your code.
3. Click the Step Over button at the top of the debugger window.
The current line in the calcButton method is executed, and the highlight moves to the next line in
the code. The Variables panel updates to reflect the new execution state, and the values of
those variables also appears after each line of your source code in italics.
You can also use Run > Step Over or F8 to step over your code.
1. Continue clicking Step Over and observe that the state of the debugger changes after each
line is executed.
2. When the app execution gets to any of the calculation methods inside the switch
statement (calcAdd, calcMinus, and so on), click the Step Into icon.
Step Into jumps into the execution of a method call in the current line (versus just executing that
method and remaining on the same line). When you step into a method the Frames panel
updates to indicate the the new frame in the call stack, and the Variables panel shows the
variables in the new method scope. You can click any of the lines in the Frames panel to see
the point in the previous frame where the method was invoked.
You can also use Run > Step Into or F7 to step into a method execution.
1. Use the Step Out icon to execute the remainder of the method and pop back out to the
previous frame in the stack. You can then continue debugging the calcButton from where
you left off.
You can also use Run > Step Out or Shift-F8 to step out of a method execution.
1. Open MainActivity.java. The breakpoint you set in the last task is on the first line of the
calcButton() method.
2. Place a breakpoint at the line that defines the buttonOp variable.
3. Right-click on that new breakpoint and enter the following test in the Condition field:
num1 == 42
4. Click Done.
This second breakpoint is a conditional breakpoint. The execution of your app will only
stop at this breakpoint if the test in the condition is true.
5. Run your app in debug mode (Run > Debug). Enter two numbers other than 42 and click
any calculate button. Execution halts at the first breakpoint in calcButton.
6. Click Resume (or Run > Resume Program) to continue executing the app. Observe
that execution did not stop at your second breakpoint, because the condition was not met.
7. Right click the first breakpoint and uncheck Enabled. Click Done. Observe that the
breakpoint icon now has a green dot with a red border.
Disabling a breakpoint enables you to temporarily "mute" that breakpoint without actually
removing it from your code. If you remove a breakpoint altogether you also lose any
conditions you created for that breakpoint, so disabling it may often be a better choice.
You can also mute all breakpoints in your app at once with the Mute Breakpoints icon.
1. Click Resume (or select Run > Resume Program) to continue executing the app.
2. In the app, enter 42 in the first EditText and click any button. Observe that the conditional
breakpoint at the switch statement halts execution (the condition was met.)
3. Click Resume or select Run > Resume Program.
4. In your app, enter any numbers and click any calculate button in your app.
5. Click the View Breakpoints [ICON HERE] icon on the left edge of the debugger window.
The Breakpoints window appears.
The Breakpoints window enables you to view all the breakpoints in your app, enable or
disable individual breakpoints, and add additional features of breakpoints including
conditions, dependencies on other breakpoints, and logging.
The first breakpoint in calcButton is still muted. Execution stops at the second breakpoint (at
the definition of buttonOp), and the debugger appears.
1. Observe in the Variables panel that the num1 and num2 variables have the values you
entered into the app.
2. Observe that the et1 and et2 variables contain AppCompatEditText objects. Click the
arrow to view the internal properties of those objects.
3. Right-click the num1 variable in the Variables panel, and select Set Value. You can also
type F2.
4. Change the value of num1 to 10 and type Return.
5. Modify the value of num2 to 10 in the same way and type Return.
6. Click the Resume icon to continue running your app. Observe that the result in the app is
now 20, based on the variable values you changed in the debugger.
7. In the app, click any of the calculate buttons. Execution halts at the breakpoint.
8. Click on the et1 variable in the Variables panel. Click the Evaluate Expression [ICON
HERE] icon, or select Run > Evaluate Expression. The Evaluate Expression window
appears.
You can enter any expression in this window to explore the state of variables and objects in
your app, including calling methods on those objects.
Note that the result you get from evaluating an expression is based on the app's current
state. Depending on the values of the variables in your app at the time you evaluate
expressions, you may get different results. Note also that if you use Evaluate Expression to
change the values of variables or object properties, you change the running state of the
app.
Coding challenges
Conclusion
Resources
Testing your App
REVIEWERS: To give feedback, please review the Docs doc here.
Even if you have an app that compiles and runs and looks the way you want it to on different
devices, it's hard to make sure that your app will behave the way you expect it to in every
situation, especially as that app grows and becomes more complex. Even if you try to manually
test your app every time you make a change -- a tedious prospect all on it's own -- you'll likely
miss something or just not anticipate what another user might do with your app to cause it to
fail.
Writing and running tests as part of your app's source code can help you catch bugs early on in
development and improve the robustness of your code as your app gets larger. With tests in
your code you can exercise small portions of your app in isolation, and in an automated and
repeatable manner. The code you write to test your app doesn't end up in the production
version of your app; it lives only on your development machine, alongside your app's code in
Android Studio.
Android Studio and the Android Testing Support Library support several different kinds of tests
and testing frameworks. In this practical you'll learn about the two simplest forms of testing:
Local unit tests and instrumented tests.
Local unit tests are tests that are compiled and run entirely on your local machine with the Java
Virtual Machine (JVM). Use local unit tests to test the parts of your app (such as the internal
logic) that do not need access to the Android framework or an Android device or emulator, or
those for which you can create fake (mock) objects that pretend to behave like the framework
equivalents. Unit tests are written with JUnit, a common unit testing framework for Java.
Instrumented tests are tests that run on an Android device or emulator. These tests have
access to the Android framework and to Instrumentation information such as the app's Context.
You can use instrumented tests for unit testing, user interface (UI) testing, or making sure the
components of your app interact. Most commonly, however, you use instrumentation for UI
testing, which allows you to test that your app behaves correctly when a user interacts with
your app's activities or enters a specific input.
In this practical you'll explore Android Studio's built-in functions for testing, and learn how to
write and run unit tests. For instrumentation and UI testing, Android uses the Espresso
framework. You'll learn about Espresso in a later practical.
1. What you should already KNOW
From the previous practicals you should be familiar with:
In this task you'll look at Android Studio's project setup for unit testing, and configure and run
the default tests.
The main source set, for your app's code and resources.
The test source set, for your app's local unit tests.
The androidTest source set, for Android instrumented tests.
In this task you'll explore how source sets are displayed in Android Studio, and the contents of
the test source are set. You'll use the androidTest source set in more detail in a later practical.
1. Open Project view for the SimpleCalc app, and click the Android tab if it is not already
selected.
2. Expand app and java.
The java folder in your project's Android view lists all three source sets by package name
(com.android.example.simplecalc), with test and androidTest shown in parentheses after
the package name for those different source sets. Note that your testing code has the
same package name as your app's code.
3. Expand the com.android.example.simplecalc (test) folder.
The test folder is where you'll put the code app's local unit tests. Android Studio creates a
sample test for you in this folder, called ExampleUnitTest.
This folder is for Android instrumented tests, including tests that use Espresso for UI
testing. As with the local test folder, there is one sample test class here, called
ApplicationTest.
5. Click the view selector at the top of the project view next to the view tabs and select
Project Files.
The Project Files view shows you all the files in your project as they appear on the
filesystem, along with all of your app's dependencies on external libraries.
Note: Project view is different from Project Files view, which organizes your app's files by
build module.
6. Expand SimpleCalc/app/src.
The src folder contains three subfolders: androidTest, main, and test. These are your app's
source sets.
7. Expand main.
The folder for your app's main code includes the source files (java), the Android manifest
(AndroidManifest.xml) and the resources for your app (res).
8. Expand test.
Your test folder (and thus your app's local unit tests) contains only Java source code, with
no resources. Your test code is associated with your app, but not an actual part of your
app.
9. Expand androidTest.
As with the tests folder, the androidTest folder (for your app's instrumented tests) also
contains only Java code and is not part of your app.
10. Use the view dropdown or click the Android tab to switch back to Android view.
This is the sample unit testing that Android Studio generates when it creates your file. Note
these things about the sample:
Note: The comment in the code about switching the Test Artifact refers to a previous version of
Android Studio and can be ignored.
The project builds, if necessary, and the Run view specific to your tests appears at the
bottom of the screen. At the top of the screen, the dropdown (for the available run
configurations) also changes to ExampleUnitTest with a Java test icon.
All tests in the ExampleUnitTest class run, and if those tests are successful, the progress
bar at the top of the Run view turns green.
Use this option to run all of your local unit test classes at once. The run configurations
dropdown at the top of the screen changes to 'simplecalc in app' with a Java test icon .
In this case there's only one class in your test folder so you'll get the same results as in
Step 1.
Use this option to run all of your Android instrumented tests at once in the same way as
local unit tests. The run configurations dropdown at the top of the screen changes to 'Tests
in com.android.example.simplecalc'' with an Android test icon . Android instrumented
tests require a device or emulator to run.
4. Open the app/java/com.android.example.simplecalc (test) ExampleUnitTest file.
5. Change the assert statement to assert something that's not true. For example, Change the
assertEquals test to:
assertEquals(3, 2 + 2);
The local unit test runs again as before, but this time the test fails (3 is not equal to 2 + 2.)
The progress bar in the run view turns red, and the testing log shows where the test
(assertion) failed:
java.lang.AssertionError:
Expected :3
Actual :4
assertEquals(4, 2 + 2);
Since the run configuration dropdown is still set to ExampleUnitTest, your local tests re-run.
All tests complete successfully and the progress bar turns green.
9. In the run configurations dropdown, select app to run your app normally.
6. Task 2. Create and run local unit tests
With unit testing you take a small bit of code in your app such as a method or a class, isolate it
from the rest of your app, and write tests to make sure that one small bit works in the way you
expect. Typically unit tests call a method with a variety of different inputs, and verify that that
method does what you expect and returns what you expect it to return. Once you're confident
that the one small bit works, you can write larger tests to combine units and test the
interactions between different parts of your app.
Local unit tests in Android Studio are unit tests that run entirely on your local machine, using the
JVM, with no dependencies on the Android framework or on a device or emulator.
In this task you'll write unit tests for the simple calculation methods in the SimpleCalc app and
run those tests to make sure they produce the output we expect.
Note: Unit testing, test-driven development and the JUnit 4 API are all large and complex topics
and outside the scope of this course. See the [Resources](#resources) for links to more
information.
package com.example.android.simplecalc;
import org.junit.Test;
import static org.junit.Assert.*;
The package for the test class is the same as the app itself
( com.example.android.simplecalc) .
The import statements only import classes from the org.junit package. No Android classes
are needed for the test (nor can you use them for local unit tests, even if you wanted to).
The mMain variable contains a new instance of your app's MainActivity. To test the
methods in MainActivity, you invoke them on an instance of that class.
Create an empty method definition in the CalcMethodsTest class for testAdd(), with this
signature:
@Test
public void testAdd() {
}
The methods you create for local unit tests in your test class all have the same format:
The @Test annotation indicates that this is a test. Many other annotations are available,
but @Test is the most common. (See this Stack Overflow post for a annotations summary)
Test methods always return void. Whether a test is successful or not is determined by the
assertions within that method (see next task.)
Add similar empty test methods with the same signature for the other calculation methods
(calcSub(), calcMul(), calcDiv(). Don't forget the @Test annotation -- the test runner does
not recognize a method as a test unless it has that annotation.
Right-click the CalcMethodsTest class and select Run CalcMethodsTest.
The test methods in your skeleton CalcMethodsTest class run and the progress bar at the top
of the Run view turns green. You haven't actually tested anything in this class yet, but Android
Studio considers the tests successful because all the parts are there now.
Note that the left-hand panel of the Run window shows your test class and all the test methods
within it. You can use this to quickly see which test methods passed or failed, and to jump back
to those methods in your source code.
Solution Code
package com.example.android.simplecalctests;
import org.junit.Test;
import static org.junit.Assert.*;
@Test
public void testAdd() {
}
@Test
public void testDiv() { }
@Test
public void testSub() { }
@Test
public void testMul() { }
}
assertEquals(4, mMain.calcAdd(2,2),0);
This is an extremely simple test that asserts a call to calcAdd() with the arguments 2 and 2
will result in the value 4. The call to assertEquals() has three numeric arguments:
2. The value that makes this assertion true. (4)
3. The thing being tested, in this case the calcAdd() method, invoked in the MainActivity
object.
4. How much precision the method call needs to be considered true, here 0. More about
precision in Step 5.
5. Add these assertions just below the one in Step 1:
assertEquals(0, mMain.calcAdd(-2,2),0);
assertEquals(-4, mMain.calcAdd(-2,-2),0);
Although it is impossible to test every possible value that calcAdd may ever see, it's a
good idea to test input that might be unusual or outside what you expect. In the first
assertion we tested ordinary positive numbers; in this one we'll try negative numbers.
6. Right-click the CalcMethodsTest class in the project view and select Run
CalcMethodsTest to run the tests.
All tests complete successfully and the progress bar turns green. Note that the left-hand
panel of the Run window shows your test class and all the test methods within it.
assertEquals(6.66, mMain.calcAdd(4.44f,2.22f),0);
This assertion tests floating-point numbers. The f indicates that these numbers are of type
float, to satisfy the parameters of the calcAdd() method.
This time the tests failed, and the progress bar is red. This is the error:
java.lang.AssertionError:
Expected :6.66
Actual :6.659999847412109
Arithmetic with floating-point numbers is inexact, so this assertion is technically false. This
is where the third argument of assertEquals() is important; it enables you to specify the
precision with which the assertion is equal. The rule for the precision argument (sometimes
called the delta) is:
9. Change the precision argument in the previous assertion to 0.01, and run the tests again.
In the testAdd() method (and in the SimpleCalc app) we're only interested in two decimal
places, so two digits of precision is fine.
10. Add this assertion just below the one in Step 5, and run the tests again:
This is a test for especially large numbers, and this time if you experiment you'll find that
there's no value for precision that will make this test come out true. The error here was in
the decision to specify the values of the arguments as type float rather than double --
floats are not large enough to be able to hold large numbers without rounding errors.
11. Open MainActivity. Scroll down to the calculation utility methods (calcAdd(), calcSub() and
so on).
12. Click inside calcAdd() and select Refactor > Change signature.
13. Replace both float parameters and the return type to double. Click Refactor.
14. In the calcButton() method, change the initialization of the result variable (just before the
case statement) from float to double:
double result = 0;
When you changed the signature of calcAdd(), you created a type mismatch in
calcButton(). Changing the result variable to double corrects the problem.
15. Run the test again.
Changing the code in one way may cause tests that passed earlier to fail this time around.
Running your test suite after every major change in your code helps keep you from
introducing new bugs when you fix old ones.
16. In MainActivity, change the remaining calculation methods to use doubles instead of floats.
Note that you can change the Float.parseFloat() method calls in calcButton() to also use
floats, but that's not required -- a float can be used where a double is expected.
@Test
public void testAdd() {
//test simple addition
assertEquals(4, mMain.calcAdd(2,2),0);
//test floats
//fails, precision
//assertEquals(6.66, mMain.calcAdd(4.44f,2.22f),0);
// correct precision
assertEquals(6.66, mMain.calcAdd(4.44,2.22),0.01);
// test bignums
// Too big for floats, redefine original methods to use double
//assertEquals(234567892, mMain.calcAdd(123456781, 111111111), 0);
}
double result = 0;
switch (operation) {
case "+":
result = calcAdd(num1, num2);
break;
case "-":
result = calcSub(num1, num2);
break;
case "*":
result = calcMul(num1, num2);
break;
case "/":
result = divide(num1, num2);
break;
}
...
}
}
Coding challenge
Conclusion
Resources
Using The Android support libraries
REVIEWERS: To give feedback, please review the Docs doc here.
The Android SDK includes several libraries collectively called the Android support library. These
libraries provide a number of features that are not built into the Android framework, including:
When you use the Android support libraries for backward-compatibility, you do not need to
check for the Android SDK level or build number in your code or create different versions of
your app based on the Android version. The support library manages those system checks for
you and picks the best possible implementation -- the one provided by the framework itself, or
an implementation the library itself provides.
In a previous chapter you configured your project to use the RecyclerView support library, and
learned how to use the RecyclerView classes in your code. In this chapter you'll learn more
about how to use the support libraries in your project.
1. What you should already KNOW
From the previous practicals you should be familiar with:
2. Click the SDK Tools tab, and look for Android Support Repository in the list.
3. If Installed appears in the Status column, youre all set. Click Cancel.
4. If Not installed appears, or an update is available, click the checkbox next to Android
Support Repository. A download icon should appear next to the checkbox. Click OK.
Note: You may also see "Android Support Library" in the SDK Tools list. This is an older
version of the support libraries that is now obsolete.
5. Click OK again, and then Finish when the support repository has been installed.
com.android.support:design:23.3.0
Note that the number at the end of the line may vary.
1. In Android Studio, make sure the Project pane is open and the Android tab is clicked.
2. Expand Gradle Scripts, if necessary, and open the build.gradle (Module: app) file.
Note that build.gradle for the overall project (build.gradle (Project: app_name) is a different
file from the build.gradle for the app module.
3. Locate the dependencies section of build.gradle, near the end of the file.
The dependencies section may already include dependencies for JUnit and the v7
AppCompat support library.
4. Add a dependency for the support library that includes the statement you copied in the
previous task. For example, a complete dependency on the design support library looks
like this:
compile 'com.android.support:design:23.3.0'
If the version number you specified is lower than the currently available library version
number, Android Studio will warn you ("a newer version of com.android.support:design is
available"). Update the version number to the one Android Studio told you to use.
6. Click Sync Now to sync your updated gradle files with the project, if prompted.
6. Coding challenge
Start with the Hello Toast app, and modify it to include the following features:
How to ensure Android Studio has access to the Android support repository.
Where to find the gradle dependency statements for various support libraries.
How to add those support libraries to your app's build.gradle file.
8. Resources
Android Support Library (introduction)
Support Library Setup
Support Library Features
API Reference (all packages that start with android.support)
[TOC]
Use Keyboards, Input Controls, Alerts, and
Pickers
You can customize input methods to make entering data easier for users. In this practical, youll
learn how to use different on-screen keyboards and controls for user input, to show an alert
message that users can interact with, and to provide interface elements for selecting a time
and date.
You will also change the keyboard to one that offers the @ symbol in a prominent location for
entering email addresses, and to a phone keypad for entering phone numbers. As a challenge,
you will implement a listener for the action key in the keyboard in order to send an implicit intent
to another app to dial the phone number.
You will then copy the app to create Phone Number Spinner that offers a spinner input control
for selecting the label (Home, Work, Other, Custom) for the phone number.
The figure above shows the following:
Youll also create Alert Sample to experiment with an alert dialog, and Date Time Pickers to
experiment with a date picker and a time picker and use the selections in your app.
5. Task 1. Experiment with text entry keyboard
attributes
Touching an EditText editable text field places the cursor in the text field and automatically
displays the on-screen keyboard. You will change attributes of the text entry field so that the
keyboard suggests spelling corrections while you type, and automatically starts each new
sentence with capital letters. For example:
5.1. 1.1 Create the main layout and the showText method
You will add a Button, and change the TextView element to an EditText element so that the user
can enter text.
1. Create a new project called Keyboard Samples, and choose the Empty Activities
template.
2. Open the activity_main.xml layout file in order to edit the XML code.
3. Add a Button above the existing TextView element with the following attributes:
Button Attribute New Value
android:id @+id/button_main
android:layout_width wrap_content
android:layout_height wrap_content
android:layout_alignParentBottom "true"
android:layout_alignParentRight "true"
android:onClick "showText"
android:text "Show"
4. Change the existing TextView element to an EditText element with the following attributes:
EditText Attribute TextView Old Value EditText New Value
android:id "@+id/editText_main"
android:layout_width "wrap_content" "match_parent"
android:layout_height "wrap_content" "wrap_content"
android:layout_alignParentBottom "true"
android:layout_toLeftOf "@id/button_main"
android:hint "Enter a message"
android:text "Hello World!" (Remove this attribute)
5. Open MainActivity and enter the following showText method, which retrieves the
information entered into the EditText element and shows it in a toast message:
6. Open strings.xml (in app > res > values), and edit the app_name value to Keyboard
Samples (with a space between Keyboard and Samples).
7. Run the app and examine how the keyboard works.
Tapping the Show button shows a toast message of the text entry.
To close the on-screen keyboard, tap the down-pointing arrow in the bottom row of icons.
In the standard keyboard layout, a checkmark icon appears in the lower right corner of the
keypad, known as the Return (or Enter) key, to enter a new line. With the default attributes for
the EditText element, tapping the Return key adds another line of text. In the next section, you
will change the keyboard so that it capitalizes sentences as you type. As a result of setting the
android:inputType attribute, the default attribute for the Return key changes to shift focus away
from the EditText element and close the keyboard.
android:inputType="textCapSentences"
Capital letters will now appear on the keyboard at the beginning of sentences. When you tap
the Return key on the keyboard, the keyboard closes and your text entry is finished. You can
still tap the text entry field to add more text or edit the text. Tap Show to show the text in a
toast message.
For details about the android:inputType attribute, see Specifying the Input Method Type.
The characters the user enters turn into dots to conceal the entered password. For help, see
Text Fields.
6. Task 2. Change the keyboard type
Every text field expects a certain type of text input, such as an email address, phone number,
password, or just plain text. It's important to specify the input type for each text field in your
app so that the system displays the appropriate soft input method, such as:
android:inputType="textEmailAddress"
android:inputType="phone"
Tapping the field now brings up the on-screen phone keypad in place of the standard keyboard.
Note: When running the app on the emulator, the field will still accept text rather than numbers
if you type on the computers keyboard. However, when run on the device, the field only
accepts the numbers of the keypad.
7. Coding challenge (optional)
You can also perform an action directly from the keyboard and replace the return key with an
action key, such as for dialing a phone number. For this challenge, use the android:imeOptions
attribute for the EditText component with the actionSend value:
android:imeOptions="actionSend"
In the onCreate() method for this main activity, you can use setOnEditorActionListener() to
set the listener for the TextView to detect if the key is pressed:
For help setting the listener, see Specifying the Input Action in Handling Keyboard Input and
Specifying Keyboard Actions in Text Fields.
You can use the IME_ACTION_SEND constant in the EditorInfo class to respond to the
pressed key and call a method to dial the phone number:
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
boolean mHandled = false;
if (actionId == EditorInfo.IME_ACTION_SEND) {
dialNumber();
mHandled = true;
}
return mHandled;
}
To finish the challenge, create the dialNumber method, which uses an implicit intent with
ACTION_DIAL to pass the phone number to another app that can dial the number. It should
look something like this:
A spinner provides a quick way to select one value from a set. Touching the spinner displays a
drop-down list with all available values, from which the user can select one. If you are providing
only two or three choices, you might want to use radio buttons for the choices if you have room
in your layout for them; however, with more than three choices, a spinner works very well, and
takes up little room in your layout. On the other hand, if you have a long list of choices, a
spinner may be too cumbersome and extend beyond your layout, forcing the user to scroll it.
To provide a way to select a label for a phone number (such as Home, Work, Mobile, and
Other), you can add a spinner to the layout to appear right next to the phone number field.
8.1. 3.1 Create a new project and modify the main activitys
layout
1. Copy the KeyboardSamples project folder, rename it to PhoneNumberSpinner, and
refactor it. (See the Appendix for instructions on copying a project.)
2. After refactoring, change the value in the strings.xml file (within app > res > values) to
Phone Number Spinner (with spaces) as the apps name.
3. Open the activity_main.xml layout file.
4. Enclose the EditText and Button elements within a LinearLayout with a horizontal
orientation, placing the EditText element above the Button:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="horizontal">
<EditText
<Button
</LinearLayout>
5. Make the following changes to the EditText and Button elements (changes in bold):
EditText Attribute Value
android:id "@+id/editText_main"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:layout_alignParentBottom (Remove this attribute)
android:inputType phone
android:hint "Enter phone number
6. Add a Spinner element between the EditText element and the Button element:
<Spinner
android:id="@+id/label_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</Spinner>
The Spinner element provides the drop-down list. In the next practical you will add code
that will fill the spinner list with values. The layout code for the EditText, Spinner, and
Button elements should now look like this:
<EditText
android:id="@+id/editText_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="phone"
android:hint="Enter phone number" />
<Spinner
android:id="@+id/label_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="showText"
android:text="Show" />
7. Add another LinearLayout with a horizontal orientation to enclose two TextView elements
side-by-side a text description, and a text field to show the phone number and the
phone label and align the LinearLayout to the parents bottom:
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentBottom="true">
<TextView
<TextView
</LinearLayout>
9. Check your layout by clicking the Preview tab on the right side of the layout window.
You should now have a screen with the phone entry field at the top on the left, a skeletal
spinner next to the field, and the Show button on the right. At the bottom should appear the
text Phone Number: followed by Nothing entered.
10. Extract your strings into string resources: Place the cursor on the hard-coded string, press
Alt-Enter (Option-Enter on the Mac), and select Extract string resources. Then edit the
Resource name for the string value. Extract as follows:
Element String String resource
EditText "Enter phone number" "@string/hint_phonenumber"
Button "Show" "@string/show_button"
TextView "Phone Number: " "@string/phonenumber_label"
TextView "Nothing entered." "@string/nothing_entered"
Why: The string resource assignments are stored in the strings.xml file (under app > res >
values). You can edit this file to change the string assignments so that the app can be localized
with a different language.
8.2. 3.2 Add code to activate the spinner and its listener
The choices for this phone label spinner are predetermined, so you can use a simple text array
defined in strings.xml to hold the values for it, and use an ArrayAdapter to assign the array to
the spinner.
An adapter connects your data in this case, the array of spinner items to the spinner
view. The pattern is similar to using an adapter to connect data to the RecyclerView, as shown
in a previous lesson.
To activate the spinner and its listener, implement the AdapterView.OnItemSelectedListener
interface, which requires also adding the onItemSelected() and onNothingSelected() callback
methods.
1. Open strings.xml to define the values (Home, Work, Mobile, and Other) for the spinner as
the string array labels_array:
<string-array name="labels_array">
<item>Home</item>
<item>Work</item>
<item>Mobile</item>
<item>Other</item>
</string-array>
2. To define the selection callback for the spinner, change your MainActivity class to
implement the AdapterView.OnItemSelectedListener interface as shown:
As you type AdapterView. in the above statement, Android Studio automatically imports
the AdapterView widget. This line should appear in your block of import statements:
import android.widget.AdapterView;
After typing OnItemSelectedListener in the above statement, a red light bulb appears in
the left margin.
3. Click the bulb and choose Implement methods. The onItemSelected() and
onNothingSelected() methods, which are required for OnItemSelectedListener, should
already be highlighted, and the Insert @Override option should be checked. Click OK.
4. Instantiate a spinner object using the Spinner element in the layout (label_spinner), and
set its listener (spinner.setOnItemSelectedListener) during the onCreate method that
is, when the main screen is created. Add the code to the onCreate method:
5. Continuing to edit the onCreate method, add a statement that creates the ArrayAdapter
with the string array (labels_array) using the simple spinner layout for each item
(layout.simple_spinner_item), specify the layout for the spinners choices to be
simple_spinner_dropdown_item, and then apply the adapter to the spinner:
// Create ArrayAdapter using the string array and default spinner layout.
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.labels_array, android.R.layout.simple_spinner_item);
// Specify the layout to use when the list of choices appears.
adapter.setDropDownViewResource
(android.R.layout.simple_spinner_dropdown_item);
// Apply the adapter to the spinner.
if (spinner != null) spinner.setAdapter(adapter);
1. Declare the mSpinnerLabel string at the beginning of the MainActivity class definition:
2. Add code to the empty onItemSelected() callback method, as shown below, to retrieve
the users selected item using getItemAtPosition, and assign it to mSpinnerLabel:
public void onItemSelected(AdapterView<?> adapterView, View view, int
i, long l) {
mSpinnerLabel = adapterView.getItemAtPosition(i).toString();
}
3. Add code to the empty onNothingSelected() callback method, as shown below, to display
a logcat message if nothing is selected:
The TAG in the above statement is in red because it hasnt been defined.
4. Click TAG, click the red light bulb, and choose Create constant field TAG from the pop-
up menu. Android Studio adds the following under the MainActivity class declaration:
5. Add MainActivity.class.getSimpleName() to use the simple name of the class for TAG:
6. Change the String showString statement in the showText method to show both the
entered string and the selected spinner item (mSpinnerLabel):
The spinner appears next to the phone entry field and shows the first choice (Home). Tapping
the spinner reveals all the choices. After entering a phone number and choosing a spinner item,
tap the Show button to show a message at the bottom of the screen with the phone number
and the selected spinner item.
8.3.1. Solution Code
activity_main.xml:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="horizontal">
<EditText
android:id="@+id/editText_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="phone"
android:hint="@string/hint_phonenumber" />
<Spinner
android:id="@+id/label_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</Spinner>
<Button
android:id="@+id/button_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="showText"
android:text="@string/show_button" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentBottom="true">
<TextView
android:id="@+id/title_phonelabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/phonenumber_label"/>
<TextView
android:id="@+id/text_phonelabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nothing_entered"/>
</LinearLayout>
</RelativeLayout>
MainActivity:
package com.example.android.phonenumberspinner;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
/**
* This app is a copy of KeyboardSamples that
* adds a spinner to the layout to appear right next to the phone number.
* The spinner lets the user choose the type of phone number:
* Home, Work, Mobile, and Other.
*/
public class MainActivity extends AppCompatActivity implements
AdapterView.OnItemSelectedListener {
// Define TAG for logging.
private static final String TAG = MainActivity.class.getSimpleName();
// Define mSpinnerLabel for the label (spinner item that user chooses).
private String mSpinnerLabel = "";
/**
* Set the content view, create the spinner, and create the
* array adapter for the spinner.
* @param savedInstanceState Saved instance.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* Retrieves the text and spinner item and shows them in text_phonelabel.
* @param view The view containing editText_main.
*/
public void showText(View view) {
EditText editText = (EditText) findViewById(R.id.editText_main);
if (editText != null) {
// Assign to showString both the entered string
// and mSpinnerLabel.
String showString =
(editText.getText().toString() + " - " + mSpinnerLabel);
// Assign to phoneNumberResult the view for text_phonelabel
// to prepare to show it.
TextView phoneNumberResult = (TextView)
findViewById(R.id.text_phonelabel);
// Show the showString in the phoneNumberResult.
if (phoneNumberResult != null)
phoneNumberResult.setText(showString);
}
}
/**
* Retrieves the selected item in the spinner using getItemAtPosition,
* and assigns it to mSpinnerLabel.
* @param adapterView The adapter for the spinner.
* @param view The view within the adapterView that was clicked.
* @param i The position of the view in the adapter.
* @param l The row id of the item (not used).
*/
@Override
public void onItemSelected(AdapterView<?> adapterView,
View view, int i, long l) {
mSpinnerLabel = adapterView.getItemAtPosition(i).toString();
}
/**
* Logs the fact that nothing was selected in the spinner.
* @param adapterView The adapter for the spinner, where the
* selection should have occurred.
*/
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
Log.d(TAG, "onNothingSelected: ");
}
}
9. Task 4. Use a dialog for an alert requiring a
decision
You can provide a dialog for an alert to require users to make a decision. A dialog is a window
that appears on top of the display or fills the display, interrupting the flow of activity.
For example, an alert dialog might require the user to click Continue after reading it, or give the
user a choice to agree with an action by clicking a positive button (such as OK or Accept), or to
disagree by clicking a negative button (such as Cancel). In Android, you use the AlertDialog
subclass of the Dialog class to show a standard dialog for an alert.
Tip: Use dialogs sparingly as they interrupt the users work flow. Read the [Dialogs design
guide](https://www.google.com/design/spec/components/dialogs.html#) for best design
practices, and [Dialogs](https://developer.android.com/guide/topics/ui/dialogs.html) in the
Android developer documentation for code examples.
In this practical, you will add a button to trigger a standard alert dialog. In a real world app, you
might trigger an alert dialog based on some condition, or based on the user tapping something.
1. Create a new project called Alert Sample based on the Empty Activity template.
2. Open the activity_main.xml layout, and make the following changes:
TextView Attribute Value
android:id "@+id/topmessage"
android:text "Tap to test the alert:"
3. Extract the android:text string above into the resource tap_test to make it easier to
translate.
4. Add a Button with the following attributes:
Button Attribute Value
android:id "@+button1"
android:layout_width wrap_content
android:layout_height wrap_content
android:layout_below "@id/topmessage"
android:layout_marginTop "36dp"
android:text "Alert"
android:onClick "onClickShowAlert"
5. Extract the android:text string above into the resource alert_button to make it easier to
translate.
The builder class is usually a static member class of the class it builds. Thus, you use
AlertDialog.Builder, which is a subclass of the AlertDialog class, to build a standard alert dialog,
using setTitle to set its title, setMessage to set its message, and setPositiveButton and
setNegativeButton to set its buttons.
To make the alert, you need to make an object of AlertDialogBuilder, which is a subclass of
AlertDialog. You will add the onClickShowAlert() method, which makes this object as its first
order of business.
Note: To keep this example simple to understand, the alert dialog is created in the
**onClickShowAlert()** method. This occurs only if the **onClickShowAlert()** method is called,
which is what happens when the user clicks the button. This means the app builds a new dialog
every time the button is clicked. In a real world app, you may want to build the dialog once in
the onCreate() method, and then invoke the dialog in the onClickShowAlert() method.
1. Add the onClickShowAlert() method to MainActivity.java as follows:
Note: If AlertDialog.Builder is not recognized as you enter it, click the red bulb icon, and
choose the support library version for importing into your activity.
2. Set the title and the message inside the alert dialog:
3. Extract the title and message into string resources. The previous lines of code should now
be:
4. Add the OK button to the alert with setPositiveButton() and using onClickListener():
You set the positive (OK) and negative (Cancel) buttons using the setPositiveButton()
and setNegativeButton() methods. After the user taps the OK button in the alert, you can
grab the users selection and use it in your code. In this example, you display a toast
message if the OK button is clicked.
5. Extract the string resource for OK and for Pressed OK. The statement should now be:
6. Add the Cancel button to the alert with setNegativeButton() and onClickListener(),
display a toast message if the button is clicked, and then cancel the dialog:
7. Extract the string resource for Cancel and Pressed Cancel. The statement should now
be:
alertDialog.setNegativeButton(R.string.cancel, new
DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// User cancelled the dialog.
Toast.makeText(getApplicationContext(), R.string.pressed_cancel,
Toast.LENGTH_SHORT).show();
}
});
Tip: To learn more about onClickListener and other listeners, see [User Interface: Input
Events](http://developer.android.com/guide/topics/ui/ui-events.html).
In this task youll create a new project, and add the date picker and time picker. Rather than
implementing the pickers in the apps main activity, youll make a quantum leap forward in your
Android development experience and use fragments. A fragment is a behavior or a portion of
user interface within an activity. Its like a mini-activity within the main activity, with its own own
lifecycle. A fragment receives its own input events, and you can add or remove it while the main
activity is running. You might combine multiple fragments in a single activity to build a multiple-
pane user interface, or reuse a fragment in multiple activities. To learn about fragments, see
Fragments in the API Guide.
One benefit of using fragments for the pickers is that you can isolate the code sections for
managing the date and the time after the user selects them from the pickers. The best practice
to show a picker is to use an instance of DialogFragment, which is a subclass of Fragment. A
DialogFragment displays a dialog window floating on top of its activitys window. In this exercise
youll add a fragment for each picker dialog and use DialogFragment to manage the dialog
lifecycle.
Tip: Another benefit of using fragments for the pickers is that you can implement different
layout configurations, such as a basic dialog on handset-sized displays or an embedded part of
a layout on large displays.
LINK TO APP GOES HERE
5. Add a RelativeLayout child inside the LinearLayout to contain the Button elements, and
accept the match parent default width and height.
6. Add the first Button element within the RelativeLayout with the following attributes:
First Button Attribute Value
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:id "@+id/button_date"
android:layout_marginTop "12dp"
android:text "Date"
android:onClick "showDatePickerDialog"
Dont worry that the showDatePickerDialog reference is in red. The method hasnt been
defined yet you define it later.
7. Extract the string Date into the string resource date_button.
8. Add the second Button element inside the RelativeLayout child with the following
attributes:
Second Button Attribute Value
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:id "@+id/button_time"
android:layout_marginTop "12dp"
android:layout_alignBottom "@id/button_date"
android:layout_toRightOf "@id/button_date"
android:text "Time"
android:onClick "showTimePickerDialog"
Dont worry that the showTimePickerDialog reference is in red. The method hasnt been
defined yet you define it later.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@string/choose_datetime />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button_date"
android:layout_marginTop="12dp"
android:text="@string/date_button"
android:onClick="showDatePickerDialog"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button_time"
android:layout_marginTop="12dp"
android:layout_alignBottom="@id/button_date"
android:layout_toRightOf="@id/button_date"
android:text="@string/time_button"
android:onClick="showTimePickerDialog"/>
</RelativeLayout>
</LinearLayout>
import android.app.DatePickerDialog.OnDateSetListener;
import android.support.v4.app.DialogFragment;
After adding the empty onDateSet() method, Android Studio automatically adds the
following in the import block at the top:
import android.widget.DatePicker;
5. Replace onCreateView() with onCreateDialog(), and remove the empty public constructor
for DatePickerFragment. Annotate the onCreateDialog() method with @NonNull, and add
the following code to onCreateDialog() to initialize the year, month, and day from
Calendar, and return the dialog and these values to the main activity.
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current date as the default date in the picker.
final Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
6. To make the code more readable, change the onDateSet() methods parameters from int
i, int i1, and int i2 to int year, int month, and int day:
public void onDateSet(DatePicker view, int year, int month, int day)
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current date as the default date in the picker.
final Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
public void onDateSet(DatePicker view, int year, int month, int day) {
// Do something with the date chosen by the user.
}
}
10.3. 5.3 Create a new fragment for the time picker
Add a fragment to the DateTimePickers project for the time picker:
Note:As you make the changes, Android Studio automatically adds the following in the
**import** block at the top:
import android.app.TimePickerDialog.OnTimeSetListener;
import android.support.v4.app.DialogFragment;
import android.app.TimePickerDialog;
import android.widget.TimePicker;
import java.util.Calendar;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current time as the default values for the picker.
final Calendar c = Calendar.getInstance();
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
1. Open MainActivity.
2. Add the showDatePickerDialog() and showTimePickerDialog() methods, referring to the
solution code below. Both create an instance of FragmentManager to manage the
fragment and show the picker. For more information about managing fragments, see
Fragments.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void showDatePickerDialog(View v) {
DialogFragment newFragment = new DatePickerFragment();
newFragment.show(getSupportFragmentManager(), "datePicker");
}
Run the app. You should see the date and time pickers after tapping the buttons.
Old code:
public void onDateSet(DatePicker view, int year, int month, int day) {
// Do something with the date chosen by the user
}
New code:
public void onDateSet(DatePicker view, int year, int month, int day) {
// Do something with the date chosen by the user
Strstyle="border:1px solid black" ing month_string = Integer.toString(month+1);
String day_string = Integer.toString(day);
String year_string = Integer.toString(year);
Strstyle="border:1px solid black" ing dateMessage = (month_string + "/" + day_string
+ "/" + year_string);
MainActivity activity = (MainActivity) getActivity();
activity.onFinishDateDialog(dateMessage);
}
The first three lines beginning with String should be obvious: they convert the month, day,
and year to separate strings.
Tip:The month integer returned by the date picker starts counting at 0 for January, so you
need to add 1 to it to start show months starting at 1.
The fourth line beginning with String concatenates the three strings and includes slash
marks for the U.S. date format.
The fifth line uses getActivity() which, when used in in a fragment, returns the activity the
fragment is currently associated with. You need this because you cant call the
onFinishDateDialog() method in MainActivity without the context of MainActivity. The
activity inherits the context, so you can use it as the context for calling the method (as in
activity.onFinishDateDialog). You pass the dateMessage to that method.
Note:The **onFinishDateDialog()** method is in red because it has not yet been created in
MainActivity.
The TimePickerFragment uses the same logic.
Old code:
New code:
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
// Do something with the time chosen by the user
String hour_string = Integer.toString(hourOfDay);
String minute_string = Integer.toString(minute);
String timeMessage = (hour_string + ":" + minute_string);
MainActivity activity = (MainActivity) getActivity();
activity.onFinishTimeDialog(timeMessage);
}
Note:The **onFinishTimeDialog()** method is in red because it has not yet been created in
MainActivity.
3. Add the onFinishDateDialog() and onFinishTimeDialog() methods in the main activity to
take the date or time message and show it in a toast message:
toast message.
11. Summary
In this practical you learned the following:
Setting up XML layout attributes to control the keyboard for an EditText element:
Using the textAutoCorrect value for the android:inputType attribute to change the
keyboard so that it suggests spelling corrections.
Using the textCapSentences value for the android:inputType attribute to start each
new sentence with a capital letter.
Using the textPassword value for the android:inputType attribute to hide a
password when entering it.
Using the textEmailAddress value for the android:inputType attribute to show an
email keyboard rather than a standard keyboard.
Using the phone value for the android:inputType attribute to show a phone keypad
rather than a standard keyboard.
Challenge: Using the android:imeOptions attribute with the actionSend value to
perform an action directly from the keyboard and replace the Return key with an
action key, such as an implicit intent to another app to dial a phone number.
Using a Spinner input control to provide a drop-down menu, and writing code to control it:
Using an ArrayAdapter to assign an array of text values as the spinner menu items.
Implementing the AdapterView.OnItemSelectedListener interface, which requires
also adding the onItemSelected() and onNothingSelected() callback methods to
activate the spinner and its listener.
Using the onItemSelected() callback method to retrieve the selected item in the
spinner menu using getItemAtPosition.
Using AlertDialog.Builder, a subclass of AlertDialog, to build a standard alert dialog, using
setTitle to set its title, setMessage to set its message, and setPositiveButton and
setNegativeButton to set its buttons.
Using the standard date and time pickers:
Adding a fragment for a date picker, and extending the DialogFragment class to
implement DatePickerDialog.OnDateSetListener for a standard date picker with a
listener.
Adding a fragment for a time picker, and extending the DialogFragment class to
implement TimePickerDialog.OnTimeSetListener for a standard time picker with a
listener.
Implementing the onDateSet(), onTimeSet(), and onCreateDialog() methods.
Using the onFinishDateDialog() and onFinishTimeDialog() methods to retrieve the
selected date and time.
12. Resources
Android API Guide, Develop section:
Specifying the Input Method Type
Text Fields
Input Controls
Spinners
Dialogs
Fragments
Input Events
Pickers
DateFormat
Material Design Spec:
Dialogs design guide
[TOC]
The options menu is the primary collection of menu items for an activity. You can use the
options menu for navigation to other activities, such as placing an order, or for actions that have
a global impact on the app, such as changing settings.
Options menu items appear in the action overflow popup menu (see figure below). However,
you can place some items as icons as many as can fit in the top bar, which is called the
app bar. Its a dedicated space at the top of each screen that is generally persistent throughout
the apps screens. Using the app bar makes your app consistent with other Android apps,
allowing users to quickly understand how to operate your app and have a great experience.
1. First two options menu items appearing as icons in the app bar.
2. The action overflow button to show more options menu items.
3. More options menu items appearing in the overflow pop-up.
**Tip:** To provide a familiar and consistent user experience, you should use the Menu APIs to
present user actions and other options in your activities. See [Menus]
(http://developer.android.com/guide/topics/ui/menus.html#PopupMenu) for details.
1. What you should already KNOW
From the previous chapters, you should be familiar with how to do the following:
Continuing this exercise and the menu theme, you will create a prototype of an app for placing
orders for food items. You will use placeholder images, also known as mock-ups, that stand
in for functions you havent developed yet, such as listing food items with Add to order
buttons, so that the app appears to be a prototype of a full app. You will then connect a new
activity to the Order menu choice, and create radio buttons on the Order screen for choosing
5.1. 1.1 Start the new project and examine the app bar code
1. Start a new Android Studio project with the app name Options Menu Sample. Choose the
Basic Activity template, accept the default settings for the main activity, and click Finish.
The project opens with two layouts in the res > layout folder: activity_main.xml, and
content_main.xml.
2. Open content_main.xml to see what it does. It defines a RelativeLayout with the layout
behavior set to @string/appbar_scrolling_view_behavior, which controls the scrolling
behavior of the app bars options menu. This string, defined in the values.xml file (which is
generated by Android Studio and should not be edited), is
android.support.design.widget.AppBarLayout$ScrollingViewBehavior.
For more about scrolling behavior, see the Android Design Support Library blog entry in the
Android Developers Blog. For design practices involving scrolling menus, see Scrolling
Techniques in the Material Design spec.
3. In content_main.xml, extract the Hello World string in the TextView to use the
intro_text resource name, and then open strings.xml and redefine the resource to use
some descriptive text, such as Droid Desserts for a dessert-ordering app:
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
For more details about the AppBarLayout class, see AppBarLayout in the Android
Developer Reference. For more details about toolbars, see Toolbar in the Android
Developer Reference.
The activity_mail.xml layout also uses an include layout statement to include the entire
layout defined in content_main.xml. This separation of layout definitions makes it easier
to change the layouts content (for now, just the TextView field), apart from the layouts
toolbar definition and coordinator layout. This is a best practice for separating your content
(which may need to be translated) from the format of your layout.
5. Run the app. Notice the bar at the top of the screen showing the name of the app. It also
shows the action overflow button (three vertical dots) on the right side. Tap the overflow
button to see the menu, with Settings as the menu option.
6. Examine the AndroidManifest.xml file. The .MainActivity activity is set to use the
NoActionBar theme to prevent the app from using the native ActionBar class attributes for
the app bar:
android:theme="@style/AppTheme.NoActionBar"
The NoActionBar theme is defined in the styles.xml file (expand app > res >values >
styles.xml to see it). Styles are covered in another lesson, but you can see that the
NoActionBar theme sets the windowActionBar attribute to false (no window action bar),
and the windowNoTitle attribute to true (no title).
Why: The reason these values are set is because you are defining the app bar in your
layout with AppBarLayout, which would conflict with the default settings. You need to
essentially disable the ActionBars window if you want to replace it with a Toolbar.
7. Look at the MainActivity.java file, which extends AppCompatActivity and starts with the
onCreate() method.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
...
The activitys onCreate() method calls the activitys setSupportActionBar() method, and
passes toolbar to it, setting the toolbar defined in activity_main.xml as the app bar for
the activity.
For best practices about adding an app bar to your app, see Adding the App Bar in Best
Practices for User Interface.
In keeping with the menu idea, you will design this app as a restaurant app to show options
for ordering food, checking the status of your order, showing favorite foods, and contacting the
restaurant. You will change the options menu to have four options: Contact (replacing Settings),
Order, Status, and Favorites.
1. Take a look at menu_main.xml (expand res > menu in the Project view). It defines menu
items with the </> tag within the
block. The only menu item provided from the template is Settings, which is defined as:
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
2. Open strings.xml to see that the resource name action_settings has string value of
Settings:
<string name="action_settings">Settings</string>
3. Go back to menu_main.xml, and change the following attributes of the Settings item to
make it the Contact item:
Attribute Value
android:id "@+id/action_contact"
android:title "Contact"
app:showAsAction "never"
4. Extract the hard-coded string Contact into the string resource action_contact.
5. Add a new Order menu item using the </> tag within the
block, and give the item the following attributes:
Attribute Value
android:id "@+id/action_order"
android:orderInCategory 10
android:title "Order"
app:showAsAction "never"
The android:orderInCategory attribute specifies the order in which the menu items
appear in the menu, with the lowest number appearing higher in the menu. The Contact
item is set to 100, which is a big number in order to specify that it shows up at the bottom
rather than the top. You set the Order item to 10, which puts it above Contact, and leaves
plenty of room in the menu for more items.
6. Extract the hard-coded string Order into the string resource action_order.
7. Add two more menu items the same way with the following attributes:
Status Item Attribute Value
android:id "@+id/action_status"
android:orderInCategory 20
android:title "Status"
app:showAsAction "never"
8. Extract Status into the resource action_status, and Favorites into the resource
action_favorites.
9. You will display a toast message with an action message depending on which option menu
item the user selects. Add the following string names and values in
** strings.xml </strong> for these messages:
if (id == R.id.action_order)
Run the app, and tap the action overflow icon to see the options menu. You will soon add
callbacks to respond to items selected from this menu.
Notice the order of items in the options menu.
You used the android:orderInCategory attribute to specify the priority of the menu items
in the menu: The Order item is 10, followed by Status (20) and Favorites (40), and Contact
is last (100). The following table shows the priority of items in the menu:
Menu Item orderInCategory attribute
Order 10
Status 20
Favorites 40
Contact 100
6. Task 2. Add icons for menu items and
images for the layout
Whenever possible, you want to show the most frequently used actions using icons in the app
bar so the user can click them without having to first click the overflow icon. In this task, youll
add icons for some of the menu items, and show some of menu items in the app bar at the top
of the screen as icons.
In this example, the Order and Status actions are considered the most frequently used.
Favorites is occasionally used, and Contact is the least frequently used. You can set icons for
these actions, and specify the following:
1. Expand res in the Project view, and right-click (or Command-click) drawable.
2. Choose New > Image Asset. The Configure Image Asset dialog appears.
3. Choose Action Bar and Tab Items in the drop-down menu.
4. Change ic_action_name to ic_order_white (for the Order action). The Configure Image
Asset screen should look as follows (see Image Asset Studio for a complete description.)
5. Click the clipart image (the Android logo) to select a clipart image as the icon. A page of
icons appears. Click the icon you want to use for the Order action (for example, the
clipboard icon may be appropriate).
6. Choose HOLO_DARK from the Theme drop-down menu. This sets the icon to be white
against a dark-colored (or black) background. Click Next.
7. Click Finish in the Confirm Icon Path dialog.
8. Repeat the above steps for the Status and Favorites icons, naming them ic_status_white
and ic_favorites_white respectively. You may want to use the circled-i icon for Status
(typically used for Info), and the heart icon for Favorites.
6.2. 2.2 Show the menu items as icons in the app bar
To show menu items as icons in the action bar, use the app:showAsAction attribute in
menu_main.xml. The following values for the attribute specify whether or not the action should
appear in the action bar as an icon:
Run the app. You should now see at least two icons in the app bar: Order and Status. If
your device or the emulator is displaying in vertical orientation, the Favorites and Contact
options appear in the overflow menu.
Rotate your device to the horizontal orientation, or if youre running in the emulator, click
the Rotate Left or Rotate Right icons to rotate the display into the horizontal orientation.
You should then see all three icons in the app bar: Order, Status, and Favorites.
Tip: How many actions will fit in the app bar? It depends on the orientation and the size of the
device screen. Action buttons may not occupy more than half of the main app bar's width.
6.3. 2.3 Add images to the layout to simulate a dessert app
1. Open content_main.xml, and change the TextView in the layout to use a larger text size
of 24sp and padding of 10dp, and add the android:id attribute with the id textintro.
2. Add another TextView under the textintro TextView with the following attributes:
TextView Attribute Value
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:padding "10dp"
android:id "@+id/choose_dessert"
android:layout_below "@id/textintro"
android:text "Choose one dessert with your order."
3. Extract the string resource for the android:text attribute to the resource name choose.
4. Copy the images into the project. The images named donut_circle.jpg, froyo_circle.jpg,
and icecream_circle.jpg are provided with the app. To add the images, close the project,
copy the image files into the drawables folder (project_name > app > src > main > res >
drawable), and reopen the project.
5. Open content_main.xml file again and add an ImageView to the layout for the donut
under the TextView to show the first image, using the following attributes:
ImageView Attribute for donut Value
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:padding "10dp"
android:id "@+id/donut"
android:layout_below "@id/choose_dessert"
android:contentDescription "Donuts are glazed and sprinkled with candy."
android:src "@drawable/donut_circle"
Look at how other apps implement the floating action button. For example, the Gmail app
provides a floating action button to create a new email message, and the Contacts app
provides one to create a new contact. For more information, see Snackbar.
Now that you know how to add icons for menu items, use the same technique to add another
icon, and assign that icon to the floating action button, replacing the email icon. For example,
you might want the floating action button to start a chat session; in which case you might want
to use an icon showing a human.
While adding the icon, also change the text that appears in the snackbar after tapping the
floating action button. You will find this text in the Snackbar.make statement in the Main
Activity. Extract the string resource for this text to be snackbar_text.
8. Task 3. Add event handlers for the menu
items
In this task, youll add methods to act as event handlers when the menu items are clicked. Youll
also add a switch case block to determine which menu item was selected in order to call the
appropriate method for that menu item.
Each method gets the text from the appropriate string (such as action_contact_message),
and then calls the displayToast() method. You could implement event handlers that perform
actions, such as starting another activity, as shown later in this lesson.
You will add a switch case block to determine which menu item was selected, and what to do
for each selected item.
The if statement in the method, provided by the template, determines if a certain menu item
was clicked, using the menu items id (action_order in the below example):
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_order) {
return true;
}
return super.onOptionsItemSelected(item);
}
1. Replace the if statement with the following switch case block that calls the appropriate
method (such as showOrder) based on the menu items id:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_order:
showOrder();
return true;
case R.id.action_status:
showStatus();
return true;
case R.id.action_favorites:
showFavorites();
return true;
case R.id.action_contact:
showContact();
return true;
}
return super.onOptionsItemSelected(item);
}
2. Run the app. You should now see a different toast message on the screen based on which
menu item you choose.
In the
above figure:
3. Selecting the Contact item in the options menu.
4. The toast message that appears.
<menu 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"
tools:context="com.example.android.optionsmenusample.MainActivity">
<item
android:id="@+id/action_contact"
android:orderInCategory="100"
android:title="@string/action_contact"
app:showAsAction="never" />
<item
android:id="@+id/action_order"
android:icon="@drawable/ic_order_white"
android:orderInCategory="10"
android:title="@string/action_order"
app:showAsAction="always" />
<item
android:id="@+id/action_status"
android:icon="@drawable/ic_status_white"
android:orderInCategory="20"
android:title="@string/action_status"
app:showAsAction="always" />
<item
android:id="@+id/action_favorites"
android:icon="@drawable/ic_favorites_white"
android:orderInCategory="40"
android:title="@string/action_favorites"
app:showAsAction="ifRoom" />
</menu>
activity_main.xml:
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_fab_chat_button_white" />
</android.support.design.widget.CoordinatorLayout>
content_main.xml:
<TextView
android:id="@+id/textintro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:padding="10dp"
android:text="@string/intro_text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/choose_dessert"
android:layout_below="@id/textintro"
android:text="@string/choose"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/donut"
android:layout_below="@id/choose_dessert"
android:contentDescription="@string/donut"
android:src="@drawable/donut_circle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="35dp"
android:layout_below="@id/choose_dessert"
android:layout_toRightOf="@id/donut"
android:text="@string/donut_description"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/ice_cream"
android:layout_below="@id/donut"
android:contentDescription="@string/ice_cream_sandwich"
android:src="@drawable/icecream_circle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="35dp"
android:layout_below="@id/donut"
android:layout_toRightOf="@id/ice_cream"
android:text="@string/ice_cream_description"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/froyo"
android:layout_below="@id/ice_cream"
android:contentDescription="@string/froyo"
android:src="@drawable/froyo_circle" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="35dp"
android:layout_below="@id/ice_cream"
android:layout_toRightOf="@id/froyo"
android:text="@string/froyo_description"/>
</RelativeLayout>
MainActivity.java:
package com.example.android.optionsmenusample;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
/**
* Creates the content view, the toolbar, and
* the floating action button.
* This method is provided in the Basic Activity template.
* @param savedInstanceState Saved instance.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
/**
* Inflates the menu, and adds items to the action bar if it is present.
* @param menu Menu to inflate.
* @return Returns true if the menu inflated.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
/**
* Handles app bar item clicks.
* @param item Item clicked.
* @return Returns true if one of the defined items was clicked.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_order:
showOrder();
return true;
case R.id.action_status:
showStatus();
return true;
case R.id.action_favorites:
showFavorites();
return true;
case R.id.action_contact:
showContact();
return true;
}
return super.onOptionsItemSelected(item);
}
**
* Shows a message that Contact was clicked.
*/
public void showContact() {
String message = getString(R.string.action_contact_message);
displayToast(message);
}
/**
* Shows a message that Order was clicked.
*/
public void showOrder() {
String message = getString(R.string.action_order_message);
displayToast(message);
}
/**
* Shows a message that Favorites was clicked.
*/
public void showFavorites () {
String message = getString(R.string.action_favorites_message);
displayToast(message);
}
/**
* Shows a message that Status was clicked.
*/
public void showStatus() {
String message = getString(R.string.action_status_message);
displayToast(message);
}
/**
* Displays the actual message in a toast message.
* @param message Message to display.
*/
public void displayToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
}
}
9. Task 4. Add radio buttons, images buttons,
and up navigation
Radio buttons are useful for selecting only one option from a set of options. You should use
radio buttons if you want the user to see all available options side-by-side. If it's not necessary
to show all options side-by-side, you may want to use a spinner instead.
For an overview and sample code for radio buttons, see Radio Buttons in the User Interface
section of the Android Developer Documentation.
The OrderActivity class should now be listed under MainActivity in the java folder, and
activity_order.xml should now be listed in the layout folder. The Empty Activity template added
these files.
1. Open MainActivity, and change the showOrder() method from the previous section to the
following in order to start the Order Activity:
1. Copy a placeholder image into the project to show a prototype of the apps feature screen.
The image droid_desserts_order_page.jpg is provided with the app. To add the image to
the project, close the project, copy the image file into the drawables folder (project_name
> app > src > main > res > drawable), and reopen the project.
2. Open activity_order.xml, and add an ImageView to the RelativeLayout to show the
placeholder image, using the following attributes:
ImageView Attribute Value
android:id "@+id/order_layout_image"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:layout_alignParentTop "true"
android:contentDescription "Mockup Image"
android:src "@drawable/droid_desserts_order_page"
3. Add a TextView element under the ImageView element with the id orderintrotext:
TextView Attribute Value
android:id "@+id/orderintrotext"
android:layout_width "match_parent"
android:layout_height "match_parent"
android:layout_below "@id/order_layout_image"
android:layout_marginTop "24dp"
android:layout_marginBottom "6dp"
android:textSize "18sp"
android:text "Choose a delivery method:"
4. Extract the string resource for "Choose a delivery method:" to be
choose_delivery_string.
5. Add a RadioGroup to the layout underneath the TextView you just added:
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/orderintrotext">
</RadioGroup>
6. Add the following three RadioButton elements within the RadioGroup, using the following
attributes. The onRadioButtonClicked entry for the onClick attribute will be highlighted
until you add that method in the next task.
RadioButton #1 Attribute Value
android:id "@+id/sameday"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:text "Same day messenger service"
android:onClick "onRadioButtonClicked"
1. Open OrderActivity and add the following onRadioButtonClicked() method, which uses
a switch case expression to determine which item was selected, in order to call the
appropriate method for that item:
Toast.makeText(getApplicationContext(), getString(R.string.chosen) +
getString(R.string.next_day_ground_delivery),
Toast.LENGTH_SHORT).show();
...
Toast.makeText(getApplicationContext(), getString(R.string.chosen) +
getString(R.string.next_day_ground_delivery),
Toast.LENGTH_SHORT).show();
...
Toast.makeText(getApplicationContext(), getString(R.string.chosen) +
getString(R.string.pick_up),
Toast.LENGTH_SHORT).show();
**Tip:** The back button on the device below the screen differs from the Up button. The back
button provides navigation to whatever screen you viewed previously. If you have several
children activities that the user can navigate through, the back button would send the user back
to the previous child activity. Use an Up button if you want to provide one button to navigate
from any child activity back to the parent activity. For more information about Up navigation, see
[Providing Up Navigation](http://developer.android.com/training/implementing-
navigation/ancestral.html).
As you learned previously, when adding activities to an app, you can add Up navigation to a
child activity such as OrderActivity by declaring the activitys parent to be MainActivity in the
AndroidManifest.xml file. You can also set the android:label to a title for the activity screen,
such as Order Activity (extracted into the string resource title_activity_order in the code
below):
1. Open AndroidManifest.xml.
2. Change the activity element for OrderActivity to the following:
<activity android:name=".OrderActivity"
android:label="@string/title_activity_order"
android:parentActivityName="com.example.android.
optionsmenuorderactivity.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
/**
Shows a message that the ice cream sandwich was clicked. */ public void
showIceCreamOrder(View view) { String message =
getString(R.string.ice_cream_order_message); displayToast(message); }
/**
Shows a message that the froyo was clicked. */ public void showFroyoOrder(View
view) { String message = getString(R.string.froyo_order_message);
displayToast(message); } ```
4. Add the android:onClick attribute to the three ImageViews in content_main.xml:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/donut"
android:layout_below="@id/choose_dessert"
android:contentDescription="@string/donut"
android:src="@drawable/donut_circle"
android:onClick="showDonutOrder"/>
. . .
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/ice_cream"
android:layout_below="@id/donut"
android:contentDescription="@string/ice_cream_sandwich"
android:src="@drawable/icecream_circle"
android:onClick="showIceCreamOrder"/>
. . .
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/froyo"
android:layout_below="@id/ice_cream"
android:contentDescription="@string/froyo"
android:src="@drawable/froyo_circle"
android:onClick="showFroyoOrder"/>
Clicking the donut, ice cream sandwich, or froyo image displays a toast message about the
order.
package com.example.android.optionsmenuorderactivity;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
/**
* Inflates the menu and adds items to the action bar if it is present.
* @param menu Menu to inflate.
* @return Returns true if menu inflates.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
/**
* Handles app bar item clicks.
* @param item Menu item clicked.
* @return Returns true if one of the defined items was clicked.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_order:
showOrder();
return true;
case R.id.action_status:
showStatus();
return true;
case R.id.action_favorites:
showFavorites();
return true;
case R.id.action_contact:
showContact();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Shows a message that Contact was clicked.
*/
public void showContact() {
String message = getString(R.string.action_contact_message);
displayToast(message);
}
/**
* Starts OrderActivity if Order was clicked.
*/
public void showOrder() {
// Intent to start OrderActivity.
Intent intent = new Intent(this, OrderActivity.class);
startActivity(intent);
}
/**
* Shows a message that Favorites was clicked.
*/
public void showFavorites () {
String message = getString(R.string.action_favorites_message);
displayToast(message);
}
/**
* Shows a message that Status was clicked.
*/
public void showStatus() {
String message = getString(R.string.action_status_message);
displayToast(message);
}
/**
* Shows a message that the donut was clicked.
*/
public void showDonutOrder(View view) {
String message = getString(R.string.donut_order_message);
displayToast(message);
}
/**
* Shows a message that the ice cream sandwich was clicked.
*/
public void showIceCreamOrder(View view) {
String message = getString(R.string.ice_cream_order_message);
displayToast(message);
}
/**
* Shows a message that the froyo was clicked.
*/
public void showFroyoOrder(View view) {
String message = getString(R.string.froyo_order_message);
displayToast(message);
}
/**
* Displays the actual message in a toast message.
* @param message string Message to display.
*/
public void displayToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
}
}
content_main.xml:
<TextView
android:id="@+id/textintro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:padding="10dp"
android:text="@string/intro_text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/choose_dessert"
android:layout_below="@id/textintro"
android:text="@string/choose"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/donut"
android:layout_below="@id/choose_dessert"
android:contentDescription="@string/donut"
android:src="@drawable/donut_circle"
android:onClick="showDonutOrder"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="35dp"
android:layout_below="@id/choose_dessert"
android:layout_toRightOf="@id/donut"
android:text="@string/donut_description"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/ice_cream"
android:layout_below="@id/donut"
android:contentDescription="@string/ice_cream_sandwich"
android:src="@drawable/icecream_circle"
android:onClick="showIceCreamOrder"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="35dp"
android:layout_below="@id/donut"
android:layout_toRightOf="@id/ice_cream"
android:text="@string/ice_cream_description"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:id="@+id/froyo"
android:layout_below="@id/ice_cream"
android:contentDescription="@string/froyo"
android:src="@drawable/froyo_circle"
android:onClick="showFroyoOrder"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="35dp"
android:layout_below="@id/ice_cream"
android:layout_toRightOf="@id/froyo"
android:text="@string/froyo_description"/>
</RelativeLayout>
activity_order.xml:
<ImageView
android:id="@+id/order_layout_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:contentDescription="@string/mockup"
android:src="@drawable/droid_desserts_order_page" />
<TextView
android:id="@+id/orderintrotext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/order_layout_image"
android:layout_marginTop="24dp"
android:layout_marginBottom="6dp"
android:textSize="18sp"
android:text="@string/choose_delivery_string"/>
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/orderintrotext">
<RadioButton
android:id="@+id/sameday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/same_day_messenger_service"
android:onClick="onRadioButtonClicked"/>
<RadioButton
android:id="@+id/nextday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next_day_ground_delivery"
android:onClick="onRadioButtonClicked"/>
<RadioButton
android:id="@+id/pickup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pick_up"
android:onClick="onRadioButtonClicked"/>
</RadioGroup>
</RelativeLayout>
OrderActivity.java:
package com.example.android.optionsmenuorderactivity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.RadioButton;
import android.widget.Toast;
/**
* This activity handles radio buttons for choosing
* a delivery method for an order.
*/
public class OrderActivity extends AppCompatActivity {
/**
* Sets the content view to activity_order.
* @param savedInstanceState Saved instance.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_order);
}
/**
* Checks which radio button was clicked
* and displays a toast message to show the choice.
* @param view The radio button view.
*/
public void onRadioButtonClicked(View view) {
// Is the button now checked?
boolean checked = ((RadioButton) view).isChecked();
// Check which radio button was clicked
switch(view.getId()) {
case R.id.sameday:
if (checked)
// Same day service
Toast.makeText(getApplicationContext(),
getString(R.string.chosen) +
getString(R.string.same_day_messenger_service),
Toast.LENGTH_SHORT).show();
break;
case R.id.nextday:
if (checked)
// Next day delivery
Toast.makeText(getApplicationContext(),
getString(R.string.chosen) +
getString(R.string.next_day_ground_delivery),
Toast.LENGTH_SHORT).show();
break;
case R.id.pickup:
if (checked)
// Pick up
Toast.makeText(getApplicationContext(),
getString(R.string.chosen) +
getString(R.string.pick_up),
Toast.LENGTH_SHORT).show();
break;
}
}
}
For this challenge, change the icon for the floating action button to a map icon. In MainActivity,
change the behavior of displaying a snackbar to making an implicit intent to launch the Maps
app when the floating action button is tapped. You can use specific coordinates and the
following method to start the Maps app:
For examples of implicit intents including opening the Maps app, see Common Implicit Intents
on github.
Be sure to also change the icon for the floating action button in the MainActivity to something
more suitable for a map button, such as the world icon.
onCreate() method:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab =
(FloatingActionButton) findViewById(R.id.fab);
if (fab != null) fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
displayMap();
}
});
}
App Overview
Challenge
Solution
Resources
Practical Challenge: Tab Navigation
Tabs appear across the top of a screen, providing navigation to other screens. Tab navigation is
a very popular solution for lateral navigation from one child screen to another child screen that
is a sibling in the same position in the hierarchy and sharing the same parent screen. Tabs
are most appropriate for small sets (four or fewer) of child screens.
The main class used for displaying tabs is TabLayout. It provides a horizontal layout to display
tabs. You can show the tabs below the app bar, and use the PagerAdapter class to populate
pages inside of a ViewPager, which is a layout manager that lets the user flip left and right
through pages of data. You supply an implementation of a PagerAdapter to generate the pages
that the view shows. ViewPager is most often used in conjunction with Fragment, which is a
convenient way to supply and manage the lifecycle of each page.
There are standard adapters for using fragments with the ViewPager:
The app also includes the options menu with one menu item (Settings) that appears when the
1. Create a new project using the Empty Activity template. Name the app Tab Experiment or
something similar.
2. In the activity_main.xml layout, add a Toolbar, a TabLayout, and a ViewPager within the
root layout. They should look like this:
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_below="@id/tab_layout"/>
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.tab_fragment1, container, false);
}
}
super(fm);
this.mNumOfTabs = NumOfTabs;
switch (position) {
case 0:
return new TabFragment1();
case 1:
return new TabFragment2();
case 2:
return new TabFragment3();
default:
return null;
}
return mNumOfTabs;
} } ```
8. Create an instance of the tab layout from the tab_layout element in the layout, and set the
text for each tab using addTab()):
9. Use PagerAdapter to manage screen (page) views in the fragments. Each screen is
represented by its own fragment:
@Override
public void onTabUnselected(TabLayout.Tab tab) {
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
10. Inflate the options menu by overriding the onCreateOptionsMenu() method, and override
the onOptionsItemSelected() method to handle the Settings option menu item.
Solution
Android Project:
Resources
Developer Documentation:
TabLayout
Android Material Design working with Tabs
Android Tabs Example With Fragments and ViewPager
Creating Swipe Views with Tabs
7.1 P: Themes, Custom Styles, Drawables
Contents:
5. Task 1: Create The Scorekeeper App
In this section, you will create your Android Studio project, modify the layout, and add onClick
functionality to its buttons.
1. Inside the LinearLayout, add two RelativeLayout view groups (one to contain the score for
each team) with the following attributes:
Attribute Value
android:layout_width "match_parent"
android:layout_height "0dp"
android:layout_weight 1
You may be surprised to see that the layout_height attribute is set to 0dp in these views. This is
because we are using the layout_weight and weightSum attributes to determine how much
space these views take up in the parent layout. See the LinearLayout Documentation for more
information.
1. Add two ImageButton views (one for increasing the score and one for decreasing the
score) and a TextView for displaying the score in between the buttons to each
RelativeLayout.
2. Add android:id attributes to the score TextViews.
3. Add one more TextView to each RelativeLayout above the score to represent the Team
Names.
1. Select File > New > Vector Asset to open the Vector Asset Studio.
2. Select Choose to pick an icon and select the Content category.
3. Choose the plus icon and click OK.
4. Rename the resource file ic_plus and check the Override default size from Material
Design checkbox.
5. Change the size of the icon to 40dp x 40dp.
6. Click Next and then Finish.
7. Repeat this process to add a minus icon and name the file ic_minus.
8. Change the score TextViews to read 0 and the team Textviews to read Team 1 and
Team 2.
9. Add the following attributes to your left ImageButtons:
android:src="@drawable/ic_minus"
android:contentDescription="Minus Button"
11. Extract all of your string resources. This process removes all of your strings from the Java
code and puts them in a single file: the string.xml file. This allows for your app to be easily
localized into different languages. To learn how to accomplish this, see the Extracting
Resources section in the appendix
12. Select all of the code in the activity_main.xml file and choose Code > Rearrange Code
Solution Code:
Note: Your code may be a little different as there are multiple ways to achieve the same layout.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="@string/team_1"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="@string/minus_button"
android:src="@drawable/ic_minus" />
<TextView
android:id="@+id/score_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/initial_count"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:contentDescription="@string/plus_button"
android:src="@drawable/ic_plus"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="@string/team_2"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="@string/minus_button"
android:src="@drawable/ic_minus"/>
<TextView
android:id="@+id/score_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/initial_count"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:contentDescription="@string/plus_button"
android:src="@drawable/ic_plus"/>
</RelativeLayout>
</LinearLayout>
5.3. 1.3 Implement the onClick functionality for your buttons
1. Implement an onClick method for each ImageButton in your layout.
2. The left buttons should decrement the score TextView, while the right ones should
increment it.
Solution Code:
Note: You must also add the `"android:onClick"` attribute to every button in the
activity_main.xml file.
package com.example.android.scorekeeper;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
In Android, graphics are often handled by a resource called a Drawable. In the following
exercise you will learn how to create a certain type of drawable called a ShapeDrawable, and
apply it to your buttons as a background.
5. Add the following code which defines an oval shape with an outline:
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="@color/colorPrimary"/>
</shape>
android:background="@drawable/button_background"
8. The size of the buttons needs to be such that it renders properly on all devices. Change
the layout_height and layout_width attributes for each button to:
android:layout_width="70dp"
android:layout_height="70dp"
3. Create the style for the plus buttons by extending the ScoreButtons style:
5. In the layout file for the main activity. Remove all of the attributes for each button except
the "android:onClick" attribute and add the appropriate style:
style="@style/MinusButtons"
style="@style/PlusButtons"
Note: the style attribute does not use the android: namespace. </div>
1. Right-click anywhere in the first score TextView attributes and choose Refactor > Extract
> Style
2. Name the style ScoreText and make sure every attribute is checked as well as the
Launch Use Styles Where Possible refactoring after the style is extracted which will
scan the layout file for views with the same attributes and apply the style for you.
3. Choose OK.
4. Make sure the scope is set to the current layout file and click OK.
5. The find pane at the bottom of Android Studio will open up, select Do Refactor to apply
the new style to the views with the same attributes.
6. Repeat the process to extract the team TextView style and name it TeamText.
7. Run your app. There should be no change except that all of your styling code is now in
your resources file and your layout file contains considerably less clutter.
Solution Code:
style.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="ScoreText">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_alignParentTop">true</item>
<item name="android:layout_centerHorizontal">true</item>
</style>
<style name="TeamText">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_centerHorizontal">true</item>
<item name="android:layout_centerVertical">true</item>
</style>
</resources>
activity_main.xml
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:text="@string/team_1"
style="@style/ScoreText" />
<ImageButton
android:onClick="minus_team1"
style="@style/MinusButtons"/>
<TextView
android:id="@+id/score_1"
android:text="@string/initial_count"
style="@style/TeamText" />
<ImageButton
android:onClick="plus_team1"
style="@style/PlusButtons"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:text="@string/team_2"
style="@style/ScoreText" />
<ImageButton
android:onClick="minus_team2"
style="@style/MinusButtons"/>
<TextView
android:id="@+id/score_2"
android:text="@string/initial_count"
style="@style/TeamText" />
<ImageButton
android:onClick="plus_team2"
style="@style/PlusButtons"/>
</RelativeLayout>
</LinearLayout>
2. Run your app. With just these adjustments to the style.xml file, all of the views updated to
reflect the changes.
8. Task 4: Themes and Final Touches
Youve seen that views with similar characteristics can be styled together in the style.xml file.
But what if you want to define styles for an entire activity, or even application? It is possible to
accomplish this by modifying the AndroidManifest.xml file. In this task, you will add the night
mode theme to your app as well as a few polishing touches to the User Interface.
1. In the Android manifest file, In the tag, change the "android:theme" attribute to:
android:theme="@style/Theme.AppCompat"
This is a pre-defined theme that uses a dark background and white text, perfect for use at
night.
2. Run your app. The background, toolbar, and text color all changed to a dark theme!
3. Change the theme of the application back to AppTheme, which is a child of the
Theme.Appcompat.Light.DarkActionBar theme as can be seen in styles.xml.
Thats all there is to it. To apply a theme to an activity instead of the entire application, place
the theme attribute in the activity tag instead of the application tag. For more information on
Themes and Styles, see the Style and Theme Guide.
Your app could switch the theme automatically, an hour after sunset and before sunrise. Or it
could even use the camera detect how light it is! However, In this exercise you will add a menu
button that will toggle the application between the regular theme and a night-mode theme.
1. Right click on the res directory and choose New > Android resource file.
2. Name the file main_menu, change the Resource Type to Menu, and click OK.
3. Add a menu item with the following attributes:
<item
android:id="@+id/night_mode"
android:title="@string/night_mode"
app:showAsAction="withText|ifRoom"/>
5. In your main activity Java file, press Ctrl - O to open the Override Method menu, select the
onCreateOptionsMenu method and click OK.
6. Inflate the menu you just created in onCreateOptionsMenu.
1. Create a new boolean member variable called night_mode to represent whether the
activity is in Night Mode" or not.
2. In response to a click on the menu button, create a new Intent for MainActivity, and start
the activity.
3. Set up the logic for the Intent extra as follows:
if(!night_mode){
intent.putExtra("nightMode",true);
} else {
intent.putExtra("nightMode",false);
}
4. In the onCreate method for your activity, assign the value of the Intent extra to your
night_mode, with false as the default value:
night_mode = getIntent().getBooleanExtra("nightMode",false);
You may notice that the label for your menu item always reads Night Mode, which may be
confusing to your user if the app is already in the dark theme.
if (night_mode) {
menu.findItem(R.id.night_mode).setTitle(R.string.day_mode);
} else{
menu.findItem(R.id.night_mode).setTitle(R.string.night_mode);
}
2. Run your app. The menu button label now changes with the theme.
Your activity is also relaunched when when you change the theme from the menu bar, and in
this case onSaveInstanceState is not called because you terminated the activity intentionally.
Do the following:
1. Pass the score values as Intent extras in addition to the night mode boolean in
onOptionsItemSelected.
2. Retrieve the score values from the intent in onCreate if the savedInstanceState is null (if it
isn't, you'll be getting the score values from the savedInstanceState Bundle).
Solution Code:
MainActivity.java
package com.example.android.scorekeeper;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
night_mode = getIntent().getBooleanExtra("nightMode",false);
if(night_mode){
setTheme(android.support.v7.appcompat.R.style.Theme_AppCompat);
} else {
setTheme(R.style.AppTheme);
}
setContentView(R.layout.activity_main);
score_text_1 = (TextView)findViewById(R.id.score_1);
score_text_2 = (TextView)findViewById(R.id.score_2);
if (savedInstanceState != null) {
score_1 = savedInstanceState.getInt(STATE_SCORE_1);
score_2 = savedInstanceState.getInt(STATE_SCORE_2);
} else{
score_1 = getIntent().getIntExtra(STATE_SCORE_1,0);
score_2 = getIntent().getIntExtra(STATE_SCORE_2,0);
}
score_text_1.setText(String.valueOf(score_1));
score_text_2.setText(String.valueOf(score_2));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
if (night_mode) {
menu.findItem(R.id.night_mode).setTitle(R.string.day_mode);
} else{
menu.findItem(R.id.night_mode).setTitle(R.string.night_mode);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId()==R.id.night_mode){
Intent intent = new Intent(this,MainActivity.class);
if(!night_mode){
intent.putExtra("nightMode",true).putExtra(STATE_SCORE_1,score_1).putExtra(STATE_SCORE_2
,score_2);
} else{
intent.putExtra("nightMode",false).putExtra(STATE_SCORE_1,score_1).putExtra(STATE_SCORE_
2,score_2);
}
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_SCORE_1, score_1);
outState.putInt(STATE_SCORE_2, score_2);
super.onSaveInstanceState(outState);
}
activity_main.xml
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:text="@string/team_1"
style="@style/ScoreText" />
<ImageButton
android:onClick="minus_team1"
style="@style/MinusButtons"/>
<TextView
android:id="@+id/score_1"
android:text="@string/initial_count"
style="@style/TeamText" />
<ImageButton
android:onClick="plus_team1"
style="@style/PlusButtons"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:text="@string/team_2"
style="@style/ScoreText" />
<ImageButton
android:onClick="minus_team2"
style="@style/MinusButtons"/>
<TextView
android:id="@+id/score_2"
android:text="@string/initial_count"
style="@style/TeamText" />
<ImageButton
android:onClick="plus_team2"
style="@style/PlusButtons"/>
</RelativeLayout>
</LinearLayout>
style.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="TeamText">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_centerHorizontal">true</item>
<item name="android:layout_centerVertical">true</item>
<item name="android:textSize">60sp</item>
</style>
</resources>
9. Coding challenge
Note: All coding challenges are optional and not prerequisite for the material in the next
chapter.
Right now, your buttons do not behave intuitively because they do not change their appearance
when they are pressed. Android has another type of drawable called StateListDrawable which
allows for a different graphic to be used depending on the state of the object.
For this challenge problem, create a drawable resource that changes the background of the
button to the same color as the border when the state of the button is pressed. You should
also set the color of the text inside the buttons to a selector that makes it white when the button
is pressed.
10. Conclusion
In this exercise you learned how to apply common styles to UI elements, allowing you to reuse
styling code on multiple objects. You also learned how to set a theme to an activity or
application, as well as use drawable resources to add graphics and color to your application.
11. Resources
Developer Documentation:
LinearLayout Guide
Drawable Resource Guide
Styles and Themes Guide
Videos
1. In the styles.xml file, change the following attributes in the "ScoreButtons" style:
<item name="android:layout_height">60dp</item>
<item name="android:layout_width">60dp</item>
<item name="android:background">?selectableItemBackgroundBorderless</item>
<item name="android:tint">@color/colorPrimaryDark</item>
As you can see from the guide, the different styles include attributes like text size and weight.
You can access these attributes by having your Text styles inherit from these Material Design
styles. Do the following:
<item name="android:layout_marginTop">48dp</item>
Your app now conforms more closely to the Material Design guidelines, so it is time to
modify the app to support more than 2 teams.
6. Task 2 : Setup the RecyclerView
In this section you will setup the Scorekeeper application to use a RecyclerView to a list of
teams and buttons.
compile 'com.android.support:cardview-v7:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
compile 'com.android.support:design:23.3.0'
1. Clear the activity_main.xml file and replace it's contents with an empty CoordinatorLayout.
2. Put a RecyclerView widget inside the CoordinatorLayout and remember to give it an
android:id attribute, as well as a 16dp margin all around.
1. Remove all of the code that handles the button clicks, which will be implemented in the
ViewHolder class.
2. Remove both of the TextView variables as well as the int variables representing the score.
3. Remove the two savedInstanceState key Strings for each score.
4. In onCreate, remove the findViewById code for each TextView as well as all of the logic for
restoring the savedInstanceState.
5. Remove the onSaveInstanceState Override Method.
6. In onOptionsItemSelected, remove the Intent Extras representing the score of each team,
leaving only the night_mode Intent Extra.
Solution Code:
package com.example.android.scorekeeper;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
night_mode = getIntent().getBooleanExtra("nightMode",false);
if(night_mode){
setTheme(android.support.v7.appcompat.R.style.Theme_AppCompat);
} else {
setTheme(R.style.AppTheme);
}
setContentView(R.layout.activity_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
if (night_mode) {
menu.findItem(R.id.night_mode).setTitle(R.string.day_mode);
} else{
menu.findItem(R.id.night_mode).setTitle(R.string.night_mode);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId()==R.id.night_mode){
Intent intent = new Intent(this,MainActivity.class);
if(!night_mode){
intent.putExtra("nightMode",true);
} else{
intent.putExtra("nightMode",false);
}
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
}
7. Task 3: Create a Data Model
Previously, the Scorekeeper application only allowed for 2 teams, but as you extend the
application to support any number of teams it becomes necessary to abstract the concept of
the Team into it's own object. This principle is very useful in all object-oriented programming and
allows for all of the methods and variables pertaining to one object to be contained in a single
class.
In this section, you will create a custom Team class that will hold the relevant data and methods
for each team.
2. Initialize it in onCreate:
2. This line will be underlined in red as you have not yet implemented the required methods to
extend the RecyclerView.Adapter class. Make sure your cursor is somewhere in the
underlined code and press Alt + Enter, choose Implement Methods and click OK.
Note: The ViewHolder class has not been implemented yet so it will be highlighted in red.
You will fix that in the following section.
2. Create a Constructor for the Adapter the same way you did for the Team class and make
sure it takes mTeamList as a parameter and assigns it to the member variable.
3. Press Alt + Enter with your cursor on the red ViewHolder text and select Create class
ViewHolder.
4. Make the ViewHolder class extend RecyclerView.ViewHolder and implement the default
constructor:
5. Create variables for each of the list item views and assign them by id in the ViewHolder
constructor.
6. Implement an OnClickListener for each button that gets the appropriate Team object from
your list using getAdapterPosition and update the score.
7. Remember to update both the Team score inside the dataset and also the ScoreTextView.
8. In onCreateViewHolder, inflate your list_item.xml file and return a new ViewHolder:
@Override
public TeamAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v =
LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false);
return new ViewHolder(v);
}
9. In onBindViewHolder, set the Team Name and score from the Team ArrayList.
10. In getItemCount, return the size of the mTeamList ArrayList.
1. In your Main Activity layout file, add a Floating Action Button widget below the recyclerview
with the following attributes:
Attribute Value
android:layout_width wrap_content
android:layout_height wrap_content
android:onClick addTeam
android:layout_gravity end|bottom
android:layout_margin @dimen/activity_horizontal_margin
android:src @drawable/ic_plus
android:tint @android:color/white
2. Put your cursor on the "addTeam" attribute, press Alt + Enter and select Create
'addTeam(View)' in 'MainActivity'.
3. In this method stub, write code to add a new Team object with score 0 and the following
team name in order to properly label the teams: "Team" + " " + (mTeamList.size()+1).
4. Notify the adapter that an item was inserted in the last position, and scroll to that position.
3. In the PlusButtons style, add a layout_marginRight attribute and set the value to 40dp .
4. Do the same in the MinusButtons style but with layout_marginLeft .
5. Modify the parent style of TeamText to be TextAppearance.AppCompat.Title and
TextAppearance.AppCompat.Headline for ScoreText.
6. Add an 8dp margin to the bottom of the ScoreText to center it between the buttons.
7. Run the app. You are now using the space much more efficiently for a list display of teams.
2. Add the following code to an empty CardView Widget inside the RelativeLayout:
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/topScores"
android:layout_margin="@dimen/activity_horizontal_margin">
</android.support.v7.widget.CardView>
android:below="@id/topScores"
The last attribute allows the columns to stretch to fill the remaining space, and the
asterisk in the value extends this over all of the columns, see the [Table Guide]
(https://www.google.com/url?
sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0ahUKEwib9brm2M7MAhVEy2MKHZfCCkMQFggfMAA&url=ht
tp%3A%2F%2Fdeveloper.android.com%2Fguide%2Ftopics%2Fui%2Flayout%2Fgrid.html&usg=AFQjCNFZ
TTF5XCLnfHQ5wry-HqKAJkhMfg&bvm=bv.121421273,d.cGc) for more information.
1. In the first row, add three ImageViews which you will use to display the vector icons you
just downloaded, and add the following attributes:
<TableRow
android:background="@color/colorPrimary"
android:padding="8dp">
<ImageView
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center"
android:src="@drawable/ic_one"
android:minHeight="40dp"
android:tint="#ffd700" />
<ImageView
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center"
android:src="@drawable/ic_two"
android:minHeight="40dp"
android:tint="#c0c0c0" />
<ImageView
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center"
android:src="@drawable/ic_three"
android:minHeight="40dp"
android:tint="#cd7f32" />
</TableRow>
2. Next add the Row which will display which Teams have the top three scores:
<TableRow
android:padding="4dp">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/firstPlace"
android:gravity="center"
style="@style/TeamText" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/secondPlace"
android:gravity="center"
style="@style/TeamText" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/thirdPlace"
android:gravity="center"
style="@style/TeamText" />
</TableRow>
3. Finally, add the row which will show the top three scores:
<TableRow>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/firstScore"
android:gravity="center"
style="@style/ScoreText" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/secondScore"
android:gravity="center"
style="@style/ScoreText" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/thirdScore"
android:gravity="center"
style="@style/ScoreText" />
</TableRow>
1. In your Team.java file, make the class implement the Comparable interface. This interface
allows you to define the logic of comparing custom objects. When implementing
Comparable, be sure to specify what type of object you are going to be comparing to (i.e
implement Comparable).
2. Android Studio will highlight that you need to implement Override methods. Press Alt +
Enter when the cursor is on the highlighted line and choose the compareTo method. This is
where the logic for comparing the objects comes in. In your case, compareTo should yield
-1 when the compared Team has a lower score, 0 when the scores are equal, and 1 when
the compare Team has a greater score:
@Override
public int compareTo(Team another) {
if(this.getScore()<another.getScore()){
return 1;
} else if(this.getScore()>another.getScore()){
return -1;
} else {
return 0;
}
}
3. In MainActivity.java, create three member variables that represent the top three teams.
4. Create 6 member variables for each Team Name and score TextView and assign them the
proper views by id in onCreate.
5. Write a method called updateTopScores() in MainActivity that accomplishes the following:
i. It creates a copy of mTeamList using the clone() method.
ii. It then sorts the copy using the Collections.sort() method.
iii. It uses a case statement to catch when mTeamList is not big enough to get all of the
scores. (If this doesn't make sense, try it without a case structure and see what
happens).
switch(sortedTeam.size()){
case 0: break;
case 1:
mTopTeam = sortedTeam.get(0);
break;
case 2:
mTopTeam = sortedTeam.get(0);
mSecondTeam = sortedTeam.get(1);
break;
default:
mTopTeam = sortedTeam.get(0);
mSecondTeam = sortedTeam.get(1);
mThirdTeam = sortedTeam.get(2);
break;
}
iv. Finally, it should set the Text in all the Team Name and Score TextViews inside the top
scores Card.
1. In the case that there are no teams, set the middle TextView to read "No Teams"
2. In the case where there are one or two teams, set the empty TextViews to read "No
Team"
10. Task 6: Material Design Colors
One of the key principles of Material Design is the use of bold, intentional color. In line with this
principle, these guidelines recommend that you pick a color palette based on a primary and
accent color. These color should contrast well, and often a lighter and darker shade of the
primary color is selected to complement the palette.
Note that the primary color, darker shade and accent color are defined here, with references to
your color.xml file.
1. Define a new theme called "AppThemeDark" that inherits from "Theme.AppCompat" and
copy the items from the AppTheme style.
2. In onCreate in MainActivity.java, change the references from Theme.Appcompat to
AppThemeDark , so that they include the color palette.
Solution Code:
Videos
Challenge
Solution
Resources
Challenge: Transitions and Animations
Create an application with 4 images arranged in a grid in the center of your layout. Make the
first three solid colored backgrounds, and the fourth the Android Material Design Icon. Each of
these images should respond to clicks as follows:
1. One of the colored blocks relaunches the Activity using the Explode animation for both the
enter and exit transitions.
2. Relaunch the Activity from another colored block, this time using the Fade transition.
3. Touching the third colored block starts an in place animation of the view (such as a
rotation).
4. Finally, touching the android icon starts a secondary activity with a Shared Element
Transition swapping the Android block with one of the other blocks.
Note: You must set your minimum SDK level to 21 or higher in order to implement shared
element transitions.
1. Solution
LINK TO CODE
2. Resources
Developer Documentation:
1. Solution
2. Resources
7.4 P: Supporting Landscape and Multiple
Screen Sizes
Contents:
1. Navigate to your layout directory, right-click on the directory name and select New >
Layout resource file.
2. Name the file activity_main.xml to match the original layout file.
3. There is a list of available qualifiers on the bottom left of the dialog box. Take some time to
look through these, as any you can specify a new layout file for any of these given
qualifiers.
4. Select Orientation, and press the >> symbol in the middle of the dialog to access this
qualifier.
5. Change the Screen orientation selector to Landscape, and notice how the directory name
"layout-land" is automatically changed. This is the essence of resource qualifiers: the
directory name tells android when to use that specific layout file, in this case, when the
phone is rotated to landscape mode.
6. Click OK to generate the new layout file.
7. Run your app.
1. Change the layout_width attribute in the CardView to 300dp and the layout_height
attribute to match_parent .
2. Remove the stretchColumns attribute, since the cardview will no longer take up the whole
width of the screen.
3. Add a layout_height attribute to every TableRow and set it equal to 0dp. Likewise, add a
gravity attribute and set the value to center .
4. Add a layout_weight attribute to every TableRow and set it equal to 1 for the top row,
and 3 for every other row.
5. Collapse the CardView code by clicking on the minus symbol on the left of the code editor.
6. Modify the layout_below attribute of the RecyclerView widget to be a layout_toRightOf
attribute.
7. Run your app. Rotate the device to landscape mode, and notice how the CardView is
repositioned!
6. Task 2 : Tablet View
Although you have modified the app to look better in landscape mode, running it on a tablet (or
tablet emulator) demonstrates that the layout could use some modifications when running on a
tablet. Fortunately, resource qualifiers allow you to do just that.
Note: Android will look for the resource file with the most specific resource qualifier first, then
move on to more and more generic ones. For example if a value is defined in the landscape
resource file, it will override the value in the generic resource file.
1. Navigate to values/dimens.xml and notice that there are two files, one with a w820dp
qualifier.
2. Open both of these files and notice the difference. These files both define activity margins,
but the w820dp file specifies a much larger horizontal margin. This qualifier stands for
"width of at least 820 dp". This qualifier will only target tablets in landscape mode, since
this where you would frequently use multi-pane layouts.
3. In this example, the top scores card appears too small in the landscape tablet view, since
it was hard-coded to look good on a phone in landscape. Extract the width of the card
from the landscape layout file into both the generic and the w820dp values/dimens.xml.
4. In the w820dp dimens.xml file, modify the value to 450dp.
5. Run your app. On a tablet in landscape view, the card should now appear more
appropriately sized.
5. Repeat the process with the same qualifier for the list_item.xml layout file, and change the
row height to be 100dp in the sw600dp list_item file.
6. Run your app, although this layout is still not perfectly optimized for tablets, you are
already much closer to supporting multiple screen sizes using resource qualifiers.
7. Task 3: Density buckets and testing on
multiple emulators at once
Throughout this course, we have been using dp's as a standard unit for size of views, and sp's
for font sizes. These units are density independent meaning they take into account the
resolution of the screen when drawing your views and fonts, and therefore appear the same
size across different devices. In order to see this in action, you will change some of the views
of your app to use density dependent units and see how they appear on screens with different
resolutions.
Run your app on several emulators with different densities and note how the apparent size of
the Views changes. This is why we use dp's and sp's as the standard unit, allowing the system
to compensate for the differences in resolution.
Solution Code:
**TODO: Solution Code**
8. Coding challenge
Note: All coding challenges are optional and not prerequisite for the material in the next
chapter.
Any ideas here?
9. Conclusion
Resource qualifiers are a great way to manage different sized screens in your application.
Every time your UI does not appear the way you expected because of the orientation, size of
the screen, or many other qualifiers (such as localized strings taking up more space). Density
independent units also resolve UI issues that arise from screens with different resolutions.
10. Resources
Developer Documentation:
You can test a user interface for a complex app manually by running the app and trying the user
interface. But you cant possibly cover all permutations of user interactions and all of the apps
functionality. You would also have to repeat these manual tests on many different device
configurations in an emulator, and on many different devices.
When you automate tests of UI interactions, you free yourself for other work. You can use
suites of automated tests to perform all of the UI interactions automatically, which makes it
easier to run tests for different device configurations. Get into the habit of creating user
interface (UI) tests to verify that the UI of your app is functioning correctly.
Espresso is a testing framework for Android that makes it easy to write reliable user interface
(UI) tests for an app. The framework, which is part of the Android Support Repository, provides
APIs for writing UI tests to simulate user interactions within the app everything from clicking
buttons and navigating to the views to choosing menu selections and entering data.
1. What you should already KNOW
You should be familiar with:
Tip: For an introduction to testing Android apps, see [Test Your App]
(http://d.android.com/tools/testing/testing_android.html).
5. Task 1: Set up Espresso in your project
To use Espresso, you must already have the Android Support Repository installed with Android
Studio. You must also configure Espresso in your project.
In this task you check to see if the repository is installed and if its not, you install it. You then
configure Espresso in the TwoActivities project you created previously.
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
If the version numbers you specified are lower than the currently available library version
numbers, Android Studio will warn you (such as, "a newer version of
com.android.support:support-annotations is available"). Update the version numbers to the ones
Android Studio tells you to use.
1. Click the Sync Now link in the notification about Gradle files in top right corner of the
window.
To create a test, you create a method within the test class that uses Hamcrest expressions.
Hamcrest (an anagram of matchers) is a framework that assists writing software tests in
Java. The framework lets you create custom assertion matchers, allowing match rules to be
defined declaratively. With Espresso you use the following types of Hamcrest expressions to
help find views and interact with them:
ViewMatchers: A ViewMatcher expression lets you find a view in the current view hierarchy
so that you can examine something or perform some action.
ViewActions: A ViewAction expression lets you perform an action on the view already
found by a ViewMatcher.
ViewAssertions: A ViewAssertion expression lets you assert or checks the state of a view
found by a ViewMatcher.
You would typically combine a ViewMatcher and a ViewAction in a single statement, followed
by a ViewAssertion expression in a separate statement or included in the same statement.
You can see how all three expressions work in the following statement, which combines a
ViewMatcher to find a view, a ViewAction to perform an action, and a ViewAssertion to check if
the result of the action matches an assertion:
You will use all three expressions in the test methods you create.
6.1. 2.1 Define a class for a test and set up the activity
Android Studio creates a blank Espresso test class for you in the
src/androidTest/java/com.example.package folder:
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ActivityInputOutputTest {
@Rule
public ActivityTestRule mActivityRule = new ActivityTestRule<>(
MainActivity.class);
}
import android.support.test.rule.ActivityTestRule;
6.2. 2.2 Test switching activities
The TwoActivities app has two activities:
Main: Includes the button_main button for switching to the Second activity and the
text_header_reply view that serves as a text heading for the Main activity.
Second: Includes the button_second button for switching to the Main activity and the
text_header view that serves as a text heading for the Second activity.
When you have an app that switches activities, you should test that capability. The Two
Activities app provides a text entry field and a Send button (the button_main id). Clicking
Send launches the Second activity with the entered text shown in the text_header view of the
Second activity.
But what happens if no text is entered? Will the Second activity still appear? This test will show
that the views appear regardless of whether text is entered.
1. Add the activityLaunch() method to test whether the views appear when clicking the
buttons, and include the @Test notation on a line immediately above the method:
@Test
public void activityLaunch() { }
The @Test annotation tells JUnit that the public void method to which it is attached can
be run as a test case. A test method begins with the @Test annotation and contains the
code to exercise and verify a single function in the component that you want to test.
2. Add a combined ViewMatcher and ViewAction expression to the activityLaunch() method to
locate the view containing the button_main button, and include a ViewAction expression to
perform a click:
onView(withId(R.id.button_main)).perform(click());
The onView() method lets you use ViewMatcher arguments to find views. It searches the
view hierarchy to locate a corresponding View instance that meets some given criteria
in this case, the button_main view. The .perform(click()) expression is a ViewAction
expression that performs a click on the view.
3. In the above onView statement, onView, withID, and click are in red. For each one, click
on the term, and then click the red lightbulb icon in the left margin. Choose Static import
method from the pop-up menu that appears. After doing this for each term, the following
import statements are added:
4. Add another ViewMatcher expression to find the text_header view (which is in the Second
activity), and a ViewAction expression to perform a check to see if the view is displayed:
onView(withId(R.id.text_header)).check(matches(isDisplayed()));
This statement uses the onView() method to locate the text_header view for the Second
activity and check to see if it is displayed after clicking the button_main view.
5. In the above onView statement, other items may be in red. For each one, click on the
term, and then click the red lightbulb icon in the left margin. Choose Static import
method, and the import statements are added.
6. Add similar statements to test whether clicking the button_second button in the Second
activity switches to the Main activity:
onView(withId(R.id.button_second)).perform(click());
onView(withId(R.id.text_header_reply)).check(matches(isDisplayed()));
7. Review the method you just created. It should look like this:
@Test
public void activityLaunch() {
onView(withId(R.id.button_main)).perform(click());
onView(withId(R.id.text_header)).check(matches(isDisplayed()));
onView(withId(R.id.button_second)).perform(click());
onView(withId(R.id.text_header_reply)).check(matches(isDisplayed()));
}
8. To run the test, right-click (or Control-click) ActivityInputOutputTest and choose Run
ActivityInputOutputTest from the pop-up menu. You can then choose to run the test on
the emulator or on your device.
As the test runs, watch the test automatically start the app and click the button. The Second
activitys view appears. The test then clicks the Second activitys button, and the Main Activity
view appears.
The Run window (the bottom pane of Android Studio) shows the progress of the test, and when
finishes, it displays Tests ran to completion. In the left column Android Studio displays All
Tests Passed.
@Test
public void textInputOutput() {
onView(withId(R.id.editText_main)).perform(typeText("This is a test."));
onView(withId(R.id.button_main)).perform(click());
}
The above method uses a ViewMatcher to locate the view containing the editText_main
view, and a ViewAction to enter the text "This is a test." It then uses another
ViewMatcher to find the view with the button_main button, and another ViewAction to
click the button.
2. If typeText is in red, click on the term, and then click the red lightbulb in the left margin.
Choose Static import method, and the following import statement is added:
onView(withId(R.id.text_message)).check(matches(withText("This is a test.")));
You may have to import the following:
As the test runs, the app starts and the text is automatically entered as input; the button is
clicked, and the text appears on the second activitys screen.
The bottom pane of Android Studio shows the progress of the test, and when finished, it
displays Tests ran to completion. In the left column Android Studio displays All Tests
Passed. You have successfully tested the text input field, the Send button, and the text output
field.
Solution code:
package com.example.android.twoactivities;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ActivityInputOutputTest {
@Rule
public ActivityTestRule mActivityRule = new ActivityTestRule<>(
MainActivity.class);
@Test
public void activityLaunch() {
onView(withId(R.id.button_main)).perform(click());
onView(withId(R.id.text_header)).check(matches(isDisplayed()));
onView(withId(R.id.button_second)).perform(click());
onView(withId(R.id.text_header_reply)).check(matches(isDisplayed()));
}
@Test
public void textInputOutput() {
onView(withId(R.id.editText_main)).perform(typeText("This is a test."));
onView(withId(R.id.button_main)).perform(click());
onView(withId(R.id.text_message)).check(matches(withText("This is a test.")));
}
}
1. Change the match check on the text_message view from "This is a test." to "This is a
failing test.":
onView(withId(R.id.text_message)).check(matches(withText("This is a failing
test.")));
2. Run the test again. This time you will see the message in red, 1 test failed, above the
bottom pane, and a red exclamation point next to textInputOutput in the left column.
Scroll the bottom pane to the message Test running started and see that all of the results
after that point are in red. The very next statement after Test running started is:
android.support.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseErr
or: 'with text: is "This is a failing test."' doesn't match the selected view.
Expected: with text: is "This is a failing test."
7. Task 3: Test the display of spinner
selections
In an AdapterView such as a spinner, the view is dynamically populated with child views at
runtime. If the target view you want to test is inside a spinner, the onView() method might not
work because only a subset of the views may be loaded in the current view hierarchy.
Espresso handles this problem by providing a separate onData() entry point, which is able to
first load the adapter item and bring it into focus prior to operating on it or any of its children.
PhoneNumberSpinner is an app from a previous lesson that shows a spinner, with the id
label_spinner, for choosing the label of a phone number (Home, Work, Mobile, and Other).
The app displays the choice in a text field, concatenated with the entered phone number.
The goal of this test is to open the spinner, click each item, and then verify that the TextView
text_phonelabel contains the item. The test demonstrates that the code retrieving the spinner
selection is working properly, and the code displaying the text of the spinner item is also
working properly. You will write the test using string resources and iterate through the spinner
items so that the test works no matter how many items are in the spinner, or how those items
are worded; for example, the words could be in a different language.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SpinnerSelectionTest {
@Rule
public ActivityTestRule mActivityRule = new ActivityTestRule<>(
MainActivity.class);
}
7.2. 3.2 Access the array used for the spinner items
You want the test to click each item in the spinner based on the number of elements in the
array. But how do you access the array?
1. Assign the array used for the spinner items to a new array to use within the
iterateSpinnerItems() method:
In the statement above, the test accesses the applications array (with the id labels_array)
by establishing the context with the getActivity() method of the ActivityTestRule class, and
getting a resources instance in the applications package using getResources().
2. Get the size (length) of the array, and construct a for loop using the size as the maximum
number for a counter.
Your test must click the spinner itself in order click any item in the spinner, so it must
continually click the spinner first before clicking the item.
If any of the terms, such as onView, withId, or click, appear in red, click on the term, and then
click the red lightbulb icon in the left margin. Choose Static import method from the pop-up
menu that appears.
As you enter statement, some portions of it may appear in red; click on each red term,
click the red light bulb in the left margin, and choose Static import method from the
pop-up menu that appears. If choices are provided, choose the Hamcrest Matchers
framework versions or Android Support Library versions.
The above statement matches if the object is a specific item in the spinner, as specified by
the myArray[i] array element.
The first statement locates the button_main and clicks it. The second statement checks to
see if the resulting text_phonelabel matches the spinner item specified by myArray[i].
3. To run the test, right-click (or Control-click) SpinnerSelectionTest and choose Run
SpinnerSelectionTest from the pop-up menu. You can then choose to run the test on the
emulator or on your device.
The test runs the app, clicks the spinner, and exercises the spinner it clicks each spinner
item from top to bottom, checking to see if the item appears in the text field. It doesnt matter
how many spinner items are defined in the array, or what language is used for the spinners
items the test performs all of them and checks their output against the array.
The bottom pane of Android Studio shows the progress of the test, and when finished, it
displays Tests ran to completion. In the left column Android Studio displays All Tests
Passed.
package com.example.android.phonenumberspinner;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SpinnerSelectionTest {
@Rule
public ActivityTestRule mActivityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void iterateSpinnerItems() {
// Get the string array of spinner elements.
String[] myArray =
mActivityRule.getActivity().getResources()
.getStringArray(R.array.labels_array);
The app lets you scroll a list of words. When you click on a word such as Word 15 the word
in the list changes to Clicked! Word 15.
package com.example.android.recyclerview;
import android.support.test.espresso.ViewInteraction;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.
RecyclerViewActions.actionOnItemAtPosition;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class RecyclerViewTest {
@Rule
public ActivityTestRule<MainActivity> mActivityTestRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void recyclerViewTest() {
ViewInteraction recyclerView = onView(
allOf(withId(R.id.recyclerview), isDisplayed()));
recyclerView.perform(actionOnItemAtPosition(15, click()));
}
}
The test uses a recyclerView object of the ViewInteraction class, which is the primary interface
for performing actions or asserts on views, providing both check and perform methods. Each
interaction is associated with a view identified by a view matcher:
In the first statement below, recyclerView is defined to be the RecyclerView. The second
statement uses .perform with the actionOnItemAtPosition()) method of the
RecyclerViewActions class to scroll to the position (15) and click the item:
You can record multiple interactions with the UI in one recording session. You can also
record multiple tests, and edit the tests to perform more actions, using the recorded code
as a snippet to copy, paste, and edit.
Espresso documentation
Espresso Samples
Videos
Other:
Coding challenge
Conclusion
Resources
Using Shared Preferences
REVIEWERS: To give feedback, please review the Docs doc here.
Shared preferences allow you to read and write small amounts of primitive data (as key/value
pairs) to a file on the device storage. The SharedPreference class provides APIs for getting a
handle to a preference file and for reading, writing, and managing this data. The shared
preferences file itself is managed by the framework, and accessible to (shared with) all the
components of your app. That data is not, however, shared with or accessible to any other
apps.
nd the data you write to your preferences is only accessible to your app.
Shared preferences are different from the activity instance state you learned about earlier.
Instance state only preserves state data across activity instances in the same user session.
Shared preferences persist across user sessions, even if your app is killed and restarted.
The SharedPreference APIs are also different from the Preference APIs. The Preference APIs
can be used to build user interface for a settings page, although they do use shared
preferences for for their underlying implementation. See Settings for more information on
settings and the Preference APIs.
Use shared preferences only when you have a small amount of simple key/value pairs you want
to save. To manage larger amounts of persistent app data use the other methods described in
this unit such as SQL databases or content providers.
1. What you should already KNOW
From the previous practicals you should be familiar with:
The app restarts with the default appearance -- the count is 0, and the background color is
grey.
import android.content.SharedPreferences;
3. Add member variables to the MainActivity class to hold the name of the shared
preferences file, and a reference to a SharedPreferences object.
You can name your shared preferences file anything you want to, but conventionally it has
the same package name as your app.
4. In the onCreate() method, initialize the shared preferences:
The getSharedPreferences() method opens the file at the given file name (sharedPrefFile)
with the mode MODE_PRIVATE.
Note: Older versions of Android had other modes that allowed you to create a world-readable
or world-writable shared preferences file. These modes were deprecated in API 17, and are
now strongly discouraged for security reasons. If you need to share data with other apps,
use a service or a content provider. </div>
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//
}
}
1. Click the last line of the MainActivity class, just before the closing bracket.
2. Select Code > Generate, then select Override Methods.
3. Type "onPause", select the method signature for the onPause() method, and click OK.
A shared preferences editor is required to write to the shared preferences object. Add this
line to onPause() after the call to super.onPause().
2. Use the putInt() method to put both the mCount and mCurrentColor integers into the
shared preferences with the appropriate keys:
preferencesEditor.putInt("count", mCount);
preferencesEditor.putInt("color", mCurrentColor);
preferencesEditor.apply();
The apply() method saves the preferences asynchronously, off of the UI thread. The
shared preferences editor also has a commit() method to synchronously save the
preferences. The commit() method is discouraged as it can block other operations.
@Override
protected void onPause(){
super.onPause();
1. Locate the part of the onCreate() method that tests if the savedInstanceState argument is
null:
if (savedInstanceState != null) {
// (deleted for brevity)
}
else { // bundle is null; this is application startup
// ...add preference code here
}
3. Inside the else block, get the count from the shared preferences and update mCount:
Note that the getInt() method takes two arguments: one for the key, and the other for the
default value if the key cannot be found. With the default argument you don't have to test
whether the preference exists in the file.
4. Update the main text view to display the count:
mShowCount.setText(String.format("%s", mCount));
5. Get the color from the shared preferences and update mCurrentColor:
mShowCount.setBackgroundColor(mCurrentColor);
7. Run the app. Click the count button and change the background color to update the
instance state and the preferences.
8. Force-quit the app using one of these methods:
In Android Studio, select Run > Stop 'app.'
On the device, click the Recents button (the square button in the lower right corner).
Swipe the card for the HelloSharedPrefs app to quit, or click the X in the right corner.
9. Re-run the app.
The app restarts and loads the preferences, maintaining the state.
Solution Code (Main Activity - onCreate())
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* Restore the saved state. See onSaveInstanceState() for what gets saved. */
if (savedInstanceState != null) {
mCount = savedInstanceState.getInt("count");
if (mCount != 0) {
mShowCount.setText(String.format("%s", mCount));
}
mCurrentColor = savedInstanceState.getInt("color");
mShowCount.setBackgroundColor(mCurrentColor);
}
else {
// Restore preferences
mCount = mPreferences.getInt("count", 0);
mShowCount.setText(String.format("%s", mCount));
mCurrentColor = mPreferences.getInt("color", mCurrentColor);
mShowCount.setBackgroundColor(mCurrentColor);
}
}
7. Task 3. Add a Reset button
The HelloSharedPrefs app automatically saves both the instance state and the preferences any
time the activity is paused or restarted. In this task we'll add a button to the app that resets the
count and the background color, and clears the preferences.
<Button
android:id="@+id/button5"
style="@style/AppTheme.Button"
android:onClick="countUp"
android:text="@string/count_button" />
3. Copy the definition for the Count button, and paste a new copy just below that button.
4. Modify the new button to have these attributes:
Attribute Value
android:id "@+id/button6"
android:onClick "reset"
android:text "reset"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:gravity="center"
android:orientation="horizontal">
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/button5"
style="@style/AppTheme.Button"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:onClick="countUp"
android:text="@string/count_button" />
<Button
android:id="@+id/button6"
style="@style/AppTheme.Button"
android:onClick="reset"
android:text="@string/reset_button" />
</LinearLayout>
Solution Code (styles.xml - partial)
<resources>
<string name="app_name">HelloSharedPrefs</string>
<string name="blue_button">Blue</string>
<string name="green_button">Green</string>
<string name="red_button">Red</string>
<string name="black_button">Black</string>
<string name="default_count">0</string>
<string name="count_button">Count</string>
<string name="reset_button">Reset</string>
</resources>
1. Reset the mCount variable to 0 and update the main text view:
mCount = 0;
mShowCount.setText(String.format("%s", mCount));
2. Reset the mCurrentColor variable to the default color (from color.xml) and update the main
text view:
mCurrentColor = ContextCompat.getColor(this,
R.color.default_background);
mShowCount.setBackgroundColor(mCurrentColor);
preferencesEditor.clear();
preferencesEditor.apply();
Add an item to the action bar for Settings, and a second activity to hold those settings. The
settings activity should:
Read the count and color items stored in the shared preferences.
Display UI elements such as toggle buttons and spinners to modify those preferences.
Include Save and Reset buttons for saving and clearing the preferences.
9. Conclusion
In this chapter, you learned about using shared preferences in your app, including how to get a
shared preferences file for your app, how to write data to and read data from the shared
preferences, and how to clear the shared preferences.
10. Resources
Saving Data (Android Guides)
Storage Options (Android Guides)
Saving Key-Value Sets (Android Training)
SharedPreferences (Android API Reference)
SharedPreferences.Editor (Android API Reference)
How to use SharedPreferences in Android to store, fetch and edit values (Stack Overflow)
onSavedInstanceState vs. SharedPreferences (Stack Overflow)
Contents:
Shared PreferencesStore primitive data types as key-value pairs. Data persists across
user sessions. This data is private to the particular application.
Files on Internal storageStore files privately to the app; other applications cannot access
them (nor can the user). When the user uninstalls the app, these files are removed.
Removable storageSave world-readable files to the external storage; they can be
modified by the user when they enable USB mass storage to transfer files on a computer.
SQLite database on deviceStore structured data persistently in a database that is
accessible only to the application.
Cloud backend over the internetMany options that are beyond the scope of this book.
SQLite Database
An SQLite database is a good storage solution when you have structured data that you need to
store persistently and change frequently.
The architecture of an app that uses an SQLite database is similar to using a RecyclerView
with an adapter as an intermediary between the data and the view, as you've done in previous
practicals. Below is what you built previously, where you dynamically created data, and used an
adapter to display it into a recycler view. When you are using an SQLite database,
all interactions with the database are through an instance of the SQLiteOpenHelper class. The
benefit of this architecture is to separate how the data is stored from how it is displayed.
In this series of practicals, you will create a SQLite database for a set of data,
display retrieved data in a RecyclerView, and add functionality to add, delete, and edit data in
the RecyclerView and store it in the database.
Note:Adding a database to persistently store your data, and abstracting your data into a data
model are sufficient for small apps with minimal complexity. In later chapters, you will learn to
architect your app using Loaders and Content Providers to further separate data from the user
interface, all with the goal of making the user's experience as smooth and natural as possible,
and retain the developer's ability to extend and maintain the app.
1. What you should already KNOW
For this practical you should be familiar with:
You start with a basic app that displays generated words in a RecyclerView (the WordList app
you created in Chapter 1).
One of the challenges of building a more complex app is to construct it in such a way that after
every step you have a running app, and the ability to verify that your changes are working as
intended. The tasks in these practicals are presented and ordered in such a way.
4. App Overview
LINK TO APP GOES HERE
You will create the database and display its contents in the RecyclerView. (Left screenshot
in the image below.)
You will add functionality to edit the contents of the database and display the changes in
the user interface. (Right screenshot in the image below.)
Minimum SDK Version is API15: Android 4.0.3 IceCreamSandwich and *target* SDK is the
current version of Android (version 23 as of the writing of this book).
5. Task 0. Download and run the base code
In order to save you some work, this practical will build on an app you have already built. In
addition, rearchitecting existing application code to add features or fix problems is a common
developer task.
For this practical, the data model only contains the word and its id. (While the unique id will be
generated by the database, you need a way of passing the id to the user interface, so that you
can identify the word that the user is changing.)
An empty constructor.
Getters and setters for each variable.
3. Run your app. You will not see any changes, but there should be no errors.
Solution:
public WordItem() {}
Why: It is possible to create a SQLite database without use of the helper class. Just like an
adapter, using the helper class is a best practice, and the class does house-keeping for you.
2. In the code editor, click the red light bulb and select Implement methods.
3. Add the missing constructor. (You will define the undefined constants next.)
// ... and a string array of columns. private static final String[] COLUMNS = {KEY_ID,
KEY_WORD};
// Column names... private static final String KEY_ID = "_id"; private static final String
KEY_WORD = "word";
### 2.3. Build the SQL query and code to create the database
SQL queries can become quite complex. It is a best practice to construct the queries
separately from the code that uses them.
**Why:** This increases code readability and helps with debugging. If one part of all
queries changes, you only have to make the change in one place.
1. Below the constants, add the following code to construct the query. (Note how this
makes the query very readable.)
// Build the SQL query that creates the table. private static final String
WORD_LIST_TABLE_CREATE = "CREATE TABLE " + WORD_LIST_TABLE + " (" + KEY_ID
+ " INTEGER PRIMARY KEY, " + // will auto-increment if no value passed KEY_WORD + "
TEXT );";
db.execSQL(WORD_LIST_TABLE_CREATE);
1. To get confirmation that your database was created after you run the app, add code
to get a readable database, and then check whether it is non-null.
1. Build and run the app, and check the logs for the "Yes" message.
Your data could come from many sources. It could be completely user created, or
downloaded from the internet, or generated from a file that's part of your APK. For this
practical, you will seed your database with a small amount of hard-coded data.
**Why:** Acquiring, creating, and formatting data is a whole separate topic that is not
covered specifically in this book.
1. Open WordListOpenHelper.
1. In onCreate, after creating the database, add a function call to
fillDatabaseWithData(db);
/**
The addEntry method adds one entry to a database that is already open for writing.
The method calls db.insert), which is a SQLiteDatabase convenience method to insert
a row into the database. (It's a convenience method, because you do not have to
write the SQL query yourself.)
The first argument to db.insert is the table. Use null for the second argument. The third
argument must be a ContentValues container with values to fill the row. This sample
only has one column; for tables with multiple columns, you add the values for each
column to this container. ``` /**
Adds an entry to the database
without opening and closing it. *
@param db The database to add to.
@param word Word to define. */ private void addEntry(SQLiteDatabase db, String word){
// Create a container for the data. This becomes more important when you // have more
than one value. ContentValues mValues = new ContentValues();
// Insert the new data to the database. db.insert(WORD_LIST_TABLE, null, mValues); } ```
After adding this code, you need to destroy the current empty database and then recreate
it. You can uninstall the app from your device, or you can clear all the data in the app from
Settings > Apps > WordLIst > Clear Data.
Run your app. You will not see any changes. Check the logs and make sure there are no
errors before you continue. If you encounter errors, read the logcat messages carefully
and use resources, such as stackoverflow, if you get stuck.
3. Build a query that retrieves all the items form the WORD_LIST_TABLE.
try {
} catch (Exception e) {
Log.d(TAG, " " + e); // Just log the exception
}
return entries;
SQLiteDatabase db = this.getWritableDatabase();
7. Execute the SQL query and assign the result to a Cursor object.
9. As the last instruction of the try block, close the cursor to prevent memory leaks.
cursor.close();
10. For testing, in the WordListOpenHelper constructor, call the getAllEntries method.
getAllEntries();
11. Build and run the app. Check the logs to make sure there are no errors, and all the items
are printed, something like this:
5-09 10:39:32.150 14105-14105/com.android.example.wordlistsql D/WordListOpenHelper:
1 inflate
05-09 10:39:32.150 14105-14105/com.android.example.wordlistsql D/WordListOpenHelper:
2 Adapter
etc. until
05-09 10:39:32.150 14105-14105/com.android.example.wordlistsql D/WordListOpenHelper:
11 Android Performance
Solution:
/**
*
* @return a LinkedList of WordItem items.
*/
public LinkedList<WordItem> getAllEntries() {
// Cycle through all items and add them to the linked list.
// The numbers are the column numbers in the table.
while (cursor.moveToNext()) {
// Create an entry and add the data to it.
WordItem entry = new WordItem();
entry.setId(cursor.getInt(0));
entry.setWord(cursor.getString(1));
entry.setDefinition(cursor.getString(2));
// Add the entry to the head of the list.
entries.addFirst(entry);
return entries;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This is boilerplate.
Log.w(WordListOpenHelper.class.getName(),
"Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + WORD_LIST_TABLE);
onCreate(db);
}
8. Task 3. Display data in the RecyclerView
You have a database, with data, that you can query and retrieve. Next, you will update the
WordListAdapter and MainActivity to fetch and display this data in the RecyclerView.
To make the adapter work with database data, you have to change the data type from String,
to WordItem.
1. In MainActivity:
i. Fix the data type of the word list
ii. Remove the code that generated your fake data.
iii. Comment out all the code in the onClickListener for the FAB. (You will replace it with
new code later.)
2. Optionally, you can also delete the code that checks whether the database was created.
3. Finally, change the code to create an adapter with all the entries from the database:
4. Run your app. You should see a list of all the words in the database. When you click a
word, it should be prefixed with !Clicked. When you restart the app, the words should be
back to their original values as nothing is changed in the database.
Solution:
https://drive.google.com/open?id=0Bw2kggOeTJAfTDNOVndjb0NrWVk
9. Task 4. Edit words in the UI and store
changes in the database
In this task, you are going to use skills learned in previous practicals to add, edit, and delete
words both from the UI and in the database.
As in the previous example, you will first add the database functionality, and then integrate it
with the UI. Here is what the finished app looks like:
To make changes to the database, you need to create three new methods:
addOneEntry()
deleteEntry()
updateEntry()
In the user interface, you need functionality to get user input and then apply the changes to the
UI and the database.
The FAB button gets user input and adds a new entry.
A delete button on each item that deletes the current item.
An edit button on each item that updates an item from user input.
Solution:
/**
* This method opens and closes the database.
* Only use it to add a single entry.
*
* @param word Word to define.
*/
public void addOneEntry(String word){
try {
SQLiteDatabase db = this.getWritableDatabase();
addEntry(db, word);
db.close();
} catch (Exception e) {
Log.d(TAG, " " + e);
}
}
* Takes an integer id and a String word for its arguments and returns an integer.
SQLiteDatabase db = this.getWritableDatabase();
2. In the catch block, print a log message if any exceptions are encountered.
db.close();
4. Return the number of rows updated, which should be -1 (fail), 0 (nothing updated), or 1
(success).
return mNumberOfRowsUpdated;
Solution:
/**
* Updates the word with the supplied id to the supplied value.
*
* @param id Id of the word to update.
* @param word The new value of the word.
* @return the number of rows affected or -1 of nothing was updated.
*/
public int updateWordEntry(int id, String word) {
SQLiteDatabase db = this.getWritableDatabase();
} catch (Exception e) {
Log.d (TAG, " " + e);
}
db.close();
return mNumberOfRowsUpdated;
}
/**
* @param id ID of the entry to delete.
*/
public void deleteWordEntry(int id) {
SQLiteDatabase db = this.getWritableDatabase();
try {
db.delete(WORD_LIST_TABLE, //table name
KEY_ID + " =? ", new String[]{String.valueOf(id)});
} catch (Exception e) {
Log.d (TAG, "db.delete threw exception: " + e); }
db.close();
}
In order to add the buttons to your current wordlist_item.xml layout, you need to restructure
your layout in the following steps.
Follow good coding practices and extract all strings, styles, colors, and dimensions into
their respective resource files.
1. In styles.xml, change the word_tile style to have the following style attributes:
layout_width match_parent
layout_height 26dp
textSize 24sp
textStyle bold
layout_marginBottom 6dp
2. In wordlist_item.xml, below your word TextView, add a LinearLayout.
layout_width match_parent
layout_height wrap_content
orientation horizontal
Delete button
id delete_button
layout_width match_parent
layout_height 36dp
layout_weight 2
background @color/colorPrimaryDark
text "Delete"
textColor #d3d3d3
Edit button
id edit_button
layout_width match_parent
layout_height 36dp
layout_weight 1
background @color/colorPrimary
text "Edit"
textColor #d3d3d3
4. Add a divider as the last element below the LinearLayout. This can be accomplished in
several ways. This sample just uses a colored button.
layout_width match_parent
layout_height 3dp
background @color/colorAccent
5. Run your app and make sure your layout matches the app screenshot (quite a ways)
above.
LinearLayout
tools:context ".EditWordActivity"
tools:showIn "@layout/activity_edit_word"
EditText
id edit_word
layout_width match_parent
layout_height wrap_content
fontFamily sans-serif-light
hint "Word"
inputType textAutoComplete
padding 6dp
textSize 18sp
layout_marginTop, layout_marginBottom 16dp
Button
id button_save
layout_width match_parent
layout_height wrap_content
background @color/colorPrimary
android:onClick saveWord
android:text @string/button_save
android:textColor @color/buttonLabel
Important:You will be adding quite a bit of code in this task. This practical provides
steps that give you checkpoints where you can run your code and verify that you have
no fundamental errors, even if not all functionality is working yet. This is a strategy
used by experienced developers to make sure they don't end up with massive amounts
of code and no idea where it might have broken along the way.
1. In MainActivity, change the FAB button code to start the EditWordActivity (which you will
implement later).
Do the following:
<activity android:name=".EditWordActivity"></activity>
3. In EditWordActivity, add the code for onCreate. You've learned to work with extras
previously, so here is the annotated code. Make sure you understand what's going on
here.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_word);
4. Create a saveWord function that takes a view parameter and returns nothing.
if (word.length() != 0) {
if (mId < 0) {
helper.addOneEntry(word);
} else {
helper.updateWordEntry(mId, word);
}
} else {
Toast.makeText(
getApplicationContext(),
"Word not saved because it is empty.",
Toast.LENGTH_LONG).show();
}
// Restart the main activity, which will also update the recycler view from the
database.
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
9. Rebuild and run your code. The FAB button should now work and you can add words to
the list.
For this example's efficiency, you are going to create only one OnClickListener that will handle
clicks from either button. Another, more purist, approach is to create one handler for each
button and pass it only the minimum data needed.
Solution:
Button delete_button;
Button edit_button;
delete_button = (Button)itemView.findViewById(R.id.delete_button);
edit_button = (Button)itemView.findViewById(R.id.edit_button);
WordListOpenHelper db;
11.6. 6.6. Add the onClick Listener for the delete button
In onBindViewHolder, programmatically add a click listener to the delete button that deletes the
current entry.
@Override
public void onClick(View v ) {
Log.d (TAG, " " + position + " " + id);
mWordList.remove(position); // remove from the view
db.deleteWordEntry(id); // remove from the database
notifyDataSetChanged(); // redisplay the view
}
});
11.7. 6.7. Add the onClick Listener for the edit button
In onBindViewHolder, programmatically add a click listener to the edit button that edits the
current entry.
@Override
public void onClick(View v) {
Intent intent = new Intent(context, EditWordActivity.class);
intent.putExtra("ID", id);
intent.putExtra("POSITION", position);
intent.putExtra("WORD", word);
context.startActivity(intent);
}
});
1. Resolve the error for the context variable, add a Context context class variable and set it
in the WordListAdapter constructor.
2. Run your app. YOU ARE DONE!
12. Coding challenges
12.1. Adding a contrac helper class
For this practical, you created the the database schema/tables from the SQLiteOpenHelper
class. This is sufficient for a simple example, like this one. For a more complex app, it is a
better practice to separate the schema definitions from the rest of the code in a helper class
that cannot be instantiated. This kind of class is called a "contract class".
Storage Options
Saving Data in SQL Databases
SQLite database
Writing basic SQLite queries
2. What you will LEARN
How to add search functionality to your app via the options menu.
3. What you will DO
This practical you will add a options menu item for searching, and an activity that allows users
to enter a search string and displays the result of the search in a text view.
Why: Users should always be able to search the data on their own terms.
Note that our concern is not building a spiffy search UI, but showing you how to query the
database.
4. App Overview
Starting from the WordListSQLInteractive app, you will add an activity that lets users search for
partial and full words in the database. For example, entering "Android" will return all entries that
contain the substring "Android".
5. Task 0. Download and run the base code
In order to save you some work, this practical will build on an app you have already built. In
addition, rearchitecting existing application code to add features or fix problems is a common
developer task.
You can use your own app, or download the base app. As long as the app uses an SQLite
database, you can use these instructions to extend it.
1. In your project, create an Android Resource directory and call it menu with "menu" as the
resource type. (res > menu)
2. Add a main_menu.xml menu resource file to res > menu.
3. Create a menu with one item. Reference the code snippet for values.
<menu
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"
tools:context="com.android.example.wordlistsqlsearchable.MainActivity">
<item
android:id="@+id/action_search"
android:title="Search..."
android:orderInCategory="1"
app:showAsAction="never" />
</menu>
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
return true;
}
return super.onOptionsItemSelected(item);
}
6. Run your app. You should see the dots for the options menu. When you click it, you should
see one menu item for search that does nothing.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
<activity android:name="com.android.example.wordlistsqlsearchable.SearchActivity">
</activity>
startActivity(inten t);
2. Build and run your app to make sure the activity is fired off when the menu item is
selected.
3. Enter a search string and press "Search". You app crashes.
4. Find out why the app has crashed, then move to the next task.
6.5. 1.5. Implement the onClick handler for the Search button
Your app crashed, because the onClick handler set for the Search button in the XML code
doesn't exist yet. So you will build showResult next.
Your app will not run without at least a stub for search() implemented.
Open WordListOpenHelper.
Implement a stub for search, with a String parameter, that returns a null cursor.
Run your app and fix any errors you may have. Note that most of the code in showResult()
is not exercised yet.
Inside the search() method, you need to build a query with the search string and send the query
to the database.
The most secure way to do this is by using parameters for each part of the query.
WHY: In the previous practical, for the query in WordListOpenHelper, you could build the query
string directly and submit it as a rawQuery(), because you had full control over the contents of
the query. As soon as you are handling user input, you must assume that it could be malicious.
You should always validate user input even before you build your query!
The SQL query for searching for all entries in the wordlist matching a substring has this form:
The parametrized form of the query method you will call looks like this:
For the query in the search() method, you need to assign only the first four arguments.
4. Create the where clause. Omit "WHERE" as it's implied. Use a question mark for the
argument to LIKE. Make sure you have the correct spaces.
try {
if (mReadableDB == null) {mReadableDB = getReadableDatabase();}
cursor = mReadableDB.query(WORD_LIST_TABLE, columns, where, whereArgs, null,
null, null);
} catch (Exception e) {
Log.d(TAG, "EXCEPTION! " + e);
}
return cursor;
}
Note that you can send any SQLite query to the database in this way and receive the reply as a
cursor.
7. Coding challenges
Most of the code samples use the default AppBar that comes with the Empty Template. In
some of the previous chapters, you learned about the Toolbar, for example, when using the
Basic Template.
Change the app to use the Toolbar and SearchView and show the search icon on the toolbar.
https://developer.android.com/training/search/setup.html
https://developer.android.com/training/appbar/setting-up.html
As written, this app is not very secure. Consider how to add basic input validation for the
search string. See and Security Tips.
Try different types of queries and other forms of the query method.
8. Conclusion
In this chapter, you learned
9. Resources
Developer Documentation:
Storage Options
Saving Data in SQL Databases
Searching a Database
Review link:
https://docs.google.com/document/d/1oQZE5LWIvO59Zz3H37XOC1f1AxicpqmvCjAEvtXCZsI/e
dit#
Code:
https://devrel-review.git.corp.google.com/#/c/21330/
Content Providers and Resolvers
In this practical you will learn how to use a content provider to separate your data from the rest
of your app; and how to set up your content provider to share your data with other apps.
A content provider / content resolver pair forms an interface between an app's other
functionality and data. There are multiple parts to this architecture, as shown in the following
Data: APP 1 owns and manages data. The data is often stored in an SQLite database, but this
is not mandatory. Typically, the data is presented as tables, similar to database tables. Each
row represents one entry, and each column represents an attribute for that entry. For example,
each row contains one contact, and may have columns for email address and phone number.
Content Provider of APP1: The content provider implemented by APP 1 provides a standard
CRUD (create, read, update, delete) interface to APP 1s data. In addition, it provides a public
and secure interface to the data, so that other apps, such as APP 2, can access the data with
the appropriate permissions. For example, when you install a new app, and it asks your
permission to access your contacts, you grant that app permission to interact with the content
provider of the Contacts app.
Contract: The contract is a public class that exposes important information about APP 1s
content provider to other apps. This usually includes the URI schemes, important constants,
and the structure of the data that will be returned.
URI scheme: Apps send requests to the content provider using content Uniform Resource
Identifiers or URIs. A content URI for content providers has this general form:
The following URI could be used to request all the entries in the "words" table:
content://com.android.example.wordcontentprovider.provider/words
Designing URI schemes is a topic in itself and not covered in this practical.
Content Resolver: Content providers are always paired with a content resolver. The
ContentResolver object provides query(), insert(), update(), and delete() methods for accessing
data from a content provider. Thus, the content resolver mirrors the content providers CRUD
API and manages all interaction with the content provider for you. In most situations, the default
content resolver provided by the Android system is sufficient.
If your app does not share data with other apps, your app does not require a content provider.
However, because the content provider cleanly separates the implementation of your backend
from the user interface, it can also be useful for architecting complex applications.
Allowing multiple apps to access, use, and modify a single data source. Examples:
Contacts, game scores, spell-checking dictionary.
Storing data independently from the app. This allows to change how the data is stored.
Example: Build a prototype using mock data, then use an SQL database for the real app.
Note that this is what you will be doing in this and the next practical.
Separating data from UI. Development teams can work independently on data and UI.
Example: It is very common that the user interface and the data backend are developed by
different teams and they can even be separate apps.
Using other interesting classes that expect to interact with a content provider. Example:
You must have a content provider to use a loader.
.
In this practical, you will build a basic content provider from scratch. You will create and
process mock data so that you can focus on understanding content provider architecture.
Likewise, the user interface to display the data is minimalist. In the next practical, you will add a
content provider the WordList app, using this minimalist app as your template.
1. What you should already KNOW
For this practical you should be familiar with:
WHY: One of the challenges of building a more complex app is to first thoroughly understand
each piece. One way of building that understanding is by building a standalone, minimalist
version around that concept, then use it as a reference for building the real thing. This technique
has broad application whenever you need to learn something new that is non-trivial.
4. App Overview
The backend of this app generates mock data and stores it in a linked list, call "words".
The frontend of this app requests data through a content resolver and displays it. The UI is
minimalist, consisting of one activity with a TextView and two Buttons.
In between the frontend and the backend, a content provider abstracts and manages the
interaction between the backend and the frontend.
Drawing: https://docs.google.com/drawings/d/1DrsuFi6ah2xvu4uY2wyIC-OSu_98aRXfKfWxTU-
sgxs/edit
Screenshot: TBD
Minimum SDK Version is API15: Android 4.0.3 IceCreamSandwich and *target* SDK is the
current version of Android (version 23 as of the writing of this book).
5. Task 1. Create the
MinimalistContentProvider project
By now, you are expected to be very familiar with the basics of app creation.
@+id/textview
TextView android:text="response"
@+id/button_display_all
android:text="List all words"
Button
android:onClick="onClickDisplayEntries"
@+id/button_display_first
android:text="List first word"
Button
android:onClick="onClickDisplayEntries"
1. In the MainActivity, create a member variable for the text view and initialize it in onCreate.
2. In onClickDisplayEntries, switch on the view's id, and log when each button was pressed.
3. In onClickDisplayEntries, at the end append some text to the textview.
4. As always, run the app.
Solution:
package android.example.com.minimalistcontentprovider;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.textview);
}
switch (view.getId()) {
case R.id.button_display_all:
Log.d (TAG, "Yay, " + R.id.button_display_all + " was clicked!");
break;
case R.id.button_display_first:
Log.d (TAG, "Yay, " + R.id.button_display_first + " was clicked!");
break;
default:
Log.d (TAG, "Error. This should never happen.");
}
mTextView.append("Thus we go! \n");
}
}
6. Task 2. Create a Contract class, a URI
scheme, and mock data
Why:
Contract is public and includes important information for other apps that want to connect to
this content provider.
The URI scheme shows how to build URIs to access the data. It's the API for the data.
Separates design/definition from the implementation.
Allows to define shared constants, making it easier to maintain the application.
Makes information easy to find, because it is in one place.
The contract contains information about the data that apps need to build queries, in
particular, the names of the selectors. It therefore makes sense, to create the mock data
structure in the contract.
2. To prevent someone from accidentally instantiating the Contract class, give it an empty
private constructor. This is a standard pattern.
private Contract() {}
1. In the Contract class, create a constant for AUTHORITY. Customarily, to make Authority
unique, it's the package name extended with "provider."
2. Create a constant for the CONTENT_PATH. The content path is an abstract semantic
identifier of the data you are interested in. It does not predict or presume in what form the
data is stored or organized in the background. As such, "words" could resolve in the name
of a table, the name of a file, or in this example, the name of the list. public static final
String CONTENT_PATH = "words";
3. Create a constant for the CONTENT_URI. This is a content:// style URI to one set of data.
If you have multiple "data containers" in the backend, you would create a content URI for
each. Uri is a helper class for building and manipulating URIs. public static final Uri
CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + CONTENT_PATH);
4. Create a convenience constant for ALL_ITEMS. This means you don't have to reveal the
implementation details, and you can change it without breaking your clients' apps. static
final int ALL_ITEMS =- -2;
5. Create a convenience constant for WORD_ID. If the underlying name of the ID changes,
your clients' code won't break. static final String WORD_ID = "id";
For this practical, you can set the MIME types as follows:
1. Declare the MIME time for one data item.
1. In the AndroidManifest, inside the application tag, after the /activity closing tag, add:
<provider
android:name=".MiniContentProvider"
android:authorities="android.example.com.minimalistcontentprovider.provider" />
1. Create a new UriMatcher. The argument supplies the value to return if there is no match.
As a best practice, use UriMatcher.NO_MATCH.
2. private static UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
3. In the onCreate method, add the accepted URIs to the matcher and assign them an integer
code. For apps with more URIs, use constants for the codes, as shown in the UriMatcher
documentation.
WHY: Because if you understand this method, implementing content providers is considerably
more straightforward.
The arguments to this method represent the parts of an SQL query. Even, if you are using
another kind of backend, you must still accept a query in this style and handle the arguments
appropriately. (In the next task you will build a query in the MainActivity to see how the
arguments are used.)
For security reasons, the arguments are processed separately. </td> </tr> sortOrder Whether
to sort, and if so, whether ascending or descending. If this is null, the default sort or no sort is
applied. @return Cursor of any kind, with the response data inside. </table>
1. Identify the following processing steps in the provided query method code..
The query implementation for this basic app takes some shortcuts.
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
int id = -1;
switch (sUriMatcher.match(uri)) {
case 0:
// Matches URI to get all of the entries.
id = Contract.ALL_ITEMS;
// Look at the remaining arguments to see whether there are constraints.
// In this example, we only support getting a specific entry by id. Not full
search.
// For a real-life app, you need error-catching code; here we assume that the
// value we need is actually in selectionArgs and valid.
if (selection != null){
id = parseInt(selectionArgs[0]);
}
break;
case 1:
// The URI ends in a numeric value, which represents an id.
// Parse the URI to extract the value of the last, numeric part of the path,
// and set the id to that value.
id = parseInt(uri.getLastPathSegment());
// With a database, you would then use this value and the path to build a
query.
break;
case UriMatcher.NO_MATCH:
// You should do some error handling here.
Log.d(TAG, "NO MATCH FOR THIS URI IN SCHEME.");
id = -1;
break;
default:
// You should do some error handling here.
Log.d(TAG, "INVALID URI - URI NOT RECOGNZED.");
id = -1;
}
Log.d(TAG, "query: " + id);
return populateCursor(id);
}
If your data is stored in a SQLite database, executing the query will return a Cursor that
you can return.
If you are not using a data storage method that returns a cursor, such as files or the mock
data, you can use a simple MatrixCursor to hold the data to return.
The query part of this call takes arguments URI, projection, selectionClause, selectionArgs,
and sortOrder, which should be familiar from the query method in the MiniContentProvider
class. You must define those arguments next.
In order for this call to work, you need to declare and assign values to all the arguments.
1. URI: Declare the URI that identifies the content provider and the table. Note: Remember
that from the perspective of the app, there is always a table at the backend. You get the
information for the correct URI from the contract.
2. Projection: A string array with the names of the columns to return for each row. Setting
this to null returns all columns. When there is only one column, as in the case of this
example, setting this explicitly is optional, but can be helpful for documentation purposes.
String[] projection = new String[] {Contract.CONTENT_PATH}; // Only get words.
3. selectionClause: Argument clause for the selection criteria for which rows to return.
Formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null returns all
rows for the given URI. Since this will vary depending on which button was pressed,
declare it now and set this later.
String selectionClause;
4. selectionArgs: Argument values for the selection criteria. If you include ?s in selection,
they are replaced by values from selectionArgs, in the order that they appear.
IMPORTANT: It is a best security practice to always separate selection and selectionArgs.
String selectionArgs[];
5. sortOrder: The order in which to sort the results. Formatted as an SQL ORDER BY
clause (excluding the ORDER BY keyword). Usually ASC or DESC; null requests the
default sort order, which could be unordered.
String sortOrder = null; // For this example, accept the order returned by the
response.
switch (view.getId()) {
case R.id.button_display_all:
selectionClause = **null**;
selectionArgs = **null**;
**break**;
case R.id.button_display_first:
**break**;
default:
selectionClause = **null**;
selectionArgs = **null**;
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
**do **{
**mTextView**.append(word + **"\n"**);
} **while **(cursor.moveToNext());
} else {
} else {
}
cursor.close();
In the next practical, you will add a content provider to the WordListSQL app. You can use the
code you created here as a reference and template. There will be less guidance in the next
practical, so make sure you are confident in your understanding.
9. Coding challenges
9.1. Implement missing methods
Implement the insert, delete, and update methods for the MinimalistContentProvider app.
Provide the user with a way to insert, delete, and update data.
Hint: If you don't want to build out the user interface, create a button for each action and
hardwire the data that is inserted, updated, and deleted. The point of this exercise is to work
on the content provider, not the user interface.
Why: You will implement the fully functioning content provider with UI in the next practical, when
you will add a content provider to the WordListSQL app.
Create a Contract to expose your content provider's API to other app. Note that contracts
can be useful beyond content providers, for example, for databases. See coding challenge
in the data storing chapter.
Define a URI scheme so that other apps can access data through your content provider.
Implement a minimalist content provider to help you understand how content providers
work.
Use a content resolver to request data from a content provider and display it to the user.
11. Resources
Developer Documentation:
Review link:
https://docs.google.com/document/d/1yEDoFYGgVrZhKyxQ3EvcD5mYT50zaJ6EqXzwNSi0l4w
/edit
Code:
https://devrel-review.git.corp.google.com/#/c/21811/
Content Providers in Real Apps
Content providers in real apps are more complex than the bare-bones version you built in the
previous practical.
Your job will rarely be to build an app from scratch. More often, you will be asked to debug,
refactor, or extend an existing application.
In this practical, you will take the WordListSQL app and refactor and extend it to use a content
provider as a layer between the SQL database and the RecyclerView.
This is not an easy task, and you can go about it in two ways.
Refactor and extend the WordListSQL app. This involves changing the app architecture
and refactoring code.
Start from scratch and re-use code from WordListSQL and MinimalistContentProvider.
The practical will demonstrate how to refactor the existing WordListSQL app, because it's what
you are more likely to encounter on the job..
1. What you should already KNOW
For this practical you should be familiar with:
You start with the WordListSQLInteractive app you created in a previous practical, which
displays words from a SQLite database in a RecyclerView, and users can create, edit, and
delete words.
Separated backend and frontend by using a content provider and content resolver.
Unchanged user interface and functionality.
Public API through the content provider as specified in a Contract.
Your app will look that same as at the end of the data storage practical.
Drawing source:
https://docs.google.com/drawings/d/1o4WAw7_c82vmqm30q3skXt33iooKSnnG3DLa4LzJ5zY/e
dit
5. Task 0. App Architecture
When apps get more complex it helps to have a plan, and for that plan to follow established
practices. And to draw it out in diagrams.
Keeping it all in your head gets really difficult really fast. Even with a simple app as
WorldListSQL, if you are new to app development, not having a plan is going to result in
inconsistencies, which inevitably leads to more mistakes, which are harder to debug.
Understanding, and thus changing, an existing app is much easier if you know what it's
architecture is.
If you have a plan, and it is written, you can communicate it to your co-workers, your
investors, and especially, to your technical writers.
Having a roadmap makes development easier, because you make fewer decisions on the
fly.
The software architecture of the app. What the different pieces are, and how they relate to
each other.
The API. The public classes, methods, and functions that other applications can use, along
with their signatures.
The user interface, and how the user is guided through the workflow.
In this task, you are taking a closer look at the software architecture and API.
Architecture
This is an architecture diagram for WordListSQL that you built in the previous chapter.
Drawing source:
https://docs.google.com/drawings/d/1UgLOXR_SmRzsEIODNOq7bnekRmWmsP5_Lwo3Caf7
DCA/edit
API
Link to Drawing:
https://docs.google.com/drawings/d/1YPmyLCgpC6Eg2icz7ROrgwhE_ijLhYM_J9Y1kUtu_ck/ed
it
Solution: count()
Link to drawing:
https://docs.google.com/drawings/d/1lzhCw64RR_ktb3Ok5MnpE-
dTqz57N978NEicZOjtGvg/edit
Link to drawing:
https://docs.google.com/drawings/d/1ATB0_DEe2jNjrXI-
6pcL3yQwh9kkZi8eDsSDVgON4SA/edit
Solution:
You will not need to change the ViewHolder class, because it is wrapped by an adapter.
All the functions follow the CRUD architecture, and they have the same signature through
the application stack.
Designing your base app well, and with expansion in mind, using an established API
pattern, is going to make it easier to "insert" a content provider.
New Classes: Contract, ContentProvider
Classes that change: WordListOpenHelper, MainActivity, WordListAdapter
Classes that should not change: WordItem, MyButtonOnClickListener, ViewHolder
6. Task 1. Download and run the base code
This practical builds on the WordListSQLInteractive and MinimalistContentProvider apps that
you built previously. You can start from your own code, or download the base apps. LINK TO
APP GOES HERE LINK TO APP GOES HERE
This contract contains all the information that any app needs to use your app's content provider.
This is a standard pattern for classes that are used to hold meta information and constants for
an app.
private Contract() {}
1. Move DATABASE_NAME.
A common way of organizing a contract class is to put definitions that are global to your
database into the root level of the class. Then, create a static abstract inner class for each
table with the column names. This inner class commonly implements the BaseColumns
interface. By implementing the BaseColumns interface, your class can inherit a primary key
field called _ID that some Android classes, such as cursor adapters, expect to exist. This
is not required, but can help your database work harmoniously with the Android
framework.
2. Create an inner class WordList that implements BaseColumns.
documentation.
2. Run your app. It should run and look and act exactly as before you changed it.
8. Task 3. Create a Content Provider
Link to drawing:
https://docs.google.com/drawings/d/12pX8qAL5R1EdkbmrSLZSiXCUGkla3AYYU2gwBr6tUYk/
edit
In this task you will create a content provider, implement its query method, and hook it up with
the WordListAdapter and the WordListOpenHelper. Instead of querying the WordListOpen
Helper, the Word List Adapter will use a content resolver to query the content provider, which in
turn will query WordListOpenHelper which will query the database.
This content provider uses an UriMatcher, a utility class that maps URIs to numbers, so you can
switch on them.
This puts the codes in one place and makes them easy to change. Use tens, so that inserting
additional codes is straightforward.
@Override
public boolean onCreate() {
mDB = new WordListOpenHelper(getContext());
initializeUriMatching();
return true;
}
2. Create a private void method initializeUriMatching().
3. In initializeUriMatching(), add URIs to the matcher for getting all items, one item, and the
count.
Refer to the Contract and use the initializeUriMatching() method in the MinimalistContentProver
app as a template.
Solution:
1. Modify WordListContentProvider.query().
2. Switch on the codes returned by sUriMatcher.
3. For URI_ALL_ITEMS_CODE, URI_ONE_ITEM_CODE, URI_COUNT_CODE, call the
corresponding in WordListOpenHelper (mDB).
Notice how assigning the results from mDB.query() to a cursor, generates an error, because
WordListOpenHelper.query() returns a WordItem.
Notice how assigning the results from mDB.count() to a cursor generates an error, because
WordListOpenHelper.count() returns a long.
Solution:
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (sUriMatcher.match(uri)) {
case URI_ALL_ITEMS_CODE:
cursor = mDB.query(ALL_ITEMS);
break;
case URI_ONE_ITEM_CODE:
cursor = mDB.query(parseInt(uri.getLastPathSegment()));
break;
case URI_COUNT_CODE:
cursor = mDB.count();
break;
case UriMatcher.NO_MATCH:
// You should do some error handling here.
Log.d(TAG, "NO MATCH FOR THIS URI IN SCHEME.");
break;
default:
// You should do some error handling here.
Log.d(TAG, "INVALID URI - URI NOT RECOGNZED.");
}
return cursor;
}
NOTE: This kind of cascading errors and fixes is typical for working with real-life applications.
If an app you are working with is well architected, you can follow the bread crumbs and fix the
errors one by one
Solution:
When learning, it is better to return to a consistent state without errors as often as possible, so
that you can check your progress and have some confidence that your code is correct.
So, next, you will fix WordListAdapter.onBindViewHolder() to use a content resolver instead of
calling the WordListOpenHelper directly.
1. In WordListAdapter, delete the mDB variable, since you are not referencing the database
anymore. This shows errors in Android Studio, that guide subsequent changes.
2. In the constructor, delete the assignment to mDB.
3. Refactor>Change the signature of the constructor and remove the db argument.
4. Add class variables for the query parameters since they will be used more than once.
The content resolver takes a query parameter, which you must build. The query is similarly
structured to a SQL query, but instead of a selection statement, it uses a URI. Query
parameters are very similar to SQL queries.
5. In onBindViewholder, delete the first two lines of code setting current and setting the text
of the holder.
6. Define a string with the uri for this query, to fetch the item at the passed in position.
7. Define an empty String variable named word.
8. Define an integer variable called id and set it to -1.
9. Create a content resolver with the specified query parameters and store the results in a
Cursor called cursor. (See MainActivity of MinimalistContentProvider app for an example.)
11. Fix the parameters for the click listeners for the two buttons:
current.getId() id
current.getWord() word
12. Replace the call to mDB.delete(id) with a content resolver call to delete.
Solution:
@Override
public int getItemCount() {
int count = -1;
Cursor cursor = mContext.getContentResolver().query(
Contract.ROW_COUNT_URI, projection,selectionClause, selectionArgs,
sortOrder);
if (cursor.moveToFirst() && cursor.getCount() >= 1){
count = cursor.getInt(0);
}
cursor.close();
return count;
}
<provider
android:name=".WordListContentProvider"
android:authorities="com.android.example.wordlistsqlwithcontentprovider.provider">
</provider>
Your app should run and be fully functional. If it is not, compare your code to the supplied
solution code, and use the debugger and logging to find the problem.
You followed the errors to update methods in the WordListOpenHelper and WordListAdapter
classes to work with the content provider.
When you run your app, for queries, the method calls go through the content provider.
For the insert, delete, and update operations, your app is still calling WordListOpenHelper.
With the infrastructure you have built, implementing the remaining methods is a lot less work.
9. Task 4. Implementing Content Provider
methods
9.1. 4.1 getType()
The getType() method is called by other apps that want to use this content provider, to discover
what kind of data your app returns.
Solution:
@Nullable
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case URI_ALL_ITEMS_CODE:
return MULTIPLE_RECORDS_MIME_TYPE;
case URI_ONE_ITEM_CODE:
return SINGLE_RECORD_MIME_TYPE;
default:
return null;
}
}
Challenge: How can you test this method, as it is not called by your app. Can you think of
three different ways of testing that this method works correctly?
9.2. 4.2 Call the content provider to insert and update words in
MainActivity
To fix insert operations MainActivity().onActivityResult needs to call the content provider instead
of the database for inserting and updating words.
In OnActivityResult():
Inserting:
2. If the word length is not null, create a ContentValues values variable and add the word to it
with the key "word".
3. Replace mDB.insert(word); with an insert request to a to a content resolver.
Updating:
Solution snippet:
if (id == WORD_ADD) {
getContentResolver().insert(Contract.CONTENT_URI, values);
} else if (id >=0) {
String[] selectionArgs = {Integer.toString(id)};
getContentResolver().update(Contract.CONTENT_URI, values,
Contract.WordList.KEY_ID, selectionArgs
);
}
// Update the UI
mAdapter.notifyDataSetChanged();
Android Studio reports an error for the values parameter, which you will fix in the next steps.
Solution:
1. In WordListContentProvider, Implement update(), which is one line of code that passes the
id and the word as arguments.
return mDB.delete(parseInt(selectionArgs[0]));
9.7. 4.7 Run your app
Yup. That's it. Run your app and make sure everything works.
And if it doesn't, fix it, because you'll need the code in a later practical, when you'll write an app
that uses this content provider to load word list data into its user interface,.
10. Coding challenges
The wordlist is just a list of single words, which isn't terribly useful. Extent the app to
display definitions, as well as a link to useful information, such as developer.android.com,
stackoverflow, or wikipedia.
Add an activity that allows users to search for words.
Add basic tests for all the functions in WordListContentProvider.
11. Conclusion
Congratulations!
You have made your way through one of the most-feared Android features. In addition, you
have practiced the real-life skill of rearchitecting, refactoring, and expanding existing code to
meet a new requirement.
12. Resources
Developer Documentation:
Videos:
here.
Content providers
Adapters
2. What you will LEARN
How to access another app's content provider.
Setting basic permissions
3. What you will DO
You will make a copy of the content provider app, remove its content provider, and get data
from the original app.
You will modify WordListSQLWithContentProvider to allow read and write access to its data.
Note that this finished app will also be the starter app for the Loader practicals.
4. Apps Overview
You need two apps for this practical.
WordListSQLWithContentProvider
Stripped copy of WordListSQLWithContentProvider.
The UI and functionality of these two apps are unchanged from previous practicals.
5. Task 1. Make your content provider available
to other apps
By default, apps cannot access data of other apps. They need permission to do so. In the case
of user data, that permission comes from the user. In the case of a content provider, the
permission comes from the content provider.
System permissions are predefined by the system. For example, if your app wanted to
read a user's calendar, it needs to request the READ_CALENDAR permission from user.
Developer defined permissions. Your content provider
To make your content provider available to other apps, you need to specify the grantable
permissions in the AndroidManifest of the provider, and declare it in the Android Manifest of the
client.
Permissions are not covered in detail in these practicals. You can learn more in Declaring
Permissions, System Permissions, and Implementing Content Provider Permissions.
android:exported="true"
5. Declare the required read and write permission, which is the default. Put the declaration at
the top level, inside the <manifest> tag.
It is good practice to use your unique package name in order to keep the permission
unique.
<permission
android:name="com.android.example.wordlistsqlwithcontentprovider.PERMISSION" />
<uses-permission android:name =
"com.android.example.wordlistsqlwithcontentprovider.PERMISSION"
here.
Use the final code for WordListWithContentProvider. Make sure it has the permissions
added from 10.3. You can also get the code .
Use the WordListClient that you built in 10.3. You can also get the code .
In this practical you will learn how to load data provided by another app's content provider in
the background and display it to the user, when it is ready.
Querying a ContentProvider for data you want to display takes time. If you run the query
directly from an Activity, it may get blocked and cause the system to issue an "Application Not
Responding" message. Even if it doesn't, users will see an annoying delay in the UI. To avoid
these problems, you should initiate a query on a separate thread, wait for it to finish, and then
display the results.
You can do this in a straightforward way by using an object that runs a query asynchronously in
the background and reconnects to your Activity when it's finished. You do this with a loader,
specifically, a CursorLoader. Besides doing the initial background query, a CursorLoader
automatically re-runs the query when data associated with the query changes.
At a high level, you need the following pieces to display data from a content provider:
An Activity or fragment.
An instance of the LoaderManager in the Activity.
A CursorLoader to load data backed by a ContentProvider.
An implementation for LoaderManager.LoaderCallbacks, an abstract callback interface for
the client to interact with the LoaderManager.
A way of displaying the loader's data, commonly via an adapter.
The following diagram shows a simplified version of app architecture with a loader. The loader
performs querying for items in the background. If the data changes, it gets a new set of data
for the adapter. Using a CursorLoader, this is all taken care of automatically.
Note that it is entirely possible to build custom loaders. But since the Android system provides
you with an elegant solution that saves you a lot of work, consider how you can use it as given
before implementing your own solution from scratch.
https://docs.google.com/drawings/d/1Wg3dYUqYEGPG5sIidJl12mZmdV3qFWTWn_ZR8eJ1t4
Q/edit
1. What you should already KNOW
For this practical you should be familiar with:
1.
The LoaderManager calls this method to create the loader, if it does not already exist.
1. Create a queryUri and projection. The CursorLoader requires a URI for the query, and a
context. Use the same URI that the content resolver is using to query the content provider.
You can find it in the Contract.
2. Return the CursorLoader.
@Override
public Loader onCreateLoader(int id, Bundle args) {
Call setData, which you will implement in the next task, passing in the cursor.
@Override
mAdapter.setData(data);
@Override
mAdapter.setData(null);
However, in practice, this does not work, and so you need to ask the LoadManager to restart
the loader.
Delete:mAdapter.notifyDataSetChanged();
mCursor = cursor;
notifyDataSetChanged();
@Override
int id = -1;
if (mCursor != null) {
**int **count = **mCursor**.getCount();
**mCursor**.moveToPosition(position);
word = **mCursor**.getString(indexWord);
holder.**wordItemView**.setText(word);
id = **mCursor**.getInt(indexId);
} **else **{holder.**wordItemView**.setText(**"Waiting..."**);}
} else {
holder.**wordItemView**.setText(**"ERROR: NO WORD"**);
In this practical you'll create an app that responds to a change in the charging state of your
device, as well as sends and receives a custom Broadcast Intent.
Note: The "Exported" feature allows your application to respond to outside broadcasts,
while "Enabled" allows it to be instantiated by the system.
3. Navigate to your manifest file. Note that Android studio automatically generated a
<receiver> tag with your chosen options as attributes. BroadcastReceivers can also be
In your manifest file, add the following code between the <receiver> tags in order to register
your Receiver for the system Intents:
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
</intent-filter>
1. Navigate to your CustomReceiver file, and delete the default implementation inside the
onReceive() method.
2. Obtain the action from the intent and store it in a String variable called "intentAction":
@Override
public void onReceive(Context context, Intent intent) {
String intentAction = intent.getAction();
}
3. Create a switch statement on the intentAction string, so that you can display a different
Toast message for each specific action your receiver is registered for:
switch (intentAction){
case Intent.ACTION_POWER_CONNECTED:
break;
case Intent.ACTION_POWER_DISCONNECTED:
break;
}
4. Initialize a String variable called "toastMessage" before the switch statement, and make
it's value null so that it can be set depending on the broadcast action you receive.
5. Assign toastMessage to "Power connected!" if the action is ACTION_POWER_CONNECTED , and
"Power disconnected!" if it is ACTION_POWER_DISCONNECTED . Extract your string resources.
6. Display a Toast for a short duration after the switch statement:
7. Run your app. After it is installed, unplug your device. It may take a moment the first time,
but sure enough, a Toast is displayed each time you plug in, or unplug your device.
Note: If you are using an emulator, you can toggle the power connection state by selection the
ellipses icon for the menu, choose Battery on the left bar, and toggle using the Charger
connection setting.
</div>
1. Navigate to your activity_main.xml file, remove the "Hello World!" TextView and change the
root view to a vertical LinearLayout.
2. Add a ToggleButton view with the following attributes:
Attribute Value
android:id "@+id/receiverToggle"
android:layout_width wrap_content
android:layout_height wrap_content
android:textOn "Receiver On"
android:textOff "Receiver Off"
3. Set your LinearLayout gravity attribute to "center" and extract your String resources.
4. In MainActivity, create a member variable to keep track of the state of the Receiver:
6. Get a reference to the SharedPreference file, and initialize the mReceiverState variable,
with a default state of false (so that the receiver is disabled by default):
7. Set the checked state of the toggle button based on the mReceiverState variable.
toggle.setChecked(mReceiverState);
The state of the BroadcastReceiver can be toggled by using a the PackageManager class.
To learn more about managing your BroadcastReceivers, visit this guide. Do the following:
8. Get a reference to your receiver component by creating a ComponentName variable:
10. Create an onCheckChangedListener for your toggle button, and set the mReceiverState
variable appropriately:
toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
mReceiverState = true;
} else {
mReceiverState = false;
}
}
});
11. Create a receiverStateFlag variable that takes the value of one of two integer flags for the
setComponentEnabledSetting() method of the package manager, depending on the value
of mReceiverState:
sharedPreferences.edit().putBoolean("receiverState", mReceiverState).apply();
packageManager.setComponentEnabledSetting(receiver, receiverStateFlag,
PackageManager.DONT_KILL_APP);
<div class="note>
In order to register your receiver to respond to your custom Broadcast, you must have a unique
string identify the action the Broadcast is representing. It is best practice to begin the action
strings with your package name, so that it is assuredly unique.
Create a constant String variable in both your MainActivity and your CustomReceiver class to
be used as the Broadcast Intent Action:
1. In the sendBroadcast() method in MainActivity, create a new Intent, with your custom
action string as the argument.
2. Call LocalBroadcastManager.getInstance(this).sendBroadcast(customBroadcastIntent) to
send the broadcast using the LocalBroadcastManager class.
2. Get an instance of LocalBroadcastManager and register your receiver with the custom
intent action:
LocalBroadcastManager.getInstance(this)
.registerReceiver(mReceiver,new IntentFilter(ACTION_CUSTOM_BROADCAST));
3. Override the onDestroy() method and unregister your receiver from the
LocalBroadcastManager:
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
6.4. 2.5 Respond to the Custom Broadcast
In your CustomReceiver class, add a case statement for the custom Intent Action, modifying
the toast message to "Custom Broadcast Received", and extract your resources:
case ACTION_CUSTOM_BROADCAST:
toastMessage = context.getString(R.string.custom_broadcast_toast);
break;
Note: Broadcast Receivers that are registered programmatically are not affected by the
enabling or disabling done by the PackageManager class, which is meant for components listed
in the Android Manifest file. Enabling or disabling such receivers is done by registering or
unregistering them, respectively. In this case, turning off the "Receiver Enabled" toggle will stop
the power connected or disconnected Toasts, but not the Custom Broadcast Intent Toast for
this reason.
That's it! Your app now delivers custom Broadcast intents and is able to receive both system
and custom Broadcasts.
7. Coding challenge
Note: All coding challenges are optional.
8. Conclusion
Broadcast Receivers are one of the fundamental components of an android application, with the
ability to receive Intents broadcasted by both the system and applications. In order to use a
Broadcast Receiver, you must:
Subclass the BroadcastReceiver class and implement onReceive to process the incoming
Intent.
Register your receiver either in the manifest or programmatically.
Use LocalBroadcastManager for Broadcasts that are private to your application.
Avoid putting any long-running tasks in the onReceive() method, instead offload the work to
a Service.
9. Resources
9.0.1. Android Developer Documentation
Guides
Reference
BroadcastReceiver
In this practical you'll create an app that triggers a notification when a button is pressed, and
contains the ability to update and cancel your notification.
1. In your activity_main.xml file, change the rootview element to a vertical LinearLayout with
it's gravity attribute set to "center".
2. Add a button with the following attributes:
Attribute Value
android:id "@+id/notify"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:text "Notify Me!"
android:layout_margin "4dp"
android:onClick "sendNotification"
3. Press Alt + Enter on the onClick method value and implement the method in MainActivity.
Do the following:
1. Go to File > New > Vector Asset.
2. Click on Choose to select a material icon that you will use as the icon for your notification.
In this example, you can use the Android icon.
3. Rename the resource ic_android and click OK.
4. Open up the new drawable resource and change the color to white by changing the
fillColor attribute to "#FFFFFF".
5. In your MainActivity class, create two member variables to store the Notification Builder
and Manager:
6. Create a constant variable for the Notification id. Since there will be only one active
notification at a time, we can use the same id for all notifications:
12. Call notify() on the NotificationManager at the end of the sendNotification() method,
passing in the notification id and the notification:
mNotifyManager.notify(NOTIFICATION_ID, mNotifyBuilder.build());
13. Run your app. The "Notify me!" button now issues a notification, but it is missing some
essential features: there is no notification sound or vibration, clicking on the notification
does not do anything. Let's add some functionality to the notification.
Intents for notifications are very similar to the Intents you've been using throughout this course:
they can be explicit intents to launch an activity, implicit intents to perform an action, or
broadcast intents to notify the system of something. The major difference is that they must be
wrapped in a PendingIntent, which allows the notification to perform the action even if you
application is not running. In effect, it authorizes the notification to send the intent on the
application's behalf.
A PendingIntent is created by calling one of the following methods, depending on the type of
intent it is meant to contain:
getActivity() if the intent has a specific target (it can be both explicit or implicit).
getBroadcast() if it contains a broadcast intent.
getService() if the intent is meant to start a Service.
For this example, the content intent of the notification (that is, the one that is launched
when the notification is pressed) will simply launch the MainActivity of the application.
6. Get a PendingIntent using getActivity(), passing in the notification id constant for the
requestCode and using the FLAG_UPDATE_CURRENT flag:
.setContentIntent(notificationPendingIntent)
Priority is in an integer value from PRIORITY_MIN (-2) to PRIORITY_MAX (2) that represents
how important your notification is to the user. Notifications with a higher priority will be sorted
above lower priority ones in the notification drawer. Furthermore, HIGH or MAX priority
notifications will be delivered as "Heads - Up" Notifications, which drop down on top of the
user's active screen.
Add the following line to the Notification Builder to set the priority of the notification to HIGH:
.setPriority(NotificationCompat.PRIORITY_HIGH)
The defaults option in the Builder is used to set the sounds, vibration, and LED color pattern for
your notification (if the user's device has a LED indicator). In this example, we will use the
default options by adding the following line to your Builder:
.setDefaults(NotificationCompat.DEFAULT_ALL)
6. Task 2. Update and Cancel your Notification
After issuing a notification, it is useful to be able to update or cancel the notification if the
information changes or becomes no longer relevant. Android also allows you to add actions to
your notifications that can send other Intents than the one you used as your "Content Intent". In
this task, you will learn how to update and cancel your notification, as well as include
notification actions.
When adding update and cancel functionality, it is important not to confuse the user and disable
any functionality that doesn't make sense in a given context. For example, the update and
cancel notification buttons should not be enabled if the notification is has not been delivered. Do
the following:
1. In your layout file, create two copies of the "Notify Me!" button.
2. Change the text attribute in the copies to "Update Me!" and "Cancel Me!".
3. Change the id's to "update" and "cancel", respectively.
4. Change the onClick attributes to updateNotification, and cancelNotification.
5. Create method stubs for both of these methods.
6. Add a member variable for each of the three buttons and initialize them in onCreate().
It is time to set up the logic for enabling and disabling the various buttons depending on the
state of notification. When the app is first run, the "Notify Me!" button should be the only one
enabled as there is no notification yet to update or cancel. After a notification is sent, the cancel
and update buttons should be enabled, and the notification button should disabled since it has
already been delivered. After the notification is updated, the update and notify buttons should
be disabled, leaving only the cancel button enabled. Finally, if the notification is cancelled, the
buttons should return to the initial condition with the notify button being the only one enabled.
Here is the enabled state toggle code for each method:
onCreate():
mNotifyButton.setEnabled(true);
mUpdateButton.setEnabled(false);
mCancelButton.setEnabled(false);
sendNotification():
mNotifyButton.setEnabled(false);
mUpdateButton.setEnabled(true);
mCancelButton.setEnabled(true);
updateNotification():
mNotifyButton.setEnabled(false);
mUpdateButton.setEnabled(false);
mCancelButton.setEnabled(true);
cancelNotification():
mNotifyButton.setEnabled(true);
mUpdateButton.setEnabled(false);
mCancelButton.setEnabled(false);
mNotifyManager.cancel(NOTIFICATION_ID);
Updating a notification is a more involved topic. As your application accumulates information for
the user, an amateur developer may continue to issue new notifications, crowding the
notification tray. It is far better practice to update the existing notification, and to be as compact
as possible.
Android notifications come with alternative styles that can help condense information or
represent it differently. For example, the Gmail app uses "InboxStyle" notifications if there is
more than a single unread message, condensing the information into a single notification.
In this example, you will update your notification to use the "BigPictureStyle" notification, which
allows you to include an image in your notification. Do the following:
1. Find an image you want to put in your notification. If you can't think of any, visit Androidify
and make yourself and avatar. Then download the gif and convert it to a .png file.
2. Put this image in the res/drawables folder.
3. In your updateNotification() method, convert your Drawable into a Bitmap:
Bitmap androidImage =
BitmapFactory.decodeResource(getResources(),R.drawable.mascot_1);
4. Change the style of your notification using the same Builder as before, setting the image
and the "Big Content Title":
mNotifyBuilder
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(androidImage)
.setBigContentTitle("Notification Updated!"));
5. Change the priority to the default, so that you don't get another heads up notification when
it is updated (heads up notifications can only be shown in the default style).
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
6. Call notify() on the Notification Manager, passing in the same notification id as before.
mNotifyManager.notify(NOTIFICATION_ID, mNotifyBuilder.build());
7. Run your app. After clicking update, check the notification again. It now has the image an
updated title! You can shrink back to the regular notification style by pinching on the
extended one.
7. Task 3. Notification Actions
Sometimes, a notification requires immediate action: Snoozing an alarm, replying to a text
message, etc. You could make the actions available in your app, and the user could tap your
notification to arrive at the proper activity, but this takes them out of what they were doing and
requires the system to load the activity and perhaps functionality that the user does not need.
The notification framework let's you embed actions directly in the notification, allowing the user
to act on the notification without opening your application.
For this example, you will add two actions to your notification: a "Learn More" action with an
implicit intent, and a "Update" action with a broadcast intent.
1. Create a member String variable that contains the URL to the Notification design guide.
2. In the sendNotification() method, create an implicit Intent that opens the saved URL.
3. Create a PendingIntent from the implicit intent, using the flag FLAG_ONE_SHOT so that
the PendingIntent cannot be reused:
4. Download this vector icon using the Vector Asset Studio, and call it icmore: <img
src="../images/1._images/ic_learn_more.png" alt="Learn More Icon" title="Learn More
Icon">
5. Change the color of the icon to white.
6. Add the following line of code to your builder to add the action:
7. Run your app. You notification will now have a clickable icon that takes you to the web!
You will now implement a broadcast receiver that will run the updateNotification() method when
the "Update" action in the notification is pressed. This is a common pattern: adding functionality
to a notification that already exists in the app, so that the user does not need to launch the app
to perform the action. Do the following:
public NotificationReceiver() {
}
@Override
public void onReceive(Context context, Intent intent) {
}
}
2. In the onReceive() method, call updateNotification(), passing in null as the argument (since
the View parameter is not used).
3. Create a constant member variable in MainActivity to represent the update notification
action for your broadcast intent. Make sure it begins with your package name to insure it's
uniqueness:
4. Create a member variable for you receiver and initialize it using the empty constructor in
onCreate().
5. In the onCreate() method, register your broadcast receiver:
registerReceiver(mReceiver,new IntentFilter(ACTION_UPDATE_NOTIFICATION));
@Override
protected void onDestroy() {
unregisterReceiver(mReceiver);
super.onDestroy();
}
Note: In this example you are registering your broadcast receiver programmatically
because your receiver is defined as a non-static, inner class. Receivers defined this way
cannot be registered in the Manifest since they have the possibility of changing.
Although at first it may seem that the broadcast sent by the notification only concern your app,
and therefore should be delivered with a LocalBroadcastManager, the use of PendingIntents
delegates the responsibility of delivering the notification to the Android framework, and
therefore LocalBroadcastManager can not be used. </div>
1. Create a broadcast Intent in the sendNotification() method using the custom update action.
2. Get a PendingIntent using getBroadcast():
3. Download this vector icon using the Vector Asset Studio, call it icupdate, and make it
white. <img class="center" src="../images/1._images/No Source Provided" alt="No Title
Provided" title="No Title Provided">
4. Add the action to the builder in the sendNotification() method, giving it the title "Update":
5. It does not make sense to include the update action in the already updated notification.
Modify the actions in the updateNotification() method to only show the "Learn More" action:
mNotifyBuilder.mActions.remove(1);
6. Run your app. You can now update your notification without opening the app!
8. Coding challenge
Note: All coding challenges are optional.
Enabling and Disabling the various buttons provides an intuitive user experience, without the
possibility of trying to update a notification before one exists. However, there is one use case in
which the state of your buttons does not match the state of the notification: if a user dismisses
the notification, by swiping it away or clearing the whole notification drawer. In this case, your
app has no way of knowing that the notification was cancelled, and that the button state must
be changed.
Create another broadcast intent that will let the application know that the user has dismissed
the notification, and toggle the button states accordingly.
Hint: Check out the NotificationCompat.Builder class for a method that delivers an Intent when
the notification has been dismissed by the user.
9. Conclusion
Notifications provide an interface between your app and the user, even when the app is not
running.
Required:
Optional:
Notifications
Notification Design Guide
Reference
NotificationCompat.Builder
NotificationCompat.Style
12.3 P: Notifications
Waking the device up at a specific time has it's drawbacks: the framework can not compensate
by waiting for the appropriate network connection or battery status and therefore can be
resource intensive. It is not recommended to use AlarmManager unless the specific timing of
your process is important.
That being said, AlarmManager includes some methods that help alleviate the inefficiency of
specific timing: it is able to batch tasks together when the exact timing is not important. It is
crucial for the developer to use the least precise timing possible in order to make the
AlarmManager the most efficient it can be.
For most tasks, such as updating the weather information or news stories, it can weight until
conditions are more favorable, such as being connected to wifi and charging. For these cases,
the JobScheduler should be used, which you will learn about in the following lesson.
In this practical, you will create a parking meter timer that will use an AlarmManager to
schedule a notification when your timer is about to run out.
1. In your activity_main.xml file, change the rootview element to a vertical LinearLayout with
it's gravity attribute set to "center".
2. Add a Spinner with the following attributes:
Attribute Value
android:id "@+id/durationSpinner"
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:layout_margin "8dp"
5. Set the text of the first button to"Set Parking Timer", the onClick attribute to "setAlarm",
and the id to "setAlarmButton".
6. Set the text of the second button to "Cancel Alarm", the onClick to "cancelAlarm", and the
id to "cancelAlarmButton".
7. Implement both of the methods in MainActivity.
1. In your string.xml file, define a string array resource with the following string items:
i. 5 minutes
ii. 15 minutes
iii. 30 minutes
iv. 1 hour
v. 2 hours
vi. 4 hours
2. In MainActivity, initialize the Spinner view in onCreate().
3. Create an ArrayAdapter of parameterized type CharSequence, using the
createFromResource() method, passing in the application context, the string array
resource and the simple_spinner_item layout:
4. Set the dropdown view to the default dropdown line items by calling
setDropDownViewResource() and passing in the simple_spinner_dropdown_item:
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
durationSpinner.setAdapter(adapter);
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
switch (selection){
case "5 minutes":
mSelectedDuration = TimeUnit.MINUTES.toMillis(5);
break;
case "15 minutes":
mSelectedDuration = TimeUnit.MINUTES.toMillis(15);
break;
case "30 minutes":
mSelectedDuration = TimeUnit.MINUTES.toMillis(30);
break;
case "1 hour":
mSelectedDuration = TimeUnit.HOURS.toMillis(1);
break;
case "2 hours":
mSelectedDuration = TimeUnit.HOURS.toMillis(2);
break;
case "4 hours":
mSelectedDuration = TimeUnit.HOURS.toMillis(4);
break;
}
6. Task 2. Set up your notifications
It is now time to think through the logic of the application more thoroughly: the user sets a timer,
a low priority notification appears containing the remaining time on the parking meter and an
action to cancel the alarm. Just before the timer expires, your app will issue another, high
priority notification to let the user know they are out of time.
AlarmManager, like notifications, uses a PendingIntent that it delivers with the specified options.
From the logic of the application, you can determine that you will need the following Intents:
A content intent for the notifications, leading to the MainActivity when the notification is
clicked.
A cancel broadcast intent, sent when the cancel action in the notification is clicked.
An alarm pending broadcast intent that updates the alarm pending notification with the
remaining time periodically.
A final alarm broadcast intent that cancels the alarm pending broadcast and issues the final
notification sent right before the parking timer expires.
The broadcast intents will be received in a broadcast receiver which will take the appropriate
action (sending notifications or canceling the alarm) based on the Intent it receives.
Your broadcast receiver will be responsible for responding to all of the broadcast intents sent
by the AlarmManager and notifications. The three actions this represents are:
Do the following:
4. In the onReceive() method, get the action from the incoming intent and create a switch
statement on the action.
5. Create a case for each of the three possible actions.
6. Create the following methods in MainActivity that will issue your notifications:
Note: Auto Cancel is a parameter that determines whether a notification is dismissed when
it is clicked (the content intent is activated). It is set using the setAutoCancel() method.
9. Call notify() on the NotificationManager, using the notification id constant and the builder
you just created.
For the pending notification, it is a little more complicated. Firstly, a PendingIntent that cancels
the notification and alarm is needed for the notification action. Secondly, the content text of the
pending notification needs to be updated with the remaining time, which needs to be calculated
each time notifyPending() is called. Do the following to set up the pending notification in the
notifyPending() method:
4. Create an intent with the ACTION_CANCEL as it's action. This will be delivered when the
cancel action inside the notification is clicked.
5. Create a PendingIntent from the cancel intent using the getBroadcast() method, using the
ONE_SHOT flag since this PendingIntent won't be reused.
6. Create a string variable for the pending notification content text:
Note: There is a minute added to the pending notification text since the seconds are not
counted and 4:59 would be approximated as 4 instead of 5.
7. Download an icon to be used for the cancel action. The clear action is usually represented
by the cross icon. Change the color of the icon to white.
8. Use the builder to create a new notification with the following components:
Content Title "Parking Alert!"
Content Text notificationString
Priority NotificationCompat.PRIORITY_LOW
Ongoing true
Small Icon R.drawable.ic_car
Action R.drawable.ic_clear, "Cancel", cancelPendingIntent
Note: Ongoing notifications cannot be dismissed by the user and remain in the notification
tray. This feature is useful when there is an ongoing process (in this case, a parking timer)
that the user may want to constantly check on until it is finished.
9. Call notify() on the NotificationManager.
7. Task 3. Set up your alarms
Now that your user notifications are prepared, it is time to get the main component of your
application: the AlarmManager. This is the class that will be used to deliver your notification
broadcast intents (both the ongoing and final one) as well as periodically update the ongoing
notification. AlarmManager has many kinds of alarms built into it, both one-time and periodic,
exact and inexact. To learn more about the different kinds of alarms, look into this guide.
Both of these alarms are set using the AlarmManager class. Create a member variable for the
alarm manager class and initialize it in onCreate():
mAlarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
The single alarm will be set using the appropriately named set() method. On devices that are
pre API 19, this will create an exact alarm that will be triggered at the specified time (either
based on the real-time clock, or the elapsed time since the last boot). After API 19, these
alarms are inexact in order to optimize the resources needed, potentially batching alarms
together. For this reason, you will use the setWindow() method instead for devices running API
19+, allowing the system to better batch the alarms together.
3. Create an if block that checks if the API level of the device is greater than API level 19
(Kitkat):
The alarm type, which can be either real-time, or elapsed time since the boot. There is
also a wakeup version of both of these alarms that are able to wake up the device if is
locked. For this example you will use a real-time wakeup alarm.
The start time of the window, which you will set to be one minute before the parking
expiration time.
The duration of the window, which will be 30 seconds.
The pending intent to deliver, in this case the final alarm broadcast intent.
Call the setWindow() method on the AlarmManager if the device is running API 19+, otherwise
call set() with the following arguments:
The second alarm is a repeating alarm, which is meant to update the remaining time in the
pending notification. It is not important that the alarm be exact, since the alarm will be set
to repeat every minute, and a few seconds of difference is acceptable for the pending
alarm (but the final notification will always be on time). The second alarm will therefore use
the setInexactRepeating() method. Do the following:
Call setInexactRepeating() on the AlarmManager, starting the repeating alarm at the time
the setButton() is pushed:
mAlarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, currentTime,
TimeUnit.MINUTES.toMillis(1), mAlarmPendingIntent);
mAlarmManager.cancel(mAlarmPendingIntent);
2. Fill in each case statement in the onReceive method, calling the appropriate method
depending on the incoming action:
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_NOTIFY_PENDING:
notifyPending();
break;
case ACTION_NOTIFY_FINAL:
notifyFinal();
break;
case ACTION_CANCEL:
cancelAlarm(null);
break;
}
}
When the alarm is set, the ongoing notification does not show up immediately. This is
because the alarm used to post this notification uses inexact timing (you called
setInexactRepeating) and although it is meant to start immediately, in practice the system
may wait to deliver your broadcast. For this reason, you should trigger the notification
immediately when the setAlarm() button is pressed, allowing the alarm to simply update
the time left in the notification where the timing is not as important:
To the user, there is no feedback when the alarm is set or when the alarm is canceled. The
buttons can be pressed any number of times, and it is not clear whether this creates
multiple alarms. In order to clarify this behavior for the user, as well as limit the application
to a single parking timer at a time, you can toggle the enabled state of the buttons. Do the
following:
1. In the activity_main.xml file, disable both of the buttons.
2. Enable the "Set Parking Timer" button once a duration is selected from the spinner.
3. Once the alarm is set, disable the "Set Parking Timer" button and enable the "Cancel
Alarm" button.
4. In the cancelAlarm() method, reset the state of the buttons: enabled for the "Set
Parking Timer" button and disabled for the "Cancel Alarm" button.
8. Coding challenge
Note: All coding challenges are optional.
The AlarmManager class also handles alarm clocks in the usual sense, the kind that wake you
up in the morning. On devices running API 21+, you can get information about the next alarm
clock of this kind by calling getNextAlarmClock() on the alarm manager.
Add a button to your application that displays the time of next alarm clock that the user has set
in a toast message.
9. Conclusion
AlarmManager allows you to manage the timing of an intent by using one of its set() methods. It
allows for these intents to be delivered using the PendingIntent framework, and can be set to
rely on the real time clock or else the elapsed time since boot. This flexibility can be dangerous,
as precise timing is resource heavy and is prone to draining battery. You should therefore use
the built-in inexact timing mechanism, whereby the alarm manager can batch tasks together for
greater efficiency.
10. Resources
10.0.1. Android Developer Documentation
Guides
Reference
AlarmManager
The JobScheduler class is meant precisely for this kind of scheduling: it allows you to set the
parameters of your task, and calculates the best time to schedule it. The task to be run is
implemented in a JobService class, and executed according to the added constraints.
JobScheduler is only available on devices running API 21+, and is currently not available in the
support library. For backward compatibility, use the GCMNetworkManager (soon to be
FirebaseJobDispatcher <UPCOMING_CHANGE = "FIREBASE_JOBDISPATCHER>").
In this practical, you will create an app which schedules a notification to be posted when the
user set parameters are fulfilled, and the system requirements are met.
SCREENSHOT
5. Task 1. Implement a JobService
To begin with, you must create a service that will be run at the time determined by the
constraints. The JobService is automatically executed by the system, and the only parts you
need to implement are:
The onStartJob() callback, which is called when the system determines that your task
should be run. This is where the job to be done is implemented.
Note: onStartJob() is executed on the main thread, and therefore any long-running tasks must
be offloaded to a different thread. In this case, you are simply posting a notification, which can
be done on the main thread.
The onStopJob(), which is called if the constraints are no longer met while your work is
being performed, meaning that the job must be stopped.
Both of these callbacks return boolean values that determine their behavior.
For onStartJob(), the boolean indicates whether the job is fully completed in the
onStartJob() callback. If true, the work is offloaded to a different thread, and you must call
jobFinished() to indicate that the job is complete. If false, the framework knows that the job
is completed by the end of onStartJob() and it will automatically call jobFinished() on your
behalf.
For onStopJob(), the boolean determines the retry policy in the case where the job cannot
finish due to changing constraints. If true, the job will be rescheduled, otherwise, it will be
dropped.
1. Create a new Java class called NotificationJobService and make it extend JobService.
2. Implement the required methods: onStartJob() and onStopJob().
3. Navigate to your AndroidManfiest.xml file and register your JobService with the following
permission:
<service
android:name=".NotificationJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
5. Make sure onStartJob() return false, since all of the work is completed in that callback;
6. Make onStopJob() return true, so that the job is rescheduled if it fails.
6. Task 2. Implement the constraints
Now that you have your JobService, it is time to determine the constraints the system will use
to schedule it. To begin, will create a group of radio buttons to determine the network type
required for this job.
One of the available constraints is a network required constraint: limiting the JobService to be
executed only when the network conditions are met. The options are:
Do the following:
3. Add a RadioGroup container element below the TextView with the following attributes:
Attribute Value
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:orientation "horizontal"
android:id "@+id/networkOptions"
android:layout_margin "4dp"
Note: Using a radio group ensures that only one of its children can be selected at a time.
For more information on Radio Buttons see [this guide]
(https://developer.android.com/guide/topics/ui/controls/radiobutton.html).
4. Add three RadioButtons as children to the RadioGroup with their layout height and width
set to "wrap_content" and the following text and id's:
RadioButton 1
android:text "Default"
android:id "@+id/defaultNetwork"
RadioButton 2
android:text "Any"
android:id "@+id/anyNetwork"
RadioButton 3
android:text "Wifi"
android:id "@+id/wifiNetwork"
5. Add two buttons below the radio button group with height and width set to "wrap content"
with the following attributes:
Button 1
android:text "Schedule Job"
android:onClick "scheduleJob"
android:layout_gravity "center_horizontal"
android:layout_margin "4dp"
Button 2
android:text "Cancel Jobs"
android:onClick "cancelJobs"
android:layout_gravity "center_horizontal"
android:layout_margin "4dp"
.setRequiredNetworkType(networkOptions.getCheckedRadioButtonId());
12. Call schedule() on the JobScheduler object, passing in the JobInfo object with the build()
method:
mScheduler.schedule(builder.build());
13. Show a Toast message, letting the user know the job was scheduled.
14. In the cancelJobs() method, check if the JobScheduler object is null, and if not, call
cancelAll() on it to remove all pending jobs, reset the JobScheduler to be null, and show a
Toast message to let the user know the job was canceled:
if (mScheduler!=null){
mScheduler.cancelAll();
mScheduler = null;
Toast.makeText(MainActivity.this, "Jobs Canceled", Toast.LENGTH_SHORT).show();
}
15. Run the app, you can now set tasks with network restrictions and see how long it takes for
them to be executed.
6.1. 2.2 Implement the Device Idle and Device Charging
constraints
JobScheduler includes the ability to wait until the device is charging, or in an idle state (screen
off, CPU gone to sleep) to execute your JobService. You will now add switches to your app to
toggle these constraints on your JobService. Do the following:
1. In your activity_main.xml file, copy the network type label TextView and paste it below the
RadioGroup.
2. Change the android:text attribute to "Requires:".
3. Below this textview, insert a horizontal LinearLayout with a 4dp margin .
4. Create two Switch views as children to the horizontal LinearLayout with height and width
set to "wrap_content" and the following attributes:
Switch 1
android:text "Device Idle"
android:id "@+id/idleSwitch"
Switches 2
android:text "Device Charging"
android:id "@+id/chargingSwitch"
5. Create member variables for the switches and initialize them in onCreate().
6. In the scheduleJob() method, add the following methods to the JobInfo.Builder to set the
constraints on the JobScheduler based on the user selection in the switches:
.setRequiresDeviceIdle(mDeviceIdle.isChecked())
.setRequiresCharging(mDeviceCharging.isChecked())
7. Run your app, now with the additional constraints. Waiting until the device is idle and
plugged in is a common pattern for battery intensive tasks such as downloading or
uploading large files.
In this step you will implement a Seekbar that will allow the user to set a deadline between 0
and 100 seconds to execute your task. Do the following:
1. Copy the horizontal LinearLayout of switches and paste it directly below the first one .
2. The SeekBar will have two labels: a static one just like label for RadioGroup of buttons,
and dynamic one that will be updated with the value from the SeekBar. Change the Switch
views to TextViews and modify the attributes it the following way:
TextView 1
android:text "Override Deadline: "
android:id "@+id/overrideLabel"
android:textAppearance "@style/TextAppearance.AppCompat.Subhead"
TextView 2
android:text "Not Set"
android:id "@+id/seekBarProgress"
android:textAppearance "@style/TextAppearance.AppCompat.Subhead"
3. Add a SeekBar view below the LinearLayout with the following attributes:
Attribute Value
android:layout_height "wrap_content"
android:layout_width "match_parent"
android:id "@+id/overrideSeekBar"
android:layout_margin "4dp"
4. Create member variables for both the SeekBar and the TextViews, and initialize them in
onCreate().
5. Call setOnSeekBarChangeLinstener() on the SeekBar, passing in a new
OnSeekBarChangeListener (Android Studio should generate the required methods):
mOverrideSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
6. In the onProgressChanged() callback, check if the integer value is greater than 0, and if it
is, set the SeekBar progress label to the integer value and the seconds unit:
if (i > 0){
mSeekBarProgress.setText(String.valueOf(i) + " s");
}
else {
mSeekBarProgress.setText("Not Set");
}
8. The override deadline should only be set if the integer value of the SeekBar is greater than
0. In the scheduleJob() method, create a boolean variable that is true if the SeekBar has
an integer value greater than 0:
10. Run the app. The user can now set a hard deadline by which time the JobService must be
run!
The task is not guaranteed to run in the given period (the other conditions may not be met,
or there might not be enough system resources).
Using this constraints prevents you from also setting an override deadline or a minimum
latency (see JobInfo.Builder) documentation for information on these methods), since these
options do not make sense for repetitive tasks.
Do the following:
1. In activity_main.xml, add a Switch view between the two horizontal LinearLayouts. Use the
following attributes:
Attribute Value
android:layout_height "wrap_content"
android:layout_width "wrap_content"
android:text "Periodic"
android:id "@+id/periodicSwitch"
android:layout_margin "4dp"
Because the override deadline and periodic constraints are mutually exclusive, you will use
the switch to toggle the functionality and label of the SeekBar to represent either the
override deadline, or the periodic interval, respectively.
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
if (b){
mLabel.setText("Periodic Interval: ");
} else {
mLabel.setText("Override Deadline: ");
}
}
All that remains now is to implement the logic in the scheduleJob() method to properly set
the constraints on the JobInfo object. This is how the application stands:
5. If the periodic option is turned on and the SeekBar has a nonzero value, you can set the
constraint by calling setPeriodic() on the JobInfo.Builder object,
6. If the periodic option is turned on but the SeekBar has a value of 0, you should show a
Toast message asking the user to set a periodic interval with the SeekBar.
7. If the periodic option is turned off and the SeekBar has a nonzero value, the user has set
an override deadline which must applied using the setOverrideDeadline() option.
8. If the periodic option is turned off and and the SeekBar has a value of 0, the user has
simply not specified an override deadline or a periodic task, so nothing should be added to
the JobInfo.Builder object.
if (mPeriodic.isChecked()){
if (seekBarSet){
builder.setPeriodic(mSeekBar.getProgress()*1000);
} else {
Toast.makeText(MainActivity.this, "Please set a periodic interval",
Toast.LENGTH_SHORT).show();
}
} else {
if (seekBarSet){
builder.setOverrideDeadline(mSeekBar.getProgress()*1000);
}
}
7. Coding challenge
Note: All coding challenges are optional.
In this example, the JobService that was scheduled based on the constraints was simple: it
delivered a notification. Most of the time, however, JobScheduler is used for more robust
background tasks such as updating the weather or syncing with a database. These kinds of
tasks require a more thought since the burden falls on the developer to off - load the task as
well as notify the framework when it is complete by calling jobFinished().
Implement a JobService that starts an AsyncTask when the given constraints are met. The
AsyncTask should sleep the thread for 5 seconds. This will require you to call jobFinished()
once the task is complete. If the constraints are no longer met while the thread is sleeping,
show a Toast message saying that the job failed and reschedule the job.
8. Conclusion
JobScheduler provides a flexible framework to accomplish intelligent background services. It
has three major components:
JobScheduler batches tasks together to maximize the efficiency of system resources, so you
do not have exact control of when it will be executed.
9. Resources
9.0.1. Android Developer Documentation
Reference
JobScheduler
JobInfo
JobInfo.Builder
JobService
JobParameters
Challenge
Solution
Resources
Challenge: Creating Widgets
Start with the HelloSharedPrefs app from a previous chapter, and add a widget to that app.
Use New > Widget > App Widget in Android Studio as your starting point. Call your widget
HelloAppWidget, and give it 1 x 1 cell dimensions. Do not include a configuration screen.
Modify the default widget Android Studio generates to display the current count from
HelloSharedPrefs, and to use the current background color.
Update the widget when either the count or the color are changed in the HelloSharedPrefs
app.
When the widget is tapped, update the count for both the widget and the app.
Replace the default preview image with one that reflects the actual widget.
1. Solution
LINK TO CODE
2. Resources
App Widgets
App Widget Design Guidelines
AppWidgetProvider
AppWidgetProviderInfo
1. Solution
2. Resources
Appendix Utilities
1. Compare Custom Objects
Whenever your data model calls for objects to be sorted, it becomes necessary to define how
these objects can be compared to each other. Do they have some kind of member variable that
represents their rank? There are many reasons that you would need to compare your objects,
and the Comparable interface allows you to do just that. The Comparable interface requires
that you implement a single method: compareTo(<T> another) where <T> is the parameterized
type you implemented Comparable with, and the type of object you are comparing to (i.e if you
want to compare your Foobar instance to other Foobar instances, you would implement
Comparable<Foobar> and your compareTo method would take Foobar as a parameter). The
1. Start Android Studio, and click Open an existing Android Studio project. Navigate to the
NewProject directory, select it, and click OK.
2. Select Build > Clean Project to remove the auto-generated files.
3. Click the 1:Project side-tab to see your files in the Project view.
4. Expand app > java, select the com.example.android.existingproject folder, and choose
Refactor > Rename.
5. Click Rename Package.
6. Change existingproject to newproject.
7. Check the Search in comments and strings and Search for text occurrences options,
and then click Refactor. The Find Refactoring Preview pane appears, showing code to be
refactored.
8. Click Do Refactor.
9. Expand res > values and double-click the strings.xml file.
10. Change the name=app_name string to New Project.
In addition, some apps include the app name in readable form (such as New Project rather
than newproject) as a label in the AndroidManifest.xml file. Follow these steps to check for and,
if necessary, change the label:
android:label="@string/app_name"
3. Delete a project
All the files for an Android project are contained in the project's folder on the computer's file
system. To delete a project you can just delete the folder.
However, Android Studio also keeps a list of recent projects that you have opened, which is
different from the project folders on the file system. You can delete a project from the list of
recent projects in Android Studio, but the project files will still remain on the computer's
filesystem. Conversely if you delete a project folder on the filesystem and then try to open it
from the project list from within AS, you'll get a "can't find that folder" error.
1. Delete the folder from the filesystem by moving it to the trash or using rm -rf in the shell.
2. On the initial Android Studio screen, click the name of the project and press delete. -OR-
Select File > Open Recent > Manage Projects, click the name of the project and press delete.
4. Extract Resources
4.1. 1. Extract Strings
In order for your app to be localizable into multiple languages, it is best practice to keep all of
you string resources in the same place: your strings.xml file.
Add them manually in the strings.xml file using the following syntax:
Wherever the string will be used, i.e. the text attribute of a TextView, type in the desired
name for a string resource in the following format: @string/string_name. It will be
highlighted in red since the resource does not yet exist. Make sure your cursor is in the
highlighted text and press Alt + Enter and select Create string value resource. Enter
your desired string and press OK. That's it! The string gets automatically added to your
strings.xml file.
You can also select any existing, hard-coded string in either XML or Java and press Alt +
Enter, and select Extract string resource.
In XML, you should access the string resource using the following syntax:
@string/string_name.
1. Place your cursor in the view for which you want to extract the attributes.
2. Right click and select Refactor > Extract > Style.
3. Name the style, and select the desired attributes. If the Launch 'Use Style WHere
Possible' refactoring after the sylte is extracted is checked, Android Studio will search
the rest of the file for the selected attributes, and apply the style to views where the
attributes match.
4. Click OK. That's it!
5. Save State of Custom Objects
In Android, you will frequently create Custom objects to represent your particular Data Model.
In order to preserve the state of these objects, you must be able to pass them into the
savedInstanceState bundle. In order to do so, your custom class must implement the
Parcelable interface. This allows for primitive types (int, string, byte, etc) to be saved in the
savedInstanceState callback. Do the following:
1. After setting up the data in your custom class (only the primitive data types will be saved),
add the Parcelable implementation to your class declaration:
2. The declaration will be underlined in red, since you have to implement the interface
methods. With your cursor on the underlined text, press Alt + Enter and select Implement
methods.
3. Choose both describeContents() and writeToParcel(Parcel dest, int flags) . Click OK.
4. The class name will still be underlined, indicating that the interface is not fully implemented
yet. Select the class name, and again press Alt + Enter and choose Add Parcelable
implementation. Android studio will automatically add the required code. Note the
variables for which you want to preserve the state (primitive types) are written to the
Parcel in the writeToParcel method.
5. You can now save the state of these objects using the savedInstanceState bundles
methods: putParcelable, putParcelableArray, and putParcelableArrayList and the
respective getters.
Appendix Utilities