application archetecture with android...
TRANSCRIPT
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
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!
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 :-)
Typical methods
List<Users> getUsersFromWebservice()
!
AndroidDatabaseResults searchForSite(String searchText)
!
UserInfo getInfoFromFacebook(int key)
A Simple Servicepublic 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); } }
A simple domain objectpublic 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
• Option 1 - In the objects
@ForeignCollectionField(foreignFieldName = "user") private List<EmailAddress> addresses;
• Option 2 - In the service
public List<EmailAddress> getFriendEmails(int id)
A basic testpublic 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 testpublic 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... }
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); … }
}
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(); }
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
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!