Book Image

How to Build Android Apps with Kotlin

By : Alex Forrester, Eran Boudjnah, Alexandru Dumbravan, Jomar Tigcal
Book Image

How to Build Android Apps with Kotlin

By: Alex Forrester, Eran Boudjnah, Alexandru Dumbravan, Jomar Tigcal

Overview of this book

Are you keen to get started building Android 11 apps, but don’t know where to start? How to Build Android Apps with Kotlin is a comprehensive guide that will help kick-start your Android development practice. This book starts with the fundamentals of app development, enabling you to utilize Android Studio and Kotlin to get started building Android projects. You'll learn how to create apps and run them on virtual devices through guided exercises. Progressing through the chapters, you'll delve into Android’s RecyclerView to make the most of lists, images, and maps, and see how to fetch data from a web service. Moving ahead, you'll get to grips with testing, learn how to keep your architecture clean, understand how to persist data, and gain basic knowledge of the dependency injection pattern. Finally, you'll see how to publish your apps on the Google Play store. You'll work on realistic projects that are split up into bitesize exercises and activities, allowing you to challenge yourself in an enjoyable and attainable way. You'll build apps to create quizzes, read news articles, check weather reports, store recipes, retrieve movie information, and remind you where you parked your car. By the end of this book, you'll have the skills and confidence to build your own creative Android applications using Kotlin.
Table of Contents (17 chapters)
Preface
12
12. Dependency Injection with Dagger and Koin

Saving and Restoring the Activity State

In this section, you'll explore how your Activity saves and restores the state. As you've learned in the previous section, configuration changes, such as rotating the phone, cause the Activity to be recreated. This can also happen if the system has to kill your app in order to free up memory. In these scenarios, it is important to preserve the state of the Activity and then restore it. In the next two exercises, you'll work through an example ensuring that the user's data is restored when TextView is created and populated from a user's data after filling in a form.

Exercise 2.02: Saving and Restoring the State in Layouts

