Why my onStop() and onStart() methods work not correctly together in my stopwatch Android app?

389 views Asked by At

A stopwatch is started by click on a Start button.

I make the Activity invisible, by pushing the home button on a device.

But stopwatch timing doesn’t stop according to the onStop() method: since when the activity is visible again, the stopwatch counting looks as it has never been stopped (the numbers continue increasing in the non-focus state despite the onStop() method).

However, if I deleted the onStart() method, the timing stops correctly, according to the onStop(), after pushing the home device button.

The stopwatch, by itself, counts correctly, timing is good.

There are only visible – invisible, stop-start timing problems, onStop() - onStart() methods interaction.

I tried combination onPause() - onResume(), include onRestart() and so on, but the result is the same.

What’s wrong with my code?

I would be much appreciated for helping

package com.example.stopwatch;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.TextView;
import java.lang.System;
import java.util.Locale;


public class StopwatchActivity extends Activity {

    //Number of seconds in stopwatch.
    private int milliseconds = 0;
    //Indicates whether a stopwatch is running.
    private boolean running;
    // Presents time in millis, when the click on Start button is executed.
    private int startMillis;
    // Shows whether the stopwatch was running when activity became invisible.
    private boolean wasRunning;



    @Override
    protected void  onCreate(Bundle  saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_stopwatch);
        if(saveInstanceState != null) {
            milliseconds = saveInstanceState.getInt("milliseconds");
            running = saveInstanceState.getBoolean("running");
            wasRunning = saveInstanceState.getBoolean("wasRunning");
            startMillis = saveInstanceState.getInt("startMillis");
        }
        runTimer();
    }

    @Override
    public void onSaveInstanceState(Bundle saveInstanceState) {
        saveInstanceState.putInt("milliseconds", milliseconds);
        saveInstanceState.putBoolean("running", running);
        saveInstanceState.putBoolean("wasRunning", wasRunning);
        saveInstanceState.putInt("startMillis", startMillis);
    }

    @Override
    protected void onStop() {
        super.onStop();
        wasRunning = running;
        running = false;
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (wasRunning) {
            running = true;
        }
    }

    //Run the stopwatch on a Start button click.
    public void onClickStart(View view) {
        running = true;
        startMillis = (int)System.currentTimeMillis();
    }

    //Stop the stopwatch on a Stop button click.
    public void onCLickStop(View view) {
        running = false;
    }

    //Reset the stopwatch on a Reset button click.
    public void onClickReset(View view) {
        running = false; milliseconds = 0;
    }

    private void runTimer() {
        final TextView timeView = (TextView)findViewById(R.id.time_view);
        final  Handler handler = new Handler();

        handler.post(new Runnable() {
            @Override
            public void run() {

                int minutes = (int)((milliseconds%3600000)/60000);
                int secs = (int)((milliseconds%60000)/1000);
                int msecs = milliseconds%1000;

                String time = String.format(Locale.getDefault(),
                        "%02d:%02d:%03d", minutes, secs, msecs);

                timeView.setText(time);

                if (running) {
                    milliseconds = (int)(System.currentTimeMillis()-startMillis);
                }

                handler.postDelayed(this, 1);

            }
        });
    }

}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context="com.example.stopwatch.StopwatchActivity">

    <TextView
        android:id="@+id/time_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textAppearance="@android:style/TextAppearance.Large"
        android:textSize="56sp" />

    <Button
        android:id="@+id/start_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="20dp"
        android:onClick="onClickStart"
        android:text="@string/start" />

    <Button
        android:id="@+id/stop_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="8dp"
        android:onClick="onCLickStop"
        android:text="@string/stop" />

    <Button
        android:id="@+id/reset_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="8dp"
        android:onClick="onClickReset"
        android:text="@string/reset" />

</LinearLayout>
2

There are 2 answers

0
Николай Гольцев On BEST ANSWER

There are two problems at least:

  • Let's consider that you evaluate this expression twice, so that the second evaluation would be 5 seconds after the first evaluation in the timeline:
milliseconds = (int)(System.currentTimeMillis()-startMillis);

Thus, milliseconds value always increases as time goes forward! It means even if a user presses the start button, waits for 2 seconds, presses the stop button, waits for 3 seconds, presses the start button, milliseconds value will be equaled to 5 as the stopwatch starts ticking, not to 2 as you expect!

  • You should know that code inside Runnable in your codebase doesn't stop while a user works with another application. So you should somehow prevent this code from executing because it's a bad user experience when the code is executing while it shouldn't.
0
Igor On

Thank you, Николай Гольцев, for advise!

I solved the problem:

I changed expression which evaluates milliseconds in a method run(), by adding new variable (saveMillis). There are some changes in «click methods» too. The code below works well now

package com.example.stopwatch;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.TextView;
import java.lang.System;
import java.util.Locale;


public class StopwatchActivity extends Activity {

    //Number of seconds in stopwatch.
    private int milliseconds = 0;
    //Indicates whether a stopwatch is running.
    private boolean running;
    // Presents time in millis, when the click on Start button is executed.
    private int startMillis;
    // Shows whether the stopwatch was running when activity became invisible.
    private boolean wasRunning;
    //Save the current time in milliseconds, when the timing is stopped.
    private int saveMillis = 0;

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_stopwatch);
        if (saveInstanceState != null) {
            milliseconds = saveInstanceState.getInt("milliseconds");
            running = saveInstanceState.getBoolean("running");
            wasRunning = saveInstanceState.getBoolean("wasRunning");
            startMillis = saveInstanceState.getInt("startMillis");
            saveMillis = saveInstanceState.getInt("saveMillis");

        }
        runTimer();
    }

    @Override
    public void onSaveInstanceState(Bundle saveInstanceState) {
        saveInstanceState.putInt("milliseconds", milliseconds);
        saveInstanceState.putBoolean("running", running);
        saveInstanceState.putBoolean("wasRunning", wasRunning);
        saveInstanceState.putInt("startMillis", startMillis);
        saveInstanceState.putInt("saveMillis", saveMillis);

    }

    @Override
    protected void onStop() {
        super.onStop();
        wasRunning = running;
        running = false;
        saveMillis = milliseconds;

    }

    @Override
    protected void onStart() {
        super.onStart();
        if (wasRunning) {
            running = true;
            startMillis = (int) System.currentTimeMillis();
        }
    }

    //Run the stopwatch on a Start button click.
    public void onClickStart(View view) {
        running = true;
        startMillis = (int) System.currentTimeMillis();
    }

    //Stop the stopwatch on a Stop button click.
    public void onCLickStop(View view) {
        running = false;
        saveMillis = milliseconds;
    }

    //Reset the stopwatch on a Reset button click.
    public void onClickReset(View view) {
        running = false;
        milliseconds = 0;
        saveMillis = 0;
    }

    //Resume timing.
    public void onClickResume(View view) {
        running = true;
        startMillis = (int) System.currentTimeMillis();
    }

    private void runTimer() {
        final TextView timeView = (TextView) findViewById(R.id.time_view);
        final Handler handler = new Handler();

            handler.post(new Runnable() {
                @Override
                public void run() {

                    int minutes = (int) ((milliseconds % 3600000) / 60000);
                    int secs = (int) ((milliseconds % 60000) / 1000);
                    int msecs = milliseconds % 1000;

                    String time = String.format(Locale.getDefault(),
                            "%02d:%02d:%03d", minutes, secs, msecs);

                    timeView.setText(time);

                    if (running) {
                        milliseconds = saveMillis + ((int) (System.currentTimeMillis() - startMillis));
                    }

                    handler.postDelayed(this, 1);

                }
            });

        }

    }