Summary: This article introduces how to design the architecture of a large Android project from zero to one, combining personal thinking and understanding of architectural design.

The author | brand to cosette source | ali technology to the public

Introduction: This paper introduces how to design a large Android project architecture from 0 to 1 based on personal thinking and understanding of architecture design.

A guide

The length of this paper is quite long, and the following table can be used to guide a quick understanding of the main context of the whole paper.

Ii. Evolution of project architecture

This chapter mainly summarizes the evolution of an Android project architecture from 0 to 1 to N. (Because the development of a project is influenced by business, team, and schedule, this summary does not strictly match the evolution of each step, but it is sufficient to illustrate the general rules of the development stage of the project.)

1 Single project stage

For a new project, there are usually very few developers on each side, often only one or two. At this time, the development cycle is more important than the architectural design and various development details of the project, and the rapid implementation of idea is the most important goal in this stage. This is often the way projects are structured at this stage

At this point, almost all the code in the project will be written in an independent APP module. Under the background of time being king, the original development mode is often the best and most efficient.

2 abstract base library stage

As the minimum MVP of the project has been developed, I plan to continue to improve the App. At this time, the following problems are likely to be encountered:

  1. In order to speed up the iteration of the project, the team recruited 1-3 developers. When many people develop on the same project at the same time, there will always be conflicts in Git code merging, which will greatly affect the development efficiency.
  2. The compilation and construction of the project is a problem. With the increasing amount of code in the project, the App is compiled based on source code, so that the speed of the first whole package compilation and construction is gradually slowed down, and it may even take several minutes or more to verify the change of a line of code.
  3. The company may be developing multiple apps at the same time. The same code always needs to be reused by copying and pasting. There will also be problems in maintaining the logical consistency of the same function among multiple apps.

For one or more of these reasons, we tend to modularize functionality that, relative to the overall project, is rarely changed once it’s developed.

We took a project that originally contained only one application layer and pulled down a base layer that contained many atomic libraries such as the network library, image loading library and UI library. Doing so has greatly improved collaborative development, package building, and code reuse.

3. Core competence development stage

After the business began to take shape, the App was put online and had a steady and consistent DAU.

This is often critical, as the business grows, customer usage increases, iteration requirements increase, and so on. If the project does not have a sound architecture design, the human efficiency of the development will reverse with the expansion of the team size. In the past, one person could develop 5 requirements per unit of time, but now 10 people can not complete the development of 20 requirements in the same time. It is difficult to completely solve this problem simply by adding people. There are two things that need to be done

  1. Development responsibilities are separated, and team members need to be divided into two parts, one for business development and one for infrastructure. The business development team is responsible for the support of daily business iterations, aiming at business delivery; The infrastructure group is responsible for the core capability construction of the underlying foundation, aiming at improving efficiency, performance and core capability expansion.
  2. The optimization of project architecture is based on 1. The core architecture layer should be abstracted between the application layer and the basic layer, and the core architecture layer should be handed over to the infrastructure group together with the basic layer, as shown in the figure.

This layer will involve the construction of many core capabilities, which will not be described here. The following modules will be expanded in detail.

Note: From a global perspective, the base layer and core layer can also work as a whole to support the upper business. Here it is divided into two layers, mainly considering that the former is a mandatory option, is a necessary part of the overall architecture; The latter is optional, but also a core measure of an App’s mid-stage capabilities.

4 Modular Phase

As the business scale continues to expand, the product managers of App (hereinafter referred to as PD) will change from one to multiple, and each PD is responsible for an independent business line. For example, if the App contains multiple modules such as home page, product and me, each PD will correspond to one module here. But this adjustment poses a serious problem

The iteration time of the project version is fixed. When there is only one PD, a batch of requirements will be put forward for each version. The development will be launched as soon as it can be delivered on time.

But now that there are multiple lines of business in parallel, it is difficult to ensure that the requirements iteration of each line of business can be delivered normally in an absolute sense. It is just like when you organize an activity and agree on several sets, there will always be some special situations that cannot be arrived in time. Similarly, the difficulty of being completely consistent can be encountered in project development. Under the current project structure, although business lines are split, the business modules of our engineering projects are still a whole, which contains a variety of intricate dependency networks. Even if each business line is divided into branches, it is difficult to avoid this problem.

At this time, we need to do project modularization at the architecture level, so that multiple business lines do not depend on each other, as shown in the figure

In the business layer, there can be more fine-grained division by developer or team to ensure decoupling between businesses and demarcation of development responsibilities.

5. Cross-platform development stage

As the size of the business and the volume of users continued to grow, the whole side of the team began to consider the cost of r&d in order to cope with the consequent explosion of business demand.

Why does every business requirement need to be implemented at least once on both Android and iOS? Is there a solution that allows one piece of code to run on multiple platforms? This not only reduces the cost of communication, but also improves the efficiency of research and development. Of course, the answer is yes, at this time, part of the end-to-end business began to enter the stage of cross-platform development.

At this point, a relatively complete end-to-end system architecture has begun to take shape. The subsequent business will continue to have more iterations, but the overall structure of the project will not deviate too much, and more of it is to make deeper improvements and improvements to some nodes in the current architecture.

The above is a summary of the iterative process of Android project architecture. Next, I will expand the final architecture diagram one by one from bottom to top, and analyze and summarize the core modules involved in each layer and possible problems.

Iii. Dismantling of project architecture

1 base layer

Basic UI module

Extracting the basic UI modules serves two main purposes:

Unify App global base style

For example, the main color of the App, the color and size of the normal text, the inside and outside margins of the page, the default notification text for network load failures, the default UI for empty lists, etc. These basic UI styles become very important, especially after the modularization of the project mentioned below.

Reuse basic UI components

In order to improve the development efficiency of upper-layer business, it is necessary to uniformly encapsulate some high-frequency UI components for upper-layer business to call when the project and team scale are gradually expanding. On the other hand, the necessary abstraction encapsulation can also reduce the size of the final build installation package so that a single semantic resource file does not appear in multiple places.

The basic UI components usually consist of internal development and external reference. Internal development is understandable, and can be developed and encapsulated according to business requirements. External references emphasize that Github has a great library of reusable, project-proven UI components that are a great choice for quick business development needs.

Choosing a suitable UI library will greatly accelerate the whole development process. It may be no problem to implement it manually, but it will take a lot of time and energy. If it is not for studying the implementation principle or in-depth customization, it is recommended to choose a mature UI library first.

Network module

The vast majority of App applications need to be connected to the Internet, and the network module has become an essential part of almost all apps.

