Chapters

Hide chapters

Android Apprentice

Fourth Edition · Android 11 · Kotlin 1.4 · Android Studio 4.1

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

3. Activities
Written by Darryl Bayliss

A lifestyle of various activities — like cardio, strength training, and endurance — can keep you healthy. Although they’re all different, they each have a specific purpose or goal in mind.

Android apps are similar — they’re built around a set of screens. Each screen is known as an Activity and is built around a single task. For example, you might have a Settings screen where users can adjust the app’s settings or a Sign-in screen where users can log in with a username and password.

In this chapter, you’ll start building an Activity focused around the gameplay for Timefighter — and you’ll finally get to write some Kotlin code!

Getting started

Before you jump headfirst into writing code, you first need to understand how IDs work. In Android, IDs play a fundamental role in connecting things, for example, connecting Views to your code.

In the previous chapter, you positioned Views and established that the top-left TextView will show the score, the top-right TextView will show the time and the Button, when pressed, will increment the score. Connecting your code to these Views will require each to have its own ID.

If you were following along making your app, open it and keep using it for this chapter. If not, don’t worry — locate the projects folder for this chapter and open the Timefighter app inside the starter folder.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

Open activity_main.xml where you built your Layout and make sure you’ve selected the design editor. Next to the Palette tab, you’ll see a window called the Component Tree:

This window provides you with an overview of the Views available in your Layout and their relationships relative to one another.

In the Component Tree, click on the row labeled button, or buttonX, where X is a number. This action highlights the Button in the middle of the screen and updates the Properties window on the right with details about the Button.

Note: The various sections in the properties window may be collapsed when the window opens. If they are, expand the sections by clicking on the arrow next to the section title.

The Button in the screen above already has an ID of button, but this isn’t very descriptive.

Note: In your project, the button ID might have a different string value.

Using descriptive IDs makes it easier to know which IDs refer to which Views, it’s a good habit to get into.

Change the value of the ID field from button to tap_me_button. You may get a dialog asking if you want to refactor by renaming the button’s ID:

Click the Refactor button to accept the change.

It’s also a good idea to give the Button a more descriptive name too. Change the value of text in the TextView section of the Properties window to Tap me!

Select the TextView on the top-left from the Component Tree. Set the TextViews ID to game_score_text_view and change the text to Your Score: %1$d. Finally, select the TextView you added to the top-right and in the Component Tree, change the ID to time_left_text_view and text to Time left: %1$d.

So, what’s the deal with the “%1$d”? That’s a placeholder for any integer you want to inject into your text values. You’ll fill in those placeholders later.

With the ID’s changed for your Views, Android Studio takes the IDs at build time, turning them into constants that your code can access through what’s known as the R file.

You’ll learn more about R files in the upcoming sections, but for now, know that Android takes an ID such as game_score_text_view that you assigned to your View in your Layout and creates a constant named R.id.game_score_text_view, which you can then access in your code.

Run your app now in the emulator or on a device, and you’ll see these text changes reflected on the screen:

Now that all of the Views in the project have IDs, you can finally start exploring and understanding your first Activity.

Exploring Activities

In the Project navigator on the left, ensure that the app folder is expanded. Navigate to MainActivity.kt, which is located in src/main/java/com.raywenderlich.timefighter. Open the file, and you’ll see the following contents:

package com.raywenderlich.timefighter

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

// 1
class MainActivity : AppCompatActivity() {
  // 2
  override fun onCreate(savedInstanceState: Bundle?) {
    // 3
    super.onCreate(savedInstanceState)
    // 4
    setContentView(R.layout.activity_main)
  }
}

MainActivity.kt is where the logic for your game screen goes. Take a moment to explore what it does:

  1. MainActivity is declared as extending AppCompatActivity. It’s your first and only Activity in this app. What AppCompatActivity does isn’t important right now; all you need to know is that subclassing is required to deal with content on the screen.

  2. onCreate() is the entry point to this Activity. It starts with the keyword override, meaning you’ll have to provide a custom implementation from the base AppCompatActivity class.

  3. Calling the base’s implementation of onCreate() is not only important — it’s required. You do this by calling super.onCreate. Android needs to set up a few things itself before your own implementation executes, so you notify the base class that it can do so at this point.

  4. This line takes the Layout you created and puts it on your device screen by passing in the identifier for the Layout. Android Studio generates the identifier in the R file at build time using the Layout file name.

    So, if you had a Layout named really_good_looking_screen, then the identifier generated for the layout would be R.layout.really_good_looking_screen.

