Create a more polished user experience

1. Before you begin

As you've learned in earlier codelabs, Material is a design system created by Google with guidelines, components, and tools that support the best practices of user interface design. In this codelab, you will update the tip calculator app (from previous codelabs) to have a more polished user experience, as seen in the final screenshot below. You'll also test the app in some additional scenarios to ensure the user experience is as smooth as possible.

5743ac5ee2493d7.png

Prerequisites

  • Familiar with common UI widgets such as TextView, ImageView, Button, EditText, RadioButton, RadioGroup, and Switch
  • Familiar with ConstraintLayout and positioning child views by setting constraints
  • Comfortable with modifying XML layouts
  • Aware of the difference between bitmap images and vector drawables
  • Can set theme attributes in a theme
  • Able to turn on Dark theme on a device
  • Have previously modified the app's build.gradle file for project dependencies

What you'll learn

  • How to use Material Design Components in your app
  • How to use Material icons in your app by importing them from Image Asset Studio
  • How to create and apply new styles
  • How to set other theme attributes aside from color

What you'll build

  • A polished tip calculator app that follows recommended UI best practices

What you need

  • A computer with the latest stable version of Android Studio installed
  • Code for the Tip Time app from completing the previous codelabs from this pathway and this pathway

2. Starter app overview

Through prior codelabs, you built up the Tip Time app which is a tip calculator app with options to customize the tip. Your app UI currently looks like the below screenshot. The functionality works, but it looks more like a prototype. The fields aren't quite lined up visually. There's definitely room for improvement in terms of more consistent styling and spacing, as well as the use of Material Design Components.

6685eaafba30960a.png

3. Material Components

Material Components are common UI widgets that make it easier to implement Material styling in your app. The documentation provides information for how to use and customize the Material Design Components. There are general Material Design guidelines for each component, and Android platform-specific guidance for the components that are available on Android. The labeled diagrams give you enough information to recreate a component if it happens to not exist on your chosen platform.

c4a4db857bb36c3f.png

By using Material Components, your app will operate in a more consistent way alongside other apps on the user's device. That way the UI patterns learned in one app can be carried over to the next one. Hence users will be able to learn how to use your app much faster. It's recommended to use Material Components whenever possible (as opposed to the non-Material widgets). They are also more flexible and customizable, as you will learn in this next task.

The Material Design Components (MDC) library needs to be included as a dependency in your project. This line should already be present in your project by default. In your app's build.gradle file, make sure this dependency is included with the latest version of the library. For more details, see the Get started page on the Material site.

app/build.gradle

dependencies {
    ...
    implementation 'com.google.android.material:material:<version>'
}

Text Fields

In your tip calculator app, at the top of the layout, you currently have an EditText field for the cost of service. This EditText field works, but it doesn't follow the recent Material Design guidelines on how text fields should look and behave.

For any new component that you want to use, start by learning about it on the Material site. From the guide on Text Fields, there are two types of text fields:

Filled text field

29fab63417a5e9ed.png

Outlined text field

3f085f837e146150.png

To create a text field as shown above, use a TextInputLayout with an enclosed TextInputEditText from the MDC library. The Material text field can be easily customized to:

  • Display input text or a label that's always visible
  • Display an icon in the text field
  • Display helper or error messages

In the first task of this codelab, you'll be replacing the cost of service EditText with a Material text field (which is composed of a TextInputLayout and TextInputEditText).

  1. With the Tip Time app open in Android Studio, go to the activity_main.xml layout file. It should contain a ConstraintLayout with the tip calculator layout.
  2. To see an example of what the XML looks like for a Material text field, go back to the Android guidance for Text fields. You should see snippets like this one:
<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/textField"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/label">

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
    />

</com.google.android.material.textfield.TextInputLayout>
  1. After seeing this example, insert a Material text field as the first child of the ConstraintLayout (before the EditText field). You'll get rid of the EditText field in a later step.

You can type this into Android Studio and use autocomplete to make it easier. Or you can copy the example XML from the documentation page and paste it into your layout like this. Notice how the TextInputLayout has a child view, the TextInputEditText. Remember that ellipsis (...) are used to abbreviate snippets, so that you can focus on the lines of XML that have actually changed.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    ...>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textField"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/label">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        />

    </com.google.android.material.textfield.TextInputLayout>

    <EditText
        android:id="@+id/cost_of_service" ... />

    ...

You are expected to see errors on the TextInputLayout element. You haven't properly constrained this view yet in the parent ConstraintLayout. Also the string resource isn't recognized. You'll fix these errors in the coming steps.

