Activity: Fragment Boss

Now that your layouts are behaving properly, you can turn to adding a CrimeFragment to the detail fragment container when CrimeListActivity is sporting a two-pane layout.

You might think to simply write an alternative implementation of CrimeHolder.onClick(View) for tablets. Instead of starting a new CrimePagerActivity, this onClick(View) would get CrimeListActivity’s FragmentManager and commit a fragment transaction that adds a CrimeFragment to the detail fragment container.

The code in your CrimeListFragment.CrimeHolder would look like this:

public void onClick(View view) {
    // Stick a new CrimeFragment in the activity's layout
    Fragment fragment = CrimeFragment.newInstance(mCrime.getId());
    FragmentManager fm = getActivity().getSupportFragmentManager();
    fm.beginTransaction()
        .add(R.id.detail_fragment_container, fragment)
        .commit();
}

This works, but it is not how stylish Android programmers do things. Fragments are intended to be standalone, composable units. If you write a fragment that adds fragments to the activity’s FragmentManager, then that fragment is making assumptions about how the hosting activity works, and your fragment is no longer a standalone, composable unit.

For example, in the code above, CrimeListFragment adds a CrimeFragment to CrimeListActivity and assumes that CrimeListActivity has a detail_fragment_container in its layout. This is business that should be handled by CrimeListFragment’s hosting activity instead of CrimeListFragment.

To maintain the independence of your fragments, you will delegate work back to the hosting activity by defining callback interfaces in your fragments. The hosting activities will implement these interfaces to perform fragment-bossing duties and layout-dependent behavior.

Fragment callback interfaces

To delegate functionality back to the hosting activity, a fragment typically defines a callback interface named Callbacks. This interface defines work that the fragment needs done by its boss, the hosting activity. Any activity that will host the fragment must implement this interface.

With a callback interface, a fragment is able to call methods on its hosting activity without having to know anything about which activity is hosting it.

Implementing CrimeListFragment.Callbacks

To implement a Callbacks interface, you first define a member variable that holds an object that implements Callbacks. Then you cast the hosting activity to Callbacks and assign it to that variable.

You assign the context in the Fragment lifecycle method:

    public void onAttach(Context context)

This method is called when a fragment is attached to an activity, whether it was retained or not. Remember, Activity is a subclass of Context, so onAttach passes a Context as a parameter, which is more flexible. Ensure that you use the onAttach(Context) signature for onAttach and not the deprecated onAttach(Activity) method, which may be removed in future versions of the API.

Similarly, you will set the variable to null in the corresponding waning lifecycle method:

    public void onDetach()

You set the variable to null here because afterward you cannot access the activity or count on the activity continuing to exist.

In CrimeListFragment.java, add a Callbacks interface to CrimeListFragment. Also add an mCallbacks variable and override onAttach(Context) and onDetach() to set and unset it.

Listing 17.6  Adding callback interface (CrimeListFragment.java)

public class CrimeListFragment extends Fragment {
    ...
    private boolean mSubtitleVisible;
    private Callbacks mCallbacks;

    /**
     * Required interface for hosting activities
     */
    public interface Callbacks {
        void onCrimeSelected(Crime crime);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mCallbacks = (Callbacks) context;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(SAVED_SUBTITLE_VISIBLE, mSubtitleVisible);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mCallbacks = null;
    }
}

Now CrimeListFragment has a way to call methods on its hosting activity. It does not matter which activity is doing the hosting. As long as the activity implements CrimeListFragment.Callbacks, everything in CrimeListFragment can work the same.

Note that CrimeListFragment performs an unchecked cast of its activity to CrimeListFragment.Callbacks. This means that the hosting activity must implement CrimeListFragment.Callbacks. That is not a bad dependency to have, but it is important to document it.

Next, in CrimeListActivity, implement CrimeListFragment.Callbacks. Leave onCrimeSelected(Crime) empty for now.

Listing 17.7  Implementing callbacks (CrimeListActivity.java)

public class CrimeListActivity extends SingleFragmentActivity
    implements CrimeListFragment.Callbacks {


    @Override
    protected Fragment createFragment() {
        return new CrimeListFragment();
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.activity_masterdetail;
    }

    @Override
    public void onCrimeSelected(Crime crime) {
    }
}

