-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
245 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
gen | ||
out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
|