Sunday, August 24, 2008

The Model (MVC): Million Contacts March

Swing's approach to MVC is one of the hardest concepts for people to fully grasp, which is a real shame as it is probably the most important and powerful feature in Swing. LWUIT copied Swing's approach to MVC almost entirely but at a smaller scale. Chen already blogged about renderers in the past but that is only one piece of the puzzle, to fully understand it we need to understand models... But first lets recap, what is MVC:

Model - Represents the data for the component (list), the model can tell us exactly how many items are in it and which item resides at a given offset within the model. This differs from a simple Vector (or array) since all access to the model is controlled (the interface is simpler) and unlike a Vector/Array the model can notify us of changes that occur within it.

View - The view draws the content of the model. It is a "dumb" layer that has no notion of what is displayed and only knows how to draw. It tracks changes in the model (the model sends events) and redraws itself when it changes.

Controller - The controller accepts user input and performs changes to model which in turn cause the view to refresh.

LWUIT's List component uses the MVC paradigm to separate its implementation. List itself is the Controller (with a bit of View mixed in). The ListCellRenderer interface is a View and the ListModel is (you guessed it by now) the model.

When the list is painted it iterates over the visible elements in the model and asks for them, it then draws them using the renderer.

Why is this useful?

Since the model is a lightweight interface it can be implemented by you and replaced in runtime if so desired, this allows several very cool use cases:

1. A list can contain thousands of entries but only load the portion visible to the user. Since the model will only be queried for the elements that are visible to the user it won't need to load into memory a very large data set until the user starts scrolling down (at which point other elements may be offloaded from memory).

2. A list can cache efficiently. E.g. a list can mirror data from the server into local RAM without actually downloading all the data. Data can also be mirrored from RMS for better performance and discarded for better memory utilization.

3. No need for state copying. Since renderers allow us to display any object type, the list model interface can be implemented by the applications data structures (e.g. persistence/network engine) which would return internal application data structures saving you the need of copying application state into a list specific data structure.

4. Using the proxy pattern (as explained in a previous post) we can layer logic such as filtering, sorting, caching etc. on top of existing models without changing the model source code.

5. We can reuse generic models for several views e.g. a model that fetches data from the server can be initialized with different arguments to fetch different data for different views. View objects in different Form's can display the same model instance in different view instances thus they would update automatically when we change one global model.

Most of these use cases work best for lists that grow to a larger size or represent complex data which is what the list object is designed to do.

To show this off lets create a list with one million entries... What I am trying to prove here is that a list and a model have a very low overhead when used properly. Most of the overhead for rendering a list is in the renderer and the model implementation, both of which you can optimize to your hearts content. This is a very small price to pay for something as flexible, powerful and customizable as the LWUIT list!

class Contact {
private String name;
private String email;
private Image pic;

public Contact(String name, String email, Image pic) {
this.name = name;
this.email = email;
this.pic = pic;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public Image getPic() {
return pic;
}
}

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.getStyle().setBgTransparency(0);
email.getStyle().setBgTransparency(0);
cnt.addComponent(name);
cnt.addComponent(email);
addComponent(BorderLayout.CENTER, cnt);
}

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

public Component getListFocusComponent(List list) {
return focus;
}
}
String[][] CONTACTS_INFO = {
{"Nir V.","Nir.Vazana@Sun.COM"},
{"Tidhar G.","Tidhar.Gilor@Sun.COM"},
{"Iddo A.","Iddo.Arie@Sun.COM"},
{"Ari S.","Ari.Shapiro@Sun.COM"},
{"Chen F.","Chen.Fishbein@Sun.COM"},
{"Yoav B.","Yoav.Barel@Sun.COM"},
{"Moshe S.","Moshe.Sambol@Sun.COM"},
{"Keren S.","Keren.Strul@Sun.COM"},
{"Amit H.","Amit.Harel@Sun.COM"},
{"Arkady N.","Arcadi.Novosiolok@Sun.COM"},
{"Shai A.","Shai.Almog@Sun.COM"},
{"Elina K.","Elina.Kleyman@Sun.COM"},
{"Yaniv V.","Yaniv.Vakrat@Sun.COM"},
{"Nadav B.","Nadav.Benedek@Sun.COM"},
{"Martin L.","Martin.Lichtbrun@Sun.COM"},
{"Tamir S.","Tamir.Shabat@Sun.COM"},
{"Nir S.","Nir.Shabi@Sun.COM"},
{"Eran K.","Eran.Katz@Sun.COM"}
};