Eventually, CrimeListFragment will call this method in CrimeHolder.onClick(…) and also when the user chooses to create a new crime. First, let’s figure out CrimeListActivity.onCrimeSelected(Crime)’s implementation.

When onCrimeSelected(Crime) is called, CrimeListActivity needs to do one of two things:

  • if using the phone interface, start a new CrimePagerActivity

  • if using the tablet interface, put a CrimeFragment in detail_fragment_container

To determine which interface was inflated, you could check for a certain layout ID. But it is better to check whether the layout has a detail_fragment_container. Checking a layout’s capabilities is a more precise test of what you need. Filenames can change, and you do not really care what file the layout was inflated from; you just need to know whether it has a detail_fragment_container to put your CrimeFragment in.

If the layout does have a detail_fragment_container, then you are going to create a fragment transaction that removes the existing CrimeFragment from detail_fragment_container (if there is one in there) and adds the CrimeFragment that you want to see.

In CrimeListActivity.java, implement onCrimeSelected(Crime) to handle the selection of a crime in either interface.

Listing 17.8  Conditional CrimeFragment startup (CrimeListActivity.java)

@Override
public void onCrimeSelected(Crime crime) {
    if (findViewById(R.id.detail_fragment_container) == null) {
        Intent intent = CrimePagerActivity.newIntent(this, crime.getId());
        startActivity(intent);
    } else {
        Fragment newDetail = CrimeFragment.newInstance(crime.getId());

        getSupportFragmentManager().beginTransaction()
                .replace(R.id.detail_fragment_container, newDetail)
                .commit();
    }
}

Finally, in CrimeListFragment, you are going to call onCrimeSelected(Crime) in the places where you currently start a new CrimePagerActivity.

In CrimeListFragment.java, modify onOptionsItemSelected(MenuItem) and CrimeHolder.onClick(View) to call Callbacks.onCrimeSelected(Crime).

Listing 17.9  Calling all callbacks! (CrimeListFragment.java)

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.new_crime:
            Crime crime = new Crime();
            CrimeLab.get(getActivity()).addCrime(crime);
            Intent intent = CrimePagerActivity
                    .newIntent(getActivity(), crime.getId());
            startActivity(intent);
            updateUI();
            mCallbacks.onCrimeSelected(crime);
            return true;
        ...
    }
}
...
private class CrimeHolder extends RecyclerView.ViewHolder
        implements View.OnClickListener {
    ...
    @Override
    public void onClick(View view) {
        Intent intent = CrimePagerActivity.newIntent(getActivity(), mCrime.getId());
        startActivity(intent);
        mCallbacks.onCrimeSelected(mCrime);
    }
}

When you call back in onOptionsItemSelected(MenuItem), you also reload the list immediately upon adding a new crime. This is necessary because, on tablets, the list will remain visible after adding a new crime. Before, you were guaranteed that the detail screen would appear in front of it.

Run CriminalIntent on a tablet. Create a new crime, and a CrimeFragment will be added and shown in the detail_fragment_container. Then view an old crime to see the CrimeFragment being swapped out for a new one (Figure 17.7).

Figure 17.7  Master and detail now wired up

Master and detail now wired up

Looks great! One small problem, though: If you make changes to a crime, the list will not update to reflect them. Right now, you only reload the list immediately after adding a crime and in CrimeListFragment.onResume(). But on a tablet, CrimeListFragment stays visible alongside the CrimeFragment. The CrimeListFragment is not paused when the CrimeFragment appears, so it is never resumed. Thus, the list is not reloaded.

You can fix this problem with another callback interface – this one in CrimeFragment.

Implementing CrimeFragment.Callbacks

CrimeFragment will define the following interface:

public interface Callbacks {
    void onCrimeUpdated(Crime crime);
}

For CrimeFragment to push updates to a peer Fragment, it will need to do two things. First, since CriminalIntent’s single source of truth is its SQLite database, it will need to save its Crime to CrimeLab. Then CrimeFragment will call onCrimeUpdated(Crime) on its hosting activity. CrimeListActivity will implement onCrimeUpdated(Crime) to reload CrimeListFragment’s list, which will pull the latest data from the database and display it.