344a98d866c7f68c.png

  1. Add vertical and horizontal constraints onto the text field, to properly position it within the parent ConstraintLayout. Since you haven't deleted the EditText yet, cut and paste the following attributes from the EditText and place them onto the TextInputLayout: the constraints, resource ID cost_of_service, layout width of 160dp, layout height of wrap_content, and the hint text @string/cost_of_service.
...

<com.google.android.material.textfield.TextInputLayout
   android:id="@+id/cost_of_service"
   android:layout_width="160dp"
   android:layout_height="wrap_content"
   android:hint="@string/cost_of_service"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent">

   <com.google.android.material.textfield.TextInputEditText
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</com.google.android.material.textfield.TextInputLayout>

...

You may see an error that the cost_of_service ID is the same as the resource ID of the EditText, but you can ignore this error for now. (EditText will be removed in a couple steps).

  1. Next make sure the TextInputEditText element has all the appropriate attributes. Cut and paste over the input type from the EditText onto the TextInputEditText. Change the TextInputEditText element resource ID to cost_of_service_edit_text.
<com.google.android.material.textfield.TextInputLayout ... >

   <com.google.android.material.textfield.TextInputEditText
       android:id="@+id/cost_of_service_edit_text"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:inputType="numberDecimal" />

</com.google.android.material.textfield.TextInputLayout>

Width of match_parent and height of wrap_content is fine as-is. When setting a width of match_parent, the TextInputEditText will have the same width as its parent TextInputLayout which is 160dp.

  1. Now that you have copied over all relevant information from the EditText, go ahead and delete the EditText from the layout.
  2. In the Design view of your layout, you should see this preview. The cost of service field now looks like a Material text field.

28582b8e4103beeb.png

  1. You can't run the app yet because there's an error in your MainActivity.kt file in the calculateTip() method. Recall from an earlier codelab that with view binding enabled for your project, Android creates properties in a binding object based on the resource ID name. The field we retrieved the cost of service from has changed in the XML layout, so the Kotlin code needs to be updated accordingly.

You will now be retrieving the user input from the TextInputEditText element with resource ID cost_of_service_edit_text. In the MainActivity, use binding.costOfServiceEditText to access the text string stored within it. The rest of the calculateTip() method can stay the same.

private fun calculateTip() {
    // Get the decimal value from the cost of service text field
    val stringInTextField = binding.costOfServiceEditText.text.toString()
    val cost = stringInTextField.toDoubleOrNull()
    
    ...
}
  1. Great work! Now run the app and test that it still works. Notice how the "Cost of Service" label now appears above your input as you type. The tip should still calculate as expected.

b4a27e58f63417b7.png

Switches

In the Material Design guidelines, there's also guidance on switches. A switch is a widget where you can toggle a setting on or off.

  1. Check out the Android guidance for Material switches. You will learn about the SwitchMaterial widget (from the MDC library), which provides Material styling for switches. If you keep scrolling through the guide, you will see some examples of XML.
  2. To use SwitchMaterial, you must explicitly specify SwitchMaterial in your layout and use the fully qualified path name.

In the activity_main.xml layout, change the XML tag from Switch to com.google.android.material.switchmaterial.SwitchMaterial.

...

<com.google.android.material.switchmaterial.SwitchMaterial
    android:id="@+id/round_up_switch"
    android:layout_width="0dp"
    android:layout_height="wrap_content" ... />

...
  1. Run the app to verify it still compiles. There happens to be no visible change in the app. However, an advantage to using SwitchMaterial from the MDC library (instead of Switch from the Android platform), is that when the library's implementation for SwitchMaterial gets updated (e.g. the Material Design guidelines change), then you will get the updated widget for free without any changes required on your part. This helps future-proof your app.

At this point, you've seen two examples of how your UI can benefit from using out-of-the-box Material Design Components and how that brings your app closer in line with Material guidelines. Remember that you can always explore other Material Design Components provided on Android at this site.

4. Icons

Icons are symbols that can help users understand a user interface by visually communicating the intended function. They often take inspiration from objects in the physical world that a user is expected to have experienced. Icon design often reduces the level of detail to the minimum required to be familiar to a user. For example, a pencil in the physical world is used for writing so its icon counterpart usually indicates creating, adding, or editing an item.

Sharpened pencil with sharpener on open notebook. Photo by Angelina Litvin on Unsplash

Black and white pencil icon

Sometimes icons are linked to obsolete physical world objects as is the case with the floppy disk icon. This icon is the ubiquitous indication of saving a file or database record; however, while floppy disks were popularized in the 1970s, they ceased to be common after 2000. But its continual use today speaks to how a strong visual can transcend the lifetime of its physical form.

