Android Design Patterns

49
Android Design Patterns Godfrey Nolan

Transcript of Android Design Patterns

Android DesignPatterns

Godfrey Nolan

Android Application Architecture (Android Dev Summit) https://www.youtube.com/watch?v=BlkJzgjzL0c

What is Your Goal?Scalable MaintainableTesting

What is Your Goal?Scalable

Add new features quicklyMaintainable

No spaghetti codeDon't cross the streams

TestingEasy to mock

Why?Easier to add new featuresEasier to understandEasier to policeMake our life easierMake Unit Testing easier

How do we get there?

https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)

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 AndroidPros

Better the devil you knowCons

Activities and Fragments quickly become largePainful to make changes or add new featuresAll the logic in Activities, unit testing is impossibleClassic 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;

//importspublic class FirstScreen extends Activity {

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.firstscreen); final LinearLayout lm = (LinearLayout) findViewById(R.id.linearMain); final Button secondBtn = (Button) findViewById(R.id.second); //Get Global Controller Class object (see application tag in AndroidManifest.xml) final Controller aController = (Controller) getApplicationContext(); /* ........ */ //Product arraylist size int ProductsSize = aController.getProductsArraylistSize();

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); /******** Dynamically create view elements - Start **********/ for(int j=0;j< ProductsSize;j++) { // Get probuct data from product data arraylist String pName = aController.getProducts(j).getProductName(); int pPrice = aController.getProducts(j).getProductPrice(); // Create LinearLayout to view elemnts LinearLayout ll = new LinearLayout(this); ll.setOrientation(LinearLayout.HORIZONTAL); TextView product = new TextView(this); product.setText(" "+pName+" "); //Add textView to LinearLayout ll.addView(product); TextView price = new TextView(this); price.setText(" $"+pPrice+" "); //Add textView to LinearLayout ll.addView(price); final Button btn = new Button(this); btn.setId(j+1); btn.setText("Add To Cart"); // set the layoutParams on the button btn.setLayoutParams(params);

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; } }

MVCPros

No business logic in UIEasier to unit test

ConsDoesn't scale, separates UI but not modelController often grows too big

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 logicPresenter - 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 TestingView

Test render logic and interaction with presenter,mock Presenter.

Presenter Test that view events invoke the right modelmethod. Mock both View and Model.

Model Test the business logic, mock the data source andPresenter.

MVPPros

Complex Tasks split into simpler tasksSmaller objects, less bugs, easier to debugTestable

ConsBoilerPlate to wire the layers.Model can’t be reused, tied to specific use case.View and Presenter are tied to data objects sincethey share the same type of object with the Model.

https://speakerdeck.com/rallat/androiddevlikeaprodroidconsf

MVVMMicrosoft PatternRemoves UI code from Activities/FragmentsView has no knowledge of modelData Binding = Bind ViewModel to LayoutGoodbye 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/

MVVMPros

First Party LibraryCompile time checkingPresentation layer in XMLTestableLess code, no more Butterknife

ConsData Binding isn't always appropriateAndroid Studio integration was flaky

Reactive - RxJavaNot really an architectureUsed in many other architecturesEvent 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 ArchitectureUncle Bob Much better decoupling, better reusabiityLots more layers

https://github.com/rallat/EffectiveAndroid

Clean Architecture is...Independent of FrameworksTestableIndependent of UIIndependent of DatabaseIndependent of any External AgencyDecoupled

Clean Architecture

https://github.com/rallat/EffectiveAndroid

Clean Architecture (Kotlin)

https://github.com/pardom/CleanNews

tearDown()Act EarlyTrust but VerifyKnow what you're getting intoYAGNI

ReposMVC: 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 [email protected]@godfreynolanslideshare.com/godfreynolan