During my years of learning and growth, I gradually realized that it is very difficult and painful to build an excellent Android development framework. It not only needs to meet the growing business needs, but also needs to ensure that the framework itself is clean and extensible, which makes things very challenging. But we have to do this. Because a robust Android development framework is the foundation of a good APP.
This is the second article in my Android Refactoring tour. In previous articles, we introduced several common architectural patterns. In this article, WE will share with you how we set up a common project framework
Why do I need a framework?
In the early stage of development, we often do not need any Framework, because the good fault tolerance of Android Framework helps us avoid many problems, and even you can write a relatively perfect APP without in-depth study. A few simple Material Design-style interfaces plus some data will allow anyone to become an Android developer, but is that really enough?
Of course not!!
As our project more and more large, various problems and confusion of data storage, access, flexibility, high enough code that will be our late projects, one of the biggest obstacles to allow the free development of consequence is that a bad project, it will be difficult for us to add new functions, only for refactoring even overthrow redo it. We should not underestimate the complexity of an application before we begin programming.
In addition, in the field of software engineering, there are always some principles worth learning and abiding by, such as: single responsibility principle, dependency inversion principle, avoid side effects and so on. The Android Framework doesn’t force us to follow these principles, or it doesn’t impose any restrictions on us. Think of tightly coupled implementation classes, activities or fragments that handle a lot of business logic, EventBus everywhere, Unreadable data flows, chaotic callback hell, and so on, won’t cause the system to crash right away, but as the project grows, they can become difficult to maintain or even hard to add new code, which can be a terrible impediment to business growth.
Therefore, it is very important for developers to have a good architectural guidance specification.
Choice of architecture
There are a lot of articles about MVVM, MVP, MVC, AndroidFlux selection and analysis on the web, but I won’t describe them here. If you are interested, you can check out my Android refactoring tour: In architecture, we finally choose MVP as our development architecture. MVP has many advantages, but we finally choose it because it is easy for ordinary developers to use, and it can clearly plan the business boundary of our Activity.
Refused God Activity
Over the years of development, you’ve often seen activities with thousands of lines of code that can do anything:
-
Redefine the life cycle
-
To handle the Intent
-
Data update
-
thread
-
Basic business logic…… Even with BaseActivity defining every conceivable subclass variable and so on, it is now truly “God,” a handy and omnipotent God! As the project grows, it becomes too big to add any more code, so you write lots and lots of help classes to help the God slim down:
It looks like the business logic has been digested by the help classes. The code in BaseActivity is less fat, and the help classes take the pressure off. But as the project grows and the business expands, the help classes become more and more variable. At this point, we continue to break them down by business, maintenance costs seem to rise again, and all our efforts seem to have been wasted as messy and difficult to reuse applications return.
Of course, some people will separate out different abstract classes based on different business functions, but they are still versatile compared to that business scenario.
Regardless of the reason for the creation of “god” should be avoided, we should not focus on writing those a lot of classes, but the effort to write the ease of maintenance and testing of low coupling class, if you can, it is best not to let the business logic into pure Android world, this also is I have been trying to target.
Clean architecture and The Clean rule
The circles that look like “crust” are Clean Architecture. The different colored “rings” represent different system structures that make up the whole system, and the arrows represent dependencies. \
We have chosen MVP as the framework for the development of the framework, so we will not go into the details of Clean Architecture here. Some of the advantages of Clean Architecture will be integrated into the framework. We should follow three principles when designing the framework:
- Hierarchical principle
- Rely on the principle of
- Abstract principle
So let me explain what I think these principles are and why.
Hierarchical principle
First of all, the framework should not limit the specific layering of the application, but from the perspective of collaborative multiplayer development, I usually divide Android into three layers:
- Outer layer: Event Guide layer (View)
- Intermediate layer: interface adaptation layer (generally generated by Dagger2)
- Inner layer: business logic layer
It’s easy to think of the MVP structure by looking at the three layers above, and here’s what they contain.
Event guidance layer
It is mainly responsible for the direction of View events, such as onClick, onTouch, onRefresh, etc., and is responsible for passing events to the business logic layer.
Interface adaptation layer
The purpose of the interface adaptation layer is to connect business logic to framework-specific code and act as a bridge between the outer layer and the inner layer, which is typically generated using Dagger2.
Business logic layer
The business logic layer is the most important part of the framework, where we solve all the business logic. This layer should not contain code for event direction and should be able to be tested independently with Espresso, meaning that our business logic can be tested, developed and maintained independently, which is the main benefit of our framework architecture.
Depend on the rules
Dependency rules follow the direction of the Clean Architecture arrow. The outer layer “depends” on the inner layer. By Dependency, I don’t mean the Dependency statements you write in Gradle. Or the outer layer knows how the inner layer defines the abstraction, but the inner layer does not know how the outer layer is implemented. As mentioned earlier, the inner layer contains the business logic and the outer layer contains the implementation details, combined with the dependency rule that the business logic neither sees nor knows the implementation details.
For a project, the exact dependency is entirely up to you. You can divide them into different packages and manage them through the package structure. Be careful not to use external package code in internal packages. Using packages for management is very simple, but it also exposes fatal problems. Once someone does not know the dependency rules, they can write the wrong code. Because this management method does not prevent people from breaking the dependency rules, SO I prefer to group them into different Android Modules. Adjust the dependencies between modules so that the inner code doesn’t know about the outer layer at all.
Abstract principle
The so-called “abstraction principle” refers to the extraction of common patterns from specific problems and the use of common solutions to deal with them. For example, in our development, we often encounter switching interfaces without network and no data. We define a ViewLayoutState interface in the framework. On the one hand, the business logic layer can directly use it to switch interfaces; on the other hand, we can also implement this interface in the View layer to rewrite the style of switching different interfaces. The business logic layer just notifies the interface. It doesn’t know the implementation details, how it is implemented, or even whether the carrier of the interface is an Activity or a View.
This is a good example of how to use the principle of abstraction. When abstractions are combined with dependencies, business logic using abstract advice does not see or know the concrete implementation of ViewLayoutState, which is exactly what we want: business logic does not notice the concrete implementation details, much less know when it will change. The principle of abstraction helps us do this very well.
Build this library
With all these design principles in mind, let’s take a look at the design of Library, which is divided into three modules:
-
Instance
-
Util
-
Base
Util, Instance
Util and Instance are both essentially positioned as tools and auxiliary classes. One is static tool class for “use and go”, such as determining whether text is empty, and the other is Instance form for “long use”, such as Activity management stack, etc.
Base
The main job of Base is to give different capabilities to BaseActivity and BaseFragment. We mentioned above that we should avoid creating “God”, but it is difficult to avoid this situation during project development. In the Library, we extracted all the capabilities of BaseView. BaseActivity and BaseFragment will only be responsible for displaying views.
BaseActivity
The main functions of BaseActivity are divided into:
-
ActivityMvp provides context
-
ViewResult provides cross-screen refresh
-
ActivityToolbarBase provides the top bar
-
ViewLayoutState Provides a switching interface
-
Callback LifecycleCallbackStrategy life cycle management
As you can see here, all of the capabilities implemented in BaseActivity are view-related. This may surprise you, but isn’t there a business capability to implement ViewResult cross-screen refresh? Let’s see how it works.
Override public void resultAll() {presbyter.resultall (); } @param resultData */ @override public void result(Map<String, String> resultData) { presenter.result(resultData); }Copy the code
As you can see, we delegate presenter to ensure that BaseActivity only has view-related operations.
BaseListActivity
public abstract class ActivityListBase extends ActivityBase implements ActivityRecyclerMvp { private RecyclerView rvIndexRecycler = null; private SmartRefreshLayout srlRefresh = null; private MultiTypeAdapter adapter = null; private PresenterListBase presenter = null; @Override protected final int getLayout() { return R.layout.activity_recycler_base; } @Override protected final void onBeforeInit(Bundle savedInstanceState, Intent intent) { presenter = getPresenter(); presenter.onCreate(savedInstanceState); } @Override protected final void onInitComponent() { rvIndexRecycler = findViewById(R.id.rv_index_recycler); srlRefresh = findViewById(R.id.srl_index_refresh); onInitRecycler(); onInitListComponent(); } @Override protected final void onInitViewListener() { onInitRefresh(); } @Override protected final void onLoadHttpData() { presenter.getData(PresenterListBase.INIT); } / initialization refresh layout * * * * / protected final void onInitRefresh () {srlRefresh. SetOnLoadMoreListener (new OnLoadMoreListener () { @Override public void onLoadMore(RefreshLayout refreshLayout) { presenter.getData(PresenterListBase.LOAD_MORE); }}); srlRefresh.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh(RefreshLayout refreshLayout) { srlRefresh.setEnableLoadMore(true); srlRefresh.setNoMoreData(false); presenter.getData(PresenterListBase.REFRESH); }}); } / * * * initializes the Recycler * / protected final void onInitRecycler () {RecyclerView. LayoutManager LayoutManager = getLayoutManager(); rvIndexRecycler.setLayoutManager(layoutManager); rvIndexRecycler.setHasFixedSize(false); adapter = new MultiTypeAdapter(presenter.providerData()); addRecyclerItem(adapter); rvIndexRecycler.setAdapter(adapter); }}Copy the code
PresenterViewListImpl
public abstract class PresenterViewListImpl<T extends RespBase> implements PresenterListBase { protected ActivityRecyclerMvp viewBase = null; // Protected List<Object> data = null; // protected int pageStart = 1; // load more protected final int pageSize = PAGE_MAX_SIZE; // Load data type protected @loaddatastate int loadState; public PresenterViewListImpl(ActivityListBase activityListBase) { viewBase = activityListBase; data = new ArrayList<>(); } @Override public void onCreate(Bundle savedInstanceState) { } @Override public void result(Map<String, String > resultData) {RunTimeUtil. RunTimeException (" unrealized result interface "); } @ Override public void resultAll () {RunTimeUtil. RunTimeException (" unrealized resultAll interface "); } @Override public void getData(int state) { loadState = state; switch (loadState) { case INIT: { processPreInitData(); break; } case REFRESH: { pageStart = 1; break; } case LOAD_MORE: { pageStart = pageStart + 1; break; }} loadData(new OnLoadDataListener<T>() {@override public void loadDataComplete(T T) { handleLoadData(loadState, t); } @Override public void loadDataError(@StringRes int errorInfo) { handleLoadDataError(loadState, errorInfo); } @Override public void loadDataEnd() { handleLoadDataEnd(); }}); } /** * start loading */ protected final void processPreInitData() {pageStart = 1; viewBase.switchLoadLayout(); } /** * @param loadState * @param t */ protected void handleLoadData(int loadState, T t) { switch (loadState) { case INIT: { viewBase.switchContentLayout(); initView(t); break; } case REFRESH: { viewBase.finishRefresh(); initView(t); break; } case LOAD_MORE: { viewBase.finishRefreshLoadMore(); break; ** @param loadState * @param errorInfo */ protected void handleLoadDataError(int loadState, int errorInfo) { switch (loadState) { case INIT: { viewBase.switchReLoadLayout(errorInfo); break; } case REFRESH: { ToastUtil.showToast(viewBase.getContext(), viewBase.getContext().getString(errorInfo)); viewBase.finishRefresh(); break; } case LOAD_MORE: { pageStart = pageStart - 1; ToastUtil.showToast(viewBase.getContext(), viewBase.getContext().getString(errorInfo)); viewBase.finishRefreshLoadMore(); break; } } } protected void handleLoadDataEnd() { } @Override public void onDestroy() { viewBase = null; data = null; } @Override public List<? > providerData() { return data; } public abstract void loadData(OnLoadDataListener loadDataListener); public abstract void initView(T t); public void presenterLoadMoreData(T t) { } public interface OnLoadDataListener<Q extends RespBase> { public void loadDataComplete(Q q); public void loadDataError(@StringRes int errorInfo); public void loadDataEnd(); }}Copy the code
Show Code
Let’s try using a new framework for a simple list of data.
八九民运
public class InformationListActivity extends BaseListActivity { @Inject InformationActivityContract.Presenter mPresenter; @override public void injectAndInit() {// Interface adaptation layer DaggerInformationListActivityComponent.builder().activeInformationActivityModule(new InformationModule(this)).build().inject(this); } @Override public BaseListPresenter getBaseListPresenter() { return mPresenter; } @override protected void registerItem(MultiTypeAdapter adapter) {// display multiple RecyclerView adapter.register(ActiveDetailInfo.class,new ActiveAllListProvider(mActivity)); adapter.register(NoMoreDataBean.class,new NoMoreDataProvider()); }}Copy the code
As you can see, we’re cleanly pulling out the View, and now let’s see how Presenter is implemented, okay
public class InformationActivityPresenterImpl extends BaseListPresenterImpl<ResponseBean<ZoneActiveBean>> implements InformationActivityContract.Presenter { @Inject InformationActivityContract.View mView; @Inject ZoneApiService mZoneApiService; @Inject public InformationActivityPresenterImpl() { super(); } @Override public Observable getObservable(@Constant.RequestType int requestType) { return mZoneApiService.zoneActiveData(mView.getUserId(), pageNo, pageSize); } @Override public void initView(ResponseBean<ZoneActiveBean> responseBean) { ZoneActiveBean data = responseBean.getData(); if (data ! = null && data.activityInfo.activityList ! = null && data.activityInfo.activityList.size() > 0) { mData.clear(); for (ActiveDetailInfo item : data.activityInfo.activityList){ mData.add(item); } mView.setLoadMore(data.activityInfo.activityList.size() == pageSize); pageNo++; mView.notifyDataSetChanged(); } else { mView.setNodata(); } } @Override public void processLoadMoreData(ResponseBean<ZoneActiveBean> responseBean) { ZoneActiveBean data = responseBean.getData(); if (data ! = null && data.activityInfo.activityList ! = null && data.activityInfo.activityList.size() > 0) { for (ActiveDetailInfo item : data.activityInfo.activityList){ mData.add(item); } if (mData.size() == data.activityInfo.total) { mData.add(new NoMoreDataBean(false)); mView.setLoadMore(mData.size() == data.activityInfo.total); } pageNo ++; }else{ mView.setLoadMore(false); mData.add(new NoMoreDataBean(false)); } mView.notifyDataSetChanged(); }}Copy the code
Since we’ve specified that the event bootstrap only handles view-related operations, our activities are clean, and activities are only a path to data and events, while Presenters handle the details of events for us.
conclusion
As a common development framework within the company, the selection of functions should keep the minimum principle and only use the necessary functions, and the architecture should maintain good expansibility.
I believe you and I, in the process of building frame in all kinds of challenges, learn from your mistakes, and constantly optimize the code, adjust the dependencies, even reorganize module structure, these changes are you want to let the architecture become more robust, we have been hoping that applications can become easy to develop easy to maintain, this is the true sense of the team to benefit.
I have to say that there are many different ways to build an application architecture, and I don’t think there is a one-size-fits-all architecture, it should be iterative and tailored to the business. So, you can try to build your application with the business in mind by following the ideas provided in this article.
Finally, I hope this article will be helpful to you. If you have other better architectural ideas, please share them with me.
Public id: Programmer Cat