Floppy Disk lying flatPhoto by Vincent Botta on Unsplash

Icon of floppy disk

Representing icons in your app

For icons in your app, instead of providing different versions of a bitmap image for different screen densities, the recommended practice is to use vector drawables. Vector drawables are represented as XML files that store the instructions on how to create an image rather than saving the actual pixels that make up that image. Vector drawables can be scaled up or down without any loss of visual quality or increase in file size.

Provided Icons

Material Design provides a number of icons arranged in common categories for most of your needs. See list of icons.

bfdb896506790c69.png

These icons can also be drawn using one of five themes (Filled, Outlined, Rounded, Two-Tone, and Sharp) and can be tinted with colors.

Filled

Outlined

Rounded

Two-Tone

Sharp

Adding Icons

In this task, you'll add three vector drawable icons to the app:

  1. Icon next to the cost of service text field
  2. Icon next to the service question
  3. Icon next to the round up tip prompt

Below is a screenshot of the final version of the app. After you add the icons, you'll adjust the layout to accommodate the placement of these icons. Notice how the fields and calculate button get shifted over to the right, with the addition of the icons.

8c4225390dd1fb20.png

Add vector drawable assets

You can create these icons as vector drawables directly from Asset Studio in Android Studio.

  1. Open the Resource Manager tab located on the left of the application window.
  2. Click the + icon and select Vector Asset.

6dabda0f4bc1f6ed.png

  1. For the Asset Type, make sure the radio button labeled Clip Art is selected.

914786d2d8b4025.png

  1. Click the button next to Clip Art: to select a different clip art image. In the prompt that appears, type "call made" into the window that appears. You'll be using this arrow icon for the round up tip option. Select it and click OK.

