AndroidBaseFrameMVVM 🐽

AndroidBaseFrameMVVM is an Android engineering framework that uses the following technology stacks: Componentize, Kotlin, MVVM, Jetpack, Repository, Kotlin-Coroutine-Flow, this framework is not only an out-of-the-box engineering framework base layer, but also a good learning resource, the following documents will be used in the framework of some of the core technologies are described. The framework is a product of personal technical accumulation, and will be updated and maintained all the time. If you have any technical discussions or fault points in the framework, please mention Issues on GitHub, and I will respond in time. Hope this framework project can bring help to everyone, like can Start🌟.

Project address: AndroidBaseFrameMVVM

Demo

Demo is in another demo branch, but the current demo branch code is the previous version of the demo, the current version of the demo has not had time to write, please stay tuned.

Framework is demonstrated

Google Android Team Jetpack View model:

The module

  • app:

    App shell project is a shell that depends on all components. This module should not contain any code, it only exists as an empty shell. Because the project uses EventBusAPT technology, it needs to index the corresponding APT generation class of each business component, so this part of code is in the APP shell.

  • buildGradleScript:

    Gradle script module. This module stores each component and some encapsulated Gradle script files. The idea was to manage all the scripts in one place, and I still have the habit of looking for scripts in components.

  • buildSrc:

    This is a special folder, which is responsible for building projects, and it contains some of the things that are used in building projects, such as project configurations and dependencies. This is also where Gradle plugins are stored. Some custom Gradle plugins need to be placed here.

  • lib_base:

    The basic common module of the project, which contains various base class encapsulation, dependencies on remote libraries, and utility classes and third-party library encapsulation, is not related to the project business, and the common parts related to the project business need to be placed in lib_common.

  • lib_common:

    The business common module of the project, which stores the common parts of each business component in the project, as well as some files for the specific needs of the project, is related to the business of the project.

  • lib_net:

    Network module, network module configuration, packaging, etc., specially set up a component to be responsible for the network module part.

Componentization

Component initialization

For better code isolation and decoupling, SDKS and third-party libraries used in a particular component should be dependent only on that component and should not expose the API of that component’s specific SDK and third-party libraries to other components that do not need to use them. The problem is that the SDK and the three-party libraries often need to be initialized manually, and usually need to be initialized as soon as the project starts (i.e., the Application), but a project can only have one custom Application. The custom Application for this project is declared in the lib_base module and is also declared in the lib_base module manifest. How should the other components be initialized? With that in mind, let’s take a closer look.

Common component initialization solutions:

To my knowledge, there are two most common solutions:

  • Interface oriented programming + reflection scan implementation class:

    This scheme is based on the interface programming, custom Application to implement a custom interface (interface), this interface and some Application lifecycle corresponding to the abstract methods and other custom abstract methods, each component to write an implementation class. The implementation class is similar to a fake custom Application, and then the real custom Application dynamically finds all the implementation classes of the interface in the current runtime environment through reflection, instantiates them, and then collects them into a collection. Call the corresponding method one by one in the corresponding declaration cycle method of the Application, so that each implementation class can be synchronized with the Application lifecycle, and hold the reference to the Application and the context object. This allows us to simulate the Application lifecycle and initialize the SDK and third-party libraries within the component. Using reflection also requires some exception handling. This scheme is the most common one I have seen, and it has been seen in some commercial projects.

  • Programming for interfaces + Meta-data + Reflection:

    The second part of the solution is the same as the first method, which implements the Application lifecycle synchronization through interface programming. In fact, this step cannot be avoided. In my solution, the second part is also implemented in this way. The difference is that the first part is how to find the implementation class of the interface. This solution uses the Meta-data tag of the AndroidManifest. The Meta-data tag is declared in the AndroidManifest of each component, which contains the information about the implementation class of the component. Then find the configuration information in The Application, use reflection to create instances of the implementation classes, and collect them into a collection. The rest is basically the same. This scenario has as many exceptions to handle as the first. I’ve seen this approach in some open source projects, and personally think it’s too cumbersome and has to deal with too many exceptions.

