A simple, scalable app architecture with Android annotations Luke Sleeman Freelance Android developer lukesleeman.com.au Image CC: https://flic.kr/p/6oqCZB
Agenda • Introduction • The architecture - an overview • Services • Domain object • Testing • UI • Other issues • Closing thoughts
Introduction - why we need this at all
Goals • Quick, simple • Typically data driven apps - get some stuff from a web service, store it locally, show it to the user, submit stuff back to the web service • Scalable - Handle small apps and big ones • Unsurprising, easy to jump in and out of, clear and concise, makes sense to anybody. ‘Obvious’. • Testable • Need to be able to incrementally evolve towards it from an existing codebase • Boring!
The architecture! User Interface Domain objects Services
This is what it looks like
So what about Android annotations? AndroidAnnotations User Interface Domain objects Services
What is AndroidAnnotations? • Annotate your code - works of JDK annotations • Code generation - you can look behind the curtain to see how the magic works. MyActivity_ • Includes annotation support for a lot of other libraries I use such as ormlite • You don’t have to use it • But you would be silly not to :-)
Services User Interface Domain objects Services
Typical methods List<Users> getUsersFromWebservice() � AndroidDatabaseResults searchForSite(String searchText) � UserInfo getInfoFromFacebook(int key)
A Simple Service public class PizzaService { private static List<Pizza> pizzas; public static List<Pizza> getPizzas(){ if(pizzas == null){ pizzas = new ArrayList<Pizza>(); pizzas.add(new Pizza("Luke Special", 10.50)); pizzas.add(new Pizza("Margherita", 9.50)); } return pizzas; } }
A more complex service @EBean(scope = EBean.Scope.Singleton) public class FriendService { @OrmLiteDao(helper = ExampleDBHelper.class, model = Friend.class) protected RuntimeExceptionDao<Friend, String> friendsDao; @RestService protected FriendListWebservice webservice; public List<Friend> getFriends(){ return friendsDao.queryForAll(); } public void downloadAndSaveFriend(int id)throws IOException { Friend newFriend = webservice.getFriend(id); friendsDao.create(newFriend); } }
Domain objects User Interface Domain objects Services
A simple domain object public class Pizza { � private String name; private double price; � public Pizza(String name, double price) { this.name = name; this.price = price; } � public String getName() { return name; } � public void setName(String name) {
A more complex domain object @DatabaseTable public class Friend { � @DatabaseField(generatedId = true) private int id; � @DatabaseField private String userName; � public int getId() { return id; } � public void setId(int id) { this.id = id; } � public String getUserName() { return userName;
Domain objects Relationships
Domain objects Relationships • Option 1 - In the objects @ForeignCollectionField(foreignFieldName = "user") private List<EmailAddress> addresses; • Option 2 - In the service public List<EmailAddress> getFriendEmails(int id)
Unit tests User Interface Domain objects Unit Tests Services
A basic test public class PizzaTest extends AndroidTestCase{ public void testGetPizzas(){ List<Pizza> pizzaList = PizzaService.getPizzas(); assertNotNull(pizzaList); } } � public class FriendTest extends AndroidTestCase { public void testFriend(){ FriendService friendService = FriendService_.getInstance_(getContext()); assertNotNull(friendService.getFriends()); } }
A more advanced test public class FriendTest extends AndroidTestCase { @Override protected void setUp() throws Exception { File database = getContext().getDatabasePath(ExampleDBHelper.DB_NAME); if (database.exists()) { database.delete(); } } public void testSearchFriends(){ FriendService service = FriendService_.getInstance_(getContext()); service.createOrUpdateFriend(new Friend("Luke")); assertEquals(1, friendService.search("lu").size()); } }
A nifty trick public void downloadAndPatchFriendList() throws IOException{ String patchFile = downloadPatchFile(); List<Friend> updatedFriends = parsePatch(patchFile); mergeFriendsWithDB(updatedFriends); }
A nifty trick @EBean(scope = EBean.Scope.Singleton) public class TestFriendService extends FriendService { @Override public void mergeFriendWithDB(List<Friend> updatedFriends){ super.mergeFriendsWithDB(updatedFriends); } @Override public List<Friend> parsePatch(String patchFile) { return super.parsePatch(patchFile); } }
A nifty trick private TestFriendService testFriendService = TestFriendService_.getInstance_(getContext()); � public void testParseEmpty(){ List<Friend> friends = testFriendService.parsePatch(EMPTY_PATCH); assert... } � public void testParseModifyFriends(){ List<Friend> friends = testFriendService.parsePatch(MODIFY_PATCH); assert... }
The UI User Interface Domain objects Services
This is where android annotations really shines • @EActivity, @EFragment, @EViewGroup • @ViewByID @FragmentByID, etc • @DrawableRes @AnimationRes, @HtmlRes • @Click, @ListClick
Getting services and domain objects @EActivity(R.layout.activity_best_friend) public class BestFriendActivity extends Activity { @ViewById(R.id.best_friend_text) protected TextView bestFriendText; @Bean protected FriendService friendService; private Friend bestFriend; @AfterViews protected void setupBestFriend(){ bestFriend = friendService.getBestFriend(); bestFriendText.setText(bestFriend.getName()); } @Click(R.id.poke_friend_button) protected void poke(){ friendService.poke(bestFriend); } }
Threading @EActivity(R.layout.activity_friend_list) public class FriendListActivity extends Activity { @ViewById(R.id.friend_list) protected ListView list; @ViewById(R.id.loading_progress) protected ProgressBar progressSpinner; @Bean protected FriendService service; @AfterViews protected void startDownload(){ progressSpinner.setVisibility(View.VISIBLE); downloadFriends(); }
Threading @Background protected void downloadFriends(){ try{ List<Friends> friendList = service.downloadFriendList(); displayFriends(friendList); } catch(IOException e){ displayDownloadError(); } } @UiThread protected void displayFriends(List<Friends> friendList){ progressSpinner.setVisibility(View.GONE); … } � @UiThread protected void displayDownloadError(){ progressSpinner.setVisibility(View.GONE); … } }
There is a bug!
Threading @Background protected void downloadFriends(){ try{ List<Friends> friendList = service.downloadFriendList(); displayFriends(friendList); } catch(IOException e){ displayDownloadError(); } } @UiThread protected void displayFriends(List<Friends> friendList){ progressSpinner.setVisibility(View.GONE); … } � @UiThread protected void displayDownloadError(){ progressSpinner.setVisibility(View.GONE); … } }
Threading - dealing with activity shutdown • Option 1 - Let our background task run and don’t update ui @UiThread protected void displayFriends(List<Friend> friendList){ if(isDestroyed()) return; … Option 2 - Cancel background task @Background(id = "download") protected void downloadFriends(){ …. } @Override protected void onDestroy() { BackgroundExecutor.cancelAll("download", false); super.onDestroy(); }
Other issues
Evolving an existing apps architecture 1. Add in android annotations and clean out boilerplate 2. If you don’t have them start creating domain objects - move logic from UI into them. 3. If you don’t have them start creating services - have them mange your domain objects
Things to look at changing • Different libraries replace AA (dagger, retrofit, butterknife) • EventBus or RXJava • Pinch ideas from the google io app. • Come up with a good way to manage fragments
Closing thoughts
A humble request • Architecture is important! • Don’t be satisfied with just the simple architecture presented … • Think about architecture! • Talk about architecture! • Develop new architectures!
Questions?
Bonus slides
When not to use this architecture • Games • If your building one app over the course of years • If you can develop a large internal library of reusable components
Recommend
More recommend