Android: Editing Lists of Values
I have been working with lists in the Android app I’m working on, and I found that they have a bit of a learning curve. Its always the case in programming - there are some things, that seem like they should be easy, which can often be quite complicated. And, lists are one of those things in Android: not that intuitive; you have to learn to do things the “Android way”. However, after coming to understand the tools that the Android framework gives us, we can quite quickly construct a flexible and robust solution.
Background - Basic UX
In keeping with UI patterns on mobile platforms, we should try to keep the screens very simple. So, as a simple phone-centric solution, I created one Activity
class which would perform all the activities associated with displaying and updating the multivalued attribute. The object containing the list can be passed in and out quite simply as an extra in the Intent
, as this object implements the Parcelable
interface.
Activity Overview
At the top level, the solution breaks down into two parts:
- Loading a
ListView
with a customised item view containingEditText
’s - Assign a custom
Adapter
class which will interface between the elements in the UI and the list we want to manage.
Now, let’s break the problem down in more detail.
Implementing a ListView
Implementing the ListView
in itself is not so difficult. First we will define a top level layout for our Activity
to use, which must contain the ListView
that we will customize. This ListView
object by itself only can specify how the items in the list are to be presented (eg the alignment of the list) and not the layout of the individual items in the list. As usual, we use setContentView()
to load the class in our Activity
. So in my layout resource file below, I take a standard list, and attach to it a header and a footer (defined in external layout files), which will ensure that a button to add additional entries to the list is always present, as well as save and cancel buttons:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<include android:id="@+id/header"
layout="@layout/list_header_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true" />
<include android:id="@+id/footer"
layout="@layout/list_footer_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
<ListView android:id="@+id/listinput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header"
android:layout_above="@id/footer"/>
</RelativeLayout>
A Custom Adapter
After loading the layout file, we must find the ListView
object that is defined here so that we can attach a custom Adapter
to it in our Activity
. This Adapter
will define the layout of the items in the list, and it will take care of initialising and updating those layouts. This class can initially seem a little opaque, though we can come to understand it by analysing the contents. There are some boilerplate methods we must add, but of primary importance is the following method that is used to load the layout of individual items in the list:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.listitem_multivalueinput, null);
holder.attributeValue = (EditText) convertView.findViewById(R.id.attributeValue);
holder.attributeDeleteButton=(Button)convertView.findViewById(R.id.deleteValueButton);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
//Fill EditText with the value you have in data source
holder.attributeValue.setText(myAttributes.get(position).toString());
holder.attributeValue.setTag(position);
holder.attributeDeleteButton.setTag(position);
//we need to update adapter once we finish with editing
holder.attributeValue.setOnFocusChangeListener(new View.OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus){
final int position = (Integer) v.getTag();
final EditText Caption = (EditText) v;
if (!myAttributes.get(position).equals(Caption.getText().toString())) {
myAttributes.add(position, Caption.getText().toString());
}
}
}
});
holder.attributeDeleteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final int position = (Integer) v.getTag();
myAttributes.remove(position);
if (myAttributes.size() < 1) {
myAttributes.add("");
}
notifyDataSetChanged();
}
});
return convertView;
}
There are quite a few things going on here, so lets step through it one by one. Firstly, if we are instantiating a view for the first time, then we need to inflate (load) the view from a layout resource file - which I have defined as listitem_multivalueinput
. This file is quite simple, it is just a LinearLayout
containing an EditText
and a Button
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/attributeValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="16sp"
android:layout_weight="7" />
<Button
android:id="@+id/deleteValueButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="x" <!--don't do this-->
android:width="0dp"
android:layout_weight="1"
android:onClick="removeValue"/>
</LinearLayout>
The next step is to add the EditText
and Button
objects to a ViewHolder
object. ViewHolder
is a simple class which contains references to our layout objects. It is commonly used as we use it here, to tie the object references directly to the ListItem
View
. It is defined as a static class within the Adapter
:
static class ViewHolder {
private EditText attributeValue;
private Button attributeDeleteButton;
}
This reduces the number of calls that need to be made to the relatively expensive findViewbyId()
method. Instead, when updating an existing View
object, we can just grab the reference and go straight to populating the layout objects.
Finally, once we have added in the initial list value to the EditText
and added the position directly onto the EditText
and Button
objects (as it may be useful to refer to it later!), we will set up the event listeners on the layout objects in the list item. Here we have two:
setFocusOnChangeListener()
on theEditText
, which will write any altered value back into the underlying list whenever theEditText
loses focussetOnClickListener()
on theButton
, which removes the value at that position and then callsnotifyDataSetChanged()
to force theListView
to refresh, in order to reflect the changes
What’s Left
That covers almost everything - the only thing we have to do now is to decide whether to persist our changes or not. Since changes are being made to our list in realtime as we update the GUI, all we would need to do is save our object and pass it to the next activity - or to cancel changes we can just Finish()
our Activity
without needing to do anything else.
Further Improvements
If we wanted to extend this to make the view format more re-usable, this would be a good feature to include in an Android Fragment
- it could match the recommended use of fragments for list and details views quite well. Then we could extend this phone-centric solution to make it more suited to tablets as well. But that will be a topic for another day.
More Reading
A Comprehensive Guide to ListViews - with lots of nice diagrams!