V2-Android StopWatch ( New Era of Rx )

This post is the part of the New Era of Rx Android StopWatch App. In this part, we are going to implement version 2 of Android StopWatch. We already got the requirement which I am going to repeat again.

Code will be available on GitHub.

Until now, everyone aware of the logic of stopwatch. So, I am going to plan our architecture again from zero.

We can divide our app into layers: global constants, UI Layer, and Logical or Business Layer.

Global Constants:

In our case, we need two constants: one maximum time limit and the second number of seconds in one minute.

const val MAXIMUM_STOP_WATCH_LIMIT = 3600L
const val NUMBER_OF_SECONDS_IN_ONE_MINUTE = 60

UI Layer:

We can analyze this layer by reviewing our final design.

As per our design, we will have three buttons and one textView.

Our buttons states are dependent upon each other. We have a total of three states of our buttons. At any time they will be in one of them. Questions are how I know these states? So basically that is clear from the requirement plus the stopwatch default behavior.

A stopwatch always has three states. Idle, Running, Pause, and our buttons always have enabled or disabled state, based on these three states, as shown below:

IDLE: ( START = ENABLE, PAUSE = DISABLE, RESET = DISABLE )

RUNNING: ( START = DISABLE, PAUSE = ENABLE, RESET = ENABLE )

PAUSE: ( START = ENABLE, PAUSE = DISABLE, RESET = ENABLE )

To achieve this, I am going with enum class as shown below:

enum class StopWatch {
Idle, Running, Pause
}

I am going to define buttons states, again the StopWatch enum value.

// first = StartButton, second = PauseButton, third = ResetButton
private val IDLE_BUTTON_STATE = Triple(true, false, false)
private val RUNNING_BUTTON_STATE = Triple(false, true, true)
private val PAUSE_BUTTON_STATE = Triple(true, false, true)

Another UI part is time displayer. Time displayer again has the three states just as our buttons, as shown below:

IDLE:

RUNNING:

PAUSE:

To achieve this, I need seconds value from the business layer and after that UI layer will do format according to the UI requirement and will do render on UI.

So it’s mean I will get Long value, and then I need to write a formatted of time displayer.

private val timeFormatter: (Long) -> String =
    { secs ->
        if (secs == MAXIMUM_STOP_WATCH_LIMIT || secs == 0L) displayInitialState
        else "${secs / NUMBER_OF_SECONDS_IN_ONE_MINUTE} : ${secs % NUMBER_OF_SECONDS_IN_ONE_MINUTE}"
    }

All UI states are final, now we need a some UI State class, which will provide us new state and save that state. For that, I am going with a data class, as shown below:

data class StopWatchUIState(
val seconds: Long = 0L,
val state: StopWatch = StopWatch.Idle
)

Until now, code is available on Github.
v2-android-stop-watch-ui-layer

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".StopWatchMainActivity">

<Button
android:id="@+id/startButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:text="@string/start"
app:layout_constraintEnd_toStartOf="@+id/pauseButton"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/resetButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/reset"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/pauseButton"
app:layout_constraintTop_toTopOf="@+id/startButton" />

<TextView
android:id="@+id/display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/_0_0"
android:textColor="@android:color/black"
android:textSize="40sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/startButton" />

<Button
android:id="@+id/pauseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="Pause"
app:layout_constraintEnd_toStartOf="@+id/resetButton"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/startButton"
app:layout_constraintTop_toTopOf="@+id/startButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

If you run this code, the app will run but always show you Idle state. But before going to the next layer, we can play with this code. Simple go inside of our on create method and do a call our render method with a mock or random value, as shown below:

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

    StopWatchUIState(350, StopWatch.Running)
        .let(::render)
}

Fantastic, so our UI layer is in good shape, Independent from business logic. Only UI needs the StopWatchUIState. We don’t care about where we are getting that. So we will use this decoupling when we write espresso tests. Only do one more experiment. Try to give maximum value, as shown below:

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

    StopWatchUIState(3600, StopWatch.Running)
        .let(::render)
}