The scheme used in this project:

  • Interface oriented programming + Java SPI mechanism (ServiceLoader) +AutoService:

    Let’s take a look at Java’s SPI mechanism: In object-oriented design, it is generally recommended to program modules based on interfaces and not to hard-code implementation classes between modules. As soon as a specific implementation class is involved in the code, the principle of pluggability is violated, and if an implementation needs to be replaced, the code needs to be modified. In order to realize the module assembly without dynamically specifying in the program, this requires a service discovery mechanism. JavaSPI provides a mechanism for finding a service implementation for an interface. This is similar to IOC thinking, moving control of the assembly out of the program. We can use the SPI mechanism to expose implementation classes. How to use SPI is not stated here, but we use SPI to expose implementation classes in the components, and we use SPI API provided by Java to get these exposed services in the Application, so we get instances of these classes. The remaining steps are the same as above, with a collection of traversal implementation classes calling their corresponding methods to complete the initialization work. Since using SPI requires creating file configurations for each module, which can be cumbersome, we use Google’s AutoService library to help us create these profiles automatically by simply adding an AutoService annotation to the implementation class. The core classes in this framework are lib_base-loadModuleProxy and lib_base-applicationLifecycle. This is a solution I asked from a big guy in Meha, who told me that component initialization can be done using ServiceLoader in componentization, so I went to research it, and finally found that this solution is good, simpler and safer than the two solutions mentioned above.

Resource name conflict

Naming conflicts in modular scheme, resources is a serious problem, because when packed for the consolidation of resources, if there are two of the two modules of the same name file, so the last will only retain a friend if you don’t know the problem, when meeting the question must be a face of meng force of state. Since problem has emerged, we will go to solve, the solution is the naming of each component with a fixed prefix, so AS not to appear the phenomenon of two of the same files, we can in the build. Gradle configuration file to configure the prefix, if not in accordance with the prefix name, AS a warning will prompt, the configuration is AS follows:

android {
    resourcePrefix Prefix "_"
}
Copy the code

components

In fact, the division of components has always been a difficult part, and in fact, there are not some very suitable suggestions here, depending on the specific project.

The basic components are usually independent and directly reusable, such as network module, QR code recognition module, etc.

About the business component, business component, generally can be adjusted independently, which is can be a app running, so as to exert a great use componentization, when a project is more and more big, the business component is more and more long, compile time consuming will be a very difficult problem, but if each business module can be adjusted independently, This greatly reduces compilation time and eliminates the need for developers to focus on other components.

About the common module, lib_base put some basic code, belong to the framework base layer, should not be involved with the project business, and the project business related to the public part should be put in lib_common, do not pollute lib_base.

Dependent version control

A common problem with componentization is the dependency version. Each component may have its own dependency library, so we should centrally manage the various dependency libraries and their versions, so that all dependencies used by the project are the same version, rather than different versions. This project uses several KT files in buildSrc for dependent version unity management and some configuration of the project.

The MVVM related

  • MVVM is implemented using the Jetpack component + Repository design pattern. There are not many Jetpacks used, such as DataBinding, Hilt, Room, etc., which are not used, but can be added if needed. The purpose of using an architectural pattern is to understand the code, to layer the code, and to make each module do its own job, so if you use an architectural pattern, you need to follow the specification.
  • Repository layer is responsible for providing data, ViewModel does not need to care about the source of data, avoid using LiveData in Repository, the framework uses Kotlin coroutine Flow to process requests or access the database, The Repository function returns a Flow to the ViewModel calling function. The Flow upstream is responsible for providing the data, and the ViewModel retrives the data and stores it in LiveData. The View layer subscrises to LiveData. Implement a data-driven view
  • View -> ViewModel -> Repository

