Blog

Back to Blog

Creating custom columns with editable items in shinobigrids for Android – Part 1

Posted on 7 Oct 2015 Written by Kai Armer

Some of you have already seen how shinobigrids can help you quickly and easily display tabular data within your apps. Displaying data is one thing, but mobile apps will give a much richer user experience if users can interact with this data.

In this blog I am going to show you how you can customize the columns in your shinobigrids to contain editable text fields.

NOTE: This process can be used to create a column containing any kind of custom item.

For those of you who have seen our QuickStart app, the following code will seem quite familiar. If you are new to shinobigrids I recommend you first spend some time looking at our user guide, especially the section which explains how to import the shinobigrids library. For this exercise, we are going to revisit our trusty Person object. We will start by giving our Person an address field, which we will use for our editable column:

public class Person {

    public final String name;
    public final int age;
    public String address;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        this.address = "";
    }
}

Hold Up!

In order to hold editable data we first need to think about our views. shinobigrids currently sports columns which ultimately hold TextViews, so for now we will keep things simple and make one of our columns contain EditTexts instead. shinobigrids internally uses RecyclerView‘s ViewHolder object and the keen-eyed of you may have spotted our TextViewHolder class. We need to define our own ViewHolder class to hold EditTexts. In time, shinobigrids will look to provide alternative ViewHolder types in future releases but for now we will create our own, which as you will see is quite straight forward.

public class EditTextViewHolder extends RecyclerView.ViewHolder{

    public final EditText editText;
    private TextWatcher watcher;


    public EditTextViewHolder(EditText editText) {
        super(editText);
        this.editText = editText;
    }

    public void setWatcher(TextWatcher watcher){
        if (this.watcher != null){
            editText.removeTextChangedListener(this.watcher);
        }
        if (watcher != null) {
            editText.addTextChangedListener(watcher);
        }
        this.watcher = watcher;
    }
}

Pretty self-explanatory really (if you’re not too familiar with basic RecyclerView concepts I would definitely encourage reading up on the Android docs). Having the reference to EditText is handy for later on. Also you may spot the setWatcher method. You will see how we use this to help us save the edited data in a moment.

Let’s be Creative

Next we need to make sure when an EditTextViewHolder is requested, an EditTextViewHolder is either created or retrieved from the pool. This is done via an ItemViewHolderCreator – it’s job is to link a unique number for this type of ViewHolder to the creation code for that ViewHolder. So first we need to define a unique number for the EditTextViewHolder type. It’s not required but I would advise doing this via an XML Id resource to ensure uniqueness. So in your res/values folder create a new file called ids.xml with the following entry:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="edit_text_view" type="id" />
</resources>

With that defined we can make a Creator – I tend to do this as a nested class inside the ViewHolder but there isn’t a requirement to do so. This makes my EditTextViewHolder look like:

public class EditTextViewHolder extends RecyclerView.ViewHolder{
    public static class Creator implements ItemViewHolderCreator {

        @Override
        public int getItemViewType() {
            return R.id.edit_text_view;
        }

        @Override
        public RecyclerView.ViewHolder createItemViewHolder(ViewGroup parent) {
            Context context = parent.getContext();
            EditText editText = new EditText(context);
            return new EditTextViewHolder(editText);
        }
    }

    public final EditText editText;
    private TextWatcher watcher;

    public EditTextViewHolder(EditText editText) {
        super(editText);
        this.editText = editText;
    }

    public void setWatcher(TextWatcher watcher){
        if (this.watcher != null){
            editText.removeTextChangedListener(this.watcher);
        }
        if (watcher != null) {
            editText.addTextChangedListener(watcher);
        }
        this.watcher = watcher;
    }
}

Before this can be used however we need to register it with the ShinobiGridView. In your onCreate method of your Activity do the following:

shinobiGridView.registerItemViewHolderCreator(new EditTextViewHolder.Creator());

Now for a Column

So now we’ve sorted out defining what the EditText items are, we need to create a column that makes use of them. Instead of subclassing the Column class, you create an implementation of the ColumnSpec interface and create a column with that. The ColumnSpec interface defines the behavior and look of the column.

Below is a basic example implementation for an editable column. One thing to note is that for the header we don’t want to use an EditText so we’re reusing the HeaderTextViewHolder that the TextColumnSpec uses.

public class EditTextColumnSpec implements ColumnSpec {

    private final CharSequence columnTitle;
    private WritablePropertyBinder<CharSequence> propertyBinder;
    protected Column.Callback callback;

    public EditTextColumnSpec(WritablePropertyBinder<CharSequence> propertyBinder) {
        this(null, propertyBinder);
    }

    public EditTextColumnSpec(CharSequence columnTitle, 
                WritablePropertyBinder<CharSequence> propertyBinder) {

        if (propertyBinder == null) {
            throw new IllegalArgumentException("The propertyBinder parameter cannot be null");
        }

        this.columnTitle = columnTitle;
        this.propertyBinder = propertyBinder;
    }

    @Override
    public void initialize(Column.Callback callback) {
        this.callback = callback;
    }