These four lines are key ingredients in creating Activities for Android. You’ll see them in every Activity you create. In the most general sense, any logic you add must come after calling setContentView.

Note: onCreate() isn’t the only entry point available for Activities, but it is the one you should be most familiar with. onCreate() also works in conjunction with other methods you can override that make up an Activity’s lifecycle.

This book covers a number of those lifecycle methods, but if you’re curious to know more already, you can find out more at https://developer.android.com/guide/components/activities/activity-lifecycle.html.

Now you know the basics of how Activities work. You’re going to add some properties and placeholder functions, you’ll explore the purpose of these through the chapter.

Replace the entire contents of MainActivity.kt with the following skeleton:

package com.raywenderlich.timefighter

import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
  
  private lateinit var gameScoreTextView: TextView
  private lateinit var timeLeftTextView: TextView
  private lateinit var tapMeButton: Button

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // connect views to variables
  }

  private fun incrementScore() {
    // increment score logic
  }

  private fun resetGame() {
    // reset game logic
  }

  private fun startGame() {
    // start game logic
  }

  private fun endGame() {
    // end game logic
  }
}

Note: Sometimes, when using new objects in your classes, Android Studio won’t recognize them until you import the class definition. This is shown by Android Studio highlighting the object in red.

To import the class definition:

• macOS: Click the object and press Alt-Return

• Windows: Click the object and press Alt-Enter.

You can also choose to let Android Studio handle imports automatically for you when pasting code.

On macOS, select Android Studio â–¸ Preferences â–¸ Editor â–¸ General â–¸ Auto Import. Set Insert imports on paste to All. Finally, tick the Add unambiguous imports on the fly checkbox. Click Apply in the bottom right.

To do this on Windows or Linux, select File â–¸ Settings â–¸ Editor â–¸ General â–¸ Auto Import. Set Insert imports on paste to All. Finally, tick the Add unambiguous imports on the fly checkbox. Click Apply in the bottom right.

Hooking up Views

As an Android developer, one of the most common things your apps will do is react to button clicks, and then convert those clicks into a change reflected in the app. You’ll learn how to do that now.

In MainActivity.kt, you added three variables: gameScoreTextView, timeLeftTextView and tapMeButton. The first thing you need to do is attach these variables to the Views you added to the Layout. In onCreate(savedInstanceState: Bundle?), add the following code immediately after setContentView:

// 1
gameScoreTextView = findViewById(R.id.game_score_text_view)

timeLeftTextView = findViewById(R.id.time_left_text_view)

tapMeButton = findViewById(R.id.tap_me_button)

// 2
tapMeButton.setOnClickListener { incrementScore() }

Going through the code:

  1. findViewById searches through the activity_main Layout to find the View with the corresponding ID and provides a reference to it you store as a variable.

  2. setOnClickListener attaches a click (or tap) listener to the Button which calls incrementScore(). You’re instructing the Button to listen for a tap; then whenever it’s tapped, you increment the score.

You’re nearly ready to see your app become alive. Add a new variable to keep track of the score at the top of MainActivity.kt and initialize it to 0:

private var score = 0

Next, you need to add code to the incrementScore() method so it updates the score variable and updates gameScoreTextView with the correct text to show the user.

Replace the contents of incrementScore() with the following:

private fun incrementScore() {
  score++

  val newScore = "Your Score: $score"
  gameScoreTextView.text = newScore
}

You increment the new score variable to the next number, then use that number to create a string called newScore.

Finally, you use newScore to set the text of gameScoreTextView.

Ready to see things in action? Run the app and tap the button a few times. The score in the top-left corner of the screen increments with each tap.

You just hit a milestone in your Android app development career! You created a View, gave it an ID, accessed it in code, and reacted to user input.

These are the fundamental tasks for developing any app. You’ll repeat this cycle many times in your career, take a moment to appreciate this significant accomplishment.

Managing strings in your app

You’ve gotten your first taste of writing code, you have something up and running resembling a game, and you undoubtedly want to take things further.

