- Patchwork Plaid — A Modularization Plaid story
- Ben Weiss
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: snpmyn
Illustration by Virginia Poltrack
Why and how do we modularize, and what happens when we do?
This article delves into the modular part of Restitching Plaid.
In this article, I will give a comprehensive overview of how to turn a holistic, large, generic application into a modular application bundle. Here is what we have achieved:
- Overall volume reduced by more than 60%
- Greatly improves code robustness
- Support dynamic delivery and packaging code on demand
All the things we do, we don’t affect the user experience.
At the beginning of the Plaid impression
Navigation Plaid
Plaid is an app with a pleasant UI. Its home screen displays news from multiple sources. These stories are clicked to display details, creating a split screen effect. The application has both a search function and an about module. Based on these existing features, we chose some to modularize.
Designer News and Dribbble became dynamic modules of their own. About and search features are also modularized as dynamic features.
Dynamic functionality allows you to provide code without directly including it in the underlying application. Because of this, on-demand downloads can be achieved through successive steps.
Next, the Plaid structure
Like many Android apps, Plaid was originally built as a single module for a common app. Its installation size is just under 7MB. However, much of the data is not used at run time.
The code structure
From a code point of view, Plaid is package-based and therefore has well-defined boundaries. But with the advent of large code bases, these boundaries can be crossed and dependencies can creep in. Modularity requires us to more strictly define these boundaries to improve and improve code separation.
The local library
The largest unused chunk comes from Bypass, a library we use to render markup in Plaid. It includes local libraries for multi-core CPU architectures, which end up around 4MB in common use. The application bundle allows you to deliver only the libraries required for the device architecture, reducing the required volume by about 1MB.
Extractable resource
Many applications use rasterized assets. They are density-dependent and usually make up a large portion of the application file volume. Apps benefit from configuration apps, where each display density is placed in a separate app, allowing custom installation of devices and greatly reducing downloads and volume.
Plaid relies heavily on vector drawables when displaying graphic resources. Since these are density-independent and many files have been saved, the data savings here are not significant to us.
Go all out stick up
In modularity, we initially replaced./gradlew Assemble with./gradlew bundle. Gradle will now generate an Android App Bundle (AAB) instead of building the App. An Android app bundle requires the Gradle plugin for dynamic functionality, which we’ll cover later.
Android App Bundle
Instead of a single app, the Android app bundle generates many small configuration apps. These applications can be customized to the user’s device to save data during delivery and on disk. Application bundles are also a prerequisite for dynamic functional modules.
After uploading the app bundle to Google Play, you can generate the configuration app. As the app bundle becomes an open specification, other app stores can implement this delivery mechanism as well. To build and sign applications for Google Play, applications must be registered with applications signed by Google Play.
advantage
Where does this encapsulation change leave us?
Plaid now reduces device volume by more than 60%, equating to about 4MB of data.
This means that each user has more space for other applications. Download times are also improved by reducing file sizes.
This dramatic improvement can be achieved without changing a single line of code.
Implement modularity
The approach we have chosen to achieve modularity:
- Move all code and resource blocks into the core module.
- Identify modularizable functionality.
- Move related code and resources into functional modules.
Green: | dark grey dynamic function: application module | light gray: library
The chart above shows the current state of Plaid modularity:
The bypass module
And the outsideShare the rely on
Contained in the core moduleapplication
Depends on theThe core module
- Dynamic functional modules depend on
application
Application module
The application module is basically an existing application that is used to create the application bundle and show us Plaid. Much of the code used to run Plaid does not have to be included in this module, but can be moved anywhere else.
The PlaidThe core module
To begin the refactoring, we moved all the code and resources to a com.Android. library module. After further refactoring, our core module only contains the code and resources needed to share between functional modules. This will allow for clearer separation of dependencies.
External libraries
A third-party dependency library is included in the core module via the bypass module. In addition, all other Gradle dependencies are moved from the application to the core module through the Gradle API dependency keyword.
Gradle dependency declaration: API vs Implementation_
Dependencies can be shared throughout the program through APIS instead of implementations. This reduces the size of each functional module, since the dependencies in the core module in this example need only be contained in a single module. It also makes our dependencies easier to maintain because they are declared in a single file rather than propagated across multiple build.gradle files.
Dynamic functional module
I mentioned above that we identified modules that could be refactored into com.android.dynamic-feature. They are:
:about
:designernews
:dribbble
:search
Copy the code
Dynamic Function Description
A dynamic function module is essentially a Gradle module that can be downloaded independently from the base application module. It contains code, resources, and dependencies, just like any other Gradle module. While we haven’t used dynamic delivery in Plaid yet, we hope to reduce the initial download size in the future.
Great feature reform
After moving everything to the core module, we marked the About page as a feature with minimal dependencies, so we refactored it into a new about module. This includes Activties, Views, code only for that feature. Also, we move all resources such as drawables, strings, and animations to a new module.
We do this repeatedly for each functional module, sometimes splitting dependencies.
Finally, the core module contains most of the shared code and major functionality. Since the main functionality is only shown in the application module, we move the relevant code and resources back to the application.
Functional structure analysis
The compiled code can be structurally optimized in packages. It is highly recommended that you move your code into a functional package before splitting it into different compilation units. Fortunately, we don’t have to refactor, because Plaid already corresponds well to the functionality.
Functional and core modules and their respective architectural levels
As I mentioned, many of Plaid’s features are provided through news feeds. They consist of levels of remote and local data resources, domains, and UIs.
Data sources are displayed not only in the main feature prompt, but also in the details page associated with the corresponding feature module itself. The domain level is unique in a single package. It must be split into two parts: one that is shared in the application and one that is used only in a functional module.
The reusable part is stored in the core module, and everything else is in the respective functional module. The data layer and most of the domain layer are shared with at least one other module and are also stored in the core module.
Package change
We also optimized the package names to reflect the new modular architecture. Only the code associated with :dribbble is moved from IO. Plaidapp to IO. Plaidapp. Dribbble. The same applies to each function through the respective new module name.
This means that many guides have to change.
Modularizing resources creates some problems because we have to use qualified names to disambiguate the generated R class. For example, importing a local layout view causes a call to R.I.D.library_image, whereas using a drawable in the same file in the core module causes
io.plaidapp.core.R.drawable.avatar_placeholder
Copy the code
We mitigated this by using the Kotlin import alias feature, which allows us to import core R files as follows:
import io.plaidapp.core.R as coreR
Copy the code
Allows the call site to be shortened to
coreR.drawable.avatar_placeholder
Copy the code
This makes reading code much cleaner and more flexible than having to look at the full package name every time.
Resource Movement preparation
Resources, unlike code, do not have a package structure. This makes it extremely difficult to classify them by function. But by following a few conventions in your code, it’s not impossible.
With Plaid, files are prefixed wherever they are used. For example, the resource is only used for dribbble with the prefix dribbble_.
In the future, some files containing multiple module resources, such as styles.xml, will be structured on a module basis, with each attribute also being a prefix.
For example: in a monolithic application, strings.xml contains most of the strings used as a whole. In a modular application, each function module contains only its own string resources. Grouping strings before modularization makes it easier to split files.
Following conventions like this makes it faster and easier to move resources to the right place. This also helps avoid compilation errors and runtime timing errors.
Process challenges
Good communication with the team is critical to making an important refactoring task manageable like this. Passing on plan changes and gradually implementing them will help us merge conflicts and minimize blocking.
Kindly remind
The dependency diagrams earlier in this article show that dynamic function modules know application modules. Application modules, on the other hand, cannot easily access code from dynamic function modules. But they contain code that must be executed at some point in time.
Applications that access code without knowing enough about the function module will not be able to start an activity with its class name in an Intent(ACTION_VIEW, ActivityName::class.java) method. There are several ways to start an activity. We decide to explicitly specify the component name.
To achieve this, we developed the AddressableActivity interface in the core module.
/**
* An [android.app.Activity] that can be addressed by an intent.
*/
interface AddressableActivity {
/**
* The activity class name.
*/
val className: String
}
Copy the code
Using this approach, we create a function to unify the activity launch intent creation:
/**
* Create an Intent with [Intent.ACTION_VIEW] to an [AddressableActivity].
*/
fun intentTo(addressableActivity: AddressableActivity): Intent {
return Intent(Intent.ACTION_VIEW).setClassName(
PACKAGE_NAME,
addressableActivity.className)
}
Copy the code
The simplest way to implement AddressableActivity is to simply display the class name as a string. With Plaid, each activity is initiated through this mechanism. Some contain intent add-ons must be passed into the activity by applying the various components.
Check out our implementation process with the following files:
- AddressableActivity.kt: Helpers to start activities in a modularized world._github.com
Styleing problem
Instead of a single manifest file for the entire application, the manifest file is now separated for each dynamic function module. These manifest files mainly contain information related to the instantiation of their components, as well as information related to their delivery type reflected through DIST: tags. This means that both activities and services must be declared in functional modules that contain relevant code corresponding to components.
We ran into a problem with modularizing styles; We only extract the styles used by one function into modules related to that function, but they are often built implicitly on top of the core module.
PLaid style structure part
These styles are provided to component activities as themes through module manifest files.
Once we’ve moved them around, we run into compile-time issues like this:
* What went wrong:
Execution failed forTask ': app: processDebugResources.' > Android resource linking failed ~/plaid/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:177: AAPT: error: resource style/Plaid.Translucent.About (aka io.plaidapp:style/Plaid.Translucent.About) not found. error: failed processing manifest.Copy the code
The manifest file merge view merges manifest files from all function modules into application modules. Failure to merge causes the function module style file to be unavailable to the application module at the specified time.
To do this, we create an empty declaration in the core module style file for each style as follows:
<! — Placeholders. Implementations inFeature modules. - > < style name = "Plaid, Translucent, About" / > < style name = "Plaid, Translucent. DesignerNewsStory" / > < style Name = "Plaid. Translucent. DesignerNewsLogin" / > < style name = "Plaid, Translucent. PostDesignerNewsStory" / > < style Name = "Plaid. Translucent. Dribbble" / > < style name = "Plaid. Translucent. Dribbble. Shot" / > < style Name = "Plaid. Translucent. Search" / >Copy the code
The manifest file merge now captures styles during the merge process, although the actual implementation of styles is introduced through feature module styles.
Another way to avoid this problem is to keep the style file declared in the core module. But this only works if all resource references are also in the core module. That’s why we decided to go through this.
Dynamic power instrument test
Through modularization, we found that test tools cannot currently reside in dynamic functional modules, but must be included in application modules. We’ll cover this in more detail in an upcoming blog post about testing.
What happens next?
Dynamic code loading
We use dynamic delivery through the Application bundle, but do not download these files through the Play Core Library after the initial installation. For example, this will allow us to mark a news feed (product search) that is not enabled by default as installed only after the user allows the news feed.
Add more news sources
Through the modularization process, we keep considering the possibility of further increasing the news feed. Separating clean module work and enabling on-demand delivery possibilities makes this even more important.
Module refinement
We have made great progress in modularizing Plaid. But there is still work to be done. Product search is a new news feed that we are not currently putting into dynamic feature modules. At the same time, some functions in the extracted function modules can be removed from the core module, and then directly integrated into their respective functions.
Why did I decide to modularize Plaid?
Through this process, Plaid is now a highly modular application. None of this changes the user experience. We do get some benefit from these efforts in our daily development.
Install size
PLaid now reduces the volume of user devices by an average of 60%. This makes installation faster and saves valuable network overhead.
Compile time
A debug build with no cache now takes 32 seconds instead of 48 seconds. The number of missions increased from 50 to 250.
This time savings is mainly due to increased parallel builds and the avoidance of compilation due to modularity.
In the future, single module changes will eliminate the need to compile all individual modules and make continuous compilation faster.
- As a reference, these are some of the commits THAT I built before and After Timing.
maintainability
We separate the various dependencies in the process, which makes the code more concise. At the same time, the side effects are getting smaller and smaller. Each of our functional modules can work independently with less and less interaction. But the main benefit is that we have fewer conflicts to resolve.
conclusion
We reduced the application size by over 60%, improved the code structure and modularized PLaid into dynamic functional modules and increased the on-demand delivery potential.
Throughout the process, we always kept the application in a state that was ready to be delivered to the user. You can switch your application directly today to send an application bundle to save installation volume. Modularity takes some time, but given the benefits seen above, it’s worth the effort, especially considering dynamic delivery.
To view thePlaid ‘s source codeLearn about all of our changes and happy modular processes!
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.