Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Innerclass TextView reference when activity resumes

Tags:

android

I have a inner class that extends CountDownTimer. Basically its a simple countdown timer that updates a TextView in the activity and plays a sound when the timer is finished. The code for the inner class is:

public class SetTimer extends CountDownTimer
{

    public SetTimer(long millisInFuture, long countDownInterval)
        {
            super(millisInFuture, countDownInterval);
        }

    @Override
    public void onFinish()
        {
            timeLeft.setText("0");
            Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
            Ringtone r=RingtoneManager.getRingtone(getApplicationContext(), notification);
            r.play();

        }

    @Override
    public void onTick(long millisUntilFinished)
        {
            String t;
            t=String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(millisUntilFinished), TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)
                    -TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millisUntilFinished)));
            timeLeft.setText(t);
        }

}

The code that creates and references the TextView is:

TextView timeLeft;

and in the onCreate method:

timeLeft=(TextView) findViewById(R.id.txtTimeLeft);

This works fine until I rotate the display. At that point the timer is still running and does play the sound at the end but it doesn't update the TextView. The TextView is declared at the top of the class and referenced in the onCreate method of the activity. If I restart the timer then it works. I used Log.d to check if the onTick method was still getting called and it was. My guess is that the reference to the TextView has changed but I can't figure out how to set it back to the timer. I tried declaring a TextView in the onTick method and updating that figuring it would then pick up a reference to the current instance of the TextView but that also didn't work. The only other thing to note is that the SetTimer object is created when the user clicks on a button. That code is:

timer=new SetTimer(interval, 100);

timer.start();

Any thoughts on how to have the SetTimer keep updating the TextView after the screen is rotated?

like image 836
Pedler Avatar asked Mar 19 '14 15:03

Pedler


1 Answers

Update I completely rewrote the answer as I did not notice a terrible thing in your code at first glance.

You are possibly leaking resources and the fact your app does not crash - could be just a matter of time. First of all, your inner SetTimer class implicitly holds a reference to Activity (I guess, you declared this class in Activity, did not you?). That prevents your Activity from being garbage collected, and I guess, that's why you do not get an Exception when setting a value to a TextView that is "out-of-sight".

So you should either declare your class as private static class, static class (inner classes), or public class (in it's own file). That way, you will not hold an implicit reference to your Activity and will not cause a memory leak when it gets destroyed.

But now you won't be able to access a textview directly, as it is a member of your Activity class. Let's solve it that way:

  1. Declare an interface inside SetTimer:

    interface OnTickUpdateListener{
        public void onTickUpdate(String text);
    }
    
  2. Declare an instance of such interface in your SetTimer and modify constructor:

    public class SetTimer extends ... {//this class IS IN IT's OWN FILE!!!!
        private OnTickUpdateListener listener;
    
        public void registerListener(OnTickUpdateListener listener){
            this.listener = listener;
        }
        public void unregisterListener(){
             this.listener = null;
        }
    ...
    }
    
  3. Let's trigger the listener when the timer ticks:

    @Override
    public void onTick(long millisec){
        if(listener != null){
            String t;
            t = String.valueOf(millisec);//or whatever
            listener.onTickUpdate(t);
        }
    }
    
  4. Now, make your Activity implement your interface:

    public class MyActivity extends Activity implements SetTimer.OnTickUpdateListener{
        @Override
        public void onTickUpdate(String text){
            textView.setText(text);
    }
    

Now to the harder part. We need to save a SetTimer instance when Activity is destroyed. That would be a nice trick to put a SetTimer inside a retained Fragment invisible to user, that would work much like an "immortal container" :)

  1. Create a Fragment class:

    public class MyFragment extends Fragment{
    
        public static final String TAG = MyFragment.class.getSimpleName();
        private SetTimer timer;
        private static final int interval = 10;
        private MyActivity myActivity;
    
    
        @Override
        public void onAttach(Activity activity){
            super.onAttach(activity);
            this.myActivity = (MyActivity) activity;
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState){
            super.onCreate(savedInstanceState);
            setRetainInstanceState(true);
            timer = new SetTimer(interval, 10);
            timer.start();
        }
    
        @Override
        public void onActivityCreated(Bundle state){
            super.onActivityCreated(state);
            timer.registerListener(myActivity);//activity should receive ticks
        }
    
        @Override
        public void onDetach(){
            super.onDetach();
            timer.unregisterListener();//ensure we do not post a result to non-existing  Activity
        }
    }
    
  2. And last, add your MyFragment in onCreate method of MyActivity:

    public class MyActivity extends Activity implements SetTimer.OnTickUpdateListener{
        private MyFragment fragment;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            FragmentManager fm = getFragmentManager();
            fragment = (MyFragment) fm.findFragmentByTag(MyFragment.TAG);
            if(fragment == null){
                fragment = new MyFragment();
                fm.beginTransaction().add(R.id.container, fragment, MyFragment.TAG).commit();
    
            }
        }
    

That way, we restore the existing fragment upon Activity recreation, and MyFragment registers new MyActivity as a listener, which will receive tick updates.

PS: I wrote this from scratch and did not test it, so if you encounter any error - please post them so we could work the out.

like image 140
Drew Avatar answered Sep 24 '22 18:09

Drew