One of the most important elements of any app is the text, or strings, displayed on the screen. As you move ahead in your Android development career, you’ll do well to master the ins and outs of using strings.

For instance, you’re using English labels in your app, but that doesn’t mean it’s the only language your app can support. Supporting multiple languages in your app means more people can use it if English isn’t a language they understand.

Supporting multiple languages is a feature you should consider when putting your app on the Google Play store.

In the previous section, you set the gameScoreTextView variable to use the string "Your Score: $score". This works well if you’re only targeting English-speaking users. But how would you support one, two, or even a dozen other languages?

The answer to this is String resources.

In the Project navigator, expand res/values and open strings.xml. You’ll see a file with the following content:

<resources>
    <string name="app_name">Timefighter</string>
</resources>

strings.xml gives you a place to store all of the strings used in your app. This helps to keep strings from being sprinkled throughout your code.

strings.xml also makes it easy to add support for another language. Rather than hunting through the entire project to change all of the strings, you copy the file and change it to hold the language translations of your choice.

For Timefighter, you’ll use this file to keep your English text in a separate location. Update strings.xml so it contains all the strings needed for your app:

<resources>
  <string name="app_name">Timefighter</string>
  <string name="tap_me">Tap me!</string>
  <string name="your_score">Your Score: %1$d</string>
  <string name="time_left">Time left: %1$d</string>
  <string name="game_over_message">Time\'s up! Your score was: %1$d</string>
</resources>

Now, in MainActivity.kt, go back to the incrementScore() method and update the method so it retrieves the your_score string from strings.xml and appends the score to the text:

private fun incrementScore() {
  score++

  val newScore = getString(R.string.your_score, score)
  gameScoreTextView.text = newScore
}

getString is an Activity-provided method that allows you to reference strings from the R file name or ID. Just like Views and layouts, strings in strings.xml are also given an id during build time.

In this case, you’re retrieving the strings you added earlier to strings.xml. You also pass in an int for the placeholder %1$d you added way back at the beginning of this chapter.

Note: To learn more about String Resources in Android, review the Android developer documentation at https://developer.android.com/guide/topics/resources/string-resource.html where you can also learn about string arrays and plurals.

Besides following the best practices for strings, your app is also ready for porting to another language. Sprinkling strings throughout your app is one of the worst types of technical debt to incur.

Technical debt reflects the extra development work that arises when code that is easy to implement in the short run is used instead of applying the best overall solution.

With that out of the way, you can get back to developing Timefighter.

Progressing the game

Currently, the game lets you increment the score infinitely. However, for a game named Timefighter, there isn’t much time fighting going on.

In this section, you’ll add a countdown timer to limit the amount of time you have to increase your score. You’ll useCountDownTimer for this. CountDownTimer is an Android class that starts with a value in milliseconds and counts down to zero.

At the top of MainActivity.kt, you need to declare new properties to setup your CountDownTimer. Add the following new properties underneath the View properties:

private var gameStarted = false

private lateinit var countDownTimer: CountDownTimer
private var initialCountDown: Long = 60000
private var countDownInterval: Long = 1000
private var timeLeft = 60

Here, you declare a Boolean property called gameStarted , a property to indicate when the game has started. A countdown object named countDownTimer that counts down to zero, and a count down interval variable named countDownInterval to set the rate at which countDownTimer decrements.

Finally, you declare a variable called timeLeft to hold how many seconds are left in the countdown.

With the countdown timer setup, it’s time to use it by making sure Timefighter resets itself once the counter reaches zero.

You also need to setup the game properly when Timefighter starts, you may have noticed the TextViews showing the placeholder values before they have values set. You’ll add some code to the resetGame() method here to do both jobs.

Replace resetGame() with the following method:

private fun resetGame() {
  // 1
  score = 0

  val initialScore = getString(R.string.your_score, score)
  gameScoreTextView.text = initialScore

  val initialTimeLeft = getString(R.string.time_left, 60)
  timeLeftTextView.text = initialTimeLeft

  // 2
  countDownTimer = object : CountDownTimer(initialCountDown, countDownInterval) {
    // 3
    override fun onTick(millisUntilFinished: Long) {
      timeLeft = millisUntilFinished.toInt() / 1000

      val timeLeftString = getString(R.string.time_left, timeLeft)
      timeLeftTextView.text = timeLeftString
    }

    override fun onFinish() {
      // To Be Implemented Later
    }
  }

  // 4
  gameStarted = false
}