The tripartite library used by the project and its simple examples and materials

  • Kotlin
  • Kotlin-Coroutines-Flow
  • Lifecycle
  • ViewModel
  • LiveData
  • ViewBinding
  • Hilt
  • Android KTX
  • OkHttp: Network request
  • Retrofit: Network request
  • MMKV: Tencent key-value local storage component based on mMAP memory mapping
  • Glide: Fast and efficient Android image loading library
  • ARoute: Alibaba’s framework for componentization of Android apps — routing, communication and decoupling between modules
  • RecyclerViewAdapter BaseRecyclerViewAdapterHelper: a powerful and flexible
  • StatusBarUtil: Status bar
  • EventBus: publish/subscribe EventBus for Android and Java
  • Bugly: Tencent exception reporting and hot update (only exception reporting integrated)
  • PermissionX: Guo Lin permission request framework
  • LeakCanary: Android’s memory leak detection library
  • AndroidAutoSize: JessYan’s ultimate toutiao screen adaptation

Kotlin coroutines

About Kotlin coroutine, is really fragrant, specific tutorial can see my article:

  • 10,000-word long article – Kotlin coprogram progression

Flow is similar to RxJava in that it also has a series of operators, data:

  • Google recommends using Kotlin Flow in the MVVM architecture:
  • Learn to Use Kotlin-coroutines:
  • Kotlin Coroutines Flow Series (1-5)

PermissionX

PermissionX is a permission application framework.

Request {allGranted, grantedList, deniedList ->} permissionx. init(this).permissions(" permissions ").request {allGranted, grantedList, deniedList ->}Copy the code

Information:

Making: github.com/guolindev/P…

EventBus APT

In this framework, I encapsulate the base class of EventBus, automatic registration and deregistration, and add @EventBusRegister annotation to the class that needs to be registered. Don’t worry about memory leaks and late deregistration, it’s already done in the base class

@EventBusRegister
class MainActivity : AppCompatActivity() {}
Copy the code

This is a major update to EventBus3.0. With EventBus APT, you can generate subscribed classes at compile time and avoid inefficient reflection. Many people don’t know about this update and use version 3.0. It’s actually 2.0 efficiency. EventBus APT has been started in each module in the project. EventBus will generate subscription classes for each module in the compiler, so we need to manually write code to register these subscription classes:

// In the AppApplication class of the APP shell
EventBus
     .builder()
	 .addIndex("The instance class name of the subscription class generated by each module is set in the base_module.gradle script. For example, the module_HOME generated subscription class is module_homeIndex.")
     .installDefaultEventBus()
Copy the code

Screen for AndroidAutoSize

The screen adaptor is JessYan’s Toutiao screen adaptor ultimate version

Making: github.com/JessYanCodi…

Usage:

<manifest> <application> <meta-data android:name="design_width_in_dp" android:value="360"/> <meta-data android:name="design_height_in_dp" android:value="640"/> </application> </manifest> // <manifest> <application> // Use height as the benchmark for adaptation. <manifest> <application <meta-data android:name="design_height_in_dp" android:value="400"/> <meta-data android:name="design_height_in_dp" </app > </app > </app > </app AutoSizeConfig.getInstance().isBaseOnWidth = falseCopy the code

ARoute

ARoute is alibaba’s framework for componentization of Android apps — routing, communication and decoupling between modules

Usage:

// 1. Add @route annotation to the Activity or Fragment that needs Route jump
@Route(path = "/test/activity")
public class YourActivity extend Activity {... }// 2. Initiate a route hop
ARouter.getInstance()
    .build(Destination Routing Address)
    .navigation()
    
// 3. Jump with parameters
ARouter.getInstance()
	.build(Destination Routing Address)
    .withLong("key1".666L)
    .withString("key3"."888")
    .withObject("key4".new Test("Jack"."Rose"))
    .navigation()

// 4. Receive parameters
@Route(path = RouteUrl.MainActivity2)
class MainActivity : AppCompatActivity(a){

    // Map the different parameters in the URL by name
    @Autowired(name = "key")
    lateinit var name: String
    
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        // ARouter dependency injection. ARouter automatically assigns values to fields
        ARouter.getInstance().inject(this)}}// 5. Obtain fragments
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
Copy the code

