teaching machines

SENG 440: Lecture 6 – Explicit Intents

Dear students,

Last time we got our apps to invoke other apps indirectly through an Intent. Our requests were implicit; we only described the service we needed performed. The OS then offered us a list of all the apps that could do the job. Today, we look at invoking specific Activitys through explicit Intents. We mostly explicitly invoke other activities because they represent other screens within the same app. We’ll examine implicit Intents through the context of a flashcard app for learning hiragana and katakana.

Before we forget, here’s your TODO list for next time:

  • Share about this week’s progress on your project before tomorrow.
  • Android contains a lot of magic—documented magic. Browse Common Intents and this list activity attributes in the manifest.
  • Investigate two intents and two attributes that seem interesting to you. Define their purpose in your own words on a quarter sheet of paper to be turned in at the beginning of the next class.

Let’s start making our app, which we’ll call Kana-san.

Kana-san

Japanese actually has three symbol sets in popular use: kanji, hiragana, and katakana. Kanji are the most intricate and numerous. Kanji is not a phonetic alphabet; how each kanji is pronounced depends on its context. Hiragana and Katana, on the other hand, each contain 46 symbols representing different phonemes. To narrow the scope of our app, we will concentrate on just hiragana and katakana.

Kana-san is going to be a simple app—because we have to write it in 50 minutes. The first screen will show two buttons: Hiragana and Katakana. Clicking on a button will pop up a second activity that quizzes the user on five random symbols and then returns to the main activity. We will use the Intent class to drive the navigation between activities.

Kotlin Objects

First, let’s create a database of our two alphabets. Here’s a list of the hiragana, which are used for word endings and connecting words:

"あ", "い", "う", "え", "お",
"か", "き", "く", "け", "こ",
"さ", "し", "す", "せ", "そ",
"た", "ち", "つ", "て", "と",
"な", "に", "ぬ", "ね", "の",
"は", "ひ", "ふ", "へ", "ほ",
"ま", "み", "む", "め", "も",
"や", "ゆ", "よ",
"ら", "り", "る", "れ", "ろ",
"わ", "を",
"ん",
"が", "ぎ", "ぐ", "げ", "ご",
"ざ", "じ", "ず", "ぜ", "ぞ",
"だ", "ぢ", "づ", "で", "ど",
"ば", "び", "ぶ", "べ", "ぼ",
"ぱ", "ぴ", "ぷ", "ぺ", "ぽ",
"きゃ", "きゅ", "きょ",
"しゃ", "しゅ", "しょ",
"ちゃ", "ちゅ", "ちょ",
"にゃ", "にゅ", "にょ",
"ひゃ", "ひゅ", "ひょ",
"みゃ", "みゅ", "みょ",
"りゃ", "りゅ", "りょ",
"ぎゃ", "ぎゅ", "ぎょ",
"じゃ", "じゅ", "じょ",
"びゃ", "びゅ", "びょ",
"ぴゃ", "ぴゅ", "ぴょ"

Here’s a list of the katakana, which are used for words borrowed from other languages:

"ア", "イ", "ウ", "エ", "オ",
"カ", "キ", "ク", "ケ", "コ",
"サ", "シ", "ス", "セ", "ソ",
"タ", "チ", "ツ", "テ", "ト",
"ナ", "ニ", "ヌ", "ネ", "ノ",
"ハ", "ヒ", "フ", "ヘ", "ホ",
"マ", "ミ", "ム", "メ", "モ",
"ヤ", "ユ", "ヨ",
"ラ", "リ", "ル", "レ", "ロ",
"ワ", "ヲ",
"ン",
"ガ", "ギ", "グ", "ゲ", "ゴ",
"ザ", "ジ", "ズ", "ゼ", "ゾ",
"ダ", "ヂ", "ヅ", "デ", "ド",
"バ", "ビ", "ブ", "ベ", "ボ",
"パ", "ピ", "プ", "ペ", "ポ",
"キャ", "キュ", "キョ",
"シャ", "シュ", "ショ",
"チャ", "チュ", "チョ",
"ニャ", "ニュ", "ニョ",
"ヒャ", "ヒュ", "ヒョ",
"ミャ", "ミュ", "ミョ",
"リャ", "リュ", "リョ",
"ギャ", "ギュ", "ギョ",
"ジャ", "ジュ", "ジョ",
"ビャ", "ビュ", "ビョ",
"ピャ", "ピュ", "ピョ"