Going through the code step by step:

  1. You first set the score to 0, then create a variable called initalScore to store the score as a string, using the getString method to insert the score value into your string stored in strings.xml. You then set the text of gameScoreTextView with this value. You repeat the process for the amount of time left in the game and assign it to timeLeftTextView.

  2. You create a new CountDownTimer object and pass into the constructor initialCountDown and countDownInterval, set to 60000 and 1000. The CountDownTimer object will count from 60000 milliseconds, or 60 seconds, in 1000 milliseconds, or 1 second, increments, until it hits zero.

  3. Inside the CountDownTimer you have two overridden methods: onTick and onFinish. onTick is called at every interval you passed into the timer; in this case, once a second. onTick also passes in a parameter called millisUntilFinished, the amount of time left before the countdown is finished.

For each interval, the timeLeft property is updated with the time remaining by converting the millisUntilFinished into seconds. You then update timeLeftTextView with this new time.

You call onFinish when CountDownTimer has finished counting down. You’ll add some code to this later.

  1. You inform the gameStarted property that the game has not started by setting it to false.

The next step is to call resetGame() when the Activity is first created. You can do this in onCreate().

Add the following line to the bottom of onCreate():

resetGame()

Starting the game

Run your app. Things will look a little less jarring. The score TextView and time left TextView now show numbers instead of placeholders. Nice!

Click the Tap me button, and you’ll notice the time left TextView doesn’t countdown! What is this madness?

The countdown doesn’t start because the app doesn’t know to start the countdown once the button is clicked. Let’s do that now. Replace startGame() with the following code:

private fun startGame() {
  countDownTimer.start()
  gameStarted = true
}

You inform the countdown timer to start. You also set gameStarted to true to say the game has indeed started.

Finally, add the following lines to the top of incrementScore to make sure the game is started when the tapMeButton is tapped:

if (!gameStarted) {
  startGame()
}

Run the app again, then tap the button to see the count-down timer working.

Nice! Your countdown timer is now ticking merrily away.

Ending the game

Huzzah! Your countdown is ticking down to zero. What happens when it gets to zero though? The answer is “nothing” because the game doesn’t know what to do after 60 seconds.

You can fix that by adding code to run once the countdown is finished and the game is finished. Since it’s the end, It would make sense to also show the user what the final score is.

In MainActivity, replace the endGame() method with the following code:

private fun endGame() {
  Toast.makeText(this, getString(R.string.game_over_message, score), Toast.LENGTH_LONG).show()
  resetGame()
}

You make use of a Toast to notify something to the user. A Toast is a small alert that pops up briefly to inform you of an event that’s occurred — in this case, the end of the game.

You pass into the Toast the Activity you want the Toast to appear on and the message to display. The end game state is a good time to display the score along with the game over message you put into strings.xml.

You also inform the Toast to display for a long time with Toast.LENGTH_LONG, which is a few seconds and then show the Toast. Once that’s done, you reset the game. You need to call endGame() from somewhere. The best time to call this is when countDownTimer finishes counting.

In the resetGame() method, add the following line within the onFinish callback:

endGame()

Run your app again, and keep clicking the button. The countdown will continue to decrement until it hits 0. Once it does, you’ll see the Toast with your score and game over message, at which point the game resets.

Can you beat this high score?
Can you beat this high score?

Key Points

Well done for setting up your first Activity. It’s a skill you’ll use over and over in your Android career. To recap, you learned:

  • What an Activity is.

  • What the onCreate() lifecycle method does.

  • How to keep strings in one place by using strings.xml.

  • How to setup a CountDownTimer.

  • How to make use of a Toast object.

Where to go from here?

With a small amount of code, you created a functional game while learning some of the foundational elements of building an Android app. Although this Activity is small, activities can get complicated as you add more Views. However, no matter its size, creating an Activity has the same flow:

  1. Create a Layout for the Activity.
  2. Give the Views in your Layout valid IDs.
  3. Create properties in the Activity code and reference those IDs.
  4. Manipulate your Views as needed or required.

Next, you’ll learn how to fix potential problems in your app using Android debugging techniques.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.