Skip to content

Commit

Permalink
MVC classes
Browse files Browse the repository at this point in the history
  • Loading branch information
pforhan committed Apr 2, 2012
1 parent 1cb581e commit d69cb03
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
gen
out
7 changes: 7 additions & 0 deletions src/sans/newsreader/mvc/ArticleController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sans.newsreader.mvc;

/** Controller interface for the ArticleActivity. */
public interface ArticleController {
void setDisplay(ArticleDisplay display);
void onCreate(boolean hasTwoPanes, int categoryIndex, int articleIndex);
}
9 changes: 9 additions & 0 deletions src/sans/newsreader/mvc/ArticleDisplay.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package sans.newsreader.mvc;

import sans.newsreader.core.NewsArticle;

/** The framework implementation of an article display. */
public interface ArticleDisplay {
void finish();
void displayArticle(NewsArticle article);
}
26 changes: 26 additions & 0 deletions src/sans/newsreader/mvc/DefaultArticleController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package sans.newsreader.mvc;

import sans.newsreader.core.NewsArticle;
import sans.newsreader.core.NewsSource;

/** SANDROID note no android classes in use. */
public class DefaultArticleController implements ArticleController {
private ArticleDisplay display;

@Override public void setDisplay(ArticleDisplay display) {
this.display = display;
}

@Override public void onCreate(boolean hasTwoPanes, int categoryIndex, int articleIndex) {
// If we are in two-pane layout mode, this activity is no longer necessary
if (hasTwoPanes) {
display.finish();
return;
}

// Display the correct news article.
NewsArticle article = NewsSource.getInstance().getCategory(categoryIndex)
.getArticle(articleIndex);
display.displayArticle(article);
}
}
82 changes: 82 additions & 0 deletions src/sans/newsreader/mvc/DefaultNewsreaderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package sans.newsreader.mvc;

import sans.newsreader.core.NewsCategory;
import sans.newsreader.core.NewsSource;

public class DefaultNewsreaderController implements NewsreaderController {
private static final int NO_ARTICLE = -1;

// List of category titles
final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };

private NewsreaderDisplay display;
// Whether or not we are in dual-pane mode
private boolean hasTwoPanes;
// The news category and article index currently being displayed
private int categoryIndex;
private int articleIndex;

@Override public void setDisplay(NewsreaderDisplay display) {
this.display = display;
}

@Override public void onCreate(boolean hasTwoPanes, int categoryIndex) {
this.hasTwoPanes = hasTwoPanes;
display.setUpActionBar(CATEGORIES, hasTwoPanes, categoryIndex);
}

@Override public void onRestore(int categoryIndex, int articleIndex) {
setCategory(categoryIndex, articleIndex);
}

private void setCategory(int categoryIndex, int articleIndex) {
this.categoryIndex = categoryIndex;
this.articleIndex = articleIndex;
NewsCategory category = getCurrentCategory();
display.setCategory(CATEGORIES[categoryIndex], category);
// If we are displaying the article on the right, we have to update that too
if (hasTwoPanes) {
if (articleIndex == NO_ARTICLE) {
// Default to first article.
display.setArticle(category.getArticle(0));
} else {
display.setArticle(category.getArticle(articleIndex));
}
}
}

private NewsCategory getCurrentCategory() {
return NewsSource.getInstance().getCategory(categoryIndex);
}

@Override public void onStart() {
// This might have been 0,0 originally.
setCategory(categoryIndex, articleIndex);
}

@Override public void onHeadlineSelected(int articleIndex) {
this.articleIndex = articleIndex;
if (hasTwoPanes) {
// display it on the article fragment
display.setArticle(getCurrentCategory().getArticle(articleIndex));
} else {
display.showArticleActivity(categoryIndex, articleIndex);
}
}

@Override public void onCategorySelected(int catIndex) {
setCategory(catIndex, -1);
}

@Override public int getArticleIndex() {
return articleIndex;
}

@Override public int getCategoryIndex() {
return categoryIndex;
}

@Override public void categoryButtonClicked() {
display.showCategoryDialog(CATEGORIES);
}
}
14 changes: 14 additions & 0 deletions src/sans/newsreader/mvc/NewsreaderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sans.newsreader.mvc;

/** Controller interface for the Newsreader. */
public interface NewsreaderController {
void setDisplay(NewsreaderDisplay display);
void onCreate(boolean hasTwoPanes, int categoryIndex);
void onRestore(int categoryIndex, int articleIndex);
void onStart();
void onHeadlineSelected(int articleIndex);
void onCategorySelected(int catIndex);
int getCategoryIndex();
int getArticleIndex();
void categoryButtonClicked();
}
15 changes: 15 additions & 0 deletions src/sans/newsreader/mvc/NewsreaderDisplay.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sans.newsreader.mvc;

