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

The Activity Lifecycle

In the previous chapter, we used the onCreate(saveInstanceState: Bundle?) method to display a layout in the UI of our screen. Now, we'll explore in more detail how the Android system interacts with your application to make this happen. As soon as an Activity is launched, it goes through a series of steps to take it through initialization and preparing it to be displayed to being partially displayed, and then fully displayed. There are also steps that correspond with your application being hidden, backgrounded, and then destroyed. This process is called the Activity lifecycle. For every one of these steps, there is a callback that your Activity can use to perform actions such as creating and changing the display and saving data when your app has been put into the background and then restoring that data after your app comes back into the foreground. You can consider these callbacks as hooks into how the system interacts with your activity/screen.

Every Activity has a parent Activity class that it extends. These callbacks are made on your Activity's parent, and it's up to you to decide whether you need to implement them in your own Activity to take any corresponding action. Every one of these callback functions has the override keyword. The override keyword in Kotlin means that either this function is providing an implementation of an interface or an abstract method, or, in the case of your Activity here, which is a subclass, it is providing the implementation that will override its parent.

Now that you know how the Activity lifecycle works in general, let's go into more detail about the principal callbacks you will work with in order, from creating an Activity to the Activity being destroyed:

  • override fun onCreate(savedInstanceState: Bundle?): This is the callback that you will use the most for activities that draw a full-sized screen. It's here where you prepare your Activity layout to be displayed. At this stage, after the method completes, it is still not displayed to the user, although it will appear that way if you don't implement any other callbacks. You usually set up the UI of your Activity here by calling the setContentView method setContentView(R.layout.activity_main) and carry out any initialization that is required. This method is only called once in its lifecycle unless the Activity is created again. This happens by default for some actions (such as rotating the phone from portrait to landscape orientation, for example). The savedInstanceState parameter of the Bundle? type (? means the type can be null) in its simplest form is a map of key-value pairs optimized to save and restore data. It will be null if this is the first time that the Activity has been run after the app has started or if the Activity is being created for the first time or recreated without any states being saved. It may contain a saved state if it has been saved in the onSaveInstanceState(outState: Bundle?) callback prior to the Activity being recreated.
  • override fun onRestart(): When the Activity restarts, this is called immediately before onStart(). It is important to be clear about the difference between restarting an Activity and recreating an activity. When the Activity is backgrounded by pressing the home button—for instance, when it comes back into the foreground again—onRestart() will be called. Recreating an Activity is what happens when a configuration change happens, such as the device being rotated. The Activity is finished and then created again.
  • override fun onStart(): This is the callback made when the Activity first comes into view. Also, after the app is backgrounded by pressing either the back, home, or the recents/overview hardware buttons, on selecting the app again from the recents/overview menu or the launcher, this function will be run. It is the first of the visible lifecycle methods.
  • override fun onRestoreInstanceState(savedInstanceState: Bundle?): If the state has been saved using onSaveInstanceState(outState: Bundle?) this is the method which the system calls after onStart() where you can retrieve the Bundle state instead of restoring the state during onCreate(savedInstanceState: Bundle?)
  • override fun onResume(): This callback is run as the final stage of creating an Activity for the first time, and also when the app has been backgrounded and then is brought into the foreground. Upon the completion of this callback, the screen/activity is ready to be used, receive user events, and be responsive.
  • override fun onSaveInstanceState(outState: Bundle?): If you want to save the state of the activity, this function can do so. You add key-value pairs using one of the convenience functions depending on the data type. The data will then be available if your Activity is recreated in onCreate(saveInstanceState: Bundle?) and onRestoreInstanceState(savedInstanceState: Bundle?).
  • override fun onPause(): This function is called when the Activity starts to be backgrounded or another dialog or Activity comes into the foreground.
  • override fun onStop(): This function is called when the Activity is hidden, either because it is being backgrounded or another Activity is being launched on top of it.
  • override fun onDestroy(): This is called by the system to kill the Activity when system resources are low, when finish() is called explicitly on the Activity, or, more commonly, when the Activity is killed by the user closing the app from the recents/overview button.

Now that you understand what these common lifecycle callbacks do, let's implement them to see when they are called.

Exercise 2.01: Logging the Activity Callbacks