If you run this code, basically you will get the IDLE state on the UI as shown below:

Until now, we can make a pictorial comparison to make thing more transparent for everyone.

Now, we need to fill the black box in the above image. In the case of testing, that will be something mock value generator and in case of the app, we will create a business layer and will replace that black box with our business layer.

Business or Logical Layer:

It’s time to analyze our Business Layer what we need from this layer and what we need to provide to this layer.
I will provide a state of the StopWatch, and I will expect seconds value with the provided state. If you give more attention, this looks like something a function. For example:

fun businessLayer(state: StopWatch) : StopWatchUIState{
    TODO()
}

It’s mean our architecture is going in the right direction. Business or Logical Layer always give you this feeling. If you are not getting this feeling, then try to revise again. Sometimes unintentionally we gave the UI responsibility to this layer.

I am going to create our layer as shown below:

class StopWatchViewModel {

    fun provide(observable: Observable<StopWatch>): Disposable {
        TODO()
    }

    fun uiState(): Observable<StopWatchUIState> {
        TODO()
    }
}

Next, we need to write a glue code between provide and uiState methods, and for that, we will go with PublishSubject. As we know, Subjects are Observable + Observer.
If you are new to Subjects you can consult from my Subjects blog post.
Confusion between Subject and Observable + Observer Part8

class StopWatchViewModel {

    private val publishSubject = PublishSubject.create<StopWatch>()

    fun provide(observable: Observable<StopWatch>): Disposable =
        observable.subscribe(publishSubject::onNext)


    fun uiState(): Observable<StopWatchUIState> =
    publishSubject.switchMap {
        when (it) {
            StopWatch.Idle -> {
                Observable.just(StopWatchUIState())
            }
            StopWatch.Running -> {
                timerObservable()
                    .map { sec -> StopWatchUIState(sec, StopWatch.Running) }
            }
            StopWatch.Pause -> {
                TODO()
            }
        }
    }.hide()
        .observeOn(AndroidSchedulers.mainThread())


    private fun timerObservable() =
        Observable.interval(0L, 1L, TimeUnit.SECONDS)
            .takeWhile { it <= MAXIMUM_STOP_WATCH_LIMIT }
}

Until now, nothing fancy but we can discuss one by one.

  • timerObservable() is the same method which we used in version 1.
  • Subject, as we know, will work as observer + observable. In our case, when provide() method will be called on UI side, we will get the Observable<StopWatch>, this will emit the value of a button. So here we subscribe to our publishSubject as an observer. Next, when a user attaches with uiState() at that time, we are returning our subject as an Observable, which will provide the StopWatchUIState. After this line, our black box is complete, but without the implementation of Pause functionality.

Until now, you can check the code on Github. ( For now, do not click on the Pause button, if you click app will crash due to TODO() :))
v2-android-stop-watch-business-layer-with-out-pause-functionality
By the way, we will discuss the glue code between UI and Business layer after Pause functionality implementation.

Pause functionality in Rx stream is a little bit tricky. So first we will discuss with digrams, and later we write code.

In the above image, we created the flow of Start and Reset. Now consider all the arrows are the stream. In case of Pause, what we need, theoretically we want when user click on Pause, we will take the current value of Runnin stream and save that to resume, and we need to pause a stream, but the problem is in Rx, we are not able to pause a stream, stream need to flow continuously and will reach to its subscriber. So what we should do in this case? Okay, we can divide our requirement so that we can find out a good solution.

  1. We need to save the current value of a timer
  2. We need to pause the stream

Now, In case 1, what are the operators or concepts can help us to save the state. Scan operator is a good choice which always gives us the previous value, but in our case, we need to stop our running stream so that maybe not helpful. Other then that, Behavior Subject, Replay Subject, and Async Subject can save the state. In our case, we only want the last state or value to save, so in this case, we will use Behavior Subject as shown in the below diagram:

Now, In case 2, what should we do? Oh yes, I find out a solution. But first, we will discuss in the diagram as shown below:

As you can see in the above image, once the user will click on the Pause button, we will switch our Observable to Observable.never(). This Observable has impressive power. What this will do, never() Observable emit no value and never terminate. Its something like a Pause. Now, when a user clicks a Start button after Pause. We will have the last value in our BehaviorSubject so we can switch to timerObservable() with an initial value of my last value of BehaviorSubject. I think things are clear. It’s time to jump into code.

...
when (it) {
    StopWatch.Idle -> {
        Observable.just(StopWatchUIState())
    }
    StopWatch.Running -> {
        timerObservable()
            .map { sec -> StopWatchUIState(sec, StopWatch.Running) }
    }
    StopWatch.Pause -> {
        Observable.never()
    }
}
...

After adding, Observable never, I click on Start and then click on Pause.

As you can see in the above image. When we click Pause, our stream stop, but there is a one UI bug. Our Pause button still enables and Start button always disable. So how to fix this.

Basically, in our case, we need to sent state information from our algorithm. Here basically, I want to share with you an example of Observable.never() but to fix this we will not pause (never terminate) our stream, instead, we will complete by using Observable.just().

...
when (it) {
    StopWatch.Idle -> {
        Observable.just(StopWatchUIState())
    }
    StopWatch.Running -> {
        timerObservable()
            .map { sec -> StopWatchUIState(sec, StopWatch.Running) }
    }
    StopWatch.Pause -> {
        Observable.just(StopWatchUIState(0L, state = StopWatch.Pause))
    }
...

In this case, our UI bug will be fix but our time will start showing 0:0 because I am sending 0L seconds. So it’s time to add BehaviorSubject to save our last second value.

class StopWatchViewModel {
...
    private val pauseStateSubject = BehaviorSubject.create<Long>()

   ...
    fun uiState(): Observable<StopWatchUIState> =
        publishSubject.switchMap {
            when (it) {
                StopWatch.Idle -> {
                    Observable.just(StopWatchUIState())
                }
                StopWatch.Running -> {
                    timerObservable()
                        .doOnNext(pauseStateSubject::onNext)
                        .map { sec -> StopWatchUIState(sec, StopWatch.Running) }
                }
                StopWatch.Pause -> {
                    Observable.just(
                        StopWatchUIState(
                            pauseStateSubject.value!!,
                            state = StopWatch.Pause
                        )
                    )
                }
            }
        }.hide()
            .observeOn(AndroidSchedulers.mainThread())

    private fun timerObservable() =
        Observable.interval(0L, 1L, TimeUnit.SECONDS)
            .takeWhile { it <= MAXIMUM_STOP_WATCH_LIMIT }
}

It’s time to run again our app.

As you can see in the above image, buttons are working as expected, but we find out one more logical issue. When the user again clicks on a Start button after Pause, our timer starts again from 0:0 and not from last paused value.

Why?

So there are two issues. One is our paused value is updated with zero because timerObservable start and that emit the 0 value as shown in below code:

timerObservable()
    .doOnNext(pauseStateSubject::onNext)
    .map { sec -> StopWatchUIState(sec, StopWatch.Running) }

Second, timerObservable still always start from zero. So we need to update our timerObservable also, so in case of resume that should return us the seconds value from where we pause. But we did not have any constructor which can help us in this matter. No problem, we can fix this issue. We will add one more subject which will take care of a resume as shown in below code:

class StopWatchViewModel {

    private val publishSubject = PublishSubject.create<StopWatch>()
    private val pauseStateSubject = BehaviorSubject.create<Long>()
    private val resumeStateSubject = BehaviorSubject.createDefault(0L)
...

    fun uiState(): Observable<StopWatchUIState> =
        publishSubject.switchMap {
            when (it) {
                StopWatch.Idle -> {
                    Observable.just(StopWatchUIState())
                }
                StopWatch.Running -> {
                    timerObservable()
                        .doOnNext(pauseStateSubject::onNext)
                        .map { sec -> StopWatchUIState(sec, StopWatch.Running) }
                }
                StopWatch.Pause -> {
                    resumeStateSubject.onNext(pauseStateSubject.value!!)
                    Observable.just(
                        StopWatchUIState(
                            pauseStateSubject.value!!,
                            state = StopWatch.Pause
                        )
                    )

                }
            }
        }.hide()
            .observeOn(AndroidSchedulers.mainThread())

    private fun timerObservable() =
        Observable.interval(0L, 1L, TimeUnit.SECONDS)
            .map { it + resumeStateSubject.value!! }
            .takeWhile { it <= MAXIMUM_STOP_WATCH_LIMIT }
}

As you can see in the above code, we did three changes.
1. We added a new resumeStateSubject with initialValue 0L.

private val resumeStateSubject = BehaviorSubject.createDefault(0L)

2. On pause Click we updated resumeStateSubject value with pauseStateSubject.

resumeStateSubject.onNext(pauseStateSubject.value!!)

3. We update our timerObservable algorithm to take care of resume seconds.

private fun timerObservable() =
Observable.interval(0L, 1L, TimeUnit.SECONDS)
.map { it + resumeStateSubject.value!! }
.takeWhile { it <= MAXIMUM_STOP_WATCH_LIMIT }

It’s time to run our app.

Hurray, as you can see in the above gif. Start and Pause working correctly, but we forgot one last thing. If you click Reset, StopWatch will stop with 0:0 state but after that, if you click again Start that will resume because we forgot to reset our subjects. So it’s time to write a code to reset our subjects.

when (it) {
    StopWatch.Idle -> {
        Observable.just(StopWatchUIState())
            .doOnNext { pauseStateSubject.onNext(it.seconds) }
            .doOnNext { resumeStateSubject.onNext(it.seconds) }
    }
    StopWatch.Running -> {
        timerObservable()
            .doOnNext(pauseStateSubject::onNext)
            .map { sec -> StopWatchUIState(sec, StopWatch.Running) }
    }
    StopWatch.Pause -> {
        resumeStateSubject.onNext(pauseStateSubject.value!!)
        Observable.just(
            StopWatchUIState(
                pauseStateSubject.value!!,
                state = StopWatch.Pause
            )
        )

    }
}

Now, everything is working perfectly.

Glue Code between UI and Business Layer:

...
private val serialDisposable = SerialDisposable()

private val viewModel = StopWatchViewModel()

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

    serialDisposable.set(viewModel.provide(mergeClicks()))
    viewModel.uiState().subscribe { render(it) }
}