Framework to choose

The choice of a basic framework is often based on a few broad principles:

  1. The maintenance team and community are large, and there is enough space to solve problems by themselves.
  2. The bottom layer has powerful functions and supports as many upper-layer application scenarios as possible.
  3. Flexible ability expansion, support on the framework based ability expansion and AOP processing;
  4. Api side friendly, reduce the upper understanding and use cost;

Without elaboration, Retrofit2 is recommended as the preferred network library unless the base layer has its own additional customization for the network layer. The upper Java Interface style Api is very developer-friendly. The underlying dependencies of the powerful Okhttp framework can also meet the business needs of almost all scenarios. Use case references from the official website

The use case demonstrates the benefits of Retorfit’s declarative interface, which can be used without manually implementing the interface. The principle behind this is based on Java dynamic proxies.

Unified interception processing

Whatever network library you choose in the previous step, you need to take into account its ability to support unified interception. For example, if we want to print logs of all requests in the entire running process of an App, we need a global Interceptor that supports configuration like Interceptor.

For example, in today’s distributed server deployment scenarios, the traditional session mode can no longer meet the demands of client status recording. A commonly accepted solution is JWT (JSON WEB TOKEN), which requires the client side to pass the request header containing the user status to the server after login authentication. In this case, unified interception processing similar to the following is required at the network layer.

Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://xxx.xxxxxx.xxx") .client(new OkHttpClient.Builder() .addInterceptor(new Interceptor() { @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException {// Add a unified Request header Request newRequest = chain.request().newBuilder().addheader ("Authorization", "Bearer " + token) .build(); return chain.proceed(newRequest); } }) .build() ) .build();Copy the code

As an additional note, if there is some business related information in the application, it is also recommended to consider the unified delivery directly through the request header based on the actual business situation. For example, the community Id of the community App and the store Id of the store App have a common feature. Once switched, many subsequent business network requests will need this parameter information. If each interface is manually passed in, the development efficiency will be reduced and some unnecessary human errors will be more easily caused.

Image module

Picture library and network library are different, at present the industry is more popular a few library difference is not so big, here suggest to choose by oneself according to individual be fond of and familiar degree. Here are some examples I compiled from each gallery’s official website.

Picasso

Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);
Copy the code

Fresco

Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/main/docs/static/logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);
Copy the code

Glide

Glide.with(fragment)
    .load(myUrl)
    .into(imageView);
Copy the code

In addition, the star of each library on Github is attached here for your reference.

The selection of the image library is flexible, but its fundamentals need to be understood so that there are adequate strategies for dealing with problems with the image library.

In addition, it should be emphasized that the core of the image library is the design of the image cache. For the extension of this part, please refer to the “Summary of core Principles” section below.

Asynchronous module

Asynchrony is used a lot in Android development, and there’s a lot of knowledge involved, so I’ll separate it out here.

1) Asynchronous theorem in Android

To sum up, the main thread handles UI operations and the child thread handles time-consuming tasks. If you do the opposite, the following problems arise:

  1. The main thread to do network request, can appear NetworkOnMainThreadException exception;
  2. If the main thread does a time-consuming task, ANR (Application Not Responding) may appear.
  3. Child thread do UI operation, can appear CalledFromWrongThreadException anomalies (here only for general discussion, in fact, in the following thread satisfy certain conditions can also update the UI, and the Android neutron really can’t update the UI thread?” , this article will not discuss the case);

2) Child threads call the main thread

If you are currently in a child thread and want to call the main thread’s methods, you can do so in one of the following ways

1. Use the POST method of the main thread Handler

private static final Handler UI_HANDLER = new Handler(Looper.getMainLooper()); @WorkerThread private void doTask() throws Throwable { Thread.sleep(3000); UI_HANDLER.post(new Runnable() { @Override public void run() { refreshUI(); }}); }Copy the code

2. Use the sendMessage method of the main thread Handler

private final Handler UI_HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { if (msg.what == MSG_REFRESH_UI) { refreshUI(); }}}; @WorkerThread private void doTask() throws Throwable { Thread.sleep(3000); UI_HANDLER.sendEmptyMessage(MSG_REFRESH_UI); }Copy the code

3. Use the Activity’s runOnUiThread method

public class MainActivity extends Activity { // ... @WorkerThread private void doTask() throws Throwable { Thread.sleep(3000); runOnUiThread(new Runnable() { @Override public void run() { refreshUI(); }}); }}Copy the code

4. Use the View post method

private View view; @WorkerThread private void doTask() throws Throwable { Thread.sleep(3000); view.post(new Runnable() { @Override public void run() { refreshUI(); }}); }Copy the code

3) The main thread calls the child thread

If you are currently in a child thread and want to call the main thread method, there are several ways to do this

1. Through a new thread

@UiThread
private void startTask() {
    new Thread() {
        @Override
        public void run() {
            doTask();
        }
    }.start();
}
Copy the code

2. Through the ThreadPoolExecutor

private final Executor executor = Executors.newFixedThreadPool(10); @UiThread private void startTask() { executor.execute(new Runnable() { @Override public void run() { doTask(); }}); }Copy the code

3. Through the AsyncTask

@UiThread
private void startTask() {
    new AsyncTask< Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... voids) {
            doTask();
            return null;
        }
    }.execute();
}
Copy the code

Asynchronous programming pain points

Android development uses two languages, Java and Kotlin, and it would be nice if Kotlin were included in our project. For asynchronous calls, just do the following.

Kotlin scheme

val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
Copy the code

To extend this appropriately, asynchronous calls like async + await are already supported in many other languages, as follows

The Dart program

Future< String> fetchUserOrder() =>
    Future.delayed(const Duration(seconds: 2), () => 'Large Latte');

Future< String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}
Copy the code

JavaScript solution

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => { resolve(x); }, 2000);
  });
}

async function f1() {
  var x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}
f1();
Copy the code

However, if our project is a pure Java project, we will often encounter serial asynchronous business logic in complex business interaction scenarios, and our code readability will become very poor. An alternative solution is to introduce RxJava to solve this problem, as shown below

RxJava scheme

source
  .operator1()
  .operator2()
  .operator3()
  .subscribe(consumer)
Copy the code

2 core layer

Dynamic configuration

Service switch, ABTest

Dynamic configuration of online functions

background

  1. Android (Native development) is different from Web can be released online at any time, Android release almost all need to go through the review of application platform;
  2. In most cases, AB tests or configuration switches are required to meet the needs of diversified services.