Let's create an application called Activity Callbacks with an empty Activity, as you did previously in Chapter 1, Creating Your First App. The aim of this exercise is to log the Activity callbacks and the order that they occur for common operations:

  1. After the application has been created, MainActivity will appear as follows:
    package com.example.activitycallbacks
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    }

    In order to verify the order of the callbacks, let's add a log statement at the end of each callback. To prepare the Activity for logging, import the Android log package by adding import android.util.Log to the import statements. Then, add a constant to the class to identify your Activity. Constants in Kotlin are identified by the const keyword and can be declared at the top level (outside the class) or in an object within the class. Top level constants are generally used if they are required to be public. For private constants, Kotlin provides a convenient way to add static functionality to classes by declaring a companion object. Add the following at the bottom of the class below onCreate(savedInstanceState: Bundle?):

    companion object {
        private const val TAG = "MainActivity"
    }

    Then add a log statement at the end of onCreate(savedInstanceState: Bundle?):

    Log.d(TAG, "onCreate")

    Our Activity should now have the following code:

    package com.example.activitycallbacks
    import android.os.Bundle
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            Log.d(TAG, "onCreate")
        }
        companion object {
            private const val TAG = "MainActivity"
        }
    }

    d in the preceding log statement refers to debug. There are six different log levels that can be used to output message information from the least to most important - v for verbose, d for debug, i for info, w for warn, e for error, and wtf for what a terrible failure. (This last log level highlights an exception that should never occur.)

            Log.v(TAG, "verbose message")
            Log.d(TAG, "debug message")
            Log.i(TAG, "info message")
            Log.w(TAG, "warning message")
            Log.e(TAG, "error message")
            Log.wtf(TAG, "what a terrible failure message")
  2. Now, let's see how the logs are displayed in Android Studio. Open the Logcat window. It can be accessed by clicking on the Logcat tab at the bottom of the screen and also from the toolbar by going to View | Tool WindowsLogcat.
  3. Run the app on the virtual device and examine the Logcat window output. You should see the log statement you have added formatted like the following line in Figure 2.1:
    Figure 2.1: Log output in Logcat

    Figure 2.1: Log output in Logcat

  4. Log statements can be quite difficult to interpret at first glance, so let's break down the following statement into its separate parts:
    2020-03-03  20:36:12.308  21415-21415/com.example.activitycallbacks D/MainActivity: onCreate

    Let's examine the elements of the log statement in detail:

    Figure 2.2: Table explaining a log statement

    Figure 2.2: Table explaining a log statement

    You can examine the output of the different log levels by changing the log filter from Debug to other options in the drop-down menu. If you select Verbose, as the name implies, you will see a lot of output.

  5. What's great about the TAG option of the log statement is that it enables you to filter the log statements that are reported in the Logcat window of Android Studio by typing in the text of the tag, as shown in Figure 2.3:
    Figure 2.3: Filtering log statements by the TAG name

    Figure 2.3: Filtering log statements by the TAG name

    So, if you are debugging an issue in your Activity, you can type in the TAG name and add logs to your Activity to see the sequence of log statements. This is what you are going to do next by implementing the principal Activity callbacks and adding a log statement to each one to see when they are run.

  6. Place your cursor on a new line after the closing brace of the onCreate(savedInstanceState: Bundle?) function and then add the onRestart() callback with a log statement. Make sure you call through to super.onRestart() so that the existing functionality of the Activity callback works as expected:
    override fun onRestart() {
        super.onRestart()
        Log.d(TAG, "onRestart")
    }
  7. You will find that once you start typing the name of the function, Android Studio's autocomplete feature will suggest options for the name of the function you want to override.

    Note

    In Android Studio you can start typing the name of a function, and autocomplete options will pop up with suggestions for functions to override. Alternatively, if you go to the top menu and then Code | Generate | Override methods, you can select the methods to override.

    Do this for all of the following callback functions:

    onCreate(savedInstanceState: Bundle?)
    onRestart()
    onStart()
    onRestoreInstanceState(savedInstanceState: Bundle?)
    onResume()
    onPause()
    onStop()
    onSaveInstanceStateoutState: Bundle?)
    onDestroy()
  8. Your Activity should now have the following code (truncated here). You can see the full code on GitHub at http://packt.live/38W7jU5

    The completed activity will now override the callbacks with your implementation, which adds a log message:

    package com.example.activitycallbacks
    import android.os.Bundle
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            Log.d(TAG, "onCreate")
        }
        override fun onRestart() {
            super.onRestart()
            Log.d(TAG, "onRestart")
        }
        //Remaining callbacks follow: see github link above
        companion object {
            private const val TAG = "MainActivity"
        }
    }
  9. Run the app, and then once it has loaded, as in Figure 2.4, look at the Logcat output; you should see the following log statements (this is a shortened version):
    D/MainActivity: onCreate
    D/MainActivity: onStart
    D/MainActivity: onResume

    The Activity has been created, started, and then prepared for the user to interact with:

    Figure 2.4: The app loaded and displaying MainActivity

    Figure 2.4: The app loaded and displaying MainActivity

  10. Press the round home button in the center of the bottom navigation controls and background the app. You should now see the following Logcat output:
    D/MainActivity: onPause
    D/MainActivity: onStop
    D/MainActivity: onSaveInstanceState

    For apps which target below Android Pie (API 28) then onSaveInstanceState(outState: Bundle?) may also be called before onPause() or onStop().

  11. Now, bring the app back into the foreground by pressing the recents/overview button (usually a square or three vertical lines) on the right and selecting the app, or by going to the launcher and opening the app. You should now see the following:
    D/MainActivity: onRestart
    D/MainActivity: onStart
    D/MainActivity: onResume

    The Activity has been restarted. You might have noticed that the onRestoreInstanceState(savedInstanceState: Bundle) function was not called. This is because the Activity was not destroyed and recreated.

  12. Press the triangle back button on the left of the bottom navigation controls (it may also be on the right) and you will see the Activity being destroyed. You can also do this by pressing the recents/overview button and then swiping the app upward to kill the activity. This is the output:
    D/MainActivity: onPause
    D/MainActivity: onStop
    D/MainActivity: onDestroy
  13. Launch your app again and then rotate the phone. You might find that the phone does not rotate and the display is sideways. If this happens drag down the status bar at the very top of the virtual device and select the auto-rotate button 2nd from the right in the settings.
    Figure 2.5: Quick settings bar with Wi-Fi and Auto-rotate button selected

    Figure 2.5: Quick settings bar with Wi-Fi and Auto-rotate button selected

    You should see the following callbacks:

    D/MainActivity: onCreate
    D/MainActivity: onStart
    D/MainActivity: onResume
    D/MainActivity: onPause
    D/MainActivity: onStop
    D/MainActivity: onSaveInstanceState
    D/MainActivity: onDestroy
    D/MainActivity: onCreate
    D/MainActivity: onStart
    D/MainActivity: onRestoreInstanceState
    D/MainActivity: onResume

    Please note that as stated in step 11, the order of the onSaveInstanceState(outState: Bundle?) callback may vary.

  14. Configuration changes, such as rotating the phone, by default recreate the activity. You can choose not to handle certain configuration changes in the app, which will then not recreate the activity. To do this for rotation, add android:configChanges="orientation|screenSize|screenLayout" to MainActivity in the AndroidManifest.xml file. Launch the app and then rotate the phone, and these are the only callbacks that you have added to MainActivity that you will see:
    D/MainActivity: onCreate
    D/MainActivity: onStart
    D/MainActivity: onResume

    The orientation and screenSize values have the same function for different Android API levels for detecting screen orientation changes. The screenLayout value detects other layout changes which might occur on foldable phones. These are some of the config changes you can choose to handle yourself (another common one is keyboardHidden to react to changes in accessing the keyboard). The app will still be notified by the system of these changes through the following callback:

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        Log.d(TAG, "onConfigurationChanged")
    }

    If you add this callback function to MainActivity, and you have added android:configChanges="orientation|screenSize|screenLayout" to MainActivity in the manifest, you will see it called on rotation.

In this exercise, you have learned about the principal Activity callbacks and how they run when a user carries out common operations with your app through the system's interaction with MainActivity. In the next section, you will cover saving the state and restoring it, as well as see more examples of how the Activity lifecycle works.