A desciption of how we, at skroutz.gr, moved our native android application from an MVC-based approach to an MVP-based approach. After having MVP, we then moved our codebase to use RecyclerViews, with the AdapterDelegates library, that promotes code reuse and composition across RecyclerView Adapters
Skroutz Android MVP and Adapter Delegates presentation
1. Using MVP in the Skroutz
application
Chris Mpitzios
2. MVC approach and
Skroutz Application facts
Skroutz is a network intensive application. Almost all of its screens require communication with Skroutz API in order to fetch and display relevant data.
● 11% responsible for static content displaying.
● 89% responsible for network communication
● Only portrait orientation supported
● Average LOC for active presentation elements ≈500 lines
(excluding placeholder activities)
● All network related screens follow the same view state pattern:
○ Load in the background
○ Display loading view
○ Display fetched data or an error message if needed
27
Screens
3. Skroutz App with MVC
Expectations from MVC approach:
Good separation of concerns
Clean, reusable, testable code
Minimizing of spaghetti effect
Keep code complexity low
5. Skroutz App with MVC
Review of MVC approach:
View just structures displayed interface
High complexity - Spaghetti code - hard to test code-
Unmaintainable state
RESULT
One god object responsible for everything
MVC-View responsibilities move to Controller
Controller should: data bind, manage
animations, user interactions ……..
7. Separates presentation layer from the logic
Presenter:
● fetches, formats, delivers data to View
● instruct View for UI actions according to data
● keeps a reference to both View and Model
View :
● completely passive
● displays data
● cannot access model
MVP to the rescue
Controller is part of the view
View-Presenter -> one-to-one relationship
Multiple Presenters for complex Views
Next alternative
8. Let's implement MVP
How? Custom implementation or use an existing library?
Check available implementations before reinventing the wheel
Most popular implementations at the time were Mosby and Nucleus
10. Mosby vs Nucleus
public interface MainActivityView extends MvpView {
void showLoading();
void setData(API.Item[] response);
void showContent();
void showError(Throwable throwable);
}
public class MainPresenter extends MvpBasePresenter<MainActivityView> {
// Public
public void loadData() {
return App.getRestAdapterInstance().getItems(mApiCallback);
}
private final Callback<API.Item[]> mApiCallback = new Callback<API.Item[]>()
{
@Override
public void failure(final Throwable throwable) {
if (isViewAttached())
getView().showError(throwable);
}
@Override
public void success(API.Item[] response) {
if (isViewAttached())
getView().setData(response);
}
};
}
Mosby Presenter implementation:
MainActivityView interface:
public class MainPresenter extends RxPresenter<MainActivity> {
private static final int REQUEST_ITEMS_RESTARTABLE_ID = 1;
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
restartableLatestCache(REQUEST_ITEMS_RESTARTABLE_ID,
new Func0<Observable<ServerAPI.Response>>() {
@Override
public Observable<ServerAPI.Response> call() {
return App.getServerAPI()
.getItems(“request Argument”)
.observeOn(AndroidSchedulers.mainThread());
}
},
new Action2<MainActivity, ServerAPI.Response>() {
@Override
public void call(MainActivity activity, ServerAPI.Response response) {
activity.setData(response.items, “requestArgument”);
}
},
new Action2<MainActivity, Throwable>() {
@Override
public void call(MainActivity activity, Throwable throwable) {
activity.showError(throwable);
}
});
if (savedState == null)
start(REQUEST_ITEMS_RESTARTABLE_ID);
}
// Public
public void loadData() {
start(REQUEST_ITEMS_RESTARTABLE_ID);
}
}
Nucleus Presenter implementation:
11. Mosby vs Nucleus
An important difference
Nucleus presenter had an onCreate() method which means that some kind of lifecycle exists
Mosby on the other hand does not retain Presenters in any way
12. Mosby vs Nucleus
Main concepts and differences
Custom annotations to inject presenters
Nucleus:Mosby:
RxJava/observables used and mandatory
No separate interface for view actions
Presenters have a primitive lifecycle
All running tasks reattached automatically
Presenters can survive an activity recreation
Based on delegation so one can use delegates
to integrate it in a custom way
Presenters must be explicitly provided
MvpView as API for view related methods
Presenters independent from view’s lifecycle
Previous running tasks not restored
Presenters are not retained in any way
Based on delegation so one can use delegates
to integrate it in a custom way
ViewState provided to restore UI state
RxJava/observables not mandatory
Mosby was more documented and
embraced from the community
13. Skroutz App Categories screen
Main concepts
One placeholder activity hosting Categories fragment
Network call to API’s Categories endpoint
Endpoint requires/provides paging
Retrofit (and OkHttp) used
Both orientations supported
One RecyclerView to display data
14. CategoriesFragment MVC implementation
public class CategoriesFragment extends Fragment implements AdapterView.OnItemClickListener {
private AbstractRecyclerViewAdapter<Category> mAdapter;
private Paginator mPaginator = new Paginator();
private boolean mIsAlreadyLoading= false;
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mCategory = getActivity().getIntent().getParcelableExtra(KEY_BUNDLE_CATEGORY);
loadData();
}
@Override
public void onDestroy() {
super.onDestroy();
mApiCallback.invalidate();
}
private void setData(List<Category> list) {
if (list == null){
showError(SKError.noResultsError());
return;
}
mAdapter.addData(list);
mAdapter.notifyDataSetChanged();
showError(null);
}
private void showLoading() {............}
private void showContent() {............}
private void showError(SKError error) {............}
private void loadData() {
if (mPaginator.isPagesCompleted())
return;
if (mIsAlreadyLoading)
return;
mIsAlreadyLoading = true;
showLoading();
Category.fetchSubCategories(mCategory.id,
mCurrentPaginator.page + 1,
mApiCallback);
}
private final SKCallback<ResponseCategories> mApiCallback = new
SKCallback<ResponseCategories>() {
@Override
public void failure(final SKError error) {
mIsAlreadyLoading = false;
showError(error);
}
@Override
public void success(ResponseCategories response) {
mIsAlreadyLoading = false;
mPaginator = response.meta.paginator;
setData(response.categories);
showContent()
}
};
15. CategoriesFragment MVC implementation review
Unneeded complexity, no separation of concerns!
Everything takes place in Fragment (View). Network calls, interface handling..
Paginator (part of business logic) included and preserved in View
Memory leaks and random crashes detected
UI flow for orientation change was broken
View is not dumb but absolutely stateful
17. CategoriesFragment Mosby implementation
public class CategoriesFragment extends MvpFragment<CategoriesView, CategoriesPresenter,
Category> implements CategoriesView{
private AbstractRecyclerViewAdapter<Category> mAdapter;
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mCategory = getActivity().getIntent().getParcelableExtra(KEY_BUNDLE_CATEGORY);
if (savedInstanceState != null) {
presenter.onRestoreInstanceState(savedInstanceState);
if (getSavedDataFromBundle() != null) {
setData(getSavedDataFromBundle());
return;
}
}
loadData();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
presenter.onSaveInstanceState(outState);
outState.putParcelableArrayList(KEY_BUNDLE_DATA, (ArrayList<Category>) mAdapter.getData());
}
@Override
public CategoriesPresenter createPresenter() {
return new CategoriesPresenter();
}
@Override
public void loadData() {
presenter.loadData(mCategory.id);
}
@Override
public void setData(List<Category> list) {
mAdapter.addData(list);
mAdapter.notifyDataSetChanged();
showError(null);
}
@Override
public void showLoading() {............}
@Override
public void showContent() {............}
@Override
public void showError(SKError error) {............}
18. CategoryPresenter and CategoryView - Mosby
public class CategoryPresenter extends MvpBasePresenter<CategoryView> {
// Attributes
private boolean mIsLoadingMore = false;
private Paginator mPaginator = new Paginator();
public void onRestoreInstanceState(Bundle savedInstanceState) {
mPaginator = savedInstanceState.getParcelable(KEY_BUNDLE_PAGINATOR);
}
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putParcelable(KEY_BUNDLE_PAGINATOR, mPaginator);
}
public void loadData(final int categoryId, final boolean nextPageWanted) {
if (mPaginator.isPagesCompleted())
return;
if (mIsLoadingMore)
return;
mIsLoadingMore = true;
getView().showLoading(true);
Category.fetchSubCategories(categoryId, mPaginator.page + 1, mApiCallback);
}
}
private final SKCallback<ResponseCategories> mApiCallback = new
SKCallback<ResponseCategories>() {
@Override
public void failure(final SKError error) {
mIsLoadingMore = false;
if (isViewAttached())
getView().showError(error);
}
@Override
public void success(ResponseCategories response) {
mIsLoadingMore = false;
mPaginator = response.meta.paginator;
if (response.categories == null) {
if (isViewAttached())
getView().showError(SKError.noResultsError());
return;
}
if (isViewAttached()) {
getView().setData(response.categories);
getView().showContent();
}
}
};
public interface CategoryView<List<Category>> extends MvpView {
void showLoading();
void showContent();
void showError(SKError error);
void setData(List<Category> data);
void loadData();
}
CategoryView interface
19. CategoriesFragment implementation review
Mosby implementation review
Low level of complexity
Code decoupling, test friendly
Paginator logic and model access moved to
Presenter
Fragment cares only about displaying data
(dumb-stateless)
Memory leaks and random crashes
minimized
Reusable code because our presenter can
be used from other views
No UI flow interruptions, or unneeded
repeated network calls
20. Mosby implementation issues faced
Issues/limitation faced during
implementation
First versions of Mosby came with a lot of external libraries dependencies
First versions of Mosby were extending from
AppCompatActivity
No available or proposed solution for the pagination pattern, so we have to save and restore
paginator variable from bundle (the only state being kept manually from the presenter)
We did not like that we have to store data to preserve a sane orientation change flow.
ViewState feature can save us from that but could not save us from paginator concept
problems. So, not an overall solution
22. Skroutz Application facts
Skroutz is a moderate sized application, with a significant number of Android building blocks. The majority of the activities are simply fragment containers. Only a
couple of activities have actual real implementations (e.g. for supporting fragment ViewPagers).
13
Activities
Mostly fragment containers,
although some have actual UI
logic
24
Fragments
Most of the UI logic is
implemented in separate
fragments.
consisting of
15
List Fragments
The majority of the fragments
display some sort of a list of
items
most of which
are
23. Migrating away from ListView
ListView limitations:
Support section headers
Buggy addHeader/addFooter when
setting adapter after header/footer
Need different components for switching
between lists and grids
Not very easy to handle multiple click
targets per cell
Multiple cell type support is cumbersome
But most important of all...
25. Transition to the RecyclerView component
Requirements
Handling clicks and multiple click targets
Add headers
Add footers
Reuse Adapter code
Keep it DRY and free of spaghetti code
Support showing unrelated entities
Demo app shows how we
tackled the problem
26. First Iteration – use inheritance
OOP promotes inheritance naturally
Use different View Types (e.g
cell types) per subclass
Base adapter functionality
in generic superclasses
Extend in subclasses
Override getItemViewType for
managing new view types
Add/extend ViewHolders
Give access to existing ViewHolders
– across packages
27. Our demo app – Adapter Inheritance vs Adapter Delegates
1st
screen - single object type view
Very basic navigation logic, click cell and open same
activity or a different one
Most common use case scenario
Show objects of type Category
Single RecyclerView with simple adapter
28. CategoriesAdapter implementation
@Override
public int getItemViewType(final int position) {
int baseItemViewType = super.getItemViewType(position);
if (baseItemViewType == INVALID_VIEW) {
return CATEGORY_VIEW_TYPE;
}
return baseItemViewType;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent,
final int viewType) {
if (viewType == CATEGORY_VIEW_TYPE) {
final View newView = mInflater.inflate(R.layout.cell_category, parent, false);
return new CategoriesViewHolder(newView, mClickListener);
}
return super.onCreateViewHolder(parent, viewType);
}
29. CategoriesAdapter implementation (2)
@Override
public void onBindViewHolder(
final RecyclerView.ViewHolder holder, final int position) {
if (holder.getItemViewType() == CATEGORY_VIEW_TYPE) {
CategoriesViewHolder viewHolder = (CategoriesViewHolder) holder;
Category category = mData.get(position);
viewHolder.categoryText.setText(category.name);
viewHolder.itemView.setTag(category);
} else {
super.onBindViewHolder(holder, position);
}
}
static class CategoriesViewHolder extends RecyclerView.ViewHolder {
TextView categoryText;
CategoriesViewHolder(final View view, final View.OnClickListener onClickListener) {
super(view);
categoryText = (TextView) view.findViewById(R.id.category_text);
view.setOnClickListener(onClickListener);
}
}
30. Our demo app - Adapter inheritance vs Adapter delegates
2nd screen - complex requirements
Different types of objects (Category vs Shop)
Style the same object type differently, according to
an object’s internal property
Reuse adapter code from the 1st
screen
31. A more complex example – CategoriesAndShopAdapter
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
if (viewType == LEAF_CATEGORY_VIEW_TYPE) {
return new LeafCategoryViewHolder(mInflater.inflate(R.layout.cell_leaf_category,
parent, false), mClickListener);
} else if (viewType == SHOP_VIEW_TYPE) {
return new ShopViewHolder(mInflater.inflate(R.layout.cell_shop,parent, false),
mClickListener);
}
return super.onCreateViewHolder(parent, viewType);
}
@Override
public int getItemViewType(final int position) {
if (position == DEFAULT_SHOP_POSITION) {
return SHOP_VIEW_TYPE;
} else if (mData.get(position).isLeaf) {
return LEAF_CATEGORY_VIEW_TYPE;
}
return super.getItemViewType(position);
}
32. Our onBindViewHolder just got a bit more complex
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
if (holder.getItemViewType() == LEAF_CATEGORY_VIEW_TYPE) {
LeafCategoryViewHolder viewHolder =(LeafCategoryViewHolder) holder;
viewHolder.categoryText.setText(mData.get(position).name);
if (position % 2 == 0) {
viewHolder.categoryIcon.setImageDrawable(mPlayDrawable);
} else {
viewHolder.categoryIcon.setImageDrawable(mSaveDrawable);
}
} else if (holder.getItemViewType() == SHOP_VIEW_TYPE) {
ShopViewHolder viewHolder = (ShopViewHolder) holder;
viewHolder.shopText.setText(mShop.name);
viewHolder.itemView.setTag(mShop);
} else {
super.onBindViewHolder(holder, position);
}
}
33. And don’t forget about the View Holders for the additional objects
static class ShopViewHolder extends RecyclerView.ViewHolder {
TextView shopText;
ShopViewHolder(final View view, View.OnClickListener listener) {
super(view);
shopText = (TextView) view.findViewById(R.id.shop_text);
view.setOnClickListener(listener);
}
}
static class LeafCategoryViewHolder extends CategoriesViewHolder {
ImageView categoryIcon;
LeafCategoryViewHolder(final View view, View.OnClickListener listener) {
super(view, listener);
categoryIcon = (ImageView) view.findViewById(R.id.category_icon);
}
}
34. Scalability issues
Welcome to Adapter Hell!
Very cumbersome to extend - requires adapter modifications
Does not scale very well
Not what inheritance is all about
There has to be something better that we can do
Adapters become maintenance nightmares
35. Second Iteration - Use Adapter Delegates
A more appropriate solution
https://github.com/sockeqwe/AdapterDelegates
Favours composition over inheritance - delegates pattern
AdapterDelegates are orchestrated through an AdapterDelegatesManager
The decision about which cell will be rendered is given to the AdapterDelegates
Easy to extend
All view rendering logic is moved to the AdapterDelegates
36. Using AdapterDelegates
Migrating towards Adapter Delegates
Next initialise an instance of the AdapterDelegatesManager in your Base Adapter
In each adapter, create one AdapterDelegate per View Type
Override the base getItemViewType, onCreateViewHolder and onBindViewHolder and
delegate them to the AdapterDelegatesManager
First of all add the gradle dependency
Consider breaking down complex layouts into separate
AdapterDelegates (if possible)
37. Base Adapter Code
protected final AdapterDelegatesManager<List<T>> mAdapterDelegateManager;
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
mAdapterDelegateManager.onBindViewHolder(mData, position, holder);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int
viewType) {
return mAdapterDelegateManager.onCreateViewHolder(parent, viewType);
}
@Override
public int getItemViewType(final int position) {
return mAdapterDelegateManager.getItemViewType(mData, position);
}
38. Modify the concrete adapter implementations - CategoriesAdapter
The CategoriesAdapter becomes empty!
Move the code for the onBindViewHolder, onCreateViewHolder, and the ViewHolder to a
new CategoriesAdapterDelegate class
The new class will implement the AdapterDelegate interface
Control when the adapter will be used by implementing the isForViewType method
Initialise the CategoriesAdapterDelegate in the Adapter and add its instance to the
AdapterDelegatesManager
39. CategoriesAdapter
public class CategoriesAdapter extends BaseAdapter<Category> {
public CategoriesAdapter(final Context context, final LayoutInflater layoutInflater,
final View.OnClickListener onClickListener, List<Category> data) {
super(context, layoutInflater, onClickListener);
mAdapterDelegateManager.addDelegate(new CategoriesAdapterDelegate(mContext, mInflater,
mClickListener));
}
}
40. CategoriesAdapterDelegate
public class CategoriesAdapterDelegate implements AdapterDelegate<List<Category>>
@Override
public void onBindViewHolder(@NonNull final List<Category> items, final int position,
@NonNull final RecyclerView.ViewHolder holder) {
CategoriesViewHolder viewHolder = (CategoriesViewHolder) holder;
Category category = items.get(position);
viewHolder.categoryText.setText(category.name);
viewHolder.itemView.setTag(category);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent) {
return new CategoriesViewHolder(mInflater.inflate(R.layout.cell_category,
parent, false), mClickListener);
}
@Override
public boolean isForViewType(@NonNull final List<Category> items, final int position) {
return true;
}
41. What about our complex adapter? CategoriesAndShopAdapter
Modifying our complex adapter
Use inheritance and subclass the CategoriesAdapterDelegate for supporting the Leaf
Categories view type
Εxtract the Shop View type to an Adapter Delegate – cannot use it with our
AdapterDelegateManager – not the same generic type
Manage the Shop View Type and delegate everything else to the base class logic
Reuse CategoriesAdapterDelegate as a fallback delegate
42. CategoriesAndShopAdapter
public CategoriesAndShopAdapter(final Context context, final LayoutInflater layoutInflater,
final View.OnClickListener onClickListener,
final List<Category> data, final Shop shop) {
super(context, layoutInflater, onClickListener);
mAdapterDelegateManager.addDelegate(new LeafCategoriesAdapterDelegate(mContext, mInflater,
mClickListener));
mAdapterDelegateManager.setDefaultDelegate(new CategoriesAdapterDelegate(mContext, mInflater,
mClickListener));
mShopAdapterDelegate = new ShopAdapterDelegate(mContext, mInflater, mClickListener);
}
@Override
public int getItemViewType(final int position) {
if (position == 0) {
return SHOP_VIEW_TYPE;
}
return super.getItemViewType(position);
}
43. CategoriesAndShopAdapter (2)
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
if (viewType == SHOP_VIEW_TYPE) {
return mShopAdapterDelegate.onCreateViewHolder(parent);
}
return mAdapterDelegateManager.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
if (holder.getItemViewType() == SHOP_VIEW_TYPE) {
mShopAdapterDelegate.onBindViewHolder(mShop, 0, holder);
} else {
mAdapterDelegateManager.onBindViewHolder(mData, position, holder);
}
}
44. LeafCategoriesAdapterDelegate
public class LeafCategoriesAdapterDelegate extends CategoriesAdapterDelegate
@Override
public void onBindViewHolder(@NonNull final List<Category> items, final int position, @NonNull final
RecyclerView.ViewHolder holder) {
super.onBindViewHolder(items, position, holder);
LeafCategoryViewHolder viewHolder = (LeafCategoryViewHolder) holder;
if (position % 2 == 0) {
viewHolder.categoryIcon.setImageDrawable(mPlayDrawable);
} else {
viewHolder.categoryIcon.setImageDrawable(mSaveDrawable);
}
}
@Override
public boolean isForViewType(@NonNull final List<Category> items, final int position) {
return items.get(position).isLeaf;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent) {
return new LeafCategoryViewHolder(mInflater.inflate(R.layout.cell_leaf_category,parent, false),
mClickListener);
}
45. ShopAdapterDelegate
public class ShopAdapterDelegate implements AdapterDelegate<Shop>
@Override
public void onBindViewHolder(@NonNull final Shop items, final int position, @NonNull final
RecyclerView.ViewHolder holder) {
ShopViewHolder viewHolder = (ShopViewHolder) holder;
viewHolder.shopText.setText(items.name);
viewHolder.itemView.setTag(items);
}
@Override
public boolean isForViewType(@NonNull final Shop items, final int position) {
return true;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent) {
return new ShopViewHolder(mInflater.inflate(R.layout.cell_shop,parent, false),
mClickListener);
}
46. AdapterDelegate - Retrospect
AdapterDelegate benefits:
Reduces the adapter complexity
Increased performance due to:
- Specialised (smaller) layouts
- Avoid excessive branching in onBindViewHolder
Reuse the code across different adapters
Easily extend adapter functionality
Code decoupling
47. AdapterDelegate – Retrospect (2)
AdapterDelegate limitations:
No easy support for displaying objects of completely different classes
At present, you cannot access AdapterDelegates in the DelegateManager
48. Skroutz Sample Application https://github.com/skroutz/AdapterDelegatesSample
Resources
Adapter Delegates Library code https://github.com/sockeqwe/AdapterDelegates
Adapter Delegates Original Blog Post http://hannesdorfmann.com/android/adapter-delegates
MVP Library code: https://github.com/sockeqwe/mosby
MVP Original Blog Posts: http://hannesdorfmann.com/mosby/ http://hannesdorfmann.com/mosby/mvp/
49. Q & A
Thank you
George Metaxas - gmetaxas@skroutz.gr
Chris Mpitzios - cmpi@skroutz.gr