int contactWidth= 36;
int contactHeight= 48;
int cols = 4;
Resources images = Resources.open("/images.res");
Image contacts = images.getImage("people.jpg");
Image[] persons = new Image[CONTACTS_INFO.length];
for(int i = 0; i < persons.length ; i++){
persons[i] = contacts.subImage((i%cols)*contactWidth, (i/cols)*contactHeight, contactWidth, contactHeight, true);
}

final Contact[] contactArray = new Contact[persons.length];
for (int i = 0; i < contactArray.length; i++) {
int pos = i % CONTACTS_INFO.length;
contactArray[i] = new Contact(CONTACTS_INFO[pos][0], CONTACTS_INFO[pos][1], persons[pos]);
}

Form millionList = new Form("Million");
millionList.setScrollable(false);
List l = new List(new ListModel() {
private int selection;
public Object getItemAt(int index) {
return contactArray[index % contactArray.length];
}

public int getSize() {
return 1000000;
}

public int getSelectedIndex() {
return selection;
}

public void setSelectedIndex(int index) {
selection = index;
}

public void addDataChangedListener(DataChangedListener l) {
}

public void removeDataChangedListener(DataChangedListener l) {
}

public void addSelectionListener(SelectionListener l) {
}

public void removeSelectionListener(SelectionListener l) {
}

public void addItem(Object item) {
}

public void removeItem(int index) {
}
});
l.setListCellRenderer(new ContactsRenderer());
l.setFixedSelection(List.FIXED_NONE_CYCLIC);
millionList.setLayout(new BorderLayout());
millionList.addComponent(BorderLayout.CENTER, l);
millionList.show();

14 comments:

  1. Hi,

    Really great article. I tested this with some 15 contacts and it worked great. But when I tested it with 1000000 contacts the loading was very very slow.
    Pratically the list should ask from the model only those elements which need to be displayed. It happened the same way when I tested it with 15 contacts.
    But when i tested it with 1000000 contacts getSelectedIndex() was called repeatedly and that made the loading very slow.
    I have put sout in every function of the example and I felt repeated calls of some functions like getSelectedIndex() and getSize().

    ReplyDelete
  2. getSelectedIndex and getSize should just return an integer value and it has no performance difference for a million or 12. If it has then you need to cache this data within the model (e.g. in case of a network connection).
    The makeover demo brings lazily 500 elements over the network into the list without any performance penalty. This code doesn't have any performance penalty unless you use it incorrectly. You must have loaded 1000000 elements into memory which is not what you should do. You should keep elements stored in place and load them as necessary with weak references etc... but this is related to programming in general not to LWUIT.

    Try running this actual code (which we have run on devices) and you will see that it runs just as fast as the youtube video.

    ReplyDelete
  3. For some reason I keep getting cells outside of my screen, 2 cells to be precise. Any idea why this could be?

    ReplyDelete
  4. Scrolling FAQ:
    https://lwuit.dev.java.net/faq.html#Q9

    ReplyDelete
  5. Hi All,
    i wanted to put checkBox in front of each contact... how can i do this...
    please help... thanks in advance

    ReplyDelete
  6. I tried to implement in my project
    according to your example,
    and I get to much calls to
    getListCellRendererComponent.
    I made a model list with one
    object, and got printed 10 objects.
    does someone know something about
    this issue?

    ReplyDelete
  7. Since animation occurs as well as calls for every entry on the screen the renderer is called very frequently. However, it will only be invoked for elements that are actually visible on the screen.

    ReplyDelete
  8. I am trying to run your code but am facing many problems,can u upload your project source and resources?

    ReplyDelete
  9. Hi! Kiruikenn,

    I've been trying to run it too. So, I've splited it in 3 classes + images.res + LWUIT_1.3. I'm not sure if it behavis the same as proposed, but it's running.

    I don't know how to send it to you.

    ReplyDelete
  10. Hi listcellrender example is good ihave small dobut with contact and image also when clicks on another default list how can be select and back to again its needs to come normal listcellrender can you pls explain how needs to be done that where can i call another list from exiting list which method i need to use.shai i haven't get any answer for pls help me for this.

    ReplyDelete
  11. Hi! my idea is do like this list but adding a new button at right of row that can be selected. Can you help me? Thanks!

    ReplyDelete
  12. Hi,
    I am working on the custom list according to the above example. But there is the problem of focus. I can not get the focus on selected cell.Please help me.
    Thanks in advance.
    With Regards:
    Arpan Asawa

    ReplyDelete
  13. Thank you very much. Helped me alot.

    ReplyDelete
  14. what of if the image is being downloaded from the web, how do you do this with thread so as to allow the app remain interactive with users without freezing

    ReplyDelete