Introduction
Introduction to MVP
The starting point of MVP is to decouple views from business logic by separating concerns.The three parts of Model-View-Presenter can be simply understood as:
- Model is the data that will be displayed in the view.
- View is an interface for displaying data (models) and sending user instructions (events) to Presenter for processing.Views usually contain references to Presenter.In Android, Activity, Fragment and ViewGroup all play the role of a view.
- Presenter is an intermediary, with references to both.Note that the word model is very misleading.It should be the business logic to get or process the model.For example: If you have a User stored in your database table and your view wants to display a list of users, Presenter will have a reference to a database business logic (such as DAO) class that Presenter uses to query the list of users.
Think: What are the differences and connections between MVC, MVP and MVVM?
Negative View: In MVP, View is a Passive View, which means that it tries not to do things actively but lets Presenter control the View in an abstract way, such as Presenter calling view.showLoading() to show the loading effect, but Presenter should not control the specific implementation of View, such as animation, so Presenter should not call view.startAnimation() Method.
Introduction to Mosby
Designing goal: Let you build Android app s with a clear Model-View-Presenter architecture.
Note: Mosby is a library, not a framework.
Think: What is a library?What is framework?What is the difference between them?
Mosby's kernel is a very compact Library Based on delegation.You can use delegation and composition to integrate Mosby into your development technology stack.In this way, you can avoid the restrictions and constraints imposed by the framework.
Think: What is a delegation model?What is the difference between delegation and inheritance?What are the benefits of using delegates?
Dependency:
Mosby is divided into modules, and you can choose what features you need:
dependencies { compile 'com.hannesdorfmann.mosby:mvp:2.0.1' compile 'com.hannesdorfmann.mosby:viewstate:2.0.1' }
Hello MVP World
Start with the Mosby MVP library for one of the simplest functions, with two Button s and one TextView on the page, as follows:
- Click the Hello button to display the red text "Hello" +random numbers;
- Click the Goodbye button to display the blue text "Goodbye" + random numbers;
It is assumed that the generation of random numbers involves complex business logic calculations and is a time-consuming operation that takes 2 seconds.
The first step is to implement this simulated business logic using an AsyncTask, in which a listener is defined to communicate the results of the business logic execution:
public class GreetingGeneratorTask extends AsyncTask<Void, Void, Integer>{ // Callback - listener public interface GreetingTaskListener{ void onGreetingGenerated(String greetingText); } ...... // Simulate the calculation process and return a random value. @Override protected Integer doInBackground(Void... params) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return (int)(Math.random() * 100); } @Override protected void onPostExecute(Integer randomInt) { listener.onGreetingGenerated(baseText + " " + randomInt); } }
The second step defines the view interface, which needs to inherit MvpView:
public interface HelloWorldView extends MvpView{ void showHello(String greetingText); void showGoodbye(String greetingText); }
Note that here the MvpView is the top-level interface for all views, it is an empty interface, and no method is defined.
The third step is to implement Presenter, which executes business logic and calls the corresponding methods of the view for different execution results.
The top-level interface for Presenter is MvpPresenter, which has two methods:
public interface MvpPresenter<V extends MvpView> { /** * Attach View to Presenter */ public void attachView(V view); /** * Called when the view is destroyed.Typical scenarios are the Activity.onDestroy() and Fragment.onDestroyView() methods */ public void detachView(boolean retainInstance); }
Mosby provides a base class implementation of the MvpPresenter interface, where we inherit MvpBasePresenter:
public class HelloWorldPresenter extends MvpBasePresenter<HelloWorldView>{ private GreetingGeneratorTask greetingTask; private void cancelGreetingTaskIfRunning(){ if (greetingTask != null){ greetingTask.cancel(true); } } public void greetHello(){ cancelGreetingTaskIfRunning(); greetingTask = new GreetingGeneratorTask("Hello", new GreetingGeneratorTask.GreetingTaskListener() { @Override public void onGreetingGenerated(String greetingText) { if (isViewAttached()){ getView().showHello(greetingText); } } }); greetingTask.execute(); } ...... @Override public void detachView(boolean retainInstance) { super.detachView(retainInstance); if (!retainInstance){ cancelGreetingTaskIfRunning(); } } }
Note that background task processing is cancelled in the detachView method.
Step 4 Implement Activity, let our Activity inherit MvpActivity and implement the HelloWorldView interface.
MvpActivity has two generic types, Presenter and View:
public class HelloWorldActivity extends MvpActivity<HelloWorldView, HelloWorldPresenter> implements HelloWorldView{ ...... }
After inheriting MvpActivity, only one abstract method, createPresenter(), needs to be implemented:
public HelloWorldPresenter createPresenter() { return new HelloWorldPresenter(); }
HelloWorldView has two more methods to implement:
@Override public void showHello(String greetingText) { greetingTextView.setTextColor(Color.RED); greetingTextView.setText(greetingText); } @Override public void showGoodbye(String greetingText) { greetingTextView.setTextColor(Color.BLUE); greetingTextView.setText(greetingText); }
After clicking the button, use Presenter to complete the operation:
@OnClick(R.id.helloButton) public void onHelloButtonClicked(){ presenter.greetHello(); } @OnClick(R.id.goodbyeButton) public void onGoodbyeButtonClicked(){ presenter.greetGoodbye(); }
Base Classes for MvpPresenter
Presenter implements one by default: use weak references to save view references, isViewAttached() must be determined before calling getView().
public class MvpBasePresenter<V extends MvpView> implements MvpPresenter<V> { private WeakReference<V> viewRef; @Override public void attachView(V view) { viewRef = new WeakReference<V>(view); } @Nullable public V getView() { return viewRef == null ? null : viewRef.get(); } public boolean isViewAttached() { return viewRef != null && viewRef.get() != null; } @Override public void detachView(boolean retainInstance) { if (viewRef != null) { viewRef.clear(); viewRef = null; } } }
Presenter implements two by default: using Null Object Pattern, there is no need to decide when calling getView().
public class MvpNullObjectBasePresenter<V extends MvpView> implements MvpPresenter<V> { private V view; @Override public void attachView(V view) { this.view = view; } @NonNull public V getView() { if (view == null) { throw new NullPointerException("MvpView reference is null. Have you called attachView()?"); } return view; } @Override public void detachView(boolean retainInstance) { if (view != null) { Type[] types = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments(); Class<V> viewClass = (Class<V>) types[0]; view = NoOp.of(viewClass); } } }
Think: What is Null Object Pattern?
Basics
LCE View
During the development of Android applications, we find that many pages have similar structure and UI logic, so we often write duplicate code.It would be much easier to develop if you could abstract the View interface of similar pages and encapsulate the base class of the page.Mosby provides us with a view template called LCE View.
LCE stands for Loading-Content-Error (Loading-Content-Error), and this view has three states: Show Loading, Show Data Content, or Show Error View.For example, in the following scenarios:
Suppose we want to display a list of countries in the ListView, where the data is retrieved from the network and is a time-consuming operation.During the loading process, we want to display a ProgressBar, and if there is a loading error, we want to display an error message.In addition, SwipeRefreshLayout is used to allow users to refresh with a drop-down refresh.
The interface of the LCE View is defined as follows:
public interface MvpLceView<M> extends MvpView { /** * Show load view, load view ID must be R.id.loadingView */ public void showLoading(boolean pullToRefresh); /** * Display content view, content view ID must be R.id.contentView * * <b>The content view must have the id = R.id.contentView</b> */ public void showContent(); /** * Display error view, error view must be TextView, ID must be R.id.errorView */ public void showError(Throwable e, boolean pullToRefresh); /** * Set the data that will be displayed in showContent() */ public void setData(M data); /** * Loading data, which often requires calling Presenter's corresponding method.So this method cannot be used in Presenter * Use to avoid circular calls. * The parameter pullToRefresh indicates whether the load was triggered by a drop-down refresh. */ public void loadData(boolean pullToRefresh); }
Think: Drop-down refresh is considered in the LCE view, but pull-up loading is not. If the server is a paging interface and needs to add pull-up loading, how should the view interface be defined?
MvpLceActivity and MvpLceFragment
Mosby encapsulates the base class of the LCE view, and now we use MvpLceActivity or MvpLceFragment to implement the scenario described above for loading the country list.
The first step completes the interface layout, noting that the id must use the name specified above, and the error view can only be a TextView:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" ......> <include layout="@layout/loading_view" /> <include layout="@layout/error_view" /> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/contentView" ......> <ListView ....../> </android.support.v4.widget.SwipeRefreshLayout> </FrameLayout>
The second step inherits MvpLceView to implement its own view interface, where you need to specify generics as data types:
public interface CountriesView extends MvpLceView<List<Country>>{ }
The third part implements Presenter, where we make an interface and an implementation:
Interface definition:
public interface CountriesPresenter extends MvpPresenter<CountriesView>{ void loadCountries(final boolean pullToRefresh); }
Specific implementation:
public class SimpleCountriesPresenter extends MvpNullObjectBasePresenter<CountriesView> implements CountriesPresenter{ ...... @Override public void loadCountries(final boolean pullToRefresh) { getView().showLoading(pullToRefresh); ...... countriesLoader = new CountriesAsyncLoader(++failingCounter % 2 != 0, new CountriesAsyncLoader.CountriesLoaderListener() { @Override public void onSuccess(List<Country> countries) { getView().setData(countries); getView().showContent(); } @Override public void onError(Exception e) { getView().showError(e, pullToRefresh); } }); countriesLoader.execute(); } ...... }
All four methods except loadData() in MvpLceView are used in the code above.
Step 4 Implementing an Activity or Fragment requires inheriting MvpLceActivity:
public class CountriesActivity extends MvpLceActivity<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter> implements SwipeRefreshLayout.OnRefreshListener, CountriesView{
There are four generics defined in MvpLceActivity, namely, the type of ContentView, the type of Data, the type of view interface, and the type of Presenter.Here the ContentView uses SwipeRefreshLayout.
There are two methods to implement after inheriting MvpLceActivity:
@Override protected String getErrorMessage(Throwable e, boolean pullToRefresh) { if (pullToRefresh) { return "Error while loading countries"; } else { return "Error while loading countries. Click here to retry"; } } @NonNull @Override public CountriesPresenter createPresenter() { return new SimpleCountriesPresenter(); }
After implementing the CountriesView interface, override the following methods:
@Override public void setData(List<Country> data) { adapter.clear(); adapter.addAll(data); adapter.notifyDataSetChanged(); } @Override public void showContent() { super.showContent(); contentView.setRefreshing(false); } @Override public void showError(Throwable e, boolean pullToRefresh) { super.showError(e, pullToRefresh); contentView.setRefreshing(false); } @Override public void loadData(boolean pullToRefresh) { presenter.loadCountries(pullToRefresh); }
After implementing the OnRefreshListener interface, you need to implement a method:
@Override public void onRefresh() { loadData(true); }
If you want to use Fragments, the method is basically the same, you just inherit MvpLceFragment s. The only difference is that the initialization of Activity is done in onCreate(), and the initialization of Fragments is done in onViewCreated():
In Activity:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.countries_list); ButterKnife.bind(this); contentView.setOnRefreshListener(this); adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1); listView.setAdapter(adapter); loadData(false); }
In Fragment:
@Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ButterKnife.bind(this, view); contentView.setOnRefreshListener(this); contentView.setOnRefreshListener(this); adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1); listView.setAdapter(adapter); loadData(false); }
Introduction to ViewState
One of the most problematic issues in Android development is saving and restoring the view state when the interface is destroyed, rebuilt.System recycling and rebuilding of interfaces often occur in these two scenarios:
- Configuration changes, such as screen switching between vertical and horizontal screens, language environment changes, etc.
- Interfaces are cut to the background (for example, when the user presses the Home key), and Android automatically recycles the Activity when memory is low, and rebuilds the Activity when the interface is redisplayed.
Think: What is the difference between the two scenarios where activities are recycled and rebuilt?
Mosby provides a ViewState feature to solve this problem.ViewState is an interface with only one apply method:
public interface ViewState<V extends MvpView> { /** * Called to apply this viewstate on a given view. * * @param view The {@link MvpView} * @param retained true, if the components like the viewstate and the presenter have been * retained * because the {@link Fragment#setRetainInstance(boolean)} has been set to true */ public void apply(V view, boolean retained); }
For example, the MvpLceFragment mentioned above, if you want to save and restore the view state during the horizontal and vertical screen switching process, simply inherit the MvpLceViewStateFragment instead, and implement the following:
@Override public LceViewState<List<Country>, CountriesView> createViewState() { setRetainInstance(true); return new RetainingLceViewState<>(); }
This is the ViewState for the LceView provided by Mosby, and you can implement your own ViewState if it's our custom view.The implementation principle and application method of the entire ViewState feature are complex, and are not described here.
extend
Reflection
What are the differences and connections between MVC, MVP and MVVM?
What is delegation mode?What is the difference between delegation and inheritance?What are the benefits of using delegates?
What is a library?What is framework?What is the difference between them?
Tip: The key difference between Library and framework is Inversion of Control.When you call a method in library, you take control.But with frameworks, control is reversed: your code is invoked by the framework.
What is Null Object Pattern?
Drop-down refresh is considered in the LCE view, but pull-up loading is not. If the server is a paging interface and a pull-up loading needs to be added, how should the view interface be defined?
What is the difference between the two scenarios where activities are recycled and rebuilt?
Tip: Refer to the following two methods:
- Fragmemt.setRetainInstance(boolean retain)
- Activity.onRetainNonConfigurationInstance()