Author: Wang Peng, Sun Yongsheng What is MAD?https://developer.android.com/series/mad-skills The full name of MAD is Modern Android Development, which is a collection of technology stacks and tool chains, covering every aspect from programming languages to development frameworks. Android SDK has not changed much in the years since its birth in 2008, and the development method is relatively fixed. Since 2013, the technology update has gradually accelerated, especially after 2017, with the emergence of new technologies such as Kotlin and Jetpack, the Android development method has changed greatly. Jetpack Compose launched last year has pushed this change to a new stage. Google named the development method under these new technologies MAD to distinguish it from the old inefficient development method. MAD can guide developers to develop excellent mobile applications more efficiently. Its advantages are mainly reflected in the following points:
MAD helps applications go globalRecently, we have completed the launch of an AI special effects application on Google Play. This application can process the user's own avatar pictures into various artistic effects through algorithms. The application has been well received as soon as it was launched. This is all thanks to our comprehensive application of MAD technology in the project. We completed the entire development in the shortest time and created an excellent user experience. Under the guidance of MAD, the code architecture of the project is more reasonable and maintainable. The following figure shows the overall application of MAD in the project: Next, this article will share some of our experiences and cases in the practice of MAD. 1. KotlinKotlin is the preferred development language recognized by Android. In our project, all codes are developed using Kotlin. Kotlin's syntax is very concise, and the code size of the same function in Java can be reduced by 25%. In addition, Kotlin also has many excellent features that Java does not have: 1.1 SafetyKotlin has many excellent designs in terms of safety, such as null safety and data immutability. Null Safety Kotlin's null safety feature allows many runtime NPEs to be exposed and discovered in advance during the compilation period, effectively reducing the occurrence of online crashes. We attach importance to the judgment and processing of Nullable types in the code. When defining data structures, we strive to avoid nullable types to minimize the cost of null judgment; interface ISelectedStateController < DATA > { In the Java era, we can only use naming conventions such as getStateOrNull to remind people that return values can be nullable. Kotlin uses ? to help us better understand the risks of Nullable. We can also use the Elvis operator ?: to convert Nullable to NonNull for subsequent use. Kotlin's !! makes it easier for us to discover the potential risks of NPE and use static checks to give warnings. Kotlin's default parameter value feature can also be used to prevent NPE. For structure definitions like the one below, you don't have to worry about Null appearing in scenarios such as deserialization. data class BannerResponse ( After we fully embraced Kotlin, the NPE crash rate was only 0.3‰, while the NPE of Java projects usually exceeded 1‰. Immutable The security of Kotlin is also reflected in the fact that data cannot be modified at will. We use data classes extensively in the code and require attributes to be defined using val rather than var, which is conducive to the promotion of the one-way data flow paradigm in the project and the separation of data reading and writing at the architectural level. data class HomeUiState ( As above, we use data class to define UiState for use in ViewModel. The val declaration property ensures the immutability of State. Using a sealed class to define Result is conducive to enumerating various request results and simplifying the logic. private val _uiState = MutableStateFlow ( HomeUiState ()) When the State needs to be updated, the copy method of the data class can be used to quickly copy and construct a new instance. Immutable is also reflected in the type of collection class. In our project, we advocate not to use Mutable types such as MutableList unless necessary, which can reduce the occurrence of multi-threading problems such as ConcurrentModificationException, and more importantly, avoid data consistency problems caused by Item tampering: viewModel . uiState . collect { For example, in the above example, after the UI side receives the UiState update notification, it submits DiffUtil to refresh the list. The basis for the normal operation of DiffUtil is that mList and newList can always maintain the Immutable type. 1.2 FunctionalFunctions are first-class citizens in Kotlin and can be used as parameter or return value types to form higher-order functions. Higher-order functions can provide easier-to-use APIs in scenarios such as collection operators. Collection operations val bannerImageList : List < BannerImageItem > = In the above code, we sort and filter BannerModelList in turn and convert it into a list of BannerImageItem type. The use of set operators makes the code smooth. Scope functions Scope functions are a series of inline higher-order functions. They can be used as glue for code, reducing the appearance of redundant code such as temporary variables. GalleryFragment () .apply { When we create and start a Fragment, we can complete various initialization tasks based on the scope function, as shown in the example above. This example also reminds us that excessive use of these scope functions (or collection operators) will also affect the readability and debuggability of the code. Only the "appropriate" use of functional programming can truly bring out the advantages of Kotlin. 1.3 CorroutineKotlin coroutines free developers from callback hell. At the same time, the characteristics of structured concurrency also help to better manage subtasks. Various native libraries and third-party libraries of Android are beginning to turn to Kotlin coroutines when handling asynchronous tasks. Suspend function In the project, we advocate the use of suspend functions to encapsulate asynchronous logic. It goes without saying that Room or Retorfit use suspend function style APIs in the data layer. Some presentation layer logic can also be implemented based on suspend functions: suspend fun doShare ( In the above example, doShare uses a suspend function to handle the logic of sharing photos: a sharing panel pops up for the user to select a sharing channel, and the sharing result is returned to the caller. The caller starts sharing and synchronously obtains the result of sharing success or failure. The coding style is more intuitive. Flow In the project, Flow is used instead of RxJava to process streaming data. While reducing the package size, CoroutineScope can effectively avoid data leakage: fun CoroutineScope . getBannerList (): Flow < List < BannerItemModel >> = The above example is used to obtain BannerList from multiple data sources. We have added a disk cache strategy to request local database data first, then request remote data. The use of Flow can well meet this type of scenario involving multiple data source requests. On the other hand, on the calling side, as long as a suitable CoroutineScope is provided, there is no need to worry about leaks. 1.4 KTXSome Android libraries originally implemented in Java provide extended APIs for Kotlin through KTX, making them easier to use in Kotlin projects. Our project uses Jetpack Architecture Components to build the App infrastructure. KTX helps us greatly reduce the API usage cost in Kotlin projects. Here are some of the most common KTX examples: fragment-ktx fragment-ktx provides some Kotlin extension methods for Fragment, such as the creation of ViewModel: class HomeFragment : Fragment () { Compared with Java code, creating ViewMoel in Fragment becomes extremely simple. The reason behind this is that it actually makes use of various Kotlin features, which is very clever. inline fun < reified VM : ViewModel > Fragment . viewModels ( viewModels is an inline extension method of Fragment. It uses the reified keyword to obtain the generic type at runtime to create a specific ViewModel instance: fun < VM : ViewModel > Fragment . createViewModelLazy ( createViewModelLazy returns a Lazy instance, so we can create a ViewModel using the by keyword. Here, Kotlin's proxy feature is used to implement lazy instance creation. viewmodle-ktx viewModel-ktx provides extension methods for ViewModel, such as viewModelScope, which can terminate expired asynchronous tasks in time with the destruction of ViewModel, allowing ViewModel to be used more safely as a bridge between the data layer and the presentation layer. viewModelScope . launch { The implementation principle is also very simple: val ViewModel . viewModelScope : CoroutineScope viewModelScope is essentially an extended property of ViewModle. When creating CloseableCoroutineScope through custom get, it is recorded in the position of JOB_KEY. internal class CloseableCoroutineScope ( context : CoroutineContext ) : Closeable , CoroutineScope { CloseableCoroutineScope is actually a Closeable. When onClear of ViewModel is called, it searches for JOB_KEY and close is called to cancel SupervisorJob and terminate all sub-coroutines. KTX makes use of various features and syntax sugar of Kotlin. You will see more usage of KTX in the Jetpack chapter later. 2. Android JetpackAndroid provides developers with basic capabilities on top of AOSP through Jetpack, covering all levels from UI to Data, reducing the need for developers to reinvent the wheel. Recently, the architecture specifications of Jetpack components have been fully upgraded to help us better implement the design goal of separation of concerns during the development process. 2.1 ArchitectureAndroid advocates the architectural design of separating the presentation layer from the data layer, and uses unidirectional data flow to complete data communication. Jetpack supports the implementation of UDF in Android through a series of Lifecycle-aware components. The main features and advantages of UDF are as follows:
All business scenarios involving UI in the project are built based on UDF. Take HomePage as an example, it includes two sets of data display, BannerList and ContentList, and all data are centrally managed in UiState. class HomeViewModel () : ViewModel () { As shown in the above code, HomeViewModel gets data from Repo and updates UiState, and View subscribes to this state and refreshes the UI. The CoroutineScope provided by viewModelScope.launch can end the running coroutine with the onClear of ViewModel to avoid leakage. In the data layer, we use the Repository Pattern to encapsulate the specific implementation of local data sources and remote data sources: class Repository { Take getBannerList as an example. First, local data is requested from the database to speed up the display. Then, the remote data source is requested to update the data and persist it for the next request. The logic of the UI layer is very simple, just subscribe to the data of ViewModel and refresh the UI. We use Flow instead of LiveData to encapsulate UiState, lifecycleScope makes Flow a Lifecycle-aware component; repeatOnLifecycle makes Flow automatically stop the emission of data flow when the Fragment switches between the foreground and the background like LiveData, saving resource overhead. 2.2 NavigationAs a practitioner of the "single activity architecture", we chose to use Jetpack Navigation as the navigation component of the App. The Navigation component implements the navigation design principles, provides a consistent user experience for switching across applications or between pages within an application, and provides various advantages, including:
Navigation provides two configuration methods: XML and Kotlin DSL. We take advantage of Kotlin in our project, create a navigation graph based on a type-safe DSL, and use function extraction to specify transition animations for the page: fun NavHostFragment . initGraph () = run { In the Activity, call initGraph() to initialize the navigation graph for the Root Fragment: @AndroidEntryPoint In Fragment, use findNavController() provided by navigation-fragment-ktx to correctly redirect pages based on the current Destination at any time: @AndroidEntryPoint In addition, we can declare global page navigation, which is very useful in guiding users to log in, register, or go to the feedback page: fun NavHostFragment . initGraph () = run { 2.3 HiltDependency Injection is a commonly used technology in multi-module projects. As a way to implement the inversion of control design principle, dependency injection is conducive to the decoupling of the production side and the consumption side of the instance, practicing the design principle of separation of concerns, and is also more conducive to the writing of unit tests. Hilt is built on the basis of Dagger. It inherits the advantages of Dagger such as compile-time checking, high performance at runtime, and scalability, while providing a more friendly API, which greatly reduces the cost of using Dagger. Android Studio also has built-in support for Dagger/Hilt, which will be introduced later. Hilt is widely used in the project to complete dependency injection, which further improves the efficiency of code writing. We use @Singleton to provide a singleton implementation of Repository. When Repository needs Context to create SharedPreferences or DataStore, use @ApplicationContext annotation to pass in the application-level Context. Only @Inject is needed to inject objects where needed: @AndroidEntryPoint For some third-party library classes that cannot be annotated in the constructor, we can use @Provides to tell Hilt how to create related instances. For example, provide the implementation of creating the Retrofit API, eliminating the need to manually create it each time. @Module Thanks to Hilt's support for other Jetpack components, you can also use Hilt for dependency injection in ViewModel or WorkManager. @HiltViewModel 2.4 WorkManagerWorkManager is a Jetpack library designed for persistent work, which refers to tasks that can be performed continuously across application or system restarts, such as synchronizing application data with the server, or uploading logs. WorkManager automatically selects FirebaseJobDispatcher, GcmNetworkManager, or JobScheduler to perform scheduling tasks based on the strategy, and provides a simple and consistent API for easy use. WorkManager uses the Jetpack StartUp library for initialization by default. Developers only need to focus on defining and implementing Workers without any additional work. WorkManager is backward compatible to Android 6.0 and covers most models on the market. It can effectively replace Service to complete background tasks that require long-term execution. In order to reduce the time and traffic consumption required for users to upload pictures when generating avatars, the product will compress the pictures before uploading. However, the temporary files in the compression process will increase the storage space occupied by the App. Therefore, we use WorkManager to schedule the work of clearing the compressed picture cache and submit the task to WorkManager after the App is started: val deleteImageCacheRequest = OneTimeWorkRequestBuilder < DeleteImageCacheWorker > (). build () Another scenario is when a user downloads an image. Downloading requires a network connection, and this task has a higher priority, so you can use the work constraints and expedited work (WorkManager 2.7 and above) provided by WorkManager. In addition, you can monitor the result information of the task to prompt the user: val downloadImageRequest = OneTimeWorkRequestBuilder < DownLoadImageWorker > () 2.5 StartUpWhen an application starts, a lot of initialization work needs to be done, such as SDK initialization, basic module configuration, etc. Before StartUp appeared, we used ContentProvider to complete "non-invasive" initialization to avoid the appearance of init(Context) and other codes in Application. However, the creation cost of ContentProvider is high, and the creation of multiple ContentProviders at the same time will slow down the startup speed of the application and the initialization sequence is uncontrollable. StartUp only uses one ContentProvider to complete the initialization of multiple components, which solves the various problems of the above ContentProvider. In addition, StartUp can also avoid unnecessary dependencies of the app module on other modules. For example, we need to rely on a separate Module for the local test channel in the project. This Module relies on Context to complete initialization, but we don't want it to be included in the release package. At this time, just add the Gradle dependency as shown below, and the app does not need to rely on the local_test module at the code level. if ( BuildContext . isLocalTest ()) { The StartUp library is very simple to use. You only need to define an Initializer. You can also configure the initialization dependencies to ensure that the core components can be initialized first: class ServerInitializer : Initializer < ServerManager > { In the above example, the initialization of the Account module will wait until the initialization of the Server module is complete before continuing. 2.6 RoomApps with local-first architecture can provide a good user experience. When the device cannot access the network, users can still browse the corresponding content offline. Android provides SQLite as an API to access the database, but the SQLite API is relatively low-level and requires manual verification of the correctness of the SQL statement. In addition, a large amount of template code needs to be written to complete the conversion between PO and DO. Jetpack Room provides an abstraction layer based on SQLite to help developers access the database more smoothly. Room mainly consists of three components: Database is the database holder and the main access point to the underlying database; Entity represents the table in the database; DAO contains methods for accessing the database. The three components are declared through annotations: @ Entity ( tableName = "tb_banner" ) It should be noted that the cost of creating a database is relatively high, so the database must be a singleton in a single-process App: @Module When the data in the database is updated, we hope that the UI will be automatically refreshed. Thanks to Room's good support for Coroutine and RxJava, we only need to introduce the room-ktx library or room-rxjava2/3 library. The methods in DAO can also directly return Flow or Observable, or directly use suspend functions: @Dao At this time, we only need to subscribe to Flow at the UI layer, so that the UI can be updated when the database content is updated: @HiltViewModel Furthermore, in the UI layer, we only subscribe to the data in the database, and use WorkManager to initiate a network request in the background, and then write the latest data into the database after obtaining the data. Since the database access speed is much faster than the network, the page can be presented to the user faster. 3. Android StudioAndroid Studio has been actively updated since its birth. The latest version has been updated to Bumblebee | 2021.1.1.21. Since 4.3 Canary 1, Android Studio has adjusted its naming style to better align with the IntelliJ platform version. In addition to the regularly released stable version, developers can also experience more new features in advance through the RC and Preview versions. With the continuous update of the version, the experience of writing and debugging code has been continuously optimized, and more and more new features have been integrated. Needless to say, the existing functions such as Layout Instpector and Device Explorer, the following new features also provide great convenience for our development and debugging. 3.1 Database InspectorWe use Room for data persistence. Database Inspector can view the database files generated by the Jetpack Room framework in real time, and also supports real-time editing and deployment to devices. Compared with the previous SQLite command or additional export and DB tool method, it is more efficient and intuitive. 3.2 Realtime ProfilersAndroid Studio's Realtime Profilers tool can help us monitor and discover problems in the following four aspects. Sometimes, in the absence of project code, Memory Profilers can also be used to view its internal instance and variable details.
3.3 APK AnalyzerThe download of APK will consume network traffic, and the installation will also take up storage space. Its size will affect the installation and retention of App, so it is particularly necessary to analyze and optimize its size. With the help of AS's APK Analyzer, you can complete the following tasks:
3.4 DI NavigationDependency injection helps decouple modules and implements the design principle of separation of concerns. We use Dagger/Hilt to hide the relevant specific implementation through compile-time code generation, which reduces the cost of building dependency graphs while increasing the cost of debugging code for developers: it becomes difficult to find the source of the injected instance. Now Android Studio helps developers solve this pain point. Since 4.1, we can jump in Dagger-based code (such as Components, Subcomponents, Modules, etc.) to find dependencies. You can see the following icon next to the Dagger or Hilt related code: Click the icon on the left to jump to the provision of the instance object, and click the icon on the right to jump to the usage of the object. When there are multiple uses, a candidate list will be given for selection. Starting from Android 4.2, dependency queries on @EnterPoint have been added. For components such as ContentProvider that cannot be automatically injected, the scope of use of dependency injection can also be expanded through Hilt. 4. App BundleAndroid App Bundle is a packaged format for dynamic distribution launched by Google. When an application is uploaded to Google Play (or other AAB-enabled application market), it can implement dynamic distribution of functions or resources as needed. The Split APKs mechanism is the basis for AAB to implement dynamic distribution. After uploading GPs, AAB is split into a base APK and multiple Split APKs. Only Base APKs are issued for the first download, and then dynamically issue Split APKs according to the usage scenario. Split can enable Configuration APKs or a Dynamic Features APKs:
Google attaches importance to the promotion of AAB format. Since August 21, it has stipulated that new apps must use AAB format to be listed on Google Play. As a product to be launched overseas, we naturally chose the delivery method of AAB. In addition to the significant benefits in package volume, it has also helped to improve product promotion and installation opportunities. 4.1 Language SplitOur application is available at the same time in multiple countries and needs to support multiple languages such as English, Indonesian, and Portuguese. With the help of AAB, we can avoid downloading language resources from other countries. It is very simple to issue a language dynamically. First, enable the language enableSplit in Gradle: bundle { When switching system languages, the application will automatically download the required language through GP. Of course, you can also manually request language resources according to business needs, such as when selecting other languages in our built-in language switching interface: private val _splitListener = SplitInstallStateUpdatedListener { state - > 4.2 Dynamic FeatureThere are some advanced features in the product that not all users will use, such as some advanced camera effects, but rely on more so and underlying libraries to make them into on-demand loading of the Dynamic Feature implementation functions: Creating a Dynamic Feature is like creating a Gradle Module. There are two ways to download DF when creating:
After DF is created, response registration will be added in app/build.gradle: dynamicFeatures = [ ':dynamicfeature' ] Requesting Dynamic Feature in the required scenario, similar to the code in the request language, both use SplitInstallManager: val splitManager = SplitInstallManagerFactory . create ( requireContext ()) 4.3 BundletoolThe AAB format cannot be installed and debugged locally. Through the AAB > APK packaging tool provided by Google, we can compile it locally into APK, which is convenient for QA testing and developers' self-testing. The process of generating APKs in AAB is as follows. .apks will be generated in the middle, and then specific .apks will be generated for different devices. // Generate apks file through aab Generate local Apk via device.json: bundletool extract - apks You can also install it directly through apks. At this time, you actually install the apk on the phone. However, this command will automatically read the phone configuration, and then create the corresponding apk, and then install it on the phone. bundletool install - apks The final installation package is issued through language and other resources and dynamics of Dynamic Feature, and the package volume is reduced by nearly 40%, compressed from 90M+ to 55M. 5. ML KitIn addition to Jetpack's related class libraries, Google also provides a lot of other technical support for our applications, such as ML Kit. ML Kit is a mobile SDK launched by Google for mobile devices. It supports Android and iOS platforms and encapsulates many machine learning capabilities such as text recognition, face position detection, object tracking and detection. For machine learning developers, ML Kit also provides an API to help developers customize the TensorFlow lite model. ML Kit also supports Google Play runtime issuance to reduce the volume of the package. As an AI special effects application, it is necessary to support users to select a face in multiple face pictures for rendering, so face detection capabilities are essential. After research, we chose ML Kit to achieve fast face detection. ML Kit splits several machine learning capabilities, and the app only needs to introduce the required capabilities. Taking face detection as an example, it introduces face detection Google Play dynamically releases libraries, and uses suspend functions to simplify the use of APIs: dependencies { Configure in the AndroidManifest.xml file: < application > Use the suspendCancellableCoroutineAPI provided by the coroutine to transform the callback into a suspend function suspend fun faceDetect ( input : Bitmap ): List < Face > = suspendCancellableCoroutine { continuation - > at lastMAD has helped us complete the efficient development and rapid launch of our products. In the future, we will also introduce Jetpack Compose to further improve development efficiency and shorten the iteration cycle of demand. Due to space limitations, the content in the article is only a point, hoping to provide inspiration and reference for other similar overseas applications in technology selection. With the continuous improvement of the Google mobile development ecosystem represented by Jetpack, developers can focus more energy on business innovation and develop more rich functions for the majority of users. The continuous unification of underlying technologies will also help developers better carry out technical exchanges and co-construction, and get rid of the development dilemma of fighting each other and re-creating wheels. |
<<: Google is testing variable refresh rate (VRR) support for Chrome OS
Author: Luo Zijian background Feishu Intelligent ...
Nowadays, with smart TVs and smart set-top boxes ...
In terms of promotion and traffic , different pro...
As the weather warms up The barbecue industry is ...
Baidu Promotion Client is a backend account manag...
When you see this billboard , can you recognize w...
Nowadays, people buy products through many channe...
In recent years, with the rapid development of in...
Recently, He Xiaopeng, founder of Xpeng Motors, p...
Recently, WeChat quietly pushed the iOS WeChat 8....
On March 5, Zhihu officially submitted its prospe...
SaaS company sales are divided into "interna...
The simplest logic for marketing is to ask yourse...
According to official statistics, since March, Sha...
The work of operators can be said to be inseparab...