Android Design Patterns
-
Upload
godfrey-nolan -
Category
Internet
-
view
568 -
download
0
Transcript of Android Design Patterns
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
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
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
MVP Increases separation of concerns into 3 layers
Passive View - Render logicPresenter - Handle User events (Proxy)Model - Business logic
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
<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")); } } );}
Clean Architecture is...Independent of FrameworksTestableIndependent of UIIndependent of DatabaseIndependent of any External AgencyDecoupled
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