ast June, Qualcomm took the wraps off one of the most significant changes to Qualcomm Brew in over two years: the Brew UI Toolkit, an entirely new paradigm for Brew user interface development. Applications written with the Brew UI Toolkit have access to user interface elements traditionally available to Brew applications, along with a slew of new controls, along with layout management, application state management through forms, and nested user interface control hierarchies.
Unfortunately, a change this wide-sweeping is not without cost: to take advantage of the Brew UI Toolkit, you must forego using the existing Brew controls based on the IControl interface. Consequently, moving to the Brew UI Toolkit is not a trivial effort, because you need to learn the new interfaces and the programming paradigm embraced by the Brew UI Toolkit team before you begin. This investment, however, more than pays off; your application has access to features difficult or impossible to implement using the previous control-based interfaces, and your application will be better structured as well. This isn’t to say that you have to migrate your Brew 3.x application to support widgets; it’s only an option. You’re still free to use the existing control interface instead of widgets, although choosing to use the existing control interface limits your options, especially if you want to use one of the new widgets that doesn’t have a corresponding IControl implementation.
The Brew UI and the Model-view-controller Pattern
Key to understanding the Brew UI Toolkit is understanding the model-view-controller (MVC) design pattern, because it’s at the heart of the Brew UI Toolkit. Around for almost thirty years, MVC is at the heart of many of today’s user interface frameworks, including Java Swing; you may have well used it but not recognized it. You can get a good summary of the MVC pattern here.
The MVC pattern breaks graphical UI applications up into three discrete parts: the model, view, and controller. The model is responsible for managing the application-domain data, responding to requests for information about the state of the data (typically from the view), and changes state (typically in response to requests from the controller). The view is responsible for managing the graphic output of the application. The controller is responsible for interpreting key events, menu selections, and other inputs, and passing the requests those commands represent to the view (to change its view of the model) or the model (to change its data).
In the traditional implementation, the controller accepts events from the system’s event pump, uses polymorphism or traditional conditional logic to determine the appropriate action to take place within the view or model. In turn, the view listens for change notifications from the model and controller, querying the model for new data as needed when it receives a change notification. The model itself tracks the state of the application data. A common optimization for this pattern is to collapse the responsibilities of the view and the controller into a single programmatic entity; the Brew UI Toolkit uses this, letting you specify an event handler for any view. In essence, the Brew UI Toolkit provides default controllers for each view, and you’re able to override this behavior with your own controller via a new event handler.
The Brew UI Toolkit provides base classes that represent the model (IModel) along with several subclasses for specific kinds of models, including a model for text (ITextModel), lists and menus (IListModel and IMenuModel respectively) as well as a general-purpose IValueModel with which you can encapsulate arbitrarily complex structures. The toolkit also provides a horde of user interface primitives from which you can build your user interface, including widgets for text input, radio and checkbox selection, buttons, text and bitmap display, and menu items and menus. All of these inherit the IWidget interface, which is conceptually similar to (but programmatically wholly unlike) the old IControl interface. Widgets can be nested, and some widgets perform layout and focus control for the widgets they contain, giving a decidedly desktop-programming flavor to the process of building your user interface. In turn, your widgets occupy a form, which is the top-level of a user interface from the perspective of your application.
Forms, Widgets, and Containers
The notion of nesting widgets deserves extra attention, because it’s the key by which complex (yet easy-to-use) interfaces are made with the Brew UI Toolkit. As in a traditional desktop GUI, widgets can contain other widgets?some widgets can act as containers, containing other widgets and controlling their layout. Other widgets act as decorators, letting you link their functionality to enhance a given widget. For example, a scrolling text view consists of two widgets: the text widget showing the text, and a scrollbar widget that provides the scrolling behavior, decorating the functionality of the text widget. (You can learn more about the decorator pattern here, or by reading the description in Gamma, et al’s Design Patterns.)
Widgets which contain other widgets are containers, and implement the IContainer interface. Don’t be confused between IContainer and IWidget, or their methods. The IContainer interface lets you manipulate how a widget performs its job as a container; the IWidget interface lets you manipulate the widget itself. This is especially important when working with classes like IXYContainer and IConstraintContainer whose primary function is to contain objects. To use these objects as widgets, you must first obtain the corresponding widget interface from an instance via ICONSTRAINTCONTAINER_QueryInterface. This requests the widget interface for the corresponding container object.
While a specific screen consists of a hierarchy of widgets, your entire application is a collection of forms. A form consists of a top-level container; the Brew UI Toolkit provides a mechanism for your application to track its state as a top-level form (the one currently on-screen) and a stack of forms. As you use an application, the application uses events like soft keys and selection items to create new forms, fills them with widgets, and places them on the top of the stack. When you exit a form (say, by pressing the clear key), the form is popped from the stack. Just as widgets and the MVC pattern give you the ability to conceptualize a value and its relationship to the user, forms and the form stack give you a way to conceptualize application flow within the application.
At the highest level, then, your application consists of one or more forms, each of which has a collection of widgets (the view) which act together on a set of models that represent the state of your application’s data. Many widgets will do their work automatically, enabling the user to enter text or select choices from a menu; others might need additional code in the form of a controller.
Getting Started with An Application
It’s a little daunting to begin with the Brew UI Toolkit, simply because there are so many new classes to consider. It’s best to begin small, exploring directly the relationship between an application’s form, some widgets, and their model, and then build out, gradually exploring new widgets and multiple forms as you build your application user interface. To help you get started, I’ve written IOU, an application which uses the Brew UI Toolkit to track the money that changes hands at the local Starbucks. You can see the application in Figure 1.
|Figure 1. The IOU Entry Form: This form demonstrates an IForm instance containing static and dynamic Brew widgets.
Building an application with the Brew UI Toolkit is a little different than starting a traditional application in Brew, because the toolkit components aren’t bundled with the Qualcomm Brew Simulator. Because these components are packaged as separate Brew extensions, make sure that the path to your application in the simulator also includes the MIF and DLL files for the forms and widgets releases included with the Brew UI Toolkit. (This is the same process you’d use to include any other extensions in your application on the simulator.) While Qualcomm has targeted Brew UI Widgets for inclusion in Brew 3.1 and beyond, it’s possible to develop your application and test on handsets running Brew 2.0 now, by installing the forms.mod and widgets.mod files on your target handset as well.
While applications can use widgets without forms, it makes little sense to do so: forms give you a powerful way to conceptualize your application flow and group widgets. Any application that uses both widgets and forms must create a root form, a form for each screen, and a top-level container for each form.
This top-level form has two key responsibilities. First, it maintains the stack of forms and tracks the notion of which form has focus. Second, it presents a container user interface for your form, consisting of a title and soft key labels. By permitting the root form to own the responsibility of creating and configuring these widgets, device manufacturers can ensure a similar look and feel between all widget-based applications. (Of course, if your application has a drastically different user interface, you’re free to create your own root form class, or simply use the IRootForm interface provided to access and mutate the title bar and soft key widgets as required.)
The input form?the most complex form in IOU?is created by the InputForm_Create method, which uses lazy instantiation to create the form and its widgets.
Listing 1 seems long, but it’s actually simple, consisting of four key parts:
- Part 1: Consists of the CreateInstance invocations followed by the QueryInterface invocation, which creates a new form, its constraint container, and obtains the widget associated with the constraint container. The constraint container’s bounds are set to be the bounds of the root form, obtained by getting the root forms’ client bounds, and then the container widget is linked to the new form. Once the new form is initialized, the code sets the title and soft key widget labels using IFORM_SetText.
- Part 2: Creates two widgets: the input field for the payer/payee information, as well as a label indicating the direction of cash flow. Because the code uses an IConstraintContainer as the base container for the form, it can rely on the container to perform all of the necessary layout mathematics, aligning each component on your behalf. (The Brew UI Toolkit also provides the IXYContainer interface, which, like its name suggests, permits absolute placement of its child widgets). The IConstraintContainer is especially helpful in applications like this that mimic paper forms, because you can use it to do all of your widget layout, and your application will dynamically resize to support different screen sizes on different handsets. (I could have used arithmetic signs to indicate this, but it seemed cleaner to have the UI change appearance to reflect the flow of money.) The IConstraintContainer constraint manager requires a WidgetConstraint structure with each widget, which you use to specify the type of constraint when placing a widget’s top, left, right, and bottom points in the ConstraintContainer. For each of these points, you can specify whether the child widgets’ coordinates should be relative to its sibling or the parent, or sized based on its contents, as well as an offset to apply to the calculated value. In turn, the constraint container dynamically lays out its children widgets, and reflows the widgets as required when new items are added or removed from its widget hierarchy.
- Part 3: Involves more of the same?widget creation and placement, although instead of constraining the last two widgets relative to the bottom of the previous widget, the first (the currency symbol) is relative to the bottom, and the second (the currency input) is relative to the right of the currency symbol, so that both the symbol and input line are on the same line of the screen.
- Part 4: Pushes the form on the root form’s stack, making it the foremost form in the application. Just before returning SUCCESS, the code invokes IROOTFORM_PushForm to push the form on the top of the form stack. Of course, I do this regardless of whether the form was freshly created or not, because if the form already exists, it needs only be brought to the top of the stack.
All of the code in Listing 1 points to one other important thing to remember: when you assign an object to something in the widget library (a widget to a container, a model to a widget, and so forth), the assignee owns the object. You need to be sure to release your instance, otherwise memory will leak when your application exits.
In the next article, I show you how to use the root form to manage multiple forms as well as the role of the IValueModel and ITextModel models in managing user input from selection lists and text entry widgets.