    @Override
    public void onColumnAdded(Context context) {

    }

    @Override
    public int getItemViewType(int rowIndex) {
        return R.id.edit_text_view;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int rowIndex) {
        EditableGridActivity.EditTextViewHolder editTextViewHolder =
                (EditableGridActivity.EditTextViewHolder) holder;
        CharSequence text = propertyBinder.bind(rowIndex);

        //Prevent old watcher firing upon initial load
        editTextViewHolder.setWatcher(null);
        editTextViewHolder.editText.setText(text);

        //save back edited data
        editTextViewHolder.setWatcher(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                propertyBinder.write(s, rowIndex);
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
    }

    @Override
    public int getHeaderItemViewType() {
        return com.shinobicontrols.grids.R.id.sg_header_text_view;
    }

    @Override
    public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder) {
        HeaderTextViewHolder headerTextViewHolder = (HeaderTextViewHolder) holder;
        headerTextViewHolder.headerTextView.setText(columnTitle);
    }

    @Override
    public boolean hasHeader() {
        return columnTitle != null;
    }

}

We pass in a title for the column and a PropertyBinder. A PropertyBinder has the sole purpose of getting the right bit of information from the model object. As we are potentially editing text within our column we need a way of saving this back to our Person object. It makes sense to do this work in the PropertyBinder, so we start by extending the PropertyBinder interface, adding a new method:

public interface WritablePropertyBinder<T> extends PropertyBinder<T>{
    void write(T data, int rowIndex);
}

Now we have our interface, our EditTextColumnSpec can just call the writemethod when it needs to save data, following an edit by the user. You will recall the onBindViewHolder method:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int rowIndex) {
    EditableGridActivity.EditTextViewHolder editTextViewHolder =
            (EditableGridActivity.EditTextViewHolder) holder;
    CharSequence text = propertyBinder.bind(rowIndex);

    //Prevent old watcher firing upon initial load
    editTextViewHolder.setWatcher(null);
    editTextViewHolder.editText.setText(text);

    //save back edited data
    editTextViewHolder.setWatcher(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            propertyBinder.write(s, rowIndex);
        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });
}

As with our TextColumnSpec class, the onBindViewHolder method still gets the right information from the model for that row and sets it on the EditTextViewHolder‘s EditText (so if it’s being reused the right data will be applied to it, given the rowIndex).

The getItemViewType method you’ll notice is returning the unique number we defined for the EditTextViewHolder – this is how the Column says what type of items it uses. Note you’re given the rowIndex as a parameter to this method so if you wish you could use different types on different rows.

The Column.Callback object can be useful for obtaining information about the Column (such as its index) and the grid as a whole so you can grab hold of it in the initialize method. The onColumnAdded method is a good place to do any themeing/styling as it is here that a Context first becomes available.

Putting it All Together

Last but not least we create the column and add it to the grid. Back in the onCreate method of your Activity add something like this:

EditTextColumnSpec editTextColumnSpec = new EditTextColumnSpec("Address ",
        new WritablePropertyBinder<CharSequence>() {
    @Override
    public CharSequence bind(int rowIndex) {
        Person person = people.get(rowIndex);
        return person.address;
    }

    @Override
    public void write(CharSequence data, int rowIndex) {
        Person person = people.get(rowIndex);
        person.address = data.toString();
    }
});
shinobiGridView.addColumn(Column.create(editTextColumnSpec));

You will notice here that we implement our new WritablePropertyBinder interface as an anonymous inner class. This ensures that any edited data is saved back to the address of our Person object.

You may notice the scrolling not being particularly smooth – there is a bug in the Android framework with the EditText in a RecyclerView (https://code.google.com/p/android/issues/detail?id=82586). I found just setting the inputType (as suggested in the post) to be very effective – your Creator can do this when creating the EditText:

@Override
public RecyclerView.ViewHolder createItemViewHolder(ViewGroup parent) {
    Context context = parent.getContext();
    EditText editText = new EditText(context);
    editText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    return new EditTextViewHolder(editText);
}

Note: For simplicity our Person objects are not persisted over configuration changes so rotating the device will generate a whole new set of Person objects. Persisting configuration changes could be a nice little exercise for you to try!

Wrapping Things Up

In this blog I’ve briefly talked through the steps needed to create a column which allows data to be edited by the user. We defined a new EditTextViewHolder along with a suitable creator then we registered it with our ShinobiGridView. Next we came up with a new ColumnSpec which we used to create our column and add it to our grid. This has given us the desired result of a column in which we can edit text. The end result should look something like this:

unstyled-editable-grid

Of course, editable text is a fairly simple, albeit common use case. shinobigrids has been deliberately designed to give developers the flexibility to hold any View within grid columns. The sky really is the limit here. You could have ImageViews, more complex ViewGroups, even a shinobichart! You can find a working example of the above code on github. I hope you have found this blog useful. Have fun experimenting with shinobigrids! If you haven’t yet done so, why not download a free trial of shinobigrids. For the more adventurous among you, in part 2 of this blog I give our new editable column a little style!

Back to Blog