e7f607e4f576d75c.png

  1. Rename the icon to ic_round_up. (It's recommended to use the prefix ic_ when naming your icon files.) You can leave the Size as 24 dp x 24 dp and Color as black 000000.
  2. Click Next.
  3. Accept the default directory location and click Finish.

200aed40ee987672.png

  1. Repeat steps 2 - 7 for the other two icons:
  • Service question icon: Search for "room service" icon, save it as ic_service.
  • Cost of service icon: Search for "store" icon, save it as ic_store.
  1. Once you're done, the Resource Manager will look like the below screenshot. You will also have these three vector drawables (ic_round_up, ic_service, and ic_store) listed in your res/drawable folder.

c2d8b22f0fb55ce0.png

Support older Android versions

You just added vector drawables to your app, but it's important to note that support for vector drawables on the Android platform wasn't added until Android 5.0 (API level 21).

Based on how you set up the project, the minimum SDK version for the Tip Time app is API 19. That means the app can run on Android devices that are running Android platform version 19 or higher.

To make your app work on these older versions of Android (known as backwards compatibility), add the vectorDrawables element to your app's build.gradle file. This enables you to use vector drawables on versions of the platform less than API 21, versus converting to PNGs when the project is built. See more details here.

app/build.gradle

android {
  defaultConfig {
    ...
    vectorDrawables.useSupportLibrary = true
   }
   ...
}

With your project configured properly, you can now move onto adding the icons into the layout.

Insert icons and position elements

You'll be using ImageViews to display icons in the app. This is how your final UI will appear.

5ed07dfeb648bd62.png

  1. Open the activity_main.xml layout.
  2. First position the store icon next to the cost of service text field. Insert a new ImageView as the first child of the ConstraintLayout, before the TextInputLayout.
<androidx.constraintlayout.widget.ConstraintLayout 
   ...>

   <ImageView
       android:layout_width=""
       android:layout_height=""
      
   <com.google.android.material.textfield.TextInputLayout
       android:id="@+id/cost_of_service"
       ...
  1. Set up the appropriate attributes on the ImageView to hold the ic_store icon. Set the ID to icon_cost_of_service. Set the app:srcCompat attribute to the drawable resource @drawable/ic_store, and you'll see a preview of the icon next to that line of XML.

Also set android:importantForAccessibility="no" since this image is used for decorative purposes only.

<ImageView
    android:id="@+id/icon_cost_of_service"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:importantForAccessibility="no"
    app:srcCompat="@drawable/ic_store" />

It is expected that there will be an error on the ImageView because the view is not constrained yet. You'll fix that next.

  1. Position the icon_cost_of_service in two steps. First add constraints onto the ImageView (this step), and then update constraints on the TextInputLayout next to it (step 5). This diagram shows how the constraints should be set up.

d982b1b1f0131630.png

On the ImageView, you want its starting edge to be constrained to the starting edge of the parent view (app:layout_constraintStart_toStartOf="parent").

The icon appears centered vertically compared to the text field beside it, so constrain the top of this ImageView (layout_constraintTop_toTopOf) to the top of the text field. Constrain the bottom of this ImageView (layout_constraintBottom_toBottomOf) to the bottom of the text field. To refer to the text field, use the resource ID @id/cost_of_service. The default behavior is that when two constraints are applied to a widget in the same dimension (such as a top and bottom constraint), the constraints are applied equally. The result is that the icon gets vertically centered, in relation to the cost of service field.

<ImageView
    android:id="@+id/icon_cost_of_service"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:importantForAccessibility="no"
    app:srcCompat="@drawable/ic_store"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/cost_of_service"
    app:layout_constraintBottom_toBottomOf="@id/cost_of_service" />

The icon and text field are still overlapping in the Design view. That will be fixed in the next step.

  1. Before the addition of the icon, the text field was positioned at the start of parent. Now it needs to be shifted over to the right. Update the constraints on the cost_of_service text field in relation to icon_cost_of_service.

bb55ea0cddaa2a12.png

The starting edge of the TextInputLayout should be constrained to the ending edge of the ImageView (@id/icon_cost_of_service). To add some spacing between the two views, add a start margin of 16dp on the TextInputLayout.

<com.google.android.material.textfield.TextInputLayout
    android:id="@+id/cost_of_service"
    ...
    android:layout_marginStart="16dp"
    app:layout_constraintStart_toEndOf="@id/icon_cost_of_service">

    <com.google.android.material.textfield.TextInputEditText ... />
 
</com.google.android.material.textfield.TextInputLayout>

After all these changes, the icon should be positioned correctly next to the text field.

23dcae5c3931903f.png

  1. Next insert the service bell icon next to the "How was the service?" TextView. While you could declare the ImageView anywhere within the ConstraintLayout, your XML layout will be easier to read if you insert the new ImageView in the XML layout after the TextInputLayout, but before the service_question TextView.

For the new ImageView, assign it a resource ID of @+id/icon_service_question. Set the appropriate constraints on the ImageView and the service question TextView.

38c2dcb4cb18b5a.png

Also add a 16dp top margin to the service_question TextView so there's more vertical space between the service question and the cost of service text field above it.

...

   <ImageView
        android:id="@+id/icon_service_question"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:importantForAccessibility="no"
        app:srcCompat="@drawable/ic_service"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/service_question"
        app:layout_constraintBottom_toBottomOf="@id/service_question" />

    <TextView
        android:id="@+id/service_question"
        ...
        android:layout_marginTop="16dp"
        app:layout_constraintStart_toStartOf="@id/cost_of_service"
        app:layout_constraintTop_toBottomOf="@id/cost_of_service"/>

...
  1. At this point the Design view should look like this. The cost of service field and service question (and their respective icons) look great, but the radio buttons now look out of place. They aren't vertically aligned with the content above it.

578834f5bd3a2d2a.png

  1. Improve the positioning of the radio buttons by shifting them to the right, underneath the service question. That means updating a RadioGroup constraint. Constrain the starting edge of the RadioGroup to the starting edge of the service_question TextView. All other attributes on the RadioGroup can remain the same.

bf454f3f1617024d.png

... 

<RadioGroup
    android:id="@+id/tip_options"
    ...
    app:layout_constraintStart_toStartOf="@id/service_question">

...
  1. Then proceed with adding the ic_round_up icon to the layout next to the "Round up tip?" switch. Try doing this on your own and if you get stuck, you can consult the XML below. You can assign the new ImageView a resource ID of icon_round_up.
  2. In the layout XML, insert a new ImageView after the RadioGroup but before the SwitchMaterial widget.
  3. Assign the ImageView a resource ID of icon_round_up and set the srcCompat to the drawable of the icon @drawable/ic_round_up. Constrain the start of the ImageView to the start of the parent, and also vertically center the icon relative to the SwitchMaterial.
  4. Update SwitchMaterial to be next to the icon and have a 16dp start margin. This is what the resulting XML should look like for icon_round_up and round_up_switch.
...

   <ImageView
        android:id="@+id/icon_round_up"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:importantForAccessibility="no"
        app:srcCompat="@drawable/ic_round_up"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/round_up_switch"
        app:layout_constraintBottom_toBottomOf="@id/round_up_switch" />

    <com.google.android.material.switchmaterial.SwitchMaterial
        android:id="@+id/round_up_switch"
        ...
        android:layout_marginStart="16dp"
        app:layout_constraintStart_toEndOf="@id/icon_round_up" />

...
  1. The Design view should look like this. All three icons are correctly positioned.

8781ecbd11859cc4.png

  1. If you compare this with the final app screenshot, you'll notice the calculate button is also shifted over to align vertically with the cost of service field, service question, radio button options, and round up tip question. Achieve this look by constraining the start of the calculate button to the start of the round_up_switch. Also add 8dp of vertical margin between the calculate button and the switch above it.

84348568e13d9e32.png

...

<Button
   android:id="@+id/calculate_button"
   ...
   android:layout_marginTop="8dp"
   app:layout_constraintStart_toStartOf="@id/round_up_switch" />

...
  1. Last but not least, position tip_result by adding 8dp of top margin to the TextView.

8e21f52be710340d.png

...

<TextView
   android:id="@+id/tip_result"
   ...
   android:layout_marginTop="8dp" />

...
  1. That was a lot of steps! Great job on working through them step-by-step. It requires a lot of attention to detail to get elements aligned correctly in the layout, but it makes the end result look much better! Run the app and it should look like the below screenshot. By vertically aligning and increasing spacing between elements, they are not as crowded together.

1f2ef2c0c9a9bdc7.png

You're not done yet! You may have noticed that the font size and color of the service question and tip amount look different than the text in the radio buttons and switch. Let's make these consistent in the next task by using styles and themes.

5. Styles and Themes

A style is a collection of view attributes values for a single type of widget. For example, a TextView style can specify font color, font size, and background color, to name a few. By extracting these attributes into a style, you can easily apply the style to multiple views in the layout and maintain it in a single place.

In this task, you will first create styles for the text view, radio button, and switch widgets.

Create Styles

  1. Create a new file named styles.xml in the res > values directory if one doesn't already exist. Create it by right-clicking on the values directory and selecting New > Values Resource File. Call it styles.xml. The new file will have the following contents.
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
  1. Create a new TextView style so that text appears consistent throughout the app. Define the style once in styles.xml and then you can apply it to all the TextViews in the layout. While you could define a style from scratch, you can extend from an existing TextView style from the MDC library.

When styling a component, you should generally extend from a parent style of the widget type you are using. This is important for two reasons. First, it makes sure all important default values are set on your component, and secondly, your style will continue to inherit any future changes to that parent style.

You can name your style anything you'd like, but there is a recommended convention. If you inherit from a parent Material style, then name your style in a parallel way by substituting MaterialComponents with your app's name (TipTime). This moves your changes into its own namespace which eliminates the possibility for future conflicts when Material Components introduces new styles. Example:

Your style name: Widget.TipTime.TextView inherits from parent style: Widget.MaterialComponents.TextView.

Add this to your styles.xml file in between the resources opening and closing tags.

<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
</style>
  1. Set up your TextView style so that it overrides the following attributes: android:minHeight,android:gravity,and android:textAppearance.

android:minHeight sets a minimum height of 48dp on the TextView. The smallest height for any row should be 48dp according to the Material Design guidelines.

You can center the text in the TextView vertically by setting the android:gravity attribute. (See screenshot below.) Gravity controls how the content within a view will position itself. Since the actual text content doesn't take up the full 48dp in height, the value center_vertical centers the text within the TextView vertically (but does not change its horizontal position). Other possible gravity values include center, center_horizontal, top, and bottom. Feel free to try out the other gravity values to see the effect on the text.

6a7ecc6a49a858e9.png

Set the text appearance attribute value to ?attr/textAppearanceBody1. TextAppearance is a set of pre-made styles around text size, fonts, and other properties of text. For other possible text appearances that are provided by Material, see this list of type scales.

<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
    <item name="android:minHeight">48dp</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
  1. Apply the Widget.TipTime.TextView style to the service_question TextView by adding a style attribute on each TextView in activity_main.xml.
<TextView
    android:id="@+id/service_question"
    style="@style/Widget.TipTime.TextView"
    ... />

Before the style, the TextView looked like this with small font size and gray font color:

4d54a3179f0c6f8d.png

After adding the style, the TextView looks like this. Now this TextView looks more consistent with the rest of the layout.

416d3928f9c3d3de.png

  1. Apply that same Widget.TipTime.TextView style to the tip_result TextView.
<TextView
    android:id="@+id/tip_result"
    style="@style/Widget.TipTime.TextView"
    ... />

3ebe16aa8c5bc010.png

  1. The same text style should be applied to the text label in the switch. However, you can't set a TextView style onto a SwitchMaterial widget. TextView styles can only be applied on TextViews. Hence create a new style for the switch. The attributes are the same in terms of minHeight, gravity, and textAppearance. What's different here is the style name and parent because you're now inheriting the Switch style from the MDC library. Your name for the style should also mirror the name of the parent style.

Your style name: Widget.TipTime.CompoundButton.Switch inherits from parent style: Widget.MaterialComponents.CompoundButton.Switch.

<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
   <item name="android:minHeight">48dp</item>
   <item name="android:gravity">center_vertical</item>
   <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>

You could also specify additional attributes specific to switches in this style, but in your app, there's no need to.

  1. The radio button text is the last place you want to make sure the text appears visually consistent. You can't apply a TextView style or Switch style onto a RadioButton widget. Instead, you must create a new style for radio buttons. You can extend from the MDC library's RadioButton style.

While you are creating this style, also add some padding between the radio button text and the circle visual. paddingStart is a new attribute you haven't used yet. Padding is the amount of space between the contents of a view and the bounds of the view. The paddingStart attribute sets the padding only at the start of the component. See the difference between 0dp and 8dp of paddingStart on a radio button.

4c1aa37bbdadab1d.png

35a96c994b82539e.png

<style name="Widget.TipTime.CompoundButton.RadioButton"
parent="Widget.MaterialComponents.CompoundButton.RadioButton">
   <item name="android:paddingStart">8dp</item>
   <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
  1. (Optional) Create a dimens.xml file to improve manageability of frequently-used values. You can create the file in the same manner you did for the styles.xml file above. Select the values directory, right click and select New > Values Resource File.

In this small app, you've repeated the minimum height setting twice. That's certainly manageable for now, but would quickly get out of control if we had 4, 6, 10 or more components sharing that value. Remembering to change all of them individually is tedious and error-prone. You can create another helpful resource file in res > values called dimens.xml that holds common dimensions that you can name. By standardizing common values as named dimensions, we make it easier to manage our app. TipTime is small so we won't be using it outside this optional step. However, with more complex apps in a production environment where you might work with a design team, dimens.xml will easily allow you to change these values more often.

dimens.xml

<resources>
   <dimen name="min_text_height">48dp</dimen>
</resources>

You would update styles.xml file to use @dimen/min_text_height instead of 48dp directly.

...
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
    <item name="android:minHeight">@dimen/min_text_height</item>
    <item name="android:gravity">center_vertical</item>
    <item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
...

Add these styles to your themes

You may have noticed that you haven't applied the new RadioButton and Switch styles onto the respective widgets yet. The reason is because you will be using theme attributes to set the radioButtonStyle and switchStyle in the app theme. Let's revisit what a theme is.

A theme is a collection of named resources (called theme attributes) that can be referenced later in styles, layouts, etc. You can specify a theme for an entire app, activity, or view hierarchy—not just an individual View. Previously you modified the app's theme in themes.xml by setting theme attributes like colorPrimary and colorSecondary, which gets used throughout the app and its components.

radioButtonStyle and switchStyle are other theme attributes you can set. The style resources that you provide for these theme attributes will be applied to every radio button and every switch in the view hierarchy that the theme applies to.

There's also a theme attribute for textInputStyle where the specified style resource will be applied to all text input fields within the app. To make a TextInputLayout appear like an outlined text field (as shown in the Material Design guidelines), there is an OutlinedBox style defined in the MDC library as Widget.MaterialComponents.TextInputLayout.OutlinedBox. This is the style you'll use.

2b2a5836a5d9bedf.png

  1. Modify the themes.xml file so that the theme refers to the desired styles. Setting a theme attribute is done the same way you declared the colorPrimary and colorSecondary theme attributes in an earlier codelab. This time however, the relevant theme attributes are textInputStyle, radioButtonStyle, and switchStyle. You'll be using the styles you've created previously for the RadioButton and Switch along with the style for the Material OutlinedBox text field.

Copy the following into res/values/themes.xml into the style tag for your app theme.

<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
  1. This is what your res/values/themes.xml file should look like. You can add comments in the XML if you want (indicated by <!-- and -->).
<resources xmlns:tools="http://schemas.android.com/tools">

    <!-- Base application theme. -->
    <style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        ...
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Text input fields -->
        <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
        <!-- Radio buttons -->
        <item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
        <!-- Switches -->
        <item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
    </style>

</resources>
  1. Be sure to make the same changes to the dark theme in themes.xml (night). Your res/values-night/themes.xml file should look like this.
<resources xmlns:tools="http://schemas.android.com/tools">

    <!-- Application theme for dark theme. -->
    <style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        ...
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Text input fields -->
        <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
        <!-- For radio buttons -->
        <item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
        <!-- For switches -->
        <item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
    </style>

</resources>
  1. Run the app and see the changes. The OutlinedBox style looks much better for the text field, and all the text now looks consistent!

31ac15991713b031.png 3e861407146c9ed4.png

6. Enhance the user experience

As you near completion of your app, you should test your app not just with the expected workflow but in other user scenarios as well. You may find that some small code changes can improve the user experience in a large way.

Rotating the device

  1. Rotate your device to landscape mode. You may need to enable Auto-rotate setting first. (This is located under the device's Quick Settings or under Settings > Display > Advanced > Auto-rotate screen option.)

f2edb1ae9926d5f1.png

In the emulator, you can then use the emulator options (located on the upper right side adjacent to the device) to rotate the screen to the right or left.

2bc08f73d28968cb.png

  1. You'll notice some of the UI components including the Calculate button will get truncated. This clearly prevents you from using the app!

d73499f9c9d2b330.png

  1. To solve this bug, add a ScrollView around the ConstraintLayout. Your XML will look something like this.
<ScrollView
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_height="match_parent"
   android:layout_width="match_parent">

   <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:padding="16dp"
       tools:context=".MainActivity">

       ...
   </ConstraintLayout>

</ScrollView>
  1. Run and test the app again. When you rotate the device to landscape mode, you should be able to scroll the UI to access the calculate button and see the tip result. This fix is not only useful for landscape mode, but also for other Android devices that may have different dimensions. Now regardless of the device screen size, the user can scroll the layout.

Hide keyboard on Enter key

You may have noticed that after you enter in a cost of service, the keyboard still stays up. It's a bit cumbersome to manually hide the keyboard each time to better access the calculate button. Instead, make the keyboard automatically hide itself when the Enter key is pressed.

e2c3a3dbc40218a2.png

For the text field, you can define a key listener to respond to events when certain keys are tapped. Every possible entry option on a keyboard has a key code associated with it, including the Enter key. Note that an onscreen keyboard is also known as a soft keyboard, as opposed to a physical keyboard.

1c95d7406d3847fe.png

In this task, set up a key listener on the text field to listen for when the Enter key is pressed. When that event is detected, hide the keyboard.

  1. Copy and paste this helper method into your MainActivity class. You can insert it right before the closing brace of the MainActivity class. The handleKeyEvent() is a private helper function that hides the onscreen keyboard if the keyCode input parameter is equal to KeyEvent.KEYCODE_ENTER. The InputMethodManager controls if a soft keyboard is shown, hidden, and allows the user to choose which soft keyboard is displayed. The method returns true if the key event was handled, and returns false otherwise.

MainActivity.kt

private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
   if (keyCode == KeyEvent.KEYCODE_ENTER) {
       // Hide the keyboard
       val inputMethodManager =
           getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
       inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
       return true
   }
   return false
}
  1. Now attach a key listener on the TextInputEditText widget. Remember that you can access the TextInputEditText widget through the binding object as binding.costOfServiceEditText.

Call the setOnKeyListener() method on the costOfServiceEditText and pass in an OnKeyListener. This is similar to how you set a click listener on the calculate button in the app with binding.calculateButton.setOnClickListener { calculateTip() }.

The code for setting a key listener on a view is a little more complex, but the general idea is that OnKeyListener has an onKey() method that gets triggered when a key press happens. The onKey() method takes in 3 input arguments: the view, the code for the key that was pressed, and a key event (which you won't use, so you can call it "_"). When the onKey() method is called, you should call your handleKeyEvent() method and pass along the view and key code arguments. The syntax for writing this out is: view, keyCode, _ -> handleKeyEvent(view, keyCode). This is actually called a lambda expression, but you will learn more about lambdas in a later unit.

Add the code for setting up the key listener on the text field within the activity's onCreate() method. This is because you want your key listener to be attached as soon as the layout is created and before the user starts interacting with the activity.

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   ...

   setContentView(binding.root)

   binding.calculateButton.setOnClickListener { calculateTip() }

   binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
   }
}
  1. Test that your new changes work. Run the app and enter in a cost of service. Hit the Enter key on the keyboard and the soft keyboard should get hidden.