Before you start with CrimeFragment’s interface, change the visibility of CrimeListFragment.updateUI() so that it can be called from CrimeListActivity.

Listing 17.10  Changing updateUI()’s visibility (CrimeListFragment.java)

private public void updateUI() {
    ...
}

Then, in CrimeFragment.java, add the callback interface along with an mCallbacks variable and implementations of onAttach(…) and onDetach().

Listing 17.11  Adding CrimeFragment callbacks (CrimeFragment.java)

private ImageButton mPhotoButton;
private ImageView mPhotoView;
private Callbacks mCallbacks;

/**
 * Required interface for hosting activities
 */
public interface Callbacks {
    void onCrimeUpdated(Crime crime);
}

public static CrimeFragment newInstance(UUID crimeId) {
    ...
}

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    mCallbacks = (Callbacks) context;
}

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
}

@Override
public void onPause() {
    ...
}

@Override
public void onDetach() {
    super.onDetach();
    mCallbacks = null;
}

Now implement CrimeFragment.Callbacks in CrimeListActivity to reload the list in onCrimeUpdated(Crime).

Listing 17.12  Refreshing crime list (CrimeListActivity.java)

public class CrimeListActivity extends SingleFragmentActivity
    implements CrimeListFragment.Callbacks, CrimeFragment.Callbacks {
    ...
    public void onCrimeUpdated(Crime crime) {
        CrimeListFragment listFragment = (CrimeListFragment)
                getSupportFragmentManager()
                        .findFragmentById(R.id.fragment_container);
        listFragment.updateUI();
    }
}

CrimeFragment.Callbacks must be implemented in all activities that host CrimeFragment. So provide an empty implementation in CrimePagerActivity, too.

Listing 17.13  Providing empty callbacks implementation (CrimePagerActivity.java)

public class CrimePagerActivity extends AppCompatActivity
        implements CrimeFragment.Callbacks {
    ...
    @Override
    public void onCrimeUpdated(Crime crime) {

    }
}

CrimeFragment will be doing a Time Warp two-step a lot internally: Jump to the left, save mCrime to CrimeLab. Step to the right, call mCallbacks.onCrimeUpdated(Crime). Add a method to make it more convenient to do this jig.

Listing 17.14  Adding updateCrime() method (CrimeFragment.java)

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    ...
}

private void updateCrime() {
    CrimeLab.get(getActivity()).updateCrime(mCrime);
    mCallbacks.onCrimeUpdated(mCrime);
}

private void updateDate() {
    mDateButton.setText(mCrime.getDate().toString());
}

Then add calls in CrimeFragment.java to updateCrime() when a Crime’s title or solved status has changed.

Listing 17.15  Calling onCrimeUpdated(Crime) (CrimeFragment.java)

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
    ...
    mTitleField.addTextChangedListener(new TextWatcher() {
        ...
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            mCrime.setTitle(s.toString());
            updateCrime();
        }
        ...
    });
    ...
    mSolvedCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView,
                boolean isChecked) {
            mCrime.setSolved(isChecked);
            updateCrime();
        }
    });
    ...
}

You also need to call updateCrime() in onActivityResult(…), where the Crime’s date, photo, and suspect can be changed. Currently, the photo and suspect do not appear in the list item’s view, but CrimeFragment should still be neighborly and report those updates.

Listing 17.16  Calling updateCrime() again (CrimeFragment.java)

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    ...
    if (requestCode == REQUEST_DATE) {
        Date date = (Date) data
                .getSerializableExtra(DatePickerFragment.EXTRA_DATE);
        mCrime.setDate(date);
        updateCrime();
        updateDate();
    } else if (requestCode == REQUEST_CONTACT && data != null) {
        ...
        try {
            ...
            String suspect = c.getString(0);
            mCrime.setSuspect(suspect);
            updateCrime();
            mSuspectButton.setText(suspect);
        } finally {
            c.close();
        }
    } else if (requestCode == REQUEST_PHOTO) {
        ...
        getActivity().revokeUriPermission(uri,
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

        updateCrime();
        updatePhotoView();
    }
}

Run CriminalIntent on a tablet and confirm that your RecyclerView updates when changes are made in CrimeFragment. Then run it on a phone to confirm that the app works as before.

With that, you have an app that works on both tablets and phones.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.239.148