import sans.newsreader.core.NewsArticle;
import sans.newsreader.core.NewsCategory;


/** The framework implementation of an NewsReader display. */
public interface NewsreaderDisplay {

void setCategory(String title, NewsCategory category);
void setUpActionBar(String[] categories, boolean hasTwoPanes, int categoryIndex);
void setArticle(NewsArticle article);
void showArticleActivity(int categoryIndex, int articleIndex);
void showCategoryDialog(String[] categories);
}
90 changes: 90 additions & 0 deletions src/sans/newsreader/mvc/philosophy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
Summary

This is a proposition for how to construct Android code using a strong MVC sensibility. Since
View already has meaning in Android, and because Android reaches farther than only UI views,
think of it as a Model-Framework-Controller structure.

Purpose

"The only way to win is not to play"

For some reason Android is a very hostile development environment. Some of this stems from
poor API design, others from the surrounding SDK and tools. Two obvious examples are the
preference for abstract and concrete classes in place of interfaces, and the outright hostility
to testing in general.

To that end, I have constructed a lightweight philosophy for how to create android applications
whose logic is divorced from the Android code base. This allows for a strict separation of
view and controller and easy testability.

New Code - Controller and Display

Typically, for each activity, new code will consist of two interfaces and a class. Supposing
an activity was named PaintActivity, there would be:
- PaintController - interface defining all logic to be performed by an Activity.
- PaintDisplay - interface defining operations that interact with the user and/or Android.
Would normally be called PaintView, but this term is taken.
- DefaultPaintController - implementation of PaintController that uses methods on PaintDisplay
to perform its logic.

And of course, PaintActivity would implement PaintDisplay.

Mechanism

Quite simply, all non-Android logic and state resides in the Controller implementation. The
Activity is reduced to view interactions and Android system interactions only.

To convert an existing activity, start by moving its fields to the Controller. Then generally,
for each logic block, create a corresponding method on Controller. As needed, create methods
on the Display for the controller to call.

The Activity will flatten out to simple methods implementing the Display. During onCreate,
it will set itself on the Controller, and pass any information necessary from intents or
bundles for the controller to initialize itself and the Activity. It will attach all
appropriate listeners as well.

A key tenant is to keep all Android-related classes and method calls within the activity.
Application-specific domain classes may be passed back and forth between Controller and Display.
Application-specific logic, however, should remain confined to the Controller.

Testing

The Activity should be simplified to the point that it seems trivial. For example, it is
told to display a string in a field, and it does so. There are no branching paths based on
custom application state. It feels foolish to test. (Though full integration tests are worth
their own look.)

Rather than writing a testSetTitle_setsTitle() method on the Activity (since that's all it has
now), the meat of the testing is upon the controller implementation. Simply provide it a stub
or mock Display implementation and you can write interesting tests like
testWhenUserHasNoName_promptsUserForName, all while avoiding android testing restrictions (no
stub classes) or heavy-handed testing frameworks (sorry, Robolectric)

Criticism

A simple issue with this approach is naming. As mentioned before, we can't use the familiar
"View" of MVC, and besides, with services and i18n and so much more offered by Android, it
is not really appropriate. I chose to use 'Display' but that also seems scope-limited.
"Framework" is a bit more accurate but at the same time something like UserFramework or
PaintFramework sounds a bit odd. Is there a better name?

This proposal does add an extra layer to the software stack, though I maintain it is a
lightweight and practical one. Android seems to be trying to use fragments and sundries
to accomplish much the same goal -- the removal logic locked into Activities. This goes a
step farther.

Finally, there is no single framework or appropriate design. Some controllers may have one
method, some ten. And I'm sure the code will differ based on the author's personality.

Implementation

I've performed a simple implementation [link] of this philosophy based on the Newsreader [link] demo from
the Android developer site. This is a reasonably complex demo that performs a number of
layout and fragment tricks. Still, it was a good starting point. It has two activities,
one simple, one complex. The fragments were thankfully just thin wrappers around their
views and didn't really have to be adjusted.

I split the app into two packages, .core, containing domain objects and logic, and .ui, which
held android classes extending Fragment, Activity, etc [link to this commit]. I then constructed a third package,
.mvc, into which I placed the new Controller and Display interfaces and implementations. [link to that commit]

0 comments on commit d69cb03

Please sign in to comment.