Objectives

Extend the pacemaker-android app to enable activities to be listed. Explore three patterns in this context: Memento, Singleton, Adapter

Setup

This is v1 of the pacemaker-android project from the last lab:

You can import this for reference purposes, or keep working with your own version.

Memento I

Currently, your createActitivyButtonPressed method in the CreateActivity class looks like this:

 public void createActivityButtonPressed (View view)
  {
    double distance = distancePicker.getValue();
    MyActivity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);

    activities.add(activity);
    Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
  }

We are creating an activity, and adding it to a list held locally. If the user would like to see the activities, this handler will switch views:

  public void listActivityButtonPressed (View view) 
  {
    Log.v("Pacemaker", "List Activityies Button Pressed");
    Intent intent = new Intent(this, ActivitiesList.class);
    startActivity (intent);
  }

However, the activity is not able to access the list we have been updating with new activities.

This is the current Activity class:

package org.pacemaker.pacemaker;

import static com.google.common.base.Objects.toStringHelper;
import com.google.common.base.Objects;

public class MyActivity
{
  public Long   id;
  public String type;
  public String location;
  public double distance;

  public MyActivity()
  {
  }

  public MyActivity(String type, String location, double distance)
  {
    this.type      = type;
    this.location  = location;
    this.distance  = distance;
  }

  @Override
  public String toString()
  {
    return toStringHelper(this).addValue(id)
        .addValue(type)
        .addValue(location)
        .addValue(distance)
        .toString();
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof MyActivity)
    {
      final MyActivity other = (MyActivity) obj;
      return Objects.equal(type, other.type)
          && Objects.equal(location,  other.location)
          && Objects.equal(distance,  other.distance);
    }
    else
    {
      return false;
    }
  }

  @Override
  public int hashCode()
  {
    return Objects.hashCode(this.id, this.type, this.location, this.distance);
  }
}

Augment this class with the following features (as members of the class):

  public MyActivity(Parcel in)
  {
    this.type = in.readString();
    this.location = in.readString();
    this.distance = in.readDouble();
  }

  @Override
  public int describeContents()
  {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags)
  {
    dest.writeString(type);
    dest.writeString(location);
    dest.writeDouble(distance);
  }

  public static final Parcelable.Creator<MyActivity> CREATOR = new Parcelable.Creator<MyActivity>()
  {
    public MyActivity createFromParcel(Parcel in)
    {
      return new MyActivity(in);
    }

    public MyActivity[] newArray(int size)
    {
      return new MyActivity[size];
    }
  };

You will need to make the Activity class implement the Parcelable interface:

...
public class MyActivity implements Parcelable
{
...

This will enable instances of the class to be 'parcelabe' - i.e. externalized to an external to another object.

We can transfer such a 'parcel' to another activity by adding it to a 'bundle' for that activity, and adding it as an 'extra' for the activity to pick up:

  public void listActivityButtonPressed (View view) 
  {
    Log.v("Pacemaker", "List Activityies Button Pressed");
    Intent intent = new Intent(this, ActivitiesList.class);
    Bundle bundle = new Bundle();
    bundle.putParcelableArrayList("activities", activities);
    intent.putExtras(bundle);
    startActivity (intent);
  }

This will cause difficulty initiall, with an error on this line:

    bundle.putParcelableArrayList("activities", activities);

This is because of the way we have declared the activities list:

  private List<MyActivity> activities = new ArrayList<MyActivity>();

It need to be explicity and ArrayList for the Parcelable builder to compile:

  private ArrayList<MyActivity> activities = new ArrayList<MyActivity>();

Memento II

In the ActivitiesList class, we can recover the list from the bundle. This can be done in onCreate:

    Bundle extras = getIntent().getExtras();  
    List<Activity> activities  = extras.getParcelableArrayList("activities");

We might display the activity list to the log:

    for (MyActivity activity : activities)
    {
      Log.v("Pacemaker", "Activity: " + activity);
    }

We would expect this to use the toString helper we have provided to Activity model:

  @Override
  public String toString()
  {
    return toStringHelper(this).addValue(type).addValue(location).addValue(distance).toString();
  }

Check that activities you create now are displayed in the log window.

Adapter I

Just sending the list to the log will not suffice. We should attempt to render them to the list view directly.

Make sure we have the activitiesListView to hand:

public class ActivitiesList extends  android.app.Activity
{
  private ListView activitiesListView;

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activities_list);