override fun onDestroy() {
    serialDisposable.dispose()
    super.onDestroy()
}

Nothing fancy, everything is simple. First, we provided our clicks Observable to viewModel and our UI subscribed to our ViewModel uiState Observable.

Here, we are using SerialDisposable, not CompositeDisposable. The main benefit of SerialDisposable is, if you try adding a new disposable, SerialDisposable will automatically do the unsubscribe the previous subscription, just like switchMap but with less power :).

By the way, we can improve the readability of the glue code by using Kotlin power as shown below:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    mergeClicks()
        .let(viewModel::provide)
        .let(serialDisposable::set)

    viewModel.uiState().subscribe(::render)
}

override fun onDestroy() {
    serialDisposable.dispose()
    super.onDestroy()
}

There is one more issue which may create a memory leak, in case of uiState subscribe we need to take care of it’s disposable. Which I fixed in a complete code.

Complete code is available on GitHub.
v2-android-stop-watch-complete

Conclusion:

In this post we try to learn about:

  • Decoupling of Code by doing UI Layer separate from Business Layer
  • Enums
  • Observable.never()
  • Behavior Subjects

I hope you guys learn something new from this post. This is a version V2 of Android Stop Watch. In the next post, we will do some more refactoring to make our code easier and will take care of screen rotation. Be ready that is a little bit tricky because we need to survive our stream at device rotation :).

Previous

Facebooktwitterredditpinterestlinkedinmailby feather

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.