Group Group Group Group Group Group Group Group Group

Chapter 4 - Suggestion for Cleaner Code

#1

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()
    }
}
#2

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

#3

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. https://developers.google.com/places/android-sdk/client-migration

#4

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