V1-Android StopWatch ( New Era of Rx )

Designed By Hafiz Waleed Hussain

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

Code will be available on GitHub.

Limitations:
No imperative code is allowed. We will do our best to use Rx everywhere.

We will use RxBinding and RxJava2 for the implementation of Version 1.

Suggestion:

If you want to get the maximum benefit from this blog, try to do a checkout v1-android-stop-watch and do implement everything on your own by following the blog post.

UI:

  • Two buttons Start and Reset
  • Initial State. The start will be enabled and Reset will be disabled.
  • TextView where we will show our display
  • Initial State: 0:0

Implementation:

<?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/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:text="@string/start"
app:layout_constraintEnd_toStartOf="@+id/button2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button2"
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/button"
app:layout_constraintTop_toTopOf="@+id/button" />

<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/button" />
</androidx.constraintlayout.widget.ConstraintLayout>

Logical Implementation:

  • Timer Algorithm
  • Start and Reset Click
  • Integration of Timer Algorithm with UI
  • Integration of Timer with Buttons

Before implementation, I am going to share with you one tip. Maybe you already aware of this. For example, I have an algorithm, and I want to test that without running Android App. I can execute code by using the main function in Android Studio. Only I need to write the main function in any kotlin file, and that will work as shown below.

Timer Algorithm:

For timer, we have one Observable in Rx, and its name is an interval. That is a very powerful Observable. I am going to use that for the timer. I will write a separate post for the interval Observable in Rx Learning.

Observable.interval(0,1, TimeUnit.SECONDS)
  • First, a parameter is a delay. We want when a user clicks a start button. StopWatch should start without delay.
  • Second, a parameter is an interval of emission. Like after how many units new observable will emit
  • Third, a parameter of the unit

In our case, we want once a user clicks a button, emission will start with 0 waits and should emit new Observable after every second, for that we used one as an interval period and TimeUnit.SECONDS as a Unit. I hope it’s clear. Now, we will try to run this in main, so we can complete our algorithm without running Android App.

:(. It is not working as per our expectation. Always remember all-timer related Observables in RxJava by default work on a background thread. For now, we are only interested in the creation of our algorithm. For that, we can use a while(true); before the main function completion. So that main function will be stuck in infinite loop and never end, and we can see our output on a console as shown below: 🙂

Hurray, it’s working now.

fun main() {
    Observable.interval(0,1, TimeUnit.SECONDS)
        .subscribe(::println)

    while (true);
}

Next, our requirement is a maximum of 60 minutes or one hour. After that, StopWatch should stop. It’s effortless to achieve.

Note: For Now, I am changing SECONDS to MILLISECONDS. So we can see the completion of the Observable. 

fun main() {
    Observable.interval(0, 1, TimeUnit.MILLISECONDS)
        .takeWhile { it <= 3600L }
        .subscribe(::println)

    while (true);
}

As you can see in the above code and gif, we used the takeWhile operator to achieve the one hour limit. ( For takeWhile we will do a post in RxLearning )

By the way, if you are confused, why we used 3600L. So 3600L are the seconds in one hour, and L is for Long type because interval will return a Long type for a second.

Next, we need to format seconds into a decent hour and minute as per UI requirement 0:0.

Again, that is a simple task. Maybe a lot of my readers already know how to achieve.

fun main() {
    Observable.interval(0, 1, TimeUnit.MILLISECONDS)
        .takeWhile { it <= 3600L }
        .map { seconds -> "${seconds / 60} : ${seconds % 60}" }
        .subscribe(::println)

    while (true);
}

As you can see in the above code and gif, we used the map operator to achieve this UI presentation.

By the way, if you are interested in code until now, you can checkout branch
v1-android-stop-watch-1-timer-algorithm-complete

I think we achieved TimeAlgorithm. It’s time to create a function for timeObservable and go to the next implementation.

fun timerObservable(): Observable<String> = Observable.interval(0, 1, TimeUnit.MILLISECONDS)
    .takeWhile { it <= 3600L }
    .map { seconds -> "${seconds / 60} : ${seconds % 60}" }

Start and Reset Click:

There are two approaches. One I can use RxBindings to get Observable from a button click and second I can use PublishSubject as an emitter of an event.

I will show you both so that you can learn something, but in the long run of this Android StopWatch series, I will go with RxBindings.

First Approach:
I am expecting. Readers are aware of the Subjects. If you are not then I will recommend you, first read about Subjects. Maybe you can refer to my other posts on Subjects.
Confusion between Subject and Observable + Observer Part8

For now, to make implementation simple, I am not going with Enum or SealedClasses. Instead, we can achieve Android StopWatch version 1 with simple Boolean. So when a user clicks a Start button, we will emit a true value, and when a user clicks a Reset button, we will emit a false value.

Now back to our first approach.
Create a PublishSubject object with a Boolean data type as shown below:

class StopWatchMainActivity : AppCompatActivity() {

    private val clickEmitterSubject = PublishSubject.create<Boolean>()
    override fun onCreate(savedInstanceState: Bundle?) {
       ...
    }
    
    fun timerObservable(): Observable<String> = ...
}

Next, we added Click Listeners on buttons as shown below:

class StopWatchMainActivity : AppCompatActivity() {

    private val clickEmitterSubject = PublishSubject.create<Boolean>()
    override fun onCreate(savedInstanceState: Bundle?) {
      ...
        startButton.setOnClickListener {  }
        resetButton.setOnClickListener {  }
    }

    fun timerObservable(): Observable<String> = ...
}

Its time to emit values on Button Click.

...

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

        startButton.setOnClickListener { clickEmitterSubject.onNext(true) }
        resetButton.setOnClickListener { clickEmitterSubject.onNext(false) }
    }