Based on the above points, we decided in the Android development process, the code logic has a dynamic configuration appeal.

Based on this most basic model unit, the business can evolve very rich gameplay, such as configuring the duration of the startup page, configuring whether to display a large picture in the product, configuring how many pieces of data to load per page, configuring whether to allow users to enter a page, and so on.

Analysis of the

There are two methods for the client to obtain configuration information: push and pull.

Push refers to the establishment of a long connection between the client and the server. Once configuration changes occur on the server, the server pushes the changed data to the client for updating.

Pull means that the client reads the latest configuration through active request every time.

Based on these two modes, the combination of push and pull will evolve. Its essence is to use both modes. There are no new changes in the technical level, so I will not repeat them here. Here’s a comparison of push and pull

In general, if the business does not have very high requirements for timeliness, I personally prefer to choose pull mode. The main reason for changing the configuration is low-frequency events. It will feel like killing a chicken to make a long c-S connection for this low-frequency event.

implementation

The implementation of push configuration is relatively clear. It is only necessary to send configuration updates to the client. However, the reconnection logic after long connection disconnection is required.

Pull configuration implementation, there are some things we need to think about, here we summarize the following points:

  1. Configure multiple modules according to the namespace to avoid a large and complete configuration globally.
  2. Each namespace has a flag during initialization and each change to identify the current version.
  3. Each service request on the client is uniformly marked with flags or their combined MD5 identifiers in the request header to verify the timeliness of flags when the server intercepts them uniformly.
  4. The timeliness test result of the server is delivered through the unified response header, which is isolated from the service interface and is not perceived by the upper-layer business side.
  5. When the client receives a result with inconsistent timeliness, it pulls the namespace according to the specific namespace instead of pulling all the namespace each time.

Global intercept

background

The closest connection between App and users is interaction, which is the bridge of communication between our App products and users.

What the user sees most intuitively is what action to perform after clicking a button, what content to display after entering a page, what request to perform after an action, and what prompt to perform after a request. Global interception is a technical solution for the highest frequency interaction logic available to the user that can be customized through the previous dynamic configuration.

Interaction structure

Specific interaction response (such as pop-up a Toast or Dialog, jump to a page) is need through the code logic to control, but after this part have to do is released in the App can also achieve these interactions, therefore we need to put some basic common interactions structured processing, and then make a general embedded logic in advance in the App.

We can make the following convention to define the concept of Action, and each Action corresponds to a specific interaction that can be identified in the App, for example

1. The pop up the Toast

{" type ":" toast ", "content" : "hello, welcome to XXX," "gravity" : "< here toast to show the location of choice for (center | top | bottom), the default value is center >"}Copy the code

2. The pop-up Dialog

It is worth noting here that the Dialog actions have Toast logic nested in them, and the flexible combination of multiple actions gives us rich interaction capabilities.

{" type ":" dialog ", "title" : "prompt", "message" : "sure to exit the current page?" }}, "confirmText": "confirm ", "cancelText": "cancel ", "confirmAction": {"type": "toast", "Content ":" You clicked confirm "}}Copy the code

3. Close the current page

{
  "type": "finish"
}
Copy the code

4. Go to a page

{
  "type": "route",
  "url": "https://www.xxx.com/goods/detail?id=xxx"
}
Copy the code

5. Execute a network request as in 2, where multiple actions are nested.