Test your app with Talkback enabled

As you've been learning throughout this course, you want to build apps that are accessible to as many users as possible. Some users may use Talkback to access and navigate your app. TalkBack is the Google screen reader included on Android devices. TalkBack gives you spoken feedback so that you can use your device without looking at the screen.

With Talkback enabled, ensure that a user can complete the use case of calculating tip within your app.

  1. Enable Talkback on your device by following these instructions.
  2. Return to the Tip Time app.
  3. Explore your app with Talkback using these instructions. Swipe right to navigate through screen elements in sequence, and swipe left to go in the opposite direction. Double-tap anywhere to select. Verify that you can reach all elements of your app with swipe gestures.
  4. Ensure that a Talkback user is able to navigate to each item on the screen, enter in a cost of service, change the tip options, calculate the tip, and hear the tip announced. Remember that no spoken feedback is provided for the icons since you marked those as importantForAccessibility="no" .

For more information on how to make your app more accessible, check out these principles and this learning pathway.

(Optional) Adjust the tint of the vector drawables

In this optional task, you will tint the icons based on the primary color of the theme, so that the icons appear differently in light vs. dark themes (as seen below). This change is a nice addition to your UI to make the icons appear more cohesive with the app theme.

77092f702beb1cfb.png 80a390087905eb29.png