Information:

Official document :github.com/alibaba/ARo…

ViewBinding

View binding makes it easier to write code that interacts with views. Once view binding is enabled in a module, a binding class is generated for each XML layout file in that module. An instance of a bound class contains a direct reference to all views that have ids in the corresponding layout. In most cases, view binding replaces findViewById

Usage:

Enable ViewBinding by module

// Build. Gradle file under module
android {
	/ / open ViewBinding
    // Higher version AS
    buildFeatures {
        viewBinding = true
    }
    // Earlier version AS minimum 3.6
    viewBinding {
        enabled = true}}Copy the code

The use of ViewBinding in an Activity

// The previous method of setting the view
setContentView(R.layout.activity_main)

// Use the method after ViewBinding
val mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)

// The ActivityMainBinding class is automatically generated based on the layout
// ViewBinding converts control ids to small hump nomenclature, so for consistency, use small hump nomenclature when declaring ids in XML
// For example, if you have a control with id mText, you can use it like this
mBinding.mText.text = "ViewBinding"
Copy the code

Use of ViewBinding in fragments

// The original way
return inflater.inflate(R.layout.fragment_blank, container, false)

// Use ViewBinding
mBinding = FragmentBlankBinding.inflate(inflater)
return mBinding.root
Copy the code

Information:

The official document: developer.android.com/topic/libra…

CSDN: blog.csdn.net/u010976213/…

ViewModel

The ViewModel class is designed to store and manage data related to the interface in a lifecycle focused manner. The ViewModel class allows data to survive configuration changes such as screen rotation.

Usage:

class MainViewModel : ViewModel() {}class MainActivity : AppCompatActivity() {
		// Get the ViewModel instance constructed with no parameters
    val mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
}
Copy the code

Information:

The official document: developer.android.com/topic/libra…

Android ViewModel: juejin. Im /post/684490…

LiveData

LiveData is an observable data storage class. Unlike regular observable classes, LiveData is lifecycle aware, meaning that it follows the lifecycle of other application components, such as activities, fragments, or services. This awareness ensures that LiveData updates only application component observers that are in an active lifecycle state

LiveData is divided into MutableLiveData with variable values and LiveData with immutable values

Common methods:

fun test(a) {
        val liveData = MutableLiveData<String>()
        // Set the update data source
        liveData.value = "LiveData"
        // Publish the task to the main thread to set the given value
        liveData.postValue("LiveData")
        / / get the value
        val value = liveData.value
        // Observe data source changes (the first parameter should be owner:LifecycleOwner for example, an Activity that implements the LifecycleOwner interface)
        liveData.observe(this, {
            // Logic triggered after data source changes})}Copy the code

Information:

The official document: developer.android.com/topic/libra…

Lifecycle

Lifecycle is a class that stores information about the Lifecycle state of a component, such as an Activity or Fragment, and allows other objects to observe this state. LifecycleOwner is a single method interface that indicates that the class has Lifecycle. It has a method (getLifecycle()) that must be implemented by the class. Components that implement LifecycleObserver work seamlessly with components that implement LifecycleOwner, because the owner can provide the lifecycle and observers can register to observe the lifecycle.

Information:

The official document: developer.android.com/topic/libra…

Hilt

Hilt is Android’s dependency injection library, which reduces the boilerplate code required to perform manual dependency injection in a project. Performing manual dependency injection requires you to manually construct each class and its dependencies and reuse and manage the dependencies with the help of the container.

Hilt provides a standard way to use **DI (dependency injection) ** in applications by providing a container for each Android class in a project and automatically managing its lifecycle. Hilt is built on top of the popular DI library Dagger, and thus can benefit from Dagger’s compile-time correctness, runtime performance, scalability, and Android Studio support.

Information:

The official document that has not been updated is the alpha version of the document: using Hilt to implement dependency injection

Dagger’s Hilt documentation is currently up to date: Dagger Hilt