...

Until here, we completed our clicks by using First Approach ( PublishSubject ). Code is available on the branch.
v1-android-stop-watch-2-start-and-reset-click-using-publish-subject

From now, I am moving to Second Approach, but Android Stop Watch Version V1 code with Publish Subject approach I will do complete and will share with you Github link in the end.

Second Approach:

By using RxBindings as shown below:

class StopWatchMainActivity : AppCompatActivity() {
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        startButton.clicks()
        resetButton.clicks()
    }
}
...

So here, clicks() function will give us as Observable. As we discuss, we will use true and false for a start and reset clicks. For that, we need to add a map operator on both buttons so we can achieve what we want. Implementation, as shown below:

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

        startButton.clicks().map { true }
        resetButton.clicks().map { false }
    }
} 

Now, we achieved the Start and Reset Click. Only for our those readers who are new to Rx. I am going to show you how this will work.

v1-android-stop-watch-2-start-and-reset-click

Integration of Timer Algorithm with UI:

In this task, we are only going to attach timerObservable function with UI. We don’t care about the memory leaks for now.

class StopWatchMainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 ...
        timerObservable()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(display::setText)
    }
...

v1-android-stop-watch-3-integration-of-timeralgorithm-with-UI

As you can see in the above code and gif. TimerAlgorithm and UI are working. But that is showing as Milliseconds due to our Timer Unit. Now, I am also switching back to Seconds Unit.

Integration of Timer with Buttons:

This is a critical and enjoyable part. Here, we have more than one options to implement.

Here, I think before code, we should discuss our scenario pictorially. Currently, we are in the situation as shown below:

We have two Start and Reset streams, which are working independently and on the other side. We have a timerObservable stream which is attached with UI. In between, we have a big black box. Where we need to add Rx operators to achieve our Android Stop Watch.

First, I am going to merge the Start and Reset stream. In this way, I have only one stream to take care of Boolean values as shown below:

Now, first, we will translate the above image into code.

private fun mergeClicks(): Observable<Boolean> = Observable.merge(
    listOf(
        startButton.clicks().map { true },
        resetButton.clicks().map { false }
    )
)

Here we are using merge operator from RxJava. I will explain this operator in the separate post of RxLearning.

Next, we need to add more Rx operators to achieve complete functionality. We want to add some logic, so when user will click Start we will start our Stop Watch and when user will click Reset we need to stop or reset our Stop Watch. In imperative code, we do decisions by using if, else. In Rx we have streams and we need to do decisions on streams, not on the basis of data type states.

I think we can create two streams, one stream which will take care of timer and the other will take care of Reset and at runtime only we need to switch between these streams as shown in below image:

Above image look complex but in code, we can achieve this behavior very easily as shown in below code:

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

        mergeClicks()
            .switchMap {
                if (it) timerObservable()
                else Observable.empty()
            }
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(display::setText)
    }

    private fun mergeClicks(): Observable<Boolean> = Observable.merge(
        listOf(
            startButton.clicks().map { true },
            resetButton.clicks().map { false }
        )
    )


    private fun timerObservable(): Observable<String> =
        Observable.interval(0, 1, TimeUnit.SECONDS)
            .takeWhile { it <= 3600L }
            .map { seconds -> "${seconds / 60} : ${seconds % 60}" }
}

So swicthMap is the operator, which always come to help in decision-making scenarios. If you are not good you need to wait for my switchMap post in Rx Learning. For now, I can share with you one important point of this operator. This operator gives guarantees to the user. Anytime this will be called this will do dispose of previous subscribed observable and will start a new observable.
In our case, when I will do click on Start I will send a true and in swicthMap true mean to start a timerObservable. TimerObservable will generate emission after every second. Now, when the user will click a Reset button will emit a false value in a stream. In switch map, this time we will subscribe to new Observable, which is called Observable.empty() and swictMap will do unsubscribe the previous timerObservable.
By the way, if you are not aware of Observable.empty(). That is an amazing Observable when you subscribe with Observable.empty() that will do no emission and will invoke a Complete method. In our case, we can see the proper use case of Observable.empty(). It’s time to run our app.

