Você está na página 1de 7

Use MVC and develop a simple Star Rating widget on Android

by Ivan.Memruk on 19. July 2010 05:33 The MVC (model-view-controller) pattern has been extremely popular in web application frameworks, but even if you never developed for the Web, I am sure that you at least heard this term before. Today we will develop a simple star rating widget that looks like this:

This widget works the following way: every time you tap it, the number of stars increases by 1. If there were 5 stars, the rating wraps around and becomes 0 stars. That's it. The source is fully available at the bottom of this article, and you can do whatever you like with it.

Overview
We will base this simple widget on the MVC pattern which (in a simplified way) can be described like this:

First of all, there is the model which keeps all the business logic and data in its logical form. The model does not care about how the data is displayed to the user and how user actions change the data. Often, the model class is a POJO (plain old Java object) without any references to the platform API. The model often uses the Observer pattern to allow its listeners, such as the view, to get updates from it, while the model doesn't know anything about them and remains a nice clean business class. We'll see this later in the code. Then there is the view. The view is responsible for presenting the data and the state that is defined in the model, to the user. Separating the view from the model allows us to display the same data in various ways. For example, we can display a single star and a number to the right of it, or we could display stars vertically etc. There is also the controller. The controller takes raw user input, often as provided by the view, and converts it to model-changing operations. For example, when you tap on the star rating control, the view gets a raw MotionEvent saying the user has tapped this view. The view itself does not deal with modifying the model according

to this tap, but invokes the controller and just notifies it that a tap event has happened. Then, the controller decides to increase the star rating. Having a controller is not always beneficial in simple cases like this, but it is very handy if you want to replace or modify the behavior of the component, not only its looks. Now let's review each part in detail.

The M: Model
It is a very good idea to start developing your MVC widget with the model. This approach will make sure you focus on the desired business logic and then build the view and the controller accordingly, not the other way around. When build the model, the goal is to describe the logic of your component in abstract, non-UI terms. In many cases, this means just storing all the data that this component represents, but we also want to have data validation and other logical constraints here. In our case, we just store the star rating value and add a getter and a validating setter to it. Here's the complete code of the model class: public final class StarRatingModel { public static final int MAX_STARS = 5; public interface Listener { void handleStarRatingChanged(StarRatingModel sender); } private int stars = 1; private List<Listener> listeners = new ArrayList<Listener>(); public StarRatingModel() { } public int getStars() { return stars; } public void if (stars stars = } else if stars = } if (stars setStars(int stars) { > MAX_STARS) { MAX_STARS; (stars < 0) { 0; != this.stars) {

this.stars = stars; for (Listener listener : listeners) { listener.handleStarRatingChanged(this); } } } public void addListener(Listener listener) { this.listeners.add(listener); } public void removeListener(Listener listener) { this.listeners.remove(listener); } } In web applications, the web-based view will automatically pick up any changes in the model whenever the page is reloaded due to some user action, such as clicking a submit button or a link. In our case, we want the view to be updated whenever the model changes, so that the user always sees the up to date representation of the model. This is why we introduce the Listener interface and keep a list of listeners that we update whenever the star rating value changes. This way, we can have the view registered as a listener, and it will update itself whenever the model notifies it to do so. The great part is that with this architecture, the model does not need to know anything about the view, and remains universally reusable and decoupled from everything. On the other hand, the view does not need to think when it has to redraw itself the model will always notify it when a redraw is necessary.

The C: Controller
Yes, it is not the original order of the letters, but I recommend that we develop the controller next. The goal of the controller is to receive user action events and modify the model as defined by the component specification. In most UI frameworks, and in Android too, the view will receive raw events such as mouse clicks / taps, text input, screen orientation change etc. However, we want to remove the burden of reacting to those events from the view and put it on the controller. This way we can have the view part focus on representation of the data rather than anything else. In addition, since the controller is replaceable, we get more flexibility in controlling the behavior of our widget as a bonus. For example, we could change the controller to decrease the rating rather than increase it, or put the star rating depending on where exactly the user tapped.

In our case, the controller code is really simple: public final class StarRatingController { private StarRatingModel model; public StarRatingController(StarRatingModel model) { this.model = model; } public void handleTap(MotionEvent event) { // the old trick with % to wrap around values model.setStars((model.getStars() + 1) % (StarRatingModel.MAX_STARS + 1)); } } As you can see, we made the controller responsible for transforming the "dump" tap event into a business logic operation of changing the star rating.

The V: View
The view is usually the most platform-dependent part of the MVC troika. Remember, we want this part of our widget to represent, or show, the model data to the user. Also, the view will capture raw events and pass them to the controller so that the user can modify the model. I will not paste the complete code of the view here. The source is available so you can look at it. If you want to learn more about developing custom widgets for Android, I recommend my article about making a thermometer widget. It shows how to develop a much more interesting widget (but does not build it using MVC). The view class has to implement the model listener interface so that it can get updates from the model. It also tries to register to and unregister from the model if it's changed: public final class StarRatingView extends View implements StarRatingModel.Listener { private StarRatingModel model; private StarRatingController controller; /* .... */ public void setModel(StarRatingModel model) { if (model == null) { throw new NullPointerException("model"); }

StarRatingModel oldModel = this.model; if (oldModel != null) { oldModel.removeListener(this); } this.model = model; this.model.addListener(this); this.controller = new StarRatingController(this.model); if (oldModel != null) { invalidate(); } } @Override public void handleStarRatingChanged(StarRatingModel sender) { invalidate(); } /* .... */ } As you can see, whenever the model notifies the view about a change, the view forces itself to redraw using invalidate(). The model data is used whenever the view is redrawn, due to invalidate() or any other reason: @Override protected void onDraw(Canvas canvas) { for (int i = 0; i < StarRatingModel.MAX_STARS; ++i) { float starX = starTopLeft.x + i * starSize; float starY = starTopLeft.y; Paint paint = null; if ((i + 1) <= model.getStars()) { // draw a gold star paint = goldPaint; } else { // draw a gray star paint = grayPaint; } starRect.set(starX, starY, starX + starSize, starY + starSize); canvas.drawBitmap(starBitmap, null, starRect, paint); } }

This is how the view interacts with the model. The view also interacts with the controller whenever it gets a raw touch event: @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { controller.handleTap(event); return true; } else { return super.onTouchEvent(event); } } So, when the user taps the view, the view notifies the controller, which modifies the model. If the model actually changes, it will fire a notification to all its listeners, in this case the view, and the view will redraw itself.

Conclusion
The immediate benefits we have from the MVC architecture, even for this simple widget, include:

The ability to change the representation while keeping the same model and business logic - we could do that by defining another View subclass that would use the same model but draw the data differently. The ability to customize the response to user actions, by developing another controller. The ability to reuse the model in non-UI code, serialize it or store in the database, without affecting the UI. The strategic benefit of having a well-defined architecture that allows the functionality to be improved and extended.

However, there are some typical issues associated with MVC as well, and you have to remember those:

The Observer pattern that is often used to bind the view to the model often becomes a source for memory leaks when listeners do not unregister as they have to When the model and the view are on the same thread, model logic, and the logic of its listeners that are not always known before runtime, can block the UI thread When the model and the view are not on the same thread, you need a way to asynchronously invoke the listeners while being sure the notifications arrive in the correct order and still make sense by the time listeners get to process them There must be a clear understanding of the lifecycle of each piece - the model, the view and the controller. When are they created, who creates them and what references are kept?

If you feel safe about every point on that list, you can go ahead and enjoy the graceful architecture of MVC in your Android apps. The source: mvcdemo.zip (8.58 kb)

Você também pode gostar