In this exercise, firstly create an application called Save and Restore with an empty activity. The app you are going to create will have a simple form that offers a discount code for a user's favorite restaurant if they enter some personal details (no actual information will be sent anywhere, so your data is safe):

  1. Open up the strings.xml file (located in app | src | main | res | values | strings.xml) and create the following strings that you'll need for your app:
    <resources>
        <string name="app_name">Save And Restore</string>
        <string name="header_text">Enter your name and email       for a discount code at Your Favorite Restaurant!        </string>
        <string name="first_name_label">First Name:</string>
        <string name="email_label">Email:</string>
        <string name="last_name_label">Last Name:</string>
        <string name="discount_code_button">GET DISCOUNT</string>
        <string name="discount_code_confirmation">Your       discount code is below %s. Enjoy!</string>
    </resources>
  2. You are also going to specify some text sizes, layout margins, and padding directly, so create the dimens.xml file in the app | src | main | res | values folder and add the dimensions you'll need for the app (you can do this by right-clicking on the res | values folder within Android Studio and selecting New values):
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <dimen name="grid_4">4dp</dimen>
        <dimen name="grid_8">8dp</dimen>
        <dimen name="grid_12">12dp</dimen>
        <dimen name="grid_16">16dp</dimen>
        <dimen name="grid_24">24dp</dimen>
        <dimen name="grid_32">32dp</dimen>
        <dimen name="default_text_size">20sp</dimen>
        <dimen name="discount_code_text_size">20sp</dimen>
    </resources>

    Here, you are specifying all the dimensions you need in the exercise. You will see here that default_text_size and discount_code_text_size are specified in sp. They represent the same values as density-independent pixels, which not only define the size measurement according to the density of the device that your app is being run on but also change the text size according to the user's preference, defined in Settings | Display | Font style (this might be Font size and style or something similar, depending on the exact device you are using).

  3. In R.layout.activity_main, add the following XML, creating a containing layout file and adding header a TextView with the Enter your name and email for a discount code at Your Favorite Restaurant! text. This is done by adding the android:text attribute with the @string/header_text value:
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/grid_4"
        android:layout_marginTop="@dimen/grid_4"
        tools:context=".MainActivity">
        <TextView
            android:id="@+id/header_text"
            android:gravity="center"
            android:textSize="@dimen/default_text_size"
            android:paddingStart="@dimen/grid_8"
            android:paddingEnd="@dimen/grid_8"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/header_text"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

    You are using ConstraintLayout for constraining Views against the parent View and sibling Views.

    Although you should normally specify the display of the View with styles, you can do this directly in the XML, as is done for some attributes here. The value of the android:textSize attribute is @dimen/default_text_size, defined in the previous code block, which you use to avoid repetition, and it enables you to change all the text size in one place. Using styles is the preferred option for setting text sizes as you will get sensible defaults and you can override the value in the style or, as you are doing here, on the individual Views.

    Other attributes that affect positioning are also specified directly here in the Views. The most common ones are padding and margin. Padding is applied on the inside of Views and is the space between the text and the border. Margin is specified on the outside of Views and is the space from the outer edges of Views. For example, android:padding in ConstraintLayout sets the padding for the View with the specified value on all sides. Alternatively, you can specify the padding for one of the four sides of a View with android:paddingTop, android:paddingBottom, android:paddingStart, and android:paddingEnd. This pattern also exists to specify margins, so android:layout_margin specifies the margin value for all four sides of a View and android:layoutMarginTop, android:layoutMarginBottom, android:layoutMarginStart, and android:layoutMarginEnd allow setting the margin for individual sides.

    For API levels less than 17 (and your app supports down to 16) you also have to add android:layoutMarginLeft if you use android:layoutMarginStart and android:layoutMarginRight if you use android:layoutMarginEnd. In order to have consistency and uniformity throughout the app, you define the margin and padding values as dimensions contained within the dimens.xml file.

    To position the content within a View, you can specify android:gravity. The center value constrains the content both vertically and horizontally within the View.

  4. Next, add three EditText views below the header_text for the user to add their first name, last name, and email:
        <EditText
            android:id="@+id/first_name"
            android:textSize="@dimen/default_text_size"
            android:layout_marginStart="@dimen/grid_24"
            android:layout_marginLeft="@dimen/grid_24"
            android:layout_marginEnd="@dimen/grid_16"
            android:layout_marginRight="@dimen/grid_16"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/first_name_label"
            android:inputType="text"
            app:layout_constraintTop_toBottomOf="@id/header_text"
            app:layout_constraintStart_toStartOf="parent" />
        <EditText
            android:textSize="@dimen/default_text_size"
            android:layout_marginEnd="@dimen/grid_24"
            android:layout_marginRight="@dimen/grid_24"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/last_name_label"
            android:inputType="text"
            app:layout_constraintTop_toBottomOf="@id/header_text"
            app:layout_constraintStart_toEndOf="@id/first_name"
            app:layout_constraintEnd_toEndOf="parent" />
        <!-- android:inputType="textEmailAddress" is not enforced, 
          but is a hint to the IME (Input Method Editor) usually a 
          keyboard to configure the display for an email - 
          typically by showing the '@' symbol -->
        <EditText
            android:id="@+id/email"
            android:textSize="@dimen/default_text_size"
            android:layout_marginStart="@dimen/grid_24"
            android:layout_marginLeft="@dimen/grid_24"
            android:layout_marginEnd="@dimen/grid_32"
            android:layout_marginRight="@dimen/grid_32"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/email_label"
            android:inputType="textEmailAddress"
            app:layout_constraintTop_toBottomOf="@id/first_name"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

    The EditText fields have an inputType attribute to specify the type of input that can be entered into the form field. Some values, such as number on EditText, restrict the input that can be entered into the field, and on selecting the field, suggest how the keyboard is displayed. Others, such as android:inputType="textEmailAddress", will not enforce an @ symbol being added to the form field, but will give a hint to the keyboard to display it.

  5. Finally, add a button for the user to press to generate a discount code, and display the discount code itself and a confirmation message:
        <Button
            android:id="@+id/discount_button"
            android:textSize="@dimen/default_text_size"
            android:layout_marginTop="@dimen/grid_12"
            android:gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/discount_code_button"
            app:layout_constraintTop_toBottomOf="@id/email"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
        <TextView
            android:id="@+id/discount_code_confirmation"
            android:gravity="center"
            android:textSize="@dimen/default_text_size"
            android:paddingStart="@dimen/grid_16"
            android:paddingEnd="@dimen/grid_16"
            android:layout_marginTop="@dimen/grid_8"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/discount_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Hey John Smith! Here is your discount code" />
        <TextView
            android:id="@+id/discount_code"
            android:gravity="center"
            android:textSize="@dimen/discount_code_text_size"
            android:textStyle="bold"
            android:layout_marginTop="@dimen/grid_8"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/discount_code           _confirmation"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="XHFG6H9O" />

    There are also some attributes that you haven't seen before. The tools namespace xmlns:tools="http://schemas.android.com/tools" which was specified at the top of the xml layout file enables certain features that can be used when creating your app to assist with configuration and design. The attributes are removed when you build your app, so they don't contribute to the overall size of the app. You are using the tools:text attribute to show the text that will typically be displayed in the form fields. This helps when you switch to the Design view from viewing the XML in the Code view in Android Studio as you can see an approximation of how your layout displays on a device.

  6. Run the app and you should see the output displayed in Figure 2.6:
    Figure 2.6: The Activity screen on the first launch

    Figure 2.6: The Activity screen on the first launch

  7. Enter some text into each of the form fields:
    Figure 2.7: The EditText fields filled in

    Figure 2.7: The EditText fields filled in

  8. Now, use the second rotate button in the virtual device controls (1) to rotate the phone 90 degrees to the right:
    Figure 2.8: The virtual device turned to landscape orientation

    Figure 2.8: The virtual device turned to landscape orientation

    Can you spot what has happened? The Last Name field value is no longer set. It has been lost in the process of recreating the activity. Why is this? Well, in the case of the EditText fields, the Android framework will preserve the state of the fields if they have an ID set on them.

  9. Go back to the activity_main.xml layout file and add an ID for the Last Name value in the EditText field:
    <EditText
        android:id="@+id/last_name"
        android:textSize="@dimen/default_text_size"
        android:layout_marginEnd="@dimen/grid_24"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="@string/last_name_label"
        android:inputType="text"
        app:layout_constraintTop_toBottomOf="@id/header_text"
        app:layout_constraintStart_toEndOf="@id/first_name"
        app:layout_constraintEnd_toEndOf="parent"
        tools:text="Last Name:"/>