As we mentioned before, one of the advantages of VectorDrawables versus bitmap images is the ability to scale and tint them. Below we have the XML representing the bell icon. There are two specific color attributes to take notice of: android:tint and android:fillColor.

ic_service.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:width="24dp"
   android:height="24dp"
   android:viewportWidth="24"
   android:viewportHeight="24"
   android:tint="?attr/colorControlNormal">
 <path
     android:fillColor="@android:color/white"
     android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>

aab70a5d4eaabdc7.png

If there is a tint present, it will override any fillColor directives for the drawable. In this case, the white color is overridden with the colorControlNormal theme attribute. colorControlNormal is the color of the "normal" (unselected/unactivated state) of a widget. Currently that's a gray color.

One visual enhancement we can make to the app is to tint the drawable based on the primary color of the app theme. For light theme, the icon will appear as @color/green, whereas in dark theme, the icon will appear as @color/green_light, which is the ?attr/colorPrimary. Tinting the drawable based on the primary color of the app theme can make the elements in the layout appear more unified and cohesive. This also saves us from having to duplicate the set of icons for light theme and dark theme. There's only 1 set of vector drawables, and the tint will change based on the colorPrimary theme attribute.

  1. Change the value of the android:tint attribute in ic_service.xml
android:tint="?attr/colorPrimary"

