Android Design Patterns
Godfrey Nolan
Android Application Architecture (Android Dev Summit) https://www.youtube.com/watch?v=BlkJzgjzL0c
What is Your Goal?
-
Scalable
-
Maintainable
-
Testing
What is Your Goal?
- Scalable
- Add new features quickly
- Maintainable
- No spaghetti code
- Don't cross the streams
- Testing
- Easy to mock
Why?
- Easier to add new features
- Easier to understand
- Easier to police
- Make our life easier
- Make Unit Testing easier
How do we get there?
https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)
Which Pattern?
-
MVC
-
MVP
-
MVVM
-
Clean
Classic Android
View Model
https://github.com/konmik/konmik.github.io/wiki/Introduction-to-Model-View-Presenter-on-Android
package alexandria.israelferrer.com.libraryofalexandria;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RatingBar;
import android.widget.TextView;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.squareup.picasso.Picasso;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
public class MainActivity extends Activity {
private static final String PACKAGE = "com.israelferrer.alexandria";
private static final String KEY_FAVS = PACKAGE + ".FAVS";
private List<ArtWork> artWorkList;
private ArtWorkAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.listView);
InputStream stream = getResources().openRawResource(R.raw.artwork);
Type listType = new TypeToken<List<ArtWork>>() {
}.getType();
artWorkList = new Gson().fromJson(new InputStreamReader(stream), listType);
final SharedPreferences preferences = getSharedPreferences(getPackageName()
, Context.MODE_PRIVATE);
for (ArtWork artWork : artWorkList) {
artWork.setRating(preferences.getFloat(PACKAGE + artWork.getId(), 0F));
}
adapter = new ArtWorkAdapter();
listView.setAdapter(adapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.filter) {
adapter.orderMode();
return true;
}
return super.onOptionsItemSelected(item);
}
private class ArtWorkAdapter extends BaseAdapter {
private boolean isOrder;
private final List<ArtWork> orderedList;
public ArtWorkAdapter() {
super();
orderedList = new LinkedList<ArtWork>();
}
@Override
public int getCount() {
return artWorkList.size();
}
@Override
public Object getItem(int position) {
return artWorkList.get(position);
}
@Override
public long getItemId(int position) {
return Long.valueOf(artWorkList.get(position).getId());
}
public void orderMode() {
isOrder = !isOrder;
if (isOrder) {
orderedList.clear();
orderedList.addAll(artWorkList);
Collections.sort(orderedList);
notifyDataSetChanged();
} else {
notifyDataSetChanged();
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ArtWork artWork;
if (isOrder) {
artWork = orderedList.get(position);
} else {
artWork = artWorkList.get(position);
}
View row;
switch (artWork.getType()) {
case ArtWork.QUOTE:
row = getLayoutInflater().inflate(R.layout.text_row, null);
TextView quote = (TextView) row.findViewById(R.id.quote);
TextView author = (TextView) row.findViewById(R.id.author);
quote.setText("\"" + artWork.getText() + "\"");
author.setText(artWork.getAuthor());
break;
case ArtWork.PAINTING:
final SharedPreferences preferences = getSharedPreferences(getPackageName()
, Context.MODE_PRIVATE);
final HashSet<String> favs = (HashSet<String>) preferences
.getStringSet(KEY_FAVS,
new HashSet<String>());
row = getLayoutInflater().inflate(R.layout.painting_row, null);
ImageView image = (ImageView) row.findViewById(R.id.painting);
TextView painter = (TextView) row.findViewById(R.id.author);
painter.setText(artWork.getTitle() + " by " + artWork.getAuthor());
Picasso.with(MainActivity.this).load(artWork.getContentUrl()).fit()
.into(image);
RatingBar rating = (RatingBar) row.findViewById(R.id.rate);
rating.setRating(artWork.getRating());
rating.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() {
@Override
public void onRatingChanged(RatingBar ratingBar, float rating,
boolean fromUser) {
preferences.edit().putFloat(PACKAGE + artWork.getId(), rating).apply();
artWork.setRating(rating);
}
});
CheckBox fav = (CheckBox) row.findViewById(R.id.fav);
fav.setChecked(favs.contains(artWork.getId()));
fav.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
final HashSet<String> favs = new HashSet<String>((HashSet<String>)
preferences
.getStringSet(KEY_FAVS,
new HashSet<String>()));
if (isChecked) {
favs.add(artWork.getId());
} else {
favs.remove(artWork.getId());
}
preferences.edit().putStringSet(KEY_FAVS,
favs).apply();
}
});
break;
case ArtWork.MOVIE:
case ArtWork.OPERA:
row = new ViewStub(MainActivity.this);
break;
default:
row = getLayoutInflater().inflate(R.layout.text_row, null);
}
return row;
}
}
}
Classic Android
- Pros
- Better the devil you know
- Cons
- Activities and Fragments quickly become large
- Painful to make changes or add new features
- All the logic in Activities, unit testing is impossible
- Classic Android is not MVC
MVC
https://medium.com/@tinmegali/model-view-presenter-mvp-in-android-part-1-441bfd7998fe#.d19x8cido
http://androidexample.com/Use_MVC_Pattern_To_Create_Very_Basic_Shopping_Cart__-_Android_Example
package com.androidexample.mvc;
import java.util.ArrayList;
import android.app.Application;
public class Controller extends Application{
private ArrayList<ModelProducts> myProducts = new ArrayList<ModelProducts>();
private ModelCart myCart = new ModelCart();
public ModelProducts getProducts(int pPosition) {
return myProducts.get(pPosition);
}
public void setProducts(ModelProducts Products) {
myProducts.add(Products);
}
public ModelCart getCart() {
return myCart;
}
public int getProductsArraylistSize() {
return myProducts.size();
}
}
package com.androidexample.mvc;
public class ModelProducts {
private String productName;
private String productDesc;
private int productPrice;
public ModelProducts(String productName,String productDesc,int productPrice) {
this.productName = productName;
this.productDesc = productDesc;
this.productPrice = productPrice;
}
public String getProductName() {
return productName;
}
public String getProductDesc() {
return productDesc;
}
public int getProductPrice() {
return productPrice;
}
}
MVC
- Pros
- No business logic in UI
- Easier to unit test
- Cons
- Doesn't scale, separates UI but not model
- Controller often grows too big
- Violates Single Responsibility, Interface Segregation SOLID principles
https://github.com/konmik/konmik.github.io/wiki/Introduction-to-Model-View-Presenter-on-Android
MVP
Model-View-Presenter
https://speakerdeck.com/rallat/android-development-like-a-pro
MVP
- Increases separation of concerns into 3 layers
- Passive View - Render logic
- Presenter - Handle User events (Proxy)
- Model - Business logic
https://github.com/googlesamples/android-architecture/wiki/Samples-at-a-glance
https://www.linkedin.com/pulse/mvc-mvp-mvvm-architecture-patterns-shashank-gupta
https://github.com/erikcaffrey/Android-Spotify-MVP
public interface ArtistsMvpView extends MvpView{
void showLoading();
void hideLoading();
void showArtistNotFoundMessage();
void showConnectionErrorMessage();
void renderArtists(List<Artist> artists);
void launchArtistDetail(Artist artist);
}
public class ArtistsPresenter implements Presenter<ArtistsMvpView>, ArtistCallback {
private ArtistsMvpView artistsMvpView;
private ArtistsInteractor artistsInteractor;
public ArtistsPresenter() {
}
@Override public void setView(ArtistsMvpView view) {
if (view == null) throw new IllegalArgumentException("You can't set a null view");
artistsMvpView = view;
artistsInteractor = new ArtistsInteractor(artistsMvpView.getContext());
}
@Override public void detachView() {
artistsMvpView = null;
}
public void onSearchArtist(String string) {
artistsMvpView.showLoading();
artistsInteractor.loadDataFromApi(string, this);
}
public void launchArtistDetail(Artist artist) {
artistsMvpView.launchArtistDetail(artist);
}
//.....
}
public class ArtistsInteractor {
SpotifyService mSpotifyService;
SpotifyApp mSpotifyApp;
public ArtistsInteractor(Context context) {
this.mSpotifyApp = SpotifyApp.get(context);
this.mSpotifyService = mSpotifyApp.getSpotifyService();
}
public void loadDataFromApi(String query, ArtistCallback artistCallback) {
mSpotifyService.searchArtist(query)
.subscribeOn(mSpotifyApp.SubscribeScheduler())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(artistsSearch -> onSuccess(artistsSearch, artistCallback),
throwable -> onError(throwable, artistCallback));
}
public class MainActivity extends Activity implements MainView, AdapterView.OnItemClickListener {
private ListView listView;
private ProgressBar progressBar;
private MainPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.list);
listView.setOnItemClickListener(this);
progressBar = (ProgressBar) findViewById(R.id.progress);
presenter = new MainPresenterImpl(this);
}
@Override public void setItems(List<String> items) {
listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items));
}
@Override public void showMessage(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
@Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
presenter.onItemClicked(position);
}
}
package com.antonioleiva.mvpexample.app.main;
public interface MainPresenter {
void onResume();
void onItemClicked(int position);
void onDestroy();
}
public class MainPresenterImpl implements MainPresenter, FindItemsInteractor.OnFinishedListener {
private MainView mainView;
private FindItemsInteractor findItemsInteractor;
public MainPresenterImpl(MainView mainView) {
this.mainView = mainView;
findItemsInteractor = new FindItemsInteractorImpl();
}
@Override public void onResume() {
if (mainView != null) {
mainView.showProgress();
}
findItemsInteractor.findItems(this);
}
@Override public void onItemClicked(int position) {
if (mainView != null) {
mainView.showMessage(String.format("Position %d clicked", position + 1));
}
}
@Override public void onDestroy() {
mainView = null;
}
@Override public void onFinished(List<String> items) {
if (mainView != null) {
mainView.setItems(items);
mainView.hideProgress();
}
}
}
public interface MainView {
void showProgress();
void hideProgress();
void setItems(List<String> items);
void showMessage(String message);
}
http://antonioleiva.com/mvp-android/
MVP Testing
- View
- Test render logic and interaction with presenter, mock Presenter.
- Presenter
- Test that view events invoke the right model method. Mock both View and Model.
- Model
- Test the business logic, mock the data source and Presenter.
MVP
- Pros
- Complex Tasks split into simpler tasks
- Smaller objects, less bugs, easier to debug
- Testable
- Cons
- BoilerPlate to wire the layers.
- Model can’t be reused, tied to specific use case.
- View and Presenter are tied to data objects since they share the same type of object with the Model.
https://speakerdeck.com/rallat/androiddevlikeaprodroidconsf
MVVM
- Microsoft Pattern
- Removes UI code from Activities/Fragments
- View has no knowledge of model
- Data Binding = Bind ViewModel to Layout
- Goodbye Presenter, hello ViewModel
http://tech.vg.no/2015/07/17/android-databinding-goodbye-presenter-hello-viewmodel/
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/password_block"
android:id="@+id/email_block"
android:visibility="@{data.emailBlockVisibility}">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Email:"
android:minWidth="100dp"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
android:ems="10"
android:id="@+id/email"/>
</LinearLayout>
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
mBinding = FragmentMainBinding.bind(view);
mViewModel = new MainModel(this, getResources());
mBinding.setData(mViewModel);
attachButtonListener();
return view;
}
public void updateDependentViews() {
if (isExistingUserChecked.get()) {
emailBlockVisibility.set(View.GONE);
loginOrCreateButtonText.set(mResources.getString(R.string.log_in));
}
else {
emailBlockVisibility.set(View.VISIBLE);
loginOrCreateButtonText.set(mResources.getString(R.string.create_user));
}
}
http://tech.vg.no/2015/07/17/android-databinding-goodbye-presenter-hello-viewmodel/
MVVM
- Pros
- First Party Library
- Compile time checking
- Presentation layer in XML
- Testable
- Less code, no more Butterknife
- Cons
- Data Binding isn't always appropriate
- Violates SOLID, ViewModel still the middle man
Reactive - RxJava
- Not really an architecture
- Used in many other architectures
- Event based Publish / Subscribe
public void doLargeComputation(
final IComputationListener listener,
final OtherParams params) {
Subscription subscription = doLargeComputationCall(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<Result>>>() {
@Override public void onNext(final List<Result> results) {
listener.doLargeComputationComplete(results);
}
@Override public void onCompleted() {}
@Override public void onError(final Throwable t) {
listener.doLargeComputationFailed(t);
}
}
);
}
private Observable<List<Result>> doLargeComputationCall(final OtherParams params) {
return Observable.defer(new Func0<Observable<List<Result>>>() {
@Override public Observable<List<Result>> call() {
List<Result> results = doTheRealLargeComputation(params);
if (results != null && 0 < results.size()) {
return Observable.just(results);
}
return Observable.error(new ComputationException("Could not do the large computation"));
}
}
);
}
MVC w/RxJava
https://github.com/yigit/dev-summit-architecture-demo
Clean Architecture
- Uncle Bob
- Much better decoupling, better reusabiity
- Lots more layers
https://github.com/rallat/EffectiveAndroid
Clean Architecture is...
- Independent of Frameworks
- Testable
- Independent of UI
- Independent of Database
- Independent of any External Agency
- Decoupled
Clean Architecture
- Entities - business objects of the application
- Use Cases - orchestration, also called interactors
- Interface Adapters - converts data for UC and entities
- Presenters / Controllers, i.e. MVP or MVC
- UI - View
Dependency Rule
source code dependencies can only point inwards and nothing in an inner circle can know anything at all about something in an outer circle."
Clean Architecture
http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
Presentation Layer
Domain Layer - POJO
Data Layer
Simple Example
“Greet the user with a message when the app starts where that message is stored in the database.”
https://medium.com/@dmilicic/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern-d38d71e94029#.bvnt8tew8
Summary of Calls
MainActivity
->MainPresenter
-> WelcomingInteractor
-> WelcomeMessageRepository
-> WelcomingInteractor
-> MainPresenter
-> MainActivity
MainActivity
MainPresenter
WelcomeMessageRepository
WelcomingInteractor
MainPresenter
MainActivity
Summary of Calls
MainActivity
->MainPresenter
-> WelcomingInteractor
-> WelcomeMessageRepository
-> WelcomingInteractor
-> MainPresenter
-> MainActivity
Summary of Calls
Outer — Mid — Core — Outer — Core — Mid — Outer
Testing Interactors
Clean Architecture is...
- Independent of Frameworks
- Testable
- Independent of UI
- Independent of Database
- Independent of any External Agency
- Decoupled
Clean Architecture
https://github.com/rallat/EffectiveAndroid
Clean Architecture (Kotlin)
https://github.com/pardom/CleanNews
tearDown()
- Act Early
- Trust but Verify
- Know what you're getting into
- YAGNI
Repos
- MVC: http://androidexample.com/Use_MVC_Pattern_To_Create_Very_Basic_Shopping_Cart__-_Android_Example
- MVC-Reactive: https://github.com/yigit/dev-summit-architecture-demo
- MVP: https://github.com/antoniolg/androidmvp
- MVVM: https://github.com/ivacf/archi
- Clean: https://github.com/rallat/EffectiveAndroid
Contact Details
godfrey@riis.com
@godfreynolan
slides.com/godfreynolan
Android Design Patterns - AnDevCon
By godfreynolan
Android Design Patterns - AnDevCon
- 893