Fragments

An Activity may comprise one or more fragments, each of which represents a portion of a user interface. Multiple fragments can be combined in a single actiity to build a multi-pane screen. In this lab our purpose in introducing fragments is to later facilitat the inclusion of a horizontal swipe feature to the ResidenceActivity class.

Preview

In this lab we introduce fragments (Figure 1). Each activity is refactored and replaced by an activity-fragment pair.

Figure 1: Using fragments

ResidenceActivity

Before proceeding with refactoring ResidenceActivity, it is necessary to make some changes as follows:

MyRentApp

Declare a protected MyRentApp field, initialize it in onCreate and provide a getter:

  protected static MyRentApp app;
  @Override
  public void onCreate() {
    ...
    app = this;
    ...
  }

  public static MyRentApp getApp(){
    return app;
  }

Residence (model)

Protect against a negative id:

  public Residence() {
    id = unsignedLong();
    ...
  }

  /**
   * Generate a long greater than zero
   * @return Unsigned Long value greater than zero
   */
  private Long unsignedLong() {
    long rndVal = 0;
    do {
      rndVal = new Random().nextLong();
    } while (rndVal <= 0);
    return rndVal;
  }

Continue building the MyRent app that you developed in the previous lab.

Much of the content of ResidenceActivity inherited from the previous version is being moved to its associated new fragment class, ResidenceFragment.

We now begin refactoring:

  • Replace the contents of ResidenceActivity with the following:
package org.wit.myrent.activities;


import org.wit.myrent.R;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;

public class ResidenceActivity extends AppCompatActivity
{
  ActionBar actionBar;

  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_container);

    actionBar = getSupportActionBar();

    FragmentManager manager = getSupportFragmentManager();
    Fragment fragment = manager.findFragmentById(R.id.fragmentContainer);
    if (fragment == null)
    {
      fragment = new ResidenceFragment();
      manager.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
    }
  }
}

Add a container for the associated fragment:

Filename: res/layout/fragment_container.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragmentContainer"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  />

ResidenceFragment

Here is the new ResidenceFragment class which comprises, mostly, code migrated from ResidenceActivity in the previous version of MyRent:

package org.wit.myrent.activities;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.wit.android.helpers.ContactHelper;
import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.app.DatePickerDialog;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.DatePicker;
import android.widget.EditText;
import static org.wit.android.helpers.ContactHelper.sendEmail;
import static org.wit.android.helpers.IntentHelper.navigateUp;


public class ResidenceFragment extends Fragment implements TextWatcher,
    OnCheckedChangeListener,
    OnClickListener,
    DatePickerDialog.OnDateSetListener
{
  public static   final String  EXTRA_RESIDENCE_ID = "myrent.RESIDENCE_ID";

  private static  final int     REQUEST_CONTACT = 1;

  private EditText geolocation;
  private CheckBox rented;
  private Button   dateButton;
  private Button   tenantButton;
  private Button   reportButton;

  private Residence   residence;
  private Portfolio   portfolio;

  private String emailAddress = "";

  MyRentApp app;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);

    Long resId = (Long) getActivity().getIntent().getSerializableExtra(EXTRA_RESIDENCE_ID);

    app = MyRentApp.getApp();
    portfolio = app.portfolio;
    residence = portfolio.getResidence(resId);
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
  {
    super.onCreateView(inflater,  parent, savedInstanceState);
    View v = inflater.inflate(R.layout.fragment_residence, parent, false);

    ResidenceActivity residenceActivity = (ResidenceActivity)getActivity();
    residenceActivity.actionBar.setDisplayHomeAsUpEnabled(true);

    addListeners(v);
    updateControls(residence);

    return v;
  }

  private void addListeners(View v)
  {
    geolocation = (EditText) v.findViewById(R.id.geolocation);
    dateButton  = (Button)   v.findViewById(R.id.registration_date);
    rented      = (CheckBox) v.findViewById(R.id.isrented);
    tenantButton = (Button)  v.findViewById(R.id.tenant);
    reportButton = (Button)  v.findViewById(R.id.residence_reportButton);


    geolocation .addTextChangedListener(this);
    dateButton  .setOnClickListener(this);
    rented      .setOnCheckedChangeListener(this);
    tenantButton.setOnClickListener(this);
    reportButton.setOnClickListener(this);

  }

  public void updateControls(Residence residence)
  {
    geolocation.setText(residence.geolocation);
    rented.setChecked(residence.rented);
    dateButton.setText(residence.getDateString());
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case android.R.id.home: navigateUp(getActivity());
        return true;

      default: 
        return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public void onPause()
  {
    super.onPause();
    portfolio.saveResidences();
  }

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    if (resultCode != Activity.RESULT_OK)
    {
      return;
    }

    switch (requestCode)
    {
      case REQUEST_CONTACT:
        String name = ContactHelper.getContact(getActivity(), data);
        emailAddress = ContactHelper.getEmail(getActivity(), data);
        tenantButton.setText(name + " : " + emailAddress);
        residence.tenant = name;
        break;
    }
  }

  @Override
  public void beforeTextChanged(CharSequence s, int start, int count, int after)
  { }

  @Override
  public void onTextChanged(CharSequence s, int start, int before, int count)
  {}

  @Override
  public void afterTextChanged(Editable c)
  {
    Log.i(this.getClass().getSimpleName(), "geolocation " + c.toString());
    residence.geolocation = c.toString();
  }

  @Override
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
  {
    residence.rented = isChecked;
  }

  @Override
  public void onClick(View v)
  {
    switch (v.getId())
    {
      case R.id.registration_date      : Calendar c = Calendar.getInstance();
        DatePickerDialog dpd = new DatePickerDialog (getActivity(), this, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH));
        dpd.show();
        break;
      case R.id.tenant                 : Intent i = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
        startActivityForResult(i, REQUEST_CONTACT);
        if (residence.tenant != null)
        {
          tenantButton.setText("Tenant: "+residence.tenant);
        }
        break;
      case R.id.residence_reportButton : sendEmail(getActivity(), "emailAddress", getString(R.string.residence_report_subject), residence.getResidenceReport(getActivity()));
        break;

    }
  }

  @Override
  public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth)
  {
    Date date = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
    residence.date = date.getTime();
    dateButton.setText(residence.getDateString());
  }
}