In Android Studio, that icon now has the proper tint.

f0b8f59dbf00a20b.png

The value that the colorPrimary theme attribute points to will differ depending on light vs. dark theme.

  1. Repeat the same for changing the tint on the other vector drawables.

ic_store.xml

<vector ...
   android:tint="?attr/colorPrimary">
   ...
</vector>

ic_round_up.xml

<vector ...
   android:tint="?attr/colorPrimary">
   ...
</vector>
  1. Run the app. Verify that the icons appear differently in light vs. dark themes.
  2. As a final cleanup step, remember to reformat all XML and Kotlin code files in your app.

Congratulations, you have finally completed the tip calculator app! You should be very proud of what you've built. Hopefully this is the stepping stone for you to build even more beautiful and functional apps!

7. Solution code

The solution code for this codelab is in the GitHub repository listed below.

5743ac5ee2493d7.png ab4acfeed8390465.png

To get the code for this codelab and open it in Android Studio, do the following.

Get the code

  1. Click on the provided URL. This opens the GitHub page for the project in a browser.
  2. Check and confirm the branch name matches with the branch name specified in the codelab. For example, in the following screenshot the branch name is main.

8cf29fa81a862adb.png

  1. On the GitHub page for the project, click the Code button, which brings up a popup.

1debcf330fd04c7b.png

  1. In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
  2. Locate the file on your computer (likely in the Downloads folder).
  3. Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.

Open the project in Android Studio

  1. Start Android Studio.
  2. In the Welcome to Android Studio window, click Open.

d8e9dbdeafe9038a.png

Note: If Android Studio is already open, instead, select the File > Open menu option.

8d1fda7396afe8e5.png

  1. In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
  2. Double-click on that project folder.
  3. Wait for Android Studio to open the project.
  4. Click the Run button 8de56cba7583251f.png to build and run the app. Make sure it builds as expected.

8. Summary

  • Use Material Design Components where possible to adhere to Material Design guidelines and allow for more customization.
  • Add icons to give users visual cues about how parts of your app will function.
  • Use ConstraintLayout to position elements in your layout.
  • Test your app for edges cases (e.g. rotating your app in landscape mode) and make improvements where applicable.
  • Comment your code to help other people who are reading your code understand what your approach was.
  • Reformat your code and clean up your code to make it as concise as possible.

9. Learn more

10. Practice on your own

  • As a continuation from earlier codelabs, update your unit converter cooking app to more closely follow the Material guidelines by using the best practices you learned here (such as using Material Design Components).