Home ->
Teaching -> CSAS Android
-> iBookmark
CSAS Android Workshop: Criminal Intent Project
This is a continuation of the CriminalIntent activity started as part of our
Android workshop. Please check that page for a project
description and to download the framework for this project.
Create new CriminalIntent project with a CrimeActivity (remove the menu
inflator). That activity will host our fragment(s). The
basic framework is as follows:
- Create a Crime class with a UUID field and a text field.
- Create generic frame layout with nothing in it. We will later place
fragment(s) in that container using code, and via XML.
- Create a CrimeActivity to inflate that layout as usual.
- Create a CrimeFragment fragment and a fragment_crime.xml. Place one
EditText in the XML layout and implement the code to inflate that fragment
in CrimeFragment. In the CrimeFragment’s onCreate method, initialize a
single Crime. In the onCreateView method you (finally) inflate the view from
XML and setup the text field (different from an Activity, where you would do
both in onCreate).
Next we add “date” and “solved” fields to Crime.java and update
fragment_crime.xml to make room for these fields. NEW: We’ll use a theme
divider. Also, create an alternate layout where date and the checkbox are in one
line. NEW: We’ll use “layout_weight”.
To check your progress so far,
download CriminalIntent1.0.zip
and compare with your own version.
Next we want to see a list of crimes, each crime consisting of the title, the
date, and a checkbox whether it is solved or not. We will not use the
standard solution of creating a subclass of BaseAdapter as we have done for
iBookmark, we will instead use a more
sophisticated approach using a "singleton". Also, for the item view that will
serve as template for the Crime items in our list, we will
define that as XML and inflate it when necessary. We will also use a
“centralized data stash” for our crimes so that anyone updating a crime will
impact everyone else, and we will organize our classes somewhat differently than
before. We will do the following:
Click here to download our progress
so
far...
- We will create the list of crimes as a “singleton”: a singleton exists as
long as the application stays in memory, so storing the list in a singleton will
keep the crime data available no matter what happens with activities, fragments,
and their lifecycles. To create a singleton, you create a class "CrimeLab" with a
private
constructor and a static get() method that returns the (static) instance of
itself. If the
instance already exists, then get() simply returns it. If the instance
does not exist yet, then get() will call the (private) constructor to create it. Our new
class will be called “CrimeLab”. Add a list of sample crimes and a method to return the
list, as well as a method to return a particular crime (with a given uuid).
- Create a CrimeListActivity (extending Activity) to host the CrimeListFragment
(extending ListFragment), similar to the
CrimeActivity hosting CrimeFragment. Note that ListFragment is a utility class
that simplifies (and represents) using a fragment with a single list (since
that happens often). There is also a ListActivity if you are (still)
programming without fragments.
- Modify the manifest to include the new
activity and move the LAUNCHER activity in the manifest file to the new class. That means it will now
start instead of the previous activity. Run the app – you’ll see the “wait”
spinner which will show if a list-fragment has no list to display.
- Now get the list of crimes from the CrimeLab, initialize an ArrayAdapter
connected to that crime list with a default view and set it up as the adapter in
the ListFragment. You should now see a list of memory addresses for you
crimes, which is easy to fix by adding a “toString” method to the Crimes
class (do that).
- Next we want to create a custom view for the crimes in the list, but this
time we want to create a layout in XML. NEW: Use a RelativeLayout! Then define
an inner class in CrimeListFragment that extends ArrayAdapter (not BaseAdapter)
– most methods are defined properly except for getView, so implement that.
Click here to download our progress
so
far...
- Almost done: We could now start a new CrimeActivity when a list item is clicked
and pass
along the ID of the selected crime as an “extra” so that the CrimeActivity can
retrieve the proper crime to show (and to edit!!!!). Note that a (huge) benefit
of us using a singleton CrimeLab is that it means that if the data in a Crime is
changed (via CrimeFragment), it will also change in the list,
without us having to pass any data back! The downside is, of course, that
any changes in, say, the title of a selected crime can not be undone or
canceled! Try that approach, for practice.
Instead, we will use the fact that we are using fragments,
so instead of starting a new activity via an intent, we swap the
CrimeListFragment out against a CrimeFragment, leaving the current activity
in charge. To do this right requires a few steps (that would be applicable
for any fragment).
Only the hosting activity can swap fragments. Thus, our list fragment
must notify the hosting fragment that it should perform some action when a
list item is clicked. To do this, we do NOT want to require that the
fragment knows much about the hosting activity: activities can know
everything about the fragments they are using, but fragments should be as
independent of their hosting activity as possible! Thus, we add an
interface to our fragment and require that any hosting activity must
implement that interface. Then we implement that interface in our hosting
activity and swap the fragments when the interface method is called. Note
that we also add the swap to the "back stack", so that the user can return
to the list fragment by hitting the "back" buttn. The second detail fragment
can modify the data of the referenced Crime object, which in turn will be
reflected in the list because of our trick with the 'singleton'. However,
the list needs to updated (refresh) when its data changes, so we add a call
to the adapter's "notifyDataSetChanged" in the onReview method.
Click here to check the
"fragment swapping" app ...
- Next we will modify our app so that it shows a list of crimes only while
in portrait mode and switches to the crime details if a list item is tapped.
But if you move the device into landscape mode, the layout should consist of
the list on the left and a detail view on the right side, and tapping a list
item will update the details on the right. This time, we will define the two
layouts via XML. The usual 'layout' folder will contain a FrameLayout, which
will in turn contain the single fragment 'CrimeListFragment'. Then there is
a 'layout-land' folder, containing an XML layout file that uses a
LinearLayout to arrange two fragments horizontally. Check, in particular,
the syntax for adding a fragment in XML. Then the code is adjusted to make
use of these XML layout files without using code and the FragmentManager to
swap fragments in and out.
- NOTE: the app as available below, contains a number of
problems - try to figure out how to fix them, we will 'upgrade' the app in
class!
Click here to check the "dual-pane"
auto-adjusting app ... currently with a number of problems such
as:
- When you click on a crime in the list, you get some strange overlay –
what should happen instead if that the crime detail should show, replacing
the list (in portrait mode). That’s a ‘to-do’.
- When you are in landscape mode, you will see the details for a ‘new’
crime only, regardless of the crime selected – what should happen instead is
that the details of a crime should update depending on the crime you tap on
the list on the left. Another to-do.
- And of course I want to be able to update the info on the detail pane
and see that new info reflected in the list of crimes – yet another to-do
- Finally, it might be nice to give the list some ‘background’ color
to better define it in landscape mode – a minor to-do
- And there might be other problems lurking – more to-dos
Click here for the
working dual-pane app (not perfect yet, see HW below)
HW: The biggest problem of our current app is that I can
edit crime details in landscape mode but the list does not get updated
accordingly. Actually, the list does get updated but not its view (you
can see the list data is updated 'in secret' when you flip orientation - the
list in portrait mode will reflect your changes). Your homework is to get the
list to redraw itself whenever the crime details are edited. As a hint: you need
info to flow from the 'detail' fragment to the 'list' fragment via the hosting
activity. We already know how to do that, because our list fragment sent info to
the detail fragment to update according to the selected item. We used an
interface for that, so that's what you'll have to do to solve this problem
as well.
We also need to add 'save and retrieve' features to our app, but that will be
pretty straigh-forward. You could try this as homework but we will cover that on
Monday anyway.
- Click to copy the source code into a new class called
JSONSerializer
- Add a a few constants, a new constructor, and a new "toJSON" method to
the Crime class:
private static final String JSON_ID = "id";
private static final String JSON_TITLE = "title";
private static final String JSON_SOLVED = "solved";
private static final String JSON_DATE = "date";
...
public Crime(JSONObject json) throws JSONException
{
id = UUID.fromString( json.getString(JSON_ID));
title = json.getString(JSON_TITLE);
solved = json.getBoolean(JSON_SOLVED);
date = new Date(json.getLong(JSON_DATE));
}
...
public JSONObject toJSON() throws JSONException
{
JSONObject json = new JSONObject();
json.put(JSON_ID, id.toString());
json.put(JSON_TITLE, title);
json.put(JSON_SOLVED, solved);
json.put(JSON_DATE, date.getTime());
return json;
}
- Change the constructor of our CrimeLab class to read the crimes
from disk, and add a convenience "save" method (note that our constructor
remains private to preserve the "singleton" nature of this class
instantiazation).
private CrimeLab(Context context)
{
super();
this.context = context;
this.serializer = new JSONSerializer(context, FILE);
crimes = new ArrayList<Crime>();
try
{
crimes = serializer.loadCrimes();
}
catch (Exception e)
{
crimes = new ArrayList<Crime>();
Log.e( TAG, "Error loading crimes: ", e);
}
}
...
public void save()
{
try
{
serializer.saveCrimes(crimes);
Log.d(TAG, "File saved successfully");
}
catch(Exception ex)
{
Log.e(TAG, "Error saving file", ex);
}
}
- Finally, we need to find a convenient place to save the data. A good
spot would be the onBack method of the CrimeListFragment, so that
the data always gets saved whenever the app receeds into the background or
is even distroyed.
That should do the trick and finishes our current project. You could now add
menus to delete and add crimes, much as we've done in our iBookmarks, which you
could use as a model. We could also add data fields to get a name from the
address book or take a picture via implicit intents. We also looked into
implicit intents, so this does not add anything new to our project (the image
would be new, especially how to store it) so we'll consider this project
done for now.