And here’s a list of the phonetic pronunciations of these symbols, which we sometimes call romaji:

"a", "i", "u", "e", "o",
"ka", "ki", "ku", "ke", "ko",
"sa", "shi", "su", "se", "so",
"ta", "chi", "tsu", "te", "to",
"na", "ni", "nu", "ne", "no",
"ha", "hi", "fu", "he", "ho",
"ma", "mi", "mu", "me", "mo",
"ya", "yu", "yo",
"ra", "ri", "ru", "re", "ro",
"wa", "wo",
"n",
"ga", "gi", "gu", "ge", "go",
"za", "ji", "zu", "ze", "zo",
"da", "ji", "zu", "de", "do",
"ba", "bi", "bu", "be", "bo",
"pa", "pi", "pu", "pe", "po",
"kya", "kyu", "kyo",
"sha", "shu", "sho",
"cha", "chu", "cho",
"nya", "nyu", "nyo",
"hya", "hyu", "hyo",
"mya", "myu", "myo",
"rya", "ryu", "ryo",
"gya", "gyu", "gyo",
"ja", "ju", "jo",
"bya", "byu", "byo",
"pya", "pyu", "pyo"

Let’s turn these lists into three parallel arrays nicely contained in some structure. In Java, we might create a Japanese class and declare three static arrays of String. Kotlin classes don’t have a concept of static. If we want singleton data, we create an object instead, which is a singleton by definition:

object Japanese {
  val hiragana = arrayOf(...)
  val katakana = arrayOf(...)
  val romaji = arrayOf(...)
}

We can access these just as we would in Java:

Japanese.hiragana[0]

Let’s create our first screen.

MainActivity

We’ll create our main activity’s layouts by completing the following steps:

  • Use ConstraintLayout as the root view group.
  • Add two buttons labeled Hiragana and Katakana.
  • Select the buttons and add a vertical chain.
  • Center the buttons horizontally.
  • Cycle the chain until the buttons are pushed together in the center.
  • Add a TextView to show the last quiz score.

The buttons and text are wired up in the usual way:

private lateinit var scoreText: TextView

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

  scoreText = findViewById(R.id.scoreText)
  val katakanaButton: Button = findViewById(R.id.katakanaButton)
  val hiraganaButton: Button = findViewById(R.id.hiraganaButton)
}

The buttons don’t need to be instance variables because they will only be referenced within onCreate. The TextView will get updated in a separate method that we write later, so it needs to have a wider scope.

Before we handle button clicks, let’s create the activity for quizzing the user.

QuizActivity

We’ll show the user a symbol from one of the alphabets and expect them to type in the romaji pronunciation. The UI will contain just two widgets, which we’ll create with the following steps:

  • Add a large TextView to the center of the screen.
  • Add an EditText anchored to the bottom of the screen.
  • Make the EditText accept only a single line on input with inputType set to text and maxLines set to 1.

This activity’s onCreate grabs references to the widgets in the usual manner:

private lateinit var kanaText: TextView
private lateinit var romajiBox: EditText

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_quiz)
  kanaText = findViewById(R.id.kanaText)
  romajiBox = findViewById(R.id.romajiBox)
}

Now the question is what symbol do we show the user? That depends on whether we are in hiragana or katakana mode. This activity doesn’t really care which alphabet is being used, so let’s avoid hardcoding any conditionals to choose between them. Instead, let’s just declare this placeholder array for the time being:

private lateinit var alphabet: Array<String>

Implicit Intents

Back in the main activity, we will fire off the quiz activity with an Intent. Since the main activity is where we have knowledge of which alphabet is being quizzed, we will supply the appropriate array via the Intent.

We are not asking for a service here, so our Intent will be much more explicit. We actually name the class using a class literal:

katakanaButton.setOnClickListener {
  val intent = Intent(this, QuizActivity::class.java)
}

Class literals in Kotlin are noisier compared to their Java counterparts. In Java we would have written QuizActivity.class.

We can send along the alphabet as an extra and start the activity:

katakanaButton.setOnClickListener {
  val intent = Intent(this, QuizActivity::class.java)
  intent.putExtra("alphabet", Japanese.katakana)
  startActivity(intent)
}

hiraganaButton.setOnClickListener {
  val intent = Intent(this, QuizActivity::class.java)
  intent.putExtra("alphabet", Japanese.hiragana)
  startActivity(intent)
}

Back in the quiz activity, we can pull down the alphabet in onCreate:

alphabet = intent.getStringArrayExtra("alphabet")