Rename res/layout/activity_residence.xml to the more appropriate name: fragment_residence.xml.

ResidenceListActivity

As is the case with ResidenceActivity, much of ResidenceListActivity's code is also moved to its new associated fragment class - ResidenceListFragment.

Here is the refactored ResidenceListActivity:

package org.wit.myrent.activities;

import org.wit.myrent.R;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;

public class ResidenceListActivity extends AppCompatActivity
{
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_container);

    FragmentManager manager = getSupportFragmentManager();
    Fragment fragment = manager.findFragmentById(R.id.fragmentContainer);
    if (fragment == null)
    {
      fragment = new ResidenceListFragment();
      manager.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
    }
  }
}

ResidenceListFragment

Here is the ResidenceListFragment class which comprises, mostly, code migrated from ResidencyListActivity in the previous version of MyRent:

package org.wit.myrent.activities;

import java.util.ArrayList;

import org.wit.android.helpers.IntentHelper;
import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;

import android.widget.ListView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;
import android.widget.CheckBox;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.widget.AdapterView.OnItemClickListener;

public class ResidenceListFragment extends ListFragment implements OnItemClickListener
{
  private ArrayList<Residence> residences;
  private Portfolio portfolio;
  private ResidenceAdapter adapter;
  MyRentApp app;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);
    getActivity().setTitle(R.string.app_name);

    app = MyRentApp.getApp();
    portfolio = app.portfolio;
    residences = portfolio.residences;

    adapter = new ResidenceAdapter(getActivity(), residences);
    setListAdapter(adapter);

  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    View v = super.onCreateView(inflater, parent, savedInstanceState);
    return v;
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id) {
    Residence res = ((ResidenceAdapter) getListAdapter()).getItem(position);
    Intent i = new Intent(getActivity(), ResidenceActivity.class);
    i.putExtra(ResidenceFragment.EXTRA_RESIDENCE_ID, res.id);
    startActivityForResult(i, 0);
  }

  @Override
  public void onResume() {
    super.onResume();
    ((ResidenceAdapter) getListAdapter()).notifyDataSetChanged();
  }

  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    inflater.inflate(R.menu.residencelist, menu);
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case R.id.menu_item_new_residence:
        Residence residence = new Residence();
        portfolio.addResidence(residence);

        Intent i = new Intent(getActivity(), ResidenceActivity.class);
        i.putExtra(ResidenceFragment.EXTRA_RESIDENCE_ID, residence.id);
        startActivityForResult(i, 0);
        return true;

      default:
        return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Residence residence = adapter.getItem(position);
    IntentHelper.startActivityWithData(getActivity(), ResidenceActivity.class, "RESIDENCE_ID", residence.id);
  }

  class ResidenceAdapter extends ArrayAdapter<Residence>
  {
    private Context context;

    public ResidenceAdapter(Context context, ArrayList<Residence> residences) {
      super(context, 0, residences);
      this.context = context;
    }

    @SuppressLint("InflateParams")
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      if (convertView == null) {
        convertView = inflater.inflate(R.layout.list_item_residence, null);
      }
      Residence res = getItem(position);

      TextView geolocation = (TextView) convertView.findViewById(R.id.residence_list_item_geolocation);
      geolocation.setText(res.geolocation);

      TextView dateTextView = (TextView) convertView.findViewById(R.id.residence_list_item_dateTextView);
      dateTextView.setText(res.getDateString());

      CheckBox rentedCheckBox = (CheckBox) convertView.findViewById(R.id.residence_list_item_isrented);
      rentedCheckBox.setChecked(res.rented);

      return convertView;
    }
  }
}

Build and install the app on a device and verify that it functions correctly. No observable difference should exist in the performance of tha application as a result of introducing fragments.

Test

Build and install the app on a phsical device or emulator:

  • Create a new residence
  • Explore the various features.
  • No difference in the behaviour of the app will be obvious as a consequence of introducing fragments.
  • The need for and use of fragments will be demonstrated later in the course.

The application at the end of this lab is available for reference here: myrent-07