2
\$\begingroup\$

I create simple Android timer application and I try to use MVP design pattern. I read some tutorials and after that I started writing code. My code is working correctly and as intended. There is really simple activity with 3 buttons (start, pause and stop) and text view with time to finish. I have always had problem with proper implementation of app architecture patterns so I'd like to know whether or not I properly separated view and business logic (in a way MVP pattern intended to). My code looks like this.

View:

interface TimerView {
    fun updateTime(time: Long)
    fun disableStop()
    fun disableStart()
    fun disablePause()
    fun enableStop()
    fun enableStart()
    fun enablePause()
}

class MainActivity : AppCompatActivity(), TimerView {
    private var presenter = TimerPresenter(this)

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

    override fun onResume() {
        super.onResume()
        presenter.restoreState(this)
    }

    override fun onPause() {
        super.onPause()
        presenter.saveState(this)
        presenter.stopTimer()
    }

    fun stopTimer(view: View) {
        presenter.stopTimer()
        presenter.resetTimer()
    }

    fun startTimer(view: View) {
        presenter.startTimer()
    }

    fun pauseTimer(view: View) {
        presenter.pauseTimer()
    }

    override fun updateTime(time: Long) {
        timeText.text = timeToString(time)
    }

    override fun disableStop() {
        stopFab.isEnabled = false
    }

    override fun disableStart() {
        startFab.isEnabled = false
    }

    override fun disablePause() {
        pauseFab.isEnabled = false
    }

    override fun enableStop() {
        stopFab.isEnabled = true
    }

    override fun enableStart() {
        startFab.isEnabled = true
    }

    override fun enablePause() {
        pauseFab.isEnabled = true
    }
}

Presenter:

class TimerPresenter(private val view: TimerView) : TimerListener {
    private val timer = Timer(this)

    fun stopTimer() {
        timer.stop()
    }

    fun startTimer() {
        timer.start()
    }

    fun pauseTimer() {
        timer.pause()
    }

    fun resetTimer() {
        timer.reset()
    }

    fun saveState(context: Context) {
        timer.saveState(context)
    }

    fun restoreState(context: Context) {
        timer.restoreState(context)
    }

    private fun setStopView() {
        view.disableStop()
        view.disablePause()
        view.enableStart()
    }

    private fun setStartView() {
        view.disableStart()
        view.enableStop()
        view.enablePause()
    }

    private fun setPauseView() {
        view.disablePause()
        view.enableStop()
        view.enableStart()
    }

    private fun setViewState(state: TimerData.State) {
        when (state) {
            TimerData.State.Stop -> setStopView()
            TimerData.State.Run -> setStartView()
            TimerData.State.Pause -> setPauseView()
        }
    }

    private fun updateTimer(timeRemaining: Long) {
        view.updateTime(timeRemaining)
    }

    override fun onTimerFinish() {
        timer.reset()
    }

    override fun onTimerTick(timeRemaining: Long) {
        updateTimer(timeRemaining)
    }

    override fun onTimerStateChange(newState: TimerData.State) {
        setViewState(newState)
    }

    override fun onTimerReset(timeRemaining: Long) {
        updateTimer(timeRemaining)
    }

    override fun onTimerStateRestored(timeRemaining: Long, state: TimerData.State) {
        updateTimer(timeRemaining)
        setViewState(state)
        if (state == TimerData.State.Run) {
            timer.start()
        }
    }
}

Model:

interface TimerListener {
    fun onTimerFinish()
    fun onTimerTick(timeRemaining: Long)
    fun onTimerStateChange(state: TimerData.State)
    fun onTimerReset(timeRemaining: Long)
    fun onTimerStateRestored(timeRemaining: Long, state: TimerData.State)
}

data class TimerData(var state: State = State.Stop,
                     var timerLength: Long = 0L,
                     var timeRemaining: Long = 0L) {
    enum class State {
        Stop,
        Run,
        Pause
    }

    fun loadData(context: Context) {
        state = PrefUtils.getState(context)
        timerLength = PrefUtils.getTimerLength(context)
        timeRemaining = if (state != State.Stop) {
            PrefUtils.getTimeRemaining(context)
        } else {
            timerLength
        }
    }

    fun saveData(context: Context) {
        PrefUtils.setState(context, state)
        PrefUtils.setTimeRemaining(context, timerLength)
        PrefUtils.setTimeToStop(context, timeRemaining)
    }

    fun reset() {
        timeRemaining = timerLength
    }
}

class Timer(private val listener: TimerListener) {
    private var data = TimerData()
    private var timer: CountDownTimer? = null

    fun start() {
        timer = object : CountDownTimer(data.timeRemaining * 1000L, 1000L) {
            override fun onFinish() {
                stop()
                listener.onTimerFinish()
            }

            override fun onTick(millisUntilFinished: Long) {
                --data.timeRemaining
                listener.onTimerTick(data.timeRemaining)
            }
        }.start()
        data.state = TimerData.State.Run
        listener.onTimerStateChange(data.state)
    }

    fun stop() {
        timer?.cancel()
        data.state = TimerData.State.Stop
        listener.onTimerStateChange(data.state)
    }

    fun pause() {
        timer?.cancel()
        data.state = TimerData.State.Pause
        listener.onTimerStateChange(data.state)
    }

    fun reset() {
        data.reset()
        listener.onTimerReset(data.timeRemaining)
    }

    fun saveState(context: Context) {
        data.saveData(context)
    }

    fun restoreState(context: Context) {
        data.loadData(context)
        listener.onTimerStateRestored(data.timeRemaining, data.state)
    }
}

Did I separate tasks correctly between classes or maybe there is something wrong with my code?

\$\endgroup\$
  • 2
    \$\begingroup\$ "I create simple Android application" Ok, good. Tell us more. Does it work as intended? What is it supposed to do? You used MVP, ok, but to do what? What problem does your code solve? \$\endgroup\$ – Mast Oct 24 '18 at 15:26
  • \$\begingroup\$ Yes, everything is working as intended. I won't be asking here if it does't. If you ask about application it simple timer with 3 buttons (start, pause and stop) and text view showing time to finish. When it comes to code I want to separate view from business logic using MVP pattern. I think I did it, as far as I know, but I want to know whether or not I did it correctly. \$\endgroup\$ – ostojan Oct 24 '18 at 16:54
  • 2
    \$\begingroup\$ @ostojan, you should edit the question (including its title) to incorporate that information - the purpose of the code should be in the question. Thanks. \$\endgroup\$ – Toby Speight Oct 24 '18 at 17:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.