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