Simple Lists in MVC Land

Overview

This tutorial is intended to go over a basic implementation of lists in the Chrome on Android MVC framework. If you're not sure what MVC is, see Additional Resources.

In this example we'll be creating a simple menu list where each list item consists of an icon and label.

Additional Resources

File Structure

The file structure of our component will be the following:

SimpleMenuCoordinator

This class will own the ModelListAdapter that knows how to show PropertyModels. In this example we'll be combining the responsibilities of what would otherwise be the coordinator and mediator for simplicity.

public class SimpleMenuCoordinator {

    private SimpleMenuMediator mMediator;

    public SimpleMenuCoordinator(Context context, ListView listView) {
        ModelList listItems = new ModelList();

        // Once this is attached to the ListView, there is no need to hold a reference to it.
        ModelListAdapter adapter = new ModelListAdapter(listItems);

        // If this is a heterogeneous list, register more than one type.
        adapter.registerType(
                ListItemType.DEFAULT,
                () -> LayoutInflater.from(context).inflate(R.layout.simple_menu_item, null),
                SimpleMenuItemViewBinder::bind);

        listView.setAdapter(adapter);

        mMediator = new SimpleMenuMediator(context, listItems);
    }
}

SimpleMenuMediator

This class is responsible for pushing updates into the ModelList. Updates to that object are automatically pushed and bound to the list view. For a more complex system, the ModelList may be part of a larger PropertyModel that the mediator maintains.

class SimpleMenuMediator {

    private ModelList mModelList;

    SimpleMenuMediator(Context context, ModelList modelList) {
        mModelList = modelList;
        PropertyModel itemModel = generateListItem(
                ApiCompatibilityUtils.getDrawable(context.getResources(), R.drawable.icon),
                context.getResources().getString(R.string.label));
        mModelList.add(new ModelListAdapter.ListItem(ListItemType.DEFAULT, itemModel));
    }

    private PropertyModel generateListItem(Drawable icon, String text) {
        return new PropertyModel.Builder(SimpleMenuProperties.ALL_KEYS)
                .with(SimpleMenuProperties.ICON, icon)
                .with(SimpleMenuProperties.LABEL, text)
                .with(SimpleMenuProperties.CLICK_LISTENER, (view) -> handleClick(view))
                .build();
    }

    private void handleClick(View view) {
        // Do some click logic here. This would typically be done in the mediator.
    }
}

SimpleMenuProperties

These are the types of data that we want to apply to each list item in our menu.

class SimpleMenuProperties {
    @IntDef({ListItemType.DEFAULT})
    @Retention(RetentionPolicy.SOURCE)
    /**
     * This can be one or more items depending on if the list is homogeneous. If homogeneous,
     * this definition can be skipped and 0 can be used in place of that parameter.
     */
    public @interface ListItemType {
        int DEFAULT = 0;
    }

    /** The icon for the list item. */
    public static final WritableObjectPropertyKey<Drawable> ICON =
            new WritableObjectPropertyKey<>();

    /** The text shown next to the icon. */
    public static final WritableObjectPropertyKey<String> LABEL =
            new WritableObjectPropertyKey<>();

    /** The action that occurs when the list item is tapped. */
    public static final WritableObjectPropertyKey<OnClickListener> CLICK_LISTENER =
            new WritableObjectPropertyKey<>();

    public static final PropertyKey[] ALL_KEYS = {ICON, LABEL, CLICK_LISTENER};
}

SimpleMenuItemViewBinder

As per the MVC architecture, this class is responsible for taking a model and applying that information in to to a provided view.

class SimpleMenuItemViewBinder {

    // This can optionally be in the coordinator file depending on the complexity.
    public static void bind(PropertyModel model, View view, PropertyKey propertyKey) {
        if (SimpleMenuProperties.ICON == propertyKey) {
            ((ImageView) view.findViewById(R.id.simple_menu_icon)).setImageDrawable(
                    model.get(SimpleMenuProperties.ICON));

        } else if (SimpleMenuProperties.LABEL == propertyKey) {
            ((TextView) view.findViewById(R.id.simple_menu_label)).setText(
                    model.get(SimpleMenuProperties.LABEL));

        } else if (SimpleMenuProperties.CLICK_LISTENER == propertyKey) {
            view.setOnClickListener(model.get(SimpleMenuProperties.CLICK_LISTENER));
        }
    }
}

simple_menu_item.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 The Chromium Authors
     Use of this source code is governed by a BSD-style license that can be
     found in the LICENSE file. -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="@color/modern_primary_color">

    <org.chromium.ui.widget.ChromeImageView
        android:id="@+id/simple_menu_icon"
        android:layout_width="18dp"
        android:layout_height="18dp"
        android:layout_gravity="center_vertical"
        android:scaleType="centerInside"/>

    <TextView
        android:id="@+id/simple_menu_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:textAppearance="@style/TextAppearance.BlackBody"/>

</LinearLayout>