    activitiesListView = (ListView) findViewById(R.id.activitiesListView);

    Bundle extras = getIntent().getExtras();  
    List<Activity> activities  = extras.getParcelableArrayList("activities");
    ..

}

The simplest way of rendering to this list view is to use an off-the-shelf adapter:

    ArrayAdapter<MyActivity> activitiesAdapter = new ArrayAdapter<MyActivity>(this, android.R.layout.simple_list_item_1, activities);
    activitiesListView.setAdapter(activitiesAdapter);
    activitiesAdapter.notifyDataSetChanged();

This should display the items as expected. However, they will be still formatted using the toString formatter. Also, is is using 'simple_list_item_1' layout - explained here:

Pacemaker#Singleton

We can introduce an Application object, guaranteed to be a singleton - and accessible to all activities in our application.

Create a new package called 'org.pacemaker.main' - and introduce this class:

package org.pacemaker.pacemaker;

import java.util.ArrayList;
import java.util.List;

import android.app.Application;
import android.util.Log;

public class PacemakerApp extends Application
{
  public List<MyActivity> actvities = new ArrayList<MyActivity>();

  @Override
  public void onCreate()
  {
    super.onCreate();
    Log.v("Pacemaker", "Pacemaker App Started");
  }
}

If an application object is to be created, it must be specified in the manifest AndroidManifst.xml

    <application
        android:name="org.pacemaker.pacemaker.PacemakerApp"

Run the app now and make sure the log message appears (once only)

CreateActivity can now reach for this object when it is created:

public class CreateActivity extends AppCompatActivity
{
  private PacemakerApp app;

  //...

  private ArrayList<MyActivity> activities = new ArrayList<MyActivity>();

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    //...
    app = (PacemakerApp) getApplication();
    //...
  }
  //...

.. and we can simplify createActivityButton pressed, removing the parcelable mechanism, and simply adding the activity to the list in the application object:

  public void createActivityButtonPressed (View view)
  {
    double distance = distancePicker.getValue();
    MyActivity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);

    app.actvities.add(activity);
    Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
  }


  public void listActivityButtonPressed (View view)
  {
    Log.v("Pacemaker", "List Activityies Button Pressed");
    Intent intent = new Intent(this, ActivitiesList.class);
    startActivity (intent);
  }

ActivitiesList can also be simplified, we just retrieve the application object and the list of activities from that:

public class ActivitiesList extends AppCompatActivity
{
  private PacemakerApp app;
  private ListView     activitiesListView;

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_activities_list);

    app = (PacemakerApp) getApplication();

    activitiesListView = (ListView) findViewById(R.id.activitiesListView);

    List<MyActivity> activities  = app.actvities;

    ArrayAdapter <MyActivity>activitiesAdapter = new ArrayAdapter<MyActivity>(this, android.R.layout.simple_list_item_1, activities);
    activitiesListView.setAdapter(activitiesAdapter);
    activitiesAdapter.notifyDataSetChanged();
  }
}

This should now work as before. We can also remove the Parcelable methods from the Activity model as we are no longer using them.

Adapter II

The existing adapter we are using in the ActivitiesList class is a stock adapter from the SDK:

We can write our own custom adapter to do the same job:

class ActivityAdapter extends ArrayAdapter<MyActivity>
{
  private Context        context;
  public  List<MyActivity> activities;

  public ActivityAdapter(Context context, List<MyActivity> activities)
  {
    super(context, android.R.layout.simple_list_item_1, activities);
    this.context   = context;
    this.activities = activities;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent)
  {
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    View     view     = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);
    MyActivity activity = activities.get(position);
    TextView textView = (TextView) view.findViewById(android.R.id.text1);

    textView.setText("" + activity);

    return view;
  }

  @Override
  public int getCount()
  {
    return activities.size();
  }
}

Place this in the same source file as ActivitiesList class.

In the above, we have precise control over how we layout the activity data in each row. We are not using this yet, so our display is still primitive.

Using this adapter in ActivitiesList.onCreate instead of the library one is straightforward :

    ActivityAdapter activitiesAdapter = new ActivityAdapter(this,  activities);
    activitiesListView.setAdapter(activitiesAdapter);
    activitiesAdapter.notifyDataSetChanged();

This should now work as expected.