Avoiding Lists

Datetime:2016-08-22 22:34:47          Topic: Coder  Andrew Development           Share

When picking up a new UI API people often start with a list of items. Lists are often used for navigation, logic and data so it’s a natural place to start. Codename One’s List class is a bad place to start though…​ It’s complex and encumbered and has far better alternatives.

How did we Get Here?

When we initially created the List API we were heavily inspired by Swing’s architecture. The ability to create an infinitely sized list without a performance penalty was attractive and seemed like a good direction for our original 2mb RAM target devices (back in 2006-7). We knew the renderer/model approach was hard for developers to perceive but we also assumed a lot of Swing developers would find it instantly familiar.

We made attempts to improve List in the years since e.g.: MultiList , GenericListCellRenderer , ContainerList etc.

These helped but the core problems of List remain.

What Changed?

Modern interfaces are far more dynamic, we have features such as swipable containers, drag and drop to rearrange etc. The renderer approach complicates trivial tasks e.g.:

  • Variable sized entries - this is impossible in a standard List or MultiList . We designed ContainerList to solve this but it’s both ridiculously inefficient and buggy

  • More than one clickable item per row - it’s common to have more than one item within a row in the List that can handle an event. E.g. a delete button. This is a difficult (albeit possible) task for List items.

  • Performance - List can perform well but writing performant List code is a challenge. Anything under 5000 entries would perform better with alternative solutions. If you need more than 5000 rows, reconsider…​

    Scrolling beyond 1000 rows on a mobile device is challenging.

  • Customizability - You can customize the look of the List component but there are nuances and some limits.

  • Model - MVC is a good idea but it’s hard. Features like dynamic image download in lists challenge even experienced Codename One developers.

What Should we use Instead?

This varies based on your needs but the general answer is a scrollable BoxLayout.Y_AXIS container.

The Simple Use Case

E.g. if I write a simple List such as this:

com.codename1.ui.List<String> lst = new com.codename1.ui.List<String>("A", "B", "C");
lst.addActionListener(e -> Log.p("You picked: " + lst.getSelectedItem()));

I can convert it to this:

String[] abc = new String[] {"A", "B", "C"};
Container list = new Container(BoxLayout.y());
list.setScrollableY(true);
for(String s : abc) {
    Button b = new Button(s);
    list.add(b);
    b.addActionListener(e -> Log.p("You picked: " + b.getText()));
}

Admittedly there is more code in the second version, but it’s far more powerful and as your UI design grows the code will shrink by comparison!

E.g. if you don’t want the default look of the list or want thumbnail image, or want a single entry to behave differently the latter option is far simpler.

Lead Component

When you click an entry within the list you can click anywhere and it will work. If you compose an entry in the list from more than one piece those pieces act as one.

E.g. we have this code in the developer guide section covering List renderers:

class ContactsRenderer extends Container implements ListCellRenderer {
 private Label name = new Label("");
 private Label email = new Label("");
 private Label pic = new Label("");
 private Label focus = new Label("");

 public ContactsRenderer() {
     setLayout(new BorderLayout());
     addComponent(BorderLayout.WEST, pic);
     Container cnt = new Container(new BoxLayout(BoxLayout.Y_AXIS));
     name.getAllStyles().setBgTransparency(0);
     name.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM));
     email.getAllStyles().setBgTransparency(0);
     cnt.addComponent(name);
     cnt.addComponent(email);
     addComponent(BorderLayout.CENTER, cnt);

     focus.getStyle().setBgTransparency(100);
 }

 public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected) {
     Contact person = (Contact) value;
     name.setText(person.getName());
     email.setText(person.getEmail());
     pic.setIcon(person.getPic());
     return this;
 }

 public Component getListFocusComponent(List list) {
     return focus;
 }
}

We can create a similar container using this approach:

Container list = new Container(BoxLayout.y());
list.setScrollableY(true);
for(Contact c : contacts) {
    list.add(createContactContainer(c));
}

private Container createContactContainer(Contact person) {
    Label name = new Label("");
    Label email = new Label("");
    Label pic = new Label("");
    Container cnt = new Container(new BoxLayout(BoxLayout.Y_AXIS));
    name.getAllStyles().setBgTransparency(0);
    name.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM));
    email.getAllStyles().setBgTransparency(0);
    cnt.add(name);
    cnt.add(email);
    name.setText(person.getName());
    email.setText(person.getEmail());
    pic.setIcon(person.getPic());
    return BorderLayout.center(cnt).
        add(BorderLayout.EAST, pic);
}

The problem with this approach becomes obvious when we try to add an event listener…​.

We can make name into a Button but then what happens when a user clicks email ?

We can make all the entries into buttons but that isn’t practical. That’s whatlead component is for, we can make one component into a button and it "takes the lead". If we make name into a button and set it as the lead of the Container it will handle all the events and state changes for the entire row!

For more information on lead components check outthe sidebar in the developer guide.

We can change the code above like this and support lead components:

private Container createContactContainer(Contact person) {
    Button name = new Button("", "Label");
    name.addActionListener(e -> Log.p("You clicked: " + person));
    // ...
    Container b = BorderLayout.center(cnt).
        add(BorderLayout.EAST, pic);
    b.setLeadComponent(name);
    return b;
}
What do you do if you want to exclude an item from the lead component hierarchy (e.g. a delete button)?
Check outthis blog post.

Infinite Scroll

One of our earliest demos showed off a million entries running on a 3mb Nokia mobile phone. While that is an impressive feat it isn’t useful.

Most real world UI’s use pagination to fetch more data when they reach the bottom of the scroll. This is predictable and easy to integrate both in the client and server code.

Two classes simplify the process of infinite scrolling list: InfiniteContainer and InfiniteScrollAdapter .

InfiniteContainer is an easy to use drop-in replacement to Container . InfiniteScrollAdapter is more versatile, you can apply it to any Container including the content pane. We have samples for bothInfiniteContainer and InfiniteScrollAdapter in the JavaDocs.

Don’t Use Lists

In closing I’d like to re-iterate our recommendation: "Don’t use lists". We didn’t deprecate those API’s because developers rely heavily on them & this might induce "panic".

There’s no valid reason to use a List as opposed to a Container . List is harder to use, slower & not as flexible.

We can’t cover every conceivable use case in this post so if you have a List or code you can’t imagine any other way, post it in the comments below.





About List