Table of Contents

Chapter 8

pp. 328-329

Don't get discouraged if you miss a lot of the details in converting the code. The authors pretty much throw you into the deep end with this exercise. Be sure to study the solution on pp. 330-331 and make sure you understand why each change is necessary.

The summary of converting Activities to Fragments is:

The last item above is often the most challenging.

pp. 332-334: Code block

StopwatchFragment.java
package com.hfad.workout;
 
import android.os.Bundle;
import android.os.Handler;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
 
public class StopwatchFragment extends Fragment {
    //Number of seconds displayed on the stopwatch.
    private int seconds = 0;
    //Is the stopwatch running?
    private boolean running;
    private boolean wasRunning;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState != null) {
            seconds = savedInstanceState.getInt("seconds");
            running = savedInstanceState.getBoolean("running");
            wasRunning = savedInstanceState.getBoolean("wasRunning");
            if (wasRunning) {
                running = true;
            }
        }
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View layout = inflater.inflate(R.layout.fragment_stopwatch, container, false);
        runTimer(layout);
        return layout;
    }
 
    @Override
    public void onPause() {
        super.onPause();
        wasRunning = running;
        running = false;
    }
 
    @Override
    public void onResume() {
        super.onResume();
        if (wasRunning) {
            running = true;
        }
    }
 
    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        savedInstanceState.putInt("seconds", seconds);
        savedInstanceState.putBoolean("running", running);
        savedInstanceState.putBoolean("wasRunning", wasRunning);
    }
 
    public void onClickStart(View view) {
        running = true;
    }
 
    public void onClickStop(View view) {
        running = false;
    }
 
    public void onClickReset(View view) {
        running = false;
        seconds = 0;
    }
 
    private void runTimer(View view) {
        final TextView timeView = (TextView) view.findViewById(R.id.time_view);
        final Handler handler = new Handler();
        handler.post(new Runnable() {
            @Override
            public void run() {
                int hours = seconds / 3600;
                int minutes = (seconds % 3600) / 60;
                int secs = seconds % 60;
                String time = String.format("%d:%02d:%02d",
                        hours, minutes, secs);
                timeView.setText(time);
                if (running) {
                    seconds++;
                }
                handler.postDelayed(this, 1000);
            }
        });
    }
}

pp. 335-336: Code block

fragment_stopwatch.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
 
    <TextView
        android:id="@+id/time_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="0dp"
        android:text=""
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textSize="92sp" />
 
    <Button
        android:id="@+id/start_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/time_view"
        android:layout_centerHorizontal="true"
        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_below="@+id/start_button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        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_below="@+id/stop_button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:onClick="onClickReset"
        android:text="@string/reset" />
</RelativeLayout>

On Android fragment managers

rant: on

Given my current understanding of Android, I think the way it deals with nested fragments is pretty awful. The concepts are ok-ish, but the implementation is confusing.

Some key points are:

It's awkward to need two different function names for these two very similar cases, but fine. What I think is really awful is the naming of the functions: getFragmentManager() implies a general-purpose function and getChildFragmentManager implies a manager of a child in the present context, but neither of these is case.

I suspect the reason for requiring two different functions is that the AOSP developers didn't fully think through things when they first developed Fragments, and when they figured out a better architecture, they probably didn't want to break the API (which would have required people to rewrite the code in their apps). But still …

rant: off

p. 342 Code block

FragmentTransaction ft = getChildFragmentManager().beginTransaction();
StopwatchFragment stopwatchFragment = new StopwatchFragment();
ft.replace(R.id.stopwatch_container, stopwatchFragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();

p. 350: Code block

switch (v.getId()) {
	case R.id.start_button:
		onClickStart(v);
		break;
	case R.id.stop_button:
		onClickStop(v);
		break;
	case R.id.reset_button:
		onClickReset(v);
		break;
}

p. 351: Code block

Button startButton = (Button)layout.findViewById(R.id.start_button);
startButton.setOnClickListener(this);
Button stopButton = (Button)layout.findViewById(R.id.stop_button);
stopButton.setOnClickListener(this);
Button resetButton = (Button)layout.findViewById(R.id.reset_button);
resetButton.setOnClickListener(this);

p. 356-358

The takeaway from this section is that the host Activity's setContentView() method automagically tries to restore Fragments to their last known state. This can have unintended consequences when an onCreateView() method creates new Fragments that are added to the UI. In this case, you will need to use some logic that only creates and adds a new Fragment if one doesn't already exist.