When you run up the app again and rotate the device, it will preserve the value you have entered. You've now seen that you need to set an ID on the EditText fields to preserve the state. For the EditText fields, it's common to retain the state on a configuration change when the user is entering details into a form so that it is the default behavior if the field has an ID. Obviously, you want to get the details of the EditText field once the user has entered some text, which is why you set an ID, but setting an ID for other field types, such as TextView, does not retain the state if you update them and you need to save the state yourself. Setting IDs for Views that enable scrolling, such as RecyclerView, is also important as it enables the scroll position to be maintained when the Activity is recreated.

Now, you have defined the layout for the screen, but you have not added any logic for creating and displaying the discount code. In the next exercise, we will work through this.

The layout created in this exercise is available at http://packt.live/35RSdgz.

You can find the code for the entire exercise at http://packt.live/3p1AZF3.

Exercise 2.03: Saving and Restoring the State with Callbacks

The aim of this exercise is to bring all the UI elements in the layout together to generate a discount code after the user has entered their data. In order to do this, you will have to add logic to the button to retrieve all the EditText fields and then display a confirmation to the user, as well as generate a discount code:

  1. Open up MainActivity.kt and replace the default empty Activity from the project creation. A snippet of the code is shown here, but you'll need to use the link given below to find the full code block you need to add:

    MainActivity.kt

    14  class MainActivity : AppCompatActivity() {
    15
    16    private val discountButton: Button
    17        get() = findViewById(R.id.discount_button)
    18
    19    private val firstName: EditText
    20        get() = findViewById(R.id.first_name)
    21
    22    private val lastName: EditText
    23        get() = findViewById(R.id.last_name)
    24
    25    private val email: EditText
    26        get() = findViewById(R.id.email)
    27  
    28    private val discountCodeConfirmation: TextView
    29        get() = findViewById(R.id             .discount_code_confirmation)
    30
    31    private val discountCode: TextView
    32        get() = findViewById(R.id.discount_code)    
    33  
    34    override fun onCreate(savedInstanceState: Bundle?) {
    35        super.onCreate(savedInstanceState)
    36        setContentView(R.layout.activity_main)
    37        Log.d(TAG, "onCreate")

    The get() = … is a custom accessor for a property.

    Upon clicking the discount button, you retrieve the values from the first_name and last_name fields, concatenate them with a space, and then use a string resource to format the discount code confirmation text. The string you reference in the strings.xml file is as follows:

    <string name="discount_code_confirmation">Hey  %s! Here is   your discount code</string>

    The %s value specifies a string value to be replaced when the string resource is retrieved. This is done by passing in the full name when getting the string:

    getString(R.string.discount_code_confirmation, fullName)

    The code is generated by using the UUID (Universally Unique Identifier) library from the java.util package. This creates a unique id, and then the take() Kotlin function is used to get the first eight characters before setting these to uppercase. Finally, discount_code is set in the view, the keyboard is hidden, and all the form fields are set back to their initial values.

  2. Run the app and enter some text into the name and email fields, and then click on GET DISCOUNT:
    Figure 2.9: Screen displayed after the user has generated a discount code

    Figure 2.9: Screen displayed after the user has generated a discount code

    The app behaves as expected, showing the confirmation.

  3. Now, rotate the phone (pressing the fifth button down with the arrow on the right-hand side of the virtual device picture) and observe the result:
    Figure 2.10: Discount code no longer displaying on the screen

    Figure 2.10: Discount code no longer displaying on the screen

    Oh, no! The discount code has gone. The TextView fields do not retain the state, so you will have to save the state yourself.

  4. Go back into MainActivity.kt and add the following Activity callbacks:
    override fun onRestoreInstanceState(savedInstanceState:   Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        Log.d(TAG, "onRestoreInstanceState")
    }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        Log.d(TAG, "onSaveInstanceState")
    }

    These callbacks, as the names declare, enable you to save and restore the instance state. onSaveInstanceState(outState: Bundle) allows you to add key-value pairs from your Activity when it is being backgrounded or destroyed, which you can retrieve in either onCreate(savedInstanceState: Bundle?) or onRestoreInstanceState(savedInstanceState: Bundle).

    So, you have two callbacks to retrieve the state once it has been set. If you are doing a lot of initialization in onCreate(savedInstanceState: Bundle), it might be better to use onRestoreInstanceState(savedInstanceState: Bundle) to retrieve this instance state when your Activity is being recreated. In this way, it's clear which state is being recreated. However, you might prefer to use onCreate(savedInstanceState: Bundle) if there is minimal setup required.

    Whichever of the two callbacks you decide to use, you will have to get the state you set in the onSaveInstanceState(outState: Bundle) call. For the next step in the exercise, you will use onRestoreInstanceState(savedInstanceState: Bundle).

  5. Add two constants to the MainActivity companion object:
    private const val DISCOUNT_CONFIRMATION_MESSAGE =   "DISCOUNT_CONFIRMATION_MESSAGE"
    private const val DISCOUNT_CODE = "DISCOUNT_CODE"
  6. Now, add these constants as keys for the values you want to save and retrieve by making the following additions to the Activity:
        override fun onRestoreInstanceState(
            savedInstanceState: Bundle) {
            super.onRestoreInstanceState(savedInstanceState)
            Log.d(TAG, "onRestoreInstanceState")
            //Get the discount code or an empty           string if it hasn't been set
            discountCode.text = savedInstanceState           .getString(DISCOUNT_CODE,"")
            //Get the discount confirmation message           or an empty string if it hasn't been set
            discountCodeConfirmation.text =          savedInstanceState.getString(            DISCOUNT_CONFIRMATION_MESSAGE,"")
        }
        override fun onSaveInstanceState(outState: Bundle) {
            super.onSaveInstanceState(outState)
            Log.d(TAG, "onSaveInstanceState")
            outState.putString(DISCOUNT_CODE,          discountCode.text.toString())
            outState.putString(DISCOUNT_CONFIRMATION_MESSAGE,          discountCodeConfirmation.text.toString())
        }
  7. Run the app, enter the values into the EditText fields, and then generate a discount code. Then, rotate the device and you will see that the discount code is restored in Figure 2.11:
    Figure 2.11: Discount code continues to be displayed on the screen

Figure 2.11: Discount code continues to be displayed on the screen

In this exercise, you first saw how the state of the EditText fields is maintained on configuration changes. You also saved and restored the instance state using the Activity lifecycle onSaveInstanceState(outState: Bundle) and onCreate(savedInstanceState: Bundle?)/onRestoreInstanceState(savedInstanceState: Bundle) functions. These functions provide a way to save and restore simple data. The Android framework also provides ViewModel, an Android architecture component that is lifecycle-aware. The mechanisms of how to save and restore this state (with ViewModel) are managed by the framework, so you don't have to explicitly manage it as you have done in the preceding example. You will learn how to use this component in Chapter 10, Android Architecture Components.

So far, you have created a single-screen app. Although it is possible for simple apps to use one Activity, it is likely that you will want to organize your app into different activities that handle different functions. So, in the next section, you will add another Activity to an app and navigate between the activities.