{ "type": "request", "url": "https://www.xxx.com/goods/detail", "method": "post", "params": { "id": "xxx" }, "response": { "successAction": { "type": "toast", "content": ${response.data.priceDesc} $"}, "errorAction": {"type": "dialog", "title": "prompt ", "message": "Query failed, will exit current page ", "confirmText": "confirm ", "confirmAction": {"type": "finish"}}}}Copy the code

Unified intercept

Interaction structured data protocol specifies the specific event corresponding to each Action, the client side of the structured data parsing and encapsulation, and then can translate the data protocol into product interaction with the user, the next thing to consider is how to make an interaction information effective. Refer to the following logic

1. Provides the ability to obtain the Action sent by the server according to the page and event identifier. The DynamicConfig used here is the dynamic configuration mentioned above.

@Nullable private static Action getClickActionIfExists(String page, String actionId = string. format("hook/click/%s/%s", page, event); Action String value = dynamicConfig. getValue(actionId, null); if (TextUtils.isEmpty(value)) { return null; } try {return json.parseObject (value, action.class); } catch (JSONException ignored) {return null; }Copy the code

2. Provide the processing logic that wraps click events (performAction is the parsing logic for specific actions, with relatively simple functions, so it is not expanded here)

@param clickListener @param clickListener @param clickListener @param clickListener @param clickListener @param clickListener View.OnClickListener handleClick(String page, String event, View.OnClickListener clickListener) {// This returns an OnClickListener object, Return new view.onClickListener () {@override public void onClick(View v) {// Retrieve the configuration Action for sending the current event action = getClickActionIfExists(page, event); if (action ! = null) {// If there is configuration, go to configuration logic performAction(action); } else if (clickListener ! Clicklistener.onclick (v); = null) {clicklistener.onclick (v); }}}; }Copy the code

With the above foundation, we can quickly implement the function that supports the remote dynamic change of App interaction behavior. The following is a comparison of the code differences of the upper business side before and after this ability.

/ / before addGoodsButton. SetOnClickListener (new View. An OnClickListener () {@ Override public void onClick (View v) { Router.open("https://www.xxx.com/goods/add"); }}); . / / after addGoodsButton setOnClickListener (ActionManager handleClick (" goods - manager ", "add - goods", new View.OnClickListener() { @Override public void onClick(View v) { Router.open("https://www.xxx.com/goods/add"); }}));Copy the code

As you can see, the business side passes through some identification parameters for the current context, but there are no other changes.

Up to now, we have completed the remote hook ability for the addGoodsButton button click event, if now suddenly occurs some reason to add goods page is not available, just need to add the following configuration in the remote dynamic configuration.

{hook/click/goods-manager/add-goods": {"type": "dialog", "title": "message": "ConfirmText ": "confirm ", "confirmAction": {"type": "finish"}}}Copy the code

At this point, the user clicks the “Add commodity” button again, and the above prompt information will appear.

The idea of remote interception for click events is introduced above. Corresponding to click events, there are also common interactions such as page hopping and network request execution. Their principles are the same and they will not be enumerated one by one.

Local configuration

During the App development test phase, it is often necessary to add some localization configuration so that a single build allows for compatibility with multiple logic. For example, the App needs to make several common environment switches (daily, pre-delivery and online) in the process of coordinating with the server interface.

Theoretically, the dynamic configuration mentioned above can also achieve this demand, but dynamic configuration is mainly for online users, and if this capability is used in the production and research stage, it will undoubtedly increase the complexity of online configuration, and will depend on the results of network requests to achieve.

Therefore, we need to abstract a solution that supports localization configuration, and that satisfies the following capabilities as far as possible

  1. The default value is supported for local configuration. If no configuration is performed, the default value is returned. For example, if the default environment is online, the daily and pre-delivery environments will not be read if the configuration is not changed.
  2. Simplify the configuration of the read and write interface, so that the upper business side as little as possible aware of implementation details. For example, we don’t need to let the upper layer know whether local configuration persistence information is written to SharedPreferences or SQLite, but simply provide an API for writing.
  3. Expose to the upper layer the API way to access the local configuration page to satisfy the upper layer’s selective access to space. For example, through our exposed API, the upper layer can choose whether to enter the configuration page by clicking an action button on the page, the volume button on the phone, or by shaking it.
  4. The control of whether the App has local configuration ability should be put at the compilation and construction level as far as possible to ensure that online users will not enter the configuration page. For example, if online users are able to access the pre-release environment, a security incident may be brewing.

Version management

In mobile clients, Android applications can only be released in AppStore, which is different from iOS. Apk files built by Android support direct installation, which provides the possibility of App silent upgrade. Based on this feature, we can realize users’ demand to directly detect and upgrade the new version without going through the App market, shorten the path of App upgrade for users, and thus improve the coverage of the new version when it is released.

We need to consider the capability support of abstract version detection and upgrade in the application, where the server needs to provide the interface to detect and obtain the new version of the App. The client invokes the version detection interface of the server to determine whether the current App is the latest version based on certain policies, for example, every time the client enters the App or manually clicks new version detection. If the current version is a new version, the App side provides the link to download the latest version of the APK file, and the client downloads the version in the background. A flow chart of the core steps is summarized below

Log monitoring

Environment isolation, local persistence, and log reporting

The log monitoring of the client is mainly used to troubleshoot abnormal problems such as Crash in the process of using the App. Several noteworthy points are summarized in the log part

  1. Environment isolation, release package forbids log output;
  2. Local persistence. For critical logs (such as Crash caused by a location error), local persistence is performed.
  3. Log reporting: Uploads temporary user local logs and analyzes specific operation links under user authorization.

Two open source logging frameworks are recommended:

logger

timber

Buried some statistical

The server can query the number and frequency of the client interface invocation, but cannot perceive the specific operation path of the user. In order to have a clearer understanding of users and analyze the advantages, disadvantages and bottlenecks of the product, we can collect and report the core operation paths of users on the App.

For example, the following is the user transaction funnel diagram of an e-commerce App. The buried point statistics of the client can obtain the data of each layer of the funnel, and then make a visual report based on the data.

By analyzing the following funnel, we can obviously see that the key node of transaction loss is between “entering the commodity page” and “buying”. Therefore, we need to consider why users “entering the commodity page” have lower purchase intention. Is it the product itself or the product interaction on the product page? Could it be that the buy button is harder to click? Or is the product description not displayed because the picture on the product page is too big? What about the amount of time spent on these pages? Thinking about these issues will further encourage us to add more ABtests and more fine-grained statistical analysis of buried points to the product page. In conclusion, buried point statistics provide important guidance for user behavior analysis and product optimization.

On the technical side, the following key points are summarized for this section

  1. The client burying point is generally divided into P point (page level), E point (event level), and C point (custom point).
  2. Buried points are divided into two steps: collection and reporting. When the number of users is large, it is necessary to merge and compress the reported buried points.
  3. The embedded logic is the secondary logic, and the product business is the main logic. When the client resources are limited, the resource allocation should be balanced.

Hot repair

Hotfix is a technical solution to dynamically update the original code logic of an App that has been published online without upgrading the App. This is mainly done in the following scenarios

  1. For example, in some models with highly customized systems (such as Xiaomi series), application Crash will occur once entering the product details page;
  2. For example, in some extreme scenarios, the user cannot close the page dialog box.
  3. Application of capital loss, customer complaints, public opinion storm and other product form problems, such as the price unit “yuan” mistakenly displayed as “fen”;

The exploration of technical solutions related to thermal repair can be extended to a great extent. The positioning of this paper is the overall architecture of the Android project, so it will not be expanded in detail.

3 the application layer

Abstraction and encapsulation

As for abstraction and encapsulation, it mainly depends on our ability to perceive and think about some pain points and redundant Coding in daily Coding.

For example, here is a standard implementation logic for a list page that is often written during Android development

public class GoodsListActivity extends Activity { private final List< GoodsModel> dataList = new ArrayList<>(); private Adapter adapter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_goods_list); RecyclerView recyclerView = findViewById(R.id.goods_recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new Adapter(); recyclerView.setAdapter(adapter); // Load dataList. AddAll (...) ; adapter.notifyDataSetChanged(); } private class Adapter extends RecyclerView.Adapter< ViewHolder> { @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view = inflater.inflate(R.layout.item_goods, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { GoodsModel model = dataList.get(position); holder.title.setText(model.title); holder.price.setText(String.format("%.2f", model.price / 100f)); } @Override public int getItemCount() { return dataList.size(); } } private static class ViewHolder extends RecyclerView.ViewHolder { private final TextView title; private final TextView price; public ViewHolder(View itemView) { super(itemView); title = itemView.findViewById(R.id.item_title); price = itemView.findViewById(R.id.item_price); }}}Copy the code

This code looks logical enough to meet the functional requirements of a list page.

For RecyclerView framework layer, in order to provide flexibility and expansion ability of the framework, so the API design to enough atomization, in order to support developers’ diverse development demands. For example, RecyclerView needs to support multiple itemTypes, so internal need to do according to itemType open group cache vitemView logic.

However, in the actual business development process, many particularities will be removed. Most of the lists we display on our page are single-item-type. After writing many lists of this single-item-type, we start to think about some questions:

  1. Why do I write a ViewHolder for each list?
  2. Why write an Adapter for each list?
  3. Why should the creation and data binding of an itemView in Adapter be done separately in onCreateViewHolder and onBindViewHolder?
  4. Why does the Adapter call the corresponding notifyXXX method every time it sets data?
  5. Why does implementing a simple list on Android require dozens of lines of code? How much of this is necessary and how much can be abstracted and encapsulated?

Thinking about the above problems finally led me to package the RecyclerViewHelper auxiliary class. Compared with the standard implementation, the user can save the tedious Adapter and ViewGolder declaration, and some high-frequency and necessary code logic, and only need to pay attention to the core function realization, as follows

public class GoodsListActivity extends Activity { private RecyclerViewHelper< GoodsModel> recyclerViewHelper; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_goods_list); RecyclerView recyclerView = findViewById(R.id.goods_recycler_view); recyclerViewHelper = RecyclerViewHelper.of(recyclerView, R.layout.item_goods, (holder, model, position, itemCount) -> { TextView title = holder.getView(R.id.item_title); TextView price = holder.getView(R.id.item_price); title.setText(model.title); price.setText(String.format("%.2f", model.price / 100f)); }); / / load the data recyclerViewHelper. AddData (...). ; }}Copy the code

The above is just a primer, and we will encounter many similar situations in actual development, as well as some common encapsulation. For example, encapsulating globally unified BaseActivity and BaseFragment includes, but is not limited to, the following capabilities

  1. Buried point of the page. Based on the buried point statistics mentioned above, the interaction of the user’s page is collected and reported when the user enters or leaves the page.
  2. Public UI, status bar and ActionBar at the top of the page, implementation of common pull-down refresh ability of the page, loading progress bar when page time-consuming operation;
  3. Permission processing: permission application required to enter the current page, callback logic after user authorization and exception processing logic after rejection;
  4. Unified interception, combined with the previously mentioned unified interception to add the customization ability to support dynamic configuration interaction after entering the page;

modular

background

The modularity mentioned here refers to the modular splitting of project engineering based on App business functions, mainly to solve the difficult problem of collaborative development of large and complex business projects.

The transformation of the project structure is shown in the figure above. The APP module that originally carried all the business was divided into multiple business modules such as HOME, goods and mine.

Universal capacity sinking

General business capabilities such as BaseActivity and BaseFragment mentioned in the previous chapter of “Abstraction and Encapsulation” also need to be transformed synchronously after the project is modularized. They need to be sunk into a single Base module in the business layer for reference by other business modules.

Implicit route modification

After modularization, modules do not depend on each other, so you cannot directly reference the classes of other modules when skipping across modules.

For example, to display a product recommendation on the home page, click to jump to the product details page, written before modularization

However, after modularization, the home page module can not reference the GoodsActivity class, so page hopping can not continue in the previous way, the page needs to be modified implicitly, as follows

1. Register the Activity identifier. Add the action identifier where the Activity is registered in androidmanifest.xml

2. Replace the jump logic with an implicit jump based on the Activity identifier registered in the previous step

Based on the transformation of these two steps, it can achieve the purpose of normal jump business page after modularization.

Further, we abstract and encapsulate the logic of implicit jumps to extract a static method that specifically provides implicit routing capabilities, as shown in the following code

Public class Router {/** ** redirects to the destination page from the url ** @param context the current page context * @param URL The destination page url */ public static void Open (Context Context, String URL) {// Parse to a Uri object Uri = uri.parse (url); Format ("%s://%s%s", uri.getScheme(), uri.gethost (), uri.getPath()); Intent intent = new Intent(urlWithoutParam); // Parse the parameter in the URL and pass it to the next page via the Intent for (String paramKey: uri.getQueryParameterNames()) { String paramValue = uri.getQueryParameter(paramKey); intent.putExtra(paramKey, paramValue); } // Perform the jump operation context.startActivity(intent); }}Copy the code

In this case, you only need to use the following call to switch to the external page

Router.open(this, "https://www.xxx.com/goods/detail?goodsId=" + model.goodsId);
Copy the code

This encapsulation can

  1. Abstract unified method, reduce the cost of external coding;
  2. Unified closing routing logic facilitates dynamic change of online App routing logic in combination with “dynamic configuration” and “unified interception” sections above;
  3. Standardize the format of page jump parameter on Android terminal, use String type uniformly, remove the ambiguity of type judgment when parsing parameters of target page;
  4. Standardized support is made for the data required by the page hopping. After the iOS terminal and synchronization transformation, the page hopping logic can be delivered completely by the business server.

Communication module

After modularization, another problem needs to be solved is module communication. There is no direct dependency between modules can not get any API of each other to call directly. The problem is usually analyzed and dealt with in the following categories

1. Notification communication only needs to inform the other party of the event and does not pay attention to the response result of the other party. For this kind of communication, the following methods are generally adopted

  • Send an event via an Intent + BroadcastReceiver (or LocalBroadcastManager) provided by the framework.
  • Send events with the framework EventBus;
  • Based on the observer mode self-realization message forwarder to send events;

2. Call communication, informing the other party of the event and paying attention to the event response result of the other party. For this kind of communication, the following methods are generally adopted

  • The biz-service module is defined, the business interface file is closed into the module, and then the interface is realized by the corresponding semantic business module of each interface, and then the implementation class is registered based on some mechanism (manual registration or dynamic scanning).
  • Abstract the communication protocol of Request => Response, and the protocol layer is responsible for routing the Request transmitted by the caller to the protocol implementation layer of the called party; Then the result returned by the implementation layer is converted into a generalized Response object. Finally, the Response is returned to the caller;

In contrast to biz-Service, the middle tier of the scenario does not contain any business semantics and only defines the key parameters needed to generalize the call.

4 Cross-platform Layer

The cross-platform layer is designed to improve developer efficiency. A set of code can run on multiple platforms.

There are generally two opportunities for cross-platform access. One is to directly select a pure cross-platform technology solution at the early stage of project investigation. The other is the stage where cross-platform capabilities need to be integrated on existing Native projects. At this stage, App belongs to the mode of mixed development, that is, the combination of Native and cross-platform.

More cross-platform selection and details are beyond the scope of this article. For details, please refer to “Analysis and Selection of Mobile Cross-platform Development Frameworks”. The development of the whole cross-platform technology, principles and advantages and disadvantages of each framework are described in great detail. See the cross-platform technology evolution diagram

For the comparison of current mainstream schemes, please refer to the following table

Above, the main modules of each layer in the project architecture are disassembled and analyzed one by one. Next, some very core principles used in the architecture design and actual development are summarized and sorted out.

Summary of four core principles

In Android development, there are so many frameworks that are constantly being updated that it’s hard to keep track of them all.

However, this does not affect our study and research of the core technologies in Android. If you have tried to dig into the underlying principles of these frameworks, you will find that many of them are similar. Once we understand these core principles, most frameworks are nothing more than generic technical solutions that take these principles and combine them with the core problems the framework is trying to solve.

Below I will sort out and summarize some of the core principles that are frequently used in THE SDK framework and in actual development.

1 double cache

Double cache refers to the technical solution of adding double cache in memory and disk to improve the speed of obtaining some resources through the network. This scheme is mainly used in the image library mentioned in the “image module” above at the beginning, and the image library uses double cache to greatly improve the loading speed of the image. A standard dual-cache scheme is illustrated below

The core idea of the dual cache scheme is to exchange space for time as much as possible for network resources with low timeliness or less changes. We know the general efficiency of data acquisition: memory > disk > network, so the essence of the scheme is to copy resources from the channel with low efficiency to the channel with high efficiency.

Based on the scheme, we can expand in the actual development another scenario, some low timeliness or change for our business less interface data, in order to improve the efficiency of their loading, packaging, can also be combined with the ideas for such a will depend on the network request first rendering of the page length from general within hundreds of ms to the dozens of ms, the optimization effect is quite obvious.

2 the thread pool

Thread pools are used a lot in Android development, for example

  1. In the development framework, network library and picture library need to use thread pool to acquire network resources.
  2. In the development of the project, I/O operations such as reading and writing SQLite and local disk files need to use thread pools;
  3. In apis such as AsyncTask, which provide task scheduling, the underlying layer is also dependent on thread pools.

With so many scenarios using thread pools, it is important to be familiar with some of the core capabilities and internals of thread pools if we want to get a clearer picture of the project.

In terms of the API that it exposes directly, the two core methods are the thread pool constructor and the method that performs the subtask.

ThreadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, keepAliveTimeUnit, workQueue, threadFactory, rejectedExecutionHandler); Execute (new Runnable() {@override public void run() {// Do the subtask}});Copy the code

The submission subtask is to pass in an instance of an object of type Runnable. The most important parameters in the thread pool are the constructor parameters.

Int corePoolSize = 5; Int maximumPoolSize = 10; Int keepAliveTime = 1; TimeUnit keepAliveTimeUnit = timeunit.minutes; // workQueue < Runnable> workQueue = new LinkedBlockingDeque<>(50); ThreadFactory ThreadFactory = new ThreadFactory() {@override public Thread newThread(Runnable r) {return new Thread(r); }}; / / task overflow handling strategy RejectedExecutionHandler RejectedExecutionHandler = new ThreadPoolExecutor. AbortPolicy ();Copy the code

There are many articles and tutorials on thread pools on the web. I will not repeat each parameter here. But I’ll separate out what’s crucial to understanding the internals of thread pools — the post-subtask torsion mechanism.

The figure above shows the internal processing mechanism of a thread pool when subtasks are submitted to the thread pool repeatedly and the task cannot be executed. This figure is especially important for understanding the internal mechanism of a thread pool and configuring thread pool parameters.

Reflection and annotations

Reflection and annotations are both officially available in the Java language. The former is used to dynamically read and write object instance (or static) properties and execute object (static) methods during program execution. The latter is used to add annotation information in code to specified fields such as classes, methods, method inputs, class member variables, and local variables.

With reflection and annotation techniques, combined with abstraction and encapsulation thinking of code, we can be very flexible in implementing many of the appeals of generalization calls, such as

  1. In the previous “hot Fix” section, the internal implementation of the ClassLoader-based scheme is almost entirely dex changed by reflection;
  2. In the previous section on “Network Modules”, Retorfit only needed to declare an interface and add annotations to use it. Its underlying layer also made use of reflective annotations and the dynamic proxy technology described below.
  3. Dagger and AndroidAnnotations make use of Java APT precompilation technology and compile-time annotations to generate injection code.
  4. If you know anything about Java server development, the mainstream development framework, SpringBoot, makes extensive use of injection and annotation technology internally;

What are the appropriate scenarios for reflection and annotation in development? Here are a few

Dependency injection scenario

The ordinary way

public class DataManager {

    private UserHelper userHelper = new UserHelper();
    private GoodsHelper goodsHelper = new GoodsHelper();
    private OrderHelper orderHelper = new OrderHelper();
}
Copy the code

Injection pattern

public class DataManager { @Inject private UserHelper userHelper; @Inject private GoodsHelper goodsHelper; @Inject private OrderHelper orderHelper; Injectmanager.inject (this); public DataManager() {injectManager.inject (this); }}Copy the code

The advantage of injection is that the user is shielded from the instantiation process of dependent objects, which facilitates the unified management of dependent objects.

Invoke private or hidden API scenarios

There is a class that contains private methods.

public class Manager { private void doSomething(String name) { // ... }}Copy the code

After we get the object instance of Manager, we want to call the private method doSomething. According to the general call method, if we do not change the method to public, there is no solution. But you can do it with reflection

try { Class< ? > managerType = manager.getClass(); Method doSomethingMethod = managerType.getMethod("doSomething", String.class); doSomethingMethod.setAccessible(true); DoSomethingMethod. Invoke (Manager, "< name parameter >"); } catch (Exception e) { e.printStackTrace(); }Copy the code

There are many such scenarios in development, and it can be said that mastering reflection and annotation techniques is not only a manifestation of the high-level Java language features, but also gives us a better understanding and perspective when abstracting and encapsulating some common capabilities.

4 Dynamic Proxy

Dynamic proxy is a technical solution that provides proxy capability for a specified interface during program execution.

The use of dynamic proxies is usually accompanied by reflection and annotations, but the role of dynamic proxies is relatively obscure compared to reflection and annotations. Let’s look at dynamic proxies in action with a specific scenario.

background

During project development, the server interface needs to be called, so the client encapsulates a common method for network requests.

Public class HttpUtil {/** * perform a network request ** @param relativePath url relativePath * @param params request parameters * @param callback function * Public static < T> void request(String relativePath, Map< String, Object> params, Callback< T> Callback) {// }}Copy the code

Because there are multiple pages in the business that need to query item list data, you need to encapsulate a GoodsApi interface.

Public interface GoodsApi {/** ** @param pageNum Page index * @param pageSize Amount of data per page * @param callback */ void getPage(int pageNum, int pageSize, Callback< Page< Goods>> callback); }Copy the code

A GoodsApiImpl implementation class is added for this interface.

public class GoodsApiImpl implements GoodsApi { @Override public void getPage(int pageNum, int pageSize, Callback< Page< Goods>> callback) { Map< String, Object> params = new HashMap<>(); params.put("pageNum", pageNum); params.put("pageSize", pageSize); HttpUtil.request("goods/page", params, callback); }}Copy the code

Based on the current encapsulation, the business can be invoked directly.

The problem

Services need to add the following interfaces for querying product details.

We need to add implementation logic to the implementation class.

Next, we need to add the Create and UPDATE interfaces, which we continue to implement.

Not only that, but then you add OrderApi, ContentApi, UserApi, and so on, and each class needs these lists. We find that every time the business needs to add a new interface, it has to write a call to the HttpUtil# Request method, and the call code is very mechanical.

Analysis of the

As mentioned earlier, the mechanized interface code is implemented. Next, we will try to abstract this mechanized code into a pseudo-code call template and analyze it.

The core nature of each method can be abstracted into the above “template” logic by looking at the phenomenon of its internal code implementation.

Is there a technology that allows us to write only the request protocol parameters necessary for a network request, without having to repeat trivial coding every time we do the following steps?

  1. Write a Map manually;
  2. Insert parameter key-value pairs into Map;
  3. Call HttpUtil#request to execute the network request.

Dynamic proxies can solve this problem.

encapsulation

Define path and parameter annotations separately.

@target ({elementtype.method}) @Retention(retentionPolicy.runtime) public @interface Path {/** * @return Interface Path */ String value(); } @target ({elementtype.parameter}) @Retention(retentionPolicy.runtime) public @interface Param {/** * @return PARAMETER name */ String value(); }Copy the code

Based on these two annotations, you can encapsulate the dynamic proxy implementation (the following code ignores parameter verification and boundary handling logic to demonstrate the core link).

@SuppressWarnings("unchecked") public static < T> T getApi(Class< T> apiType) { return (T) Proxy.newProxyInstance(apiType.getClassLoader(), new Class[]{apiType}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// Parse the interface path String path = method.getannotation (path.class).value(); Map< String, Object> params = new HashMap<>(); Parameter[] parameters = method.getParameters(); Callback for (int I = 0; i < method.getParameterCount() - 1; i++) { Parameter parameter = parameters[i]; Param param = parameter.getAnnotation(Param.class); params.put(param.value(), args[i]); } // Take the last argument as Callback<? > callback = (Callback< ? >) args[args.length - 1]; // Execute the network request httputil. request(path, params, callback); return null; }}); }Copy the code

The effect

At this point, annotations are needed to add the necessary information for the network request to the interface declaration.

public interface GoodsApi {

    @Path("goods/page")
    void getPage(@Param("pageNum") int pageNum, @Param("pageNum") int pageSize, Callback< Page< Goods>> callback);

    @Path("goods/detail")
    void getDetail(@Param("id") long id, Callback< Goods> callback);

    @Path("goods/create")
    void create(@Param("goods") Goods goods, Callback< Goods> callback);

    @Path("goods/update")
    void update(@Param("goods") Goods goods, Callback< Void> callback);
}
Copy the code

The external obtains the interface instance from the ApiProxy.

GoodsApi GoodsApi = new GoodsApiImpl(); // now GoodsApi GoodsApi = apiProxy.getapi (goodsapi.class);Copy the code

There are only minor tweaks to the way the upper layer is called; But the internal implementation has been greatly improved, directly omit all interface implementation logic, see the code comparison diagram below.

The core framework principles involved in the architecture design process have been described before, and the general design scheme in the architecture design will be discussed next.

General design scheme

The scenarios in which we design the architecture are often different, but some of the underlying design solutions are common, and this chapter summarizes those common design solutions.

Communication design

In A word, the essence of communication is to solve the problem of how to call between A and B. The following are analyzed one by one according to the abstract AB model dependency relationship.

Direct dependence

Relationship paradigm: A => B

This is the most common association, which relies directly on B in class A and requires only the most basic method calls and setup callbacks.

scenario

The relationship between page Activity (A) and Button (B).

Reference code

Indirect dependence

Relationship paradigm: A => C => B

Communication mode is the same as direct dependence, but need to add an intermediate layer for transparent transmission.

scenario

The page Activity (A) contains GoodsCardView (C), which contains the focus Button (B).

Reference code C communicates with B

public class GoodsCardView extends FrameLayout { private final Button button; private OnFollowListener followListener; public GoodsCardView(Context context, AttributeSet attrs) { super(context, attrs); / / a little... button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (followListener ! = null) {/ / C callback followListener onFollowClick (); }}}); } public void setFollowText(String followText) {// C call B button.settext (followText); } public void setOnFollowClickListener(OnFollowListener followListener) { this.followListener = followListener; }}Copy the code

A communicates with C

public class MainActivity extends Activity { private GoodsCardView goodsCard; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); / / a little... // A call C goodsCard. SetFollowText (" click on goods to follow "); GoodsCard. SetOnFollowClickListener (new OnFollowListener () {@ Override public void onFollowClick () {/ / C callback A}}); }}Copy the code

Combination relationship

Relationship paradigm: A <= C => B

Communication is similar to indirect dependencies, but the order in which one party is called needs to be reversed.

scenario

The page Activity (C) contains the list RecyclerView (A) and the top icon ImageView (B). When the top is clicked, the list needs to scroll to the top.

Reference code

public class MainActivity extends Activity { private RecyclerView recyclerView; private ImageView topIcon; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); / / a little... TopIcon. SetOnClickListener (new View. An OnClickListener () {@ Override public void onClick (View v) {/ / callback C B onTopIconClick(); }}); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, Int newState) {// A callback C if (newState == recyclerView.scroll_state_IDLE) {LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); onFirstItemVisibleChanged(layoutManager.findFirstVisibleItemPosition() == 0); }}}); } private void onFirstItemVisibleChanged (Boolean visible) {/ / C call topIcon setVisibility (visible? View.GONE : View.VISIBLE); } private void onTopIconClick () {/ / C calling A recyclerView scrollToPosition (0); // C calls B topicon.setvisibility (view.gone); }}Copy the code

Deep dependency/composition relationships

Relationship paradigm: A => C => ··· => B, A <= C => ··· => B

When the dependencies are separated by multiple layers, the code becomes redundant using plain old calls and setup callbacks, with the middle layer doing most of the pass-through logic. In this case, the alternative is to distribute events through the event manager.

scenario

After the page is componentized, component A needs to notify component B of an event.

Reference code

Event manager

public class EventManager extends Observable< EventManager.OnEventListener> { public interface OnEventListener { void onEvent(String action, Object... args); } public void dispatch(String action, Object... args) { synchronized (mObservers) { for (OnEventListener observer : mObservers) { observer.onEvent(action, args); }}}}Copy the code

A call X

public class AComponent { public static final String ACTION_SOMETHING = "a_do_something"; private final EventManager eventManager; public AComponent(EventManager eventManager) { this.eventManager = eventManager; } public void sendMessage() {// A call X eventManager.dispatch(ACTION_SOMETHING); }}Copy the code

X distributed B

public class BComponent { private final EventManager eventManager; public BComponent(EventManager eventManager) { this.eventManager = eventManager; eventManager.registerObserver(new EventManager.OnEventListener() { @Override public void onEvent(String action, Object... Args) {if (acomponent.action_something-equals (action)) {// X distribution B}}}); }}Copy the code

There is no relationship

Relationship paradigm: A, B

This refers to the irrelevance of the narrow sense, because in the broad sense if there is no relation between the two, they can never communicate.

The only difference is that the EventManager object instance is retrieved from a globally unique instance object, such as a singleton, rather than directly from the current context.

Extensible callback function design

background

When we package an SDK, we need to add callback functions to the outside, as follows.

The callback function

public interface Callback {
    void onCall1();
}
Copy the code

The SDK core classes

public class SDKManager { private Callback callback; public void setCallback(Callback callback) { this.callback = callback; } private void doSomething1() {private void doSomething1() { if (callback ! = null) { callback.onCall1(); }}}Copy the code

External customer call

SDKManager sdkManager = new SDKManager();
sdkManager.setCallback(new Callback() {
    @Override
    public void onCall1() {
    }
});
Copy the code

The problem

This is a very common way to set up callbacks, which is fine if you’re just doing business development, but can be flawed if you’re making SDKS for external customers.

This way, if the SDK is already available for use by external customers, some callbacks need to be added.

public interface Callback {
    void onCall1();

    void onCall2();
}
Copy the code

If the callback is added in this way, the external upgrade will not be able to be unaware of the upgrade, and the following code will report an error requiring additional implementation.

sdkManager.setCallback(new Callback() {
    @Override
    public void onCall1() {
    }
});
Copy the code

Another option is to create a new interface without external awareness.

public interface Callback2 {
    void onCall2();
}
Copy the code

Then add support for this method in the SDK.

Public class SDKManager {// omitted.. private Callback2 callback2; public void setCallback2(Callback2 callback2) { this.callback2 = callback2; } private void doSomething2() {private void doSomething2() { if (callback2 ! = null) { callback2.onCall2(); }}}Copy the code

Accordingly, the callback function Settings need to be added for external calls.

sdkManager.setCallback2(new Callback2() {
    @Override
    public void onCall2() {
    }
});
Copy the code

This solution does solve the problem of not being able to silently upgrade the SDK externally, but it creates another problem as the code for setting callback functions externally increases with each interface upgrade.

External optimization

For this problem, we can set an empty base class for the callback function.

public interface Callback {
}
Copy the code

The SDK callback functions all inherit it.

public interface Callback1 extends Callback {
    void onCall1();

}

public interface Callback2 extends Callback {
    void onCall2();
}
Copy the code

The SDK internally sets the callback function to receive the base class callback function, which is judged according to the type.

public class SDKManager { private Callback callback; public void setCallback(Callback callback) { this.callback = callback; } private void doSomething1() {private void doSomething1() { if ((callback instanceof Callback1)) { ((Callback1) callback).onCall1(); }} private void doSomething2() { if ((callback instanceof Callback2)) { ((Callback2) callback).onCall2(); }}}Copy the code

Provide an empty implementation class for the callback function.

public class SimpleCallback implements Callback1, Callback2 {

    @Override
    public void onCall1() {

    }

    @Override
    public void onCall2() {

    }
}
Copy the code

At this point, external callback functions can be set in a variety of ways, such as single interface, composite interface, and empty implementation class.

SetCallback (new Callback1() {@override public void onCall1() {//.. }}); // Interface CombineCallback extends Callback1, Callback2 { } sdkManager.setCallback(new CombineCallback() { @Override public void onCall1() { // .. } @Override public void onCall2() { // ... }}); SetCallback (new SimpleCallback() {@override public void onCall1() {//... } @Override public void onCall2() { //.. }});Copy the code

Now if the SDK extends the callback, all you need to do is add a new callback interface.

public interface Callback3 extends Callback {
    void onCall3();
}
Copy the code

Add new callback logic internally.

Private void doSomething3() {private void doSomething3() { if ((callback instanceof Callback3)) { ((Callback3) callback).onCall3(); }}Copy the code

Upgrading the SDK at this time has no impact on the call logic of external customers, which can achieve good forward compatibility.

Internal optimization

After the previous optimization, the external has not been aware of SDK changes; But some of the internal code is still relatively redundant, as follows.

Private void doSomething1() {private void doSomething1() { if ((callback instanceof Callback1)) { ((Callback1) callback).onCall1(); }}Copy the code

The SDK has trouble adding this judgment every time it calls an external callback, so we’ll wrap this judgment logic separately.

public class CallbackProxy implements Callback1, Callback2, Callback3 { private Callback callback; public void setCallback(Callback callback) { this.callback = callback; } @Override public void onCall1() { if (callback instanceof Callback1) { ((Callback1) callback).onCall1(); } } @Override public void onCall2() { if (callback instanceof Callback2) { ((Callback2) callback).onCall2(); } } @Override public void onCall3() { if (callback instanceof Callback3) { ((Callback3) callback).onCall3(); }}}Copy the code

Then the corresponding methods can be called directly from within the SDK without the need for redundant judgment logic.

public class SDKManager { private final CallbackProxy callbackProxy = new CallbackProxy(); public void setCallback(Callback callback) { callbackProxy.setCallback(callback); } private void doSomething1() {private void doSomething1() { callbackProxy.onCall1(); } private void doSomething2() {private void doSomething2() { callbackProxy.onCall2(); } private void doSomething3() {private void doSomething3() { callbackProxy.onCall3(); }}Copy the code

Six summarize

To do a good architectural design of the project, we need to consider many aspects such as technology selection, business status, team members and future planning, and with the development of business, we also need to carry out continuous reconstruction of engineering and code in different stages of the project.

It could be an e-commerce project, it could be a social project, it could be a financial project. The development technology has also been iterating rapidly, perhaps using pure Native development mode, Flutter and RN development mode, or hybrid development mode. However, the underlying principles and design ideas in architectural design of these projects are always the same, and these things are the core abilities that we really need to learn and master.

The original link

This article is the original content of Aliyun and shall not be reproduced without permission.