As you can in the above gif. There is a UI issue. Our reset button is still disabled. To fix this, we can use simply doOnNext() method from Observable. As we know UI is a side effect and we always try our best to do work with a side effect in doOnNext().

private fun mergeClicks(): Observable<Boolean> = Observable.merge(
    listOf(
        startButton.clicks().map { true },
        resetButton.clicks().map { false }
    )
).doOnNext {
    startButton.isEnabled = !it
    resetButton.isEnabled = it
}

In version 1, this is our final solution, maybe in version 4, we will do UI changes in a more good way. It’s time to run again our app.

Hurray, our integration is complete.
v1-android-stop-watch-4-integration-of-timer-with-buttons

Final Touch:

Now, we achieve the version1 Android Stop Watch. But there are two UI issues, which we will fix now.

First issue:
When a user clicks the Reset button. Our timer stops and is showing the last time, but for now, the requirement is, we need to go back to 0 : 0.

Second Issue:
When the timer is complete, again time is not going back to 0:0 format, also buttons are in the wrong state as shown below:

First Issue Fix:
We can fix this easily. I am going to change my Observable.empty() to Observable.just(“0 : 0”).

mergeClicks()
    .switchMap {
        if (it) timerObservable()
        else Observable.just("0:0") // Observable.empty()
    }
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(display::setText)

Second Issue Fix:
The easiest solution, we can add a doOnComplete() to our timerObservable() and in the map, we can send “0:0” format if the value is the last emission. By the way, this is not the best solution but will work for version 1.

private fun timerObservable(): Observable<String> =
    Observable.interval(0, 1, TimeUnit.MILLISECONDS)
        .takeWhile { it <= 3600L }
        .map { seconds -> if (seconds == 3600L) "0:0" else "${seconds / 60} : ${seconds % 60}" }
        .observeOn(AndroidSchedulers.mainThread())
        .doOnComplete {
            startButton.isEnabled = true
            resetButton.isEnabled = false
        }

Now, we completed our Version 1. To save from memory leak we need to add a disposable.

After small refactoring our code looks like below:

package com.uwantolearn.uwanttolearn_android_stopwatch

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.jakewharton.rxbinding3.view.clicks
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.merge
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.TimeUnit

private const val MAXIMUM_STOP_WATCH_LIMIT = 3600L
private const val NUMBER_OF_SECONDS_IN_ONE_MINUTE = 60

class StopWatchMainActivity : AppCompatActivity() {

private val disposable = CompositeDisposable()
private val displayInitialState by lazy { resources.getString(R.string._0_0) }

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

mergeClicks().switchMap {
if (it) timerObservable()
else Observable.just(displayInitialState)
}.subscribe(display::setText)
.let(disposable::add)
}

override fun onDestroy() {
disposable.clear()
super.onDestroy()
}

private fun mergeClicks(): Observable<Boolean> =
listOf(startButton.clicks().map { true }, resetButton.clicks().map { false })
.merge()
.doOnNext(::buttonStateManager)


private fun timerObservable(): Observable<String> =
Observable.interval(0, 1, TimeUnit.SECONDS)
.takeWhile { it <= MAXIMUM_STOP_WATCH_LIMIT }
.map(timeFormatter)
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete { buttonStateManager(false) }

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

private fun buttonStateManager(boolean: Boolean) {
startButton.isEnabled = !boolean
resetButton.isEnabled = boolean
}
}

In this code, we can see the power of RxJava. In 60 lines only, I can achieve an amazing stopwatch code. But due to UI bad code implementation, Rx code is couple with UI. Which is ok for now.
You can check out the whole code from Github.

v1-android-stop-watch-complete

Conclusion:

In this post, we try to learn about:

  • Observable.interval()
  • Timer Algorithm
  • takeWhile()
  • map()
  • PublishSubject for click listeners
  • Stream diagrams, how they can help to complete an algorithm
  • merge()
  • switchMap()
  • Observable.empty()
  • Observable.just()
  • doOnNext for side effects
  • doOnComplete for side effects

I hope you guys learn something new from this post. This is a simple version V1. Be ready next will be difficult because we want to pause a stream :).

Bonus:

Complete implementation of Android StopWatch Version 1 by using PublishSubject.
v1-android-stop-watch-2-start-and-reset-click-using-publish-subject-complete

Previous

Next

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.