Gameplay

We next flesh out the gameplay in the simplest way possible. Let’s first create a helper method to start a new round. We must randomly pick a symbol from the alphabet, call requestFocus to trigger the software keyboard, and clear any text from a previous round:

private var currentIndex: Int = 0

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

fun nextRound() {
  currentIndex = Random.nextInt(alphabet.size)
  kanaText.text = alphabet[currentIndex]

  romajiBox.setText("")
  romajiBox.requestFocus()
}

One annoying bit here is that when the software keyboard shows, the kana text gets pushed off the screen. By default, the software keyboard shoves the view up and off the top of the screen. In the manifest, we can force it to recompute the layout through the activity’s windowSoftInputMode attribute:

<activity
  android:name=".QuizActivity"
  android:windowSoftInputMode="adjustResize"></activity>

To check if the user types in the correct romaji, we could add a submit button. However, for simple forms, mobile platforms tend to allow us to submit directly through the keyboard. Android’s EditText will make us aware of certain editor actions—like being DONE with a form—if we register a callback like this:

romajiBox.setOnEditorActionListener { textView, actionId, keyEvent ->
  ...
  true
}

We are asked to return a boolean indicating whether or not we’ve handled the event. If we return false, it will look for some other entity to handle it. Several events get propogated to this callback. We use actionId to figure out which one. In our case, we listen for ACTION_DONE events and progress if the user has entered the correct answer:

romajiBox.setOnEditorActionListener { _, i, _ ->
  if (i == EditorInfo.IME_ACTION_DONE) {
    if (romajiBox.text.toString() == Japanese.romaji[currentIndex]) {
      nextRound()
      Toast.makeText(this, "Right!", Toast.LENGTH_SHORT).show()
    } else {
      Toast.makeText(this, "Wrong!", Toast.LENGTH_SHORT).show()
    }
    true
  } else {
    false
  }
}

The feedback is poor. But 50 minutes… In a better world, we would shake the EditText with an animation or emit sounds.

Finishing an Activity

We want to stop the quiz after five rounds, which calls for an instance variable and some extra logic in nextRound. To close the activity, we call finish:

private var round: Int = 0

fun nextRound() {
  if (round >= 5) {
    finish()
  } else {
    // old code
    round += 1
  }
}

When an activity closes, we automatically return to the topmost activity in the back stack, which in our case is the main activity.

Returning Results

Suppose we want to show a message on the main screen listing the player’s most recent score. This would require us to communicate some information back to the main activity when the quiz activity finishes. We can do this.

We first add crude scoring to the quiz:

private var score: Int = 0

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

  romajiBox.setOnEditorActionListener { _, i, _ ->
    if (i == EditorInfo.IME_ACTION_DONE) {
      if (romajiBox.text.toString() == Japanese.romaji[currentIndex]) {
        ...
        score += 10
      } else {
        ...
        score -= 5
      }
      true
    } else {
      false
    }
  }
}

We ship the score back to the caller through an Intent, interestingly. We add this code to nextRound:

val result = Intent()
result.putExtra("score", score)
setResult(Activity.RESULT_OK, result)
finish()

The main activity also needs to change. Spawning an activity is not like spawning a method call, where we return to the succeeding statement after the call. Rather, we need a callback that gets triggered when a child activity finishes:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
  super.onActivityResult(requestCode, resultCode, data)
  scoreText.text = "Last score: ${data.getIntExtra("score", 0)}"
}

Note that main activity might spawn off a number of different child activities. We can distinguish them via the requestCode parameter, which we must also supply when we invoke an activity whose return we await. The code is internal to us, so we can pick any number. Since Kotlin doesn’t have statics, let’s declare this code at the top-level:

const val QUIZ = 100

Our startActivity calls get changed accordingly:

startActivityForResult(intent, QUIZ)

In onActivityResult, we should only handle the quiz activity, and we should only do so when it finishes successfully:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
  super.onActivityResult(requestCode, resultCode, data)
  if (requestCode == QUIZ) {
    if (resultCode == Activity.RESULT_OK) {
      scoreText.text = "Last score: ${data.getIntExtra("score", 0)}"
    }
  }
}

We only have two activities, so these conditionals aren’t all that important. But an app with two activities soon becomes one with three, then four, and so on, after just a little dreaming.

That’s enough for today, however. See you next time!

Sincerelya

P.S. It’s time for a haiku!

Parenting pro-tip
Use your kid’s name in commands
“Max, eat this cookie”

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *