简体   繁体   中英

The best way to use one class, with only one changed object inside - Kotlin/Android/Java

I've created simple game on android. Now I want to add difficulty levels and I'm wondering what approach to take. I mean, I need to change only one object (CountDownTimer) inside class to change that difficulty, and first I thought that I will make just 3 classes for every level, but my intuition tells me that will be big overkill. Do a class and inherit from it? Or maybe create in some way an object with a parameter? Can you suggest something?

package com.whayway.beerrandom

import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.whayway.beerrandom.databinding.FragmentGameBinding
import kotlinx.android.synthetic.main.fragment_game.*
import java.util.*


const val KEY_SCORE = "score_key"

class GameFragment : Fragment() {
    var handler: Handler = Handler()
    var score: Int = 0
    var runnable: Runnable = Runnable { }
    var imageArray = ArrayList<ImageView>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = DataBindingUtil.inflate<FragmentGameBinding>(
            inflater, R.layout.fragment_game, container, false
        )
        score = 0

        if(savedInstanceState != null){
            score = savedInstanceState.getInt(KEY_SCORE, 0)
        }
        hideImages()

        object : CountDownTimer(15000, 1000) {
            override fun onTick(p0: Long) {
                btn_ok.visibility = View.INVISIBLE

                timeText.text = "Time: " + (p0 / 1000)
            }
            override fun onFinish() {
                timeText.text = "Time out"
                handler.removeCallbacks(runnable)
                for (image in imageArray) {
                    image.visibility = View.INVISIBLE
                    btn_ok.visibility = View.VISIBLE
                }
            }
        }.start()

        score = 0
        setHasOptionsMenu(true)

/*        val args = .fromBundle(arguments!!)
        Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()*/
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        imageArray = arrayListOf(
            imageView8,
            imageView7,
            imageView6,
            imageView5,
            imageView4,
            imageView3,
            imageViewPawel,
            imageView111
        )
        scoreText.setOnClickListener {}
        imageView8.setOnClickListener { increaseScore() }
        imageView7.setOnClickListener {  increaseScore() }
        imageView8.setOnClickListener{increaseScore() }
        imageView6.setOnClickListener {  increaseScore() }
        imageView5.setOnClickListener {  increaseScore() }
        imageView4.setOnClickListener { increaseScore()
            setImage() }
        imageView3.setOnClickListener { decreaseScore()
            setImage() }
        imageViewPawel.setOnClickListener { increaseScorePawel()
            setImage()}
        imageView111.setOnClickListener { increaseScore()
            setImage()}

        btn_ok.setOnClickListener { view: View ->
            view.findNavController().navigate(
                GameFragmentDirections
                    .actionGameFragmentToResultFragment(score)
            )
        }
        super.onViewCreated(view, savedInstanceState)
    }
        private fun  setImage(){
            var drawableArray  = ArrayList<Int>()
            drawableArray = arrayListOf(
            R.drawable.pawel,
            R.drawable.leszek,
            R.drawable.lech_free)

            val random = Random()
            val index = random.nextInt(3 - 1)
            val index2 = random.nextInt(8 - 1)
            imageArray[index2].setImageResource(drawableArray[index])
    }
    private fun hideImages() {
        runnable = Runnable {
            for (image in imageArray) {
                image.visibility = View.INVISIBLE
            }
            val random = Random()
            val index = random.nextInt(8 - 1)

            imageArray[index].visibility = View.VISIBLE

            handler.postDelayed(runnable, 500)
        }
        handler.post(runnable)
    }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(KEY_SCORE, score)
    }
    private fun increaseScore() {
        score++
        scoreText.text = "Score:" + score
    }
    private fun increaseScorePawel() {
        score += 5
        scoreText.text = "Score:" + score
    }
    private fun decreaseScore() {
        score -= 10
        scoreText.text = "Score:" + score
    }
}

There's no single best answer to your question but the solution that makes the most sense to me is to create an implementation of CountDownTimer.

I'm not sure what it is that you want to change inside of the counter, but you should be able to deal with most of the things by passing a value to the constructor:

class GameTimer(interval: Long) : CountDownTimer(15000, interval) {
    ...
}

or a callback (custom behavior):

class GameTimer(onTimeOut: () -> Unit) : CountDownTimer(15000, 1000) {
    ...
    
    override fun onFinish() { onTimeOut() }
}

I think it would be a good idea to introduce factory design pattern. Since it may sound meaningless to you, let me show you an example:

// could be also a class with constructor parameters, it all depends on your use case
object GameTimerFactory {
  fun easyGameTimer() = GameTimer(1000)
  fun mediumGameTimer() = GameTimer(800)  
  fun hardGameTimer() = GameTimer(650)
}

depending on how you will call it from higher level component you may want to use an enum:

enum class Difficulty { EASY, MEDIUM, HARD }

object GameTimerFactory {
  fun gameTimerFor(difficulty: Difficulty): GameTimer = when (difficulty) {
    ...
  }
}

Also, a tip: it would be wise to extract game logic from android logic in order to conform SoC principle . tl;dr: in every program there are some layers, for example in yours we can easily see an android-specific layer that displays the game and a layer that contains game rules. It will be useful in case you want to port the game to other platforms (eg desktop) and pretty much necessary if you want to unit test the game logic. There are lots of nice explanations out there:

You are creating the CountDownTimer using this method:

CountDownTimer(long millisInFuture, long countDownInterval)

So, just add two properties in your class: millisInFuture and countDownInterval.

Then, create a constructor to set them.

When you instantiate your class, use the constructor and pass the value you want for your CountDownTimer.

My advice is to do not repeat code to make 3 Fragment classes for your purpose. There is a popular programming term - DRY (acronym to Do Not Repeat Yourself).

As I understand - you need changes only in CountDownTimer object. So, you can write 3 of them in other files with names. As it is described in docs: objects . Use them depend on user's choice.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM