Chapter 4 - Suggestion for Cleaner Code

I am an experienced iOS developer enjoying and finding useful this book. I do, however, have a suggestion.

Avoiding magic numbers and repetition of code are important principles in software development. The Timefighter example app does not demonstrate these principles, and I am concerned that newcomers to software development might learn bad habits and potentially miss out on job opportunities because of magic numbers or repeated code in their try-out apps. I recommend modifying Timefighter so that there are no magic numbers or repeated code.

Here is the repeated code:

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

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

    override fun onFinish() {
      endGame()
    }
}

...

countDownTimer = object : CountDownTimer((timeLeft * 1000).toLong(), countDownInterval) {
    override fun onTick(millisUntilFinished: Long) {

      timeLeft = millisUntilFinished.toInt() / 1000

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

    override fun onFinish() {
      endGame()
    }
}

Here is the magic number:

val initialTimeLeft = getString(R.string.time_left, Integer.toString(60))

This is also code repetition because the 60 represents the same concept as the 6000 in this line:

internal var initialCountDown: Long = 60000

I have refactored GameActivity.kt to fix both problems.

class GameActivity : AppCompatActivity() {
    internal lateinit var gameScoreTextView: TextView
    internal lateinit var timeLeftTextView: TextView
    internal lateinit var tapMeButton: Button
    internal var gameStarted = false
    internal lateinit var countDownTimer: CountDownTimer
    internal var initialCountDown = 60000
    internal var countDownInterval = 1000
    internal var timeLeft = initialCountDown / countDownInterval
    internal var score = 0
    internal val TAG = GameActivity::class.java.simpleName
    internal val millisecondsPerSecond = 1000

    companion object {
        private val SCORE_KEY = "SCORE_KEY"
        private val TIME_LEFT_KEY = "TIME_LEFT_KEY"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_game)
        Log.d(TAG, "onCreate called. Score is: $score")
        gameScoreTextView = findViewById<TextView>(R.id.game_score_text_view)
        timeLeftTextView = findViewById<TextView>(R.id.time_left_text_view)
        tapMeButton = findViewById<Button>(R.id.tap_me_button)
        tapMeButton.setOnClickListener { v -> incrementScore() }
        if (savedInstanceState != null) {
            restoreGame(savedInstanceState.getInt(SCORE_KEY), savedInstanceState.getInt(TIME_LEFT_KEY))
        } else {
            resetGame()
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(SCORE_KEY, score)
        outState.putInt(TIME_LEFT_KEY, timeLeft)
        countDownTimer.cancel()
        Log.d(TAG, "onSaveInstanceState: Saving Score: $score & Time Left: $timeLeft")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy called.")
    }

    private fun incrementScore() {
        if (!gameStarted) {
            startGame()
        }
        score++
        gameScoreTextView.text = getString(R.string.your_score, Integer.toString(score))
    }

    private fun resetGame() {
        score = 0
        timeLeft = initialCountDown / countDownInterval
        initializeGame()
        gameStarted = false
    }

    private fun initializeGame() {
        gameScoreTextView.text = getString(R.string.your_score, Integer.toString(score))
        timeLeftTextView.text = getString(R.string.time_left, Integer.toString(timeLeft))
        countDownTimer = object : CountDownTimer((timeLeft * millisecondsPerSecond).toLong(), countDownInterval.toLong()) {
            override fun onTick(millisUntilFinished: Long) {
                timeLeft = millisUntilFinished.toInt() / millisecondsPerSecond
                val timeLeftString = getString(R.string.time_left, Integer.toString(timeLeft))
                timeLeftTextView.text = timeLeftString
            }
            override fun onFinish() { endGame() }
        }
    }

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

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

    private fun restoreGame(score: Int, timeLeft: Int) {
        this.score = score
        this.timeLeft = timeLeft
        initializeGame()
        startGame()
    }
}

Hi kithril,

Thank you for reaching out with this suggestion. You’re completely right, we don’t do a good job of adhering to these principles in chapter 4.

We’ll take this on board and make sure we adhere to these principles in a future edition of Android Apprentice.

In the meantime, we hope you enjoy the book. Let us know if you have anymore suggestions, we’ll be happy to listen.

Thanks,

Darryl

The book is strong, or at least was at the time of release, when it presumably covered the then-current versions of Android, Android Studio, and Kotlin.

Since you asked for suggestions, though, I reiterate the request of other posters that you update the book. There have been no updates for the current versions of Android, Android Studio, and Kotlin, which means that if I use the book, I must either learn old stuff or (the code in the book doesn’t compile as-is && the Android Studio instructions are incorrect). Today I found the Chapter-11 app impossible to get working, either by creating it from scratch or by using the starter project. As an Android apprentice, I lack the expertise to troubleshoot Gradle errors. This experience did not meet my high expectations for Ray Wenderlich products.

I had not mentioned this in the forum because I have read in several threads, dating back months, that the book will be updated soon.

Chapter 15 uses a deprecated Places SDK. The new one works quite differently and appears to require that billing be set up. Migrating to the New Places SDK Client  |  Places SDK for Android  |  Google Developers

Thanks for the feedback!

The plan is to release a new version of the book very soon. The new version is updated to take into account changes to Android Studio, Gradle and Kotlin. I can also confirm Chapter 15 is updated to use the new Place SDKs.

We will announce when the 2nd edition is ready for people to enjoy.

Darryl