With the rapid development of the Android platform, many old-fashioned apps have developed into so-called “super apps”, which not only have many functional modules and a huge amount of code, but also the team size has expanded to dozens or even hundreds of people. The flagship application of a first-tier manufacturer may even involve the collaborative development of multiple departments.

In this process, some “code reuse” and “synergy efficiency” issues inevitably arise. Therefore, component development has gradually been put on the agenda by various major manufacturers. Nowadays, in addition to solving “code” issues, mobile terminal development also needs to pay attention to “engineering” issues.

In 2022, Baidu Netdisk App ushered in its 10th anniversary. During the 10 years of development, we have also encountered various “engineering” problems. In order to solve these problems, we introduced component-based development. And Rubik was born out of this process.

About Rubik

Rubik is produced by the Baidu Netdisk team and started construction in 2019. So far, it has iterated two major versions and has been applied in many online products.

Rubik is a comprehensive framework that solves the componentization of the Android platform. That is to say, while Rubik helps us achieve module decoupling, it can also provide some component management capabilities. The module decoupling capability provided by Rubik is a low-coupling communication solution between components based on “function routing”. Component management capabilities can help us implement component version control, maven release, switching between aar/jar and source code, etc. Rubik can also more easily combine existing components into different APKs through configuration files.

Engineering structure of the Rubik framework

In other words, Rubik focuses on solving the componentization problem in the Android application development process. However, Rubik is an open source framework completely implemented by the Kotlin language. Even if you don’t have requirements for componentized development, you are welcome to pay attention to Rubik’s technical implementation. Welcome to read Rubik’s code, because:

First of all, Rubik is completely developed with kotlin, and it has extensively applied Kotlin’s functional programming, DSL and other features, which is very suitable for the exchange and learning of small partners who are learning Kotlin;

In addition, in the implementation process of Rubik, the technology of Kotlin code automatic generation is also applied. Almost all related solutions for automatic generation of Kotlin code have been tried, and traditional bytecode modification techniques such as Tranform have also been used.

Finally, as a tool chain project that runs through the Android compilation process, Rubik has a lot of practices about Gradle plug-ins, and also encapsulates a lot of Gradle tool classes. It is very suitable for reference and reference of small partners who want to develop Gradle plug-ins. Best of all, it’s all Kotlin-based.

There are many different definitions of componentized development. Here, the definition is not important, we should care more about what is the core problem that component development can solve for client-side development. We believe that the core of componentized development lies in two points:

  • Isolation: keep business components relatively independent

  • Reuse: Reuse business components to form runnable software

What counts as complete component isolation?

Maintaining independence between business components can usually be equated with “decoupling” at the code level. If the codes of two businesses are mixed together and the lack of boundary definition is definitely a manifestation of incomplete code isolation.

Incomplete code isolation

In addition, for componentization, the standard of business isolation is even more stringent. When two modules are not strongly related in business, if there is a direct code call, it is also considered that the code isolation is not complete. Due to the compilation order, on the Android platform, it is impossible to establish two-way code dependencies between modules, which obviously differs from the concept of componentization. We hope that the components are equal and independent, and should be free in any component. Establish loosely coupled dependencies between them. Such a one-way dependency based on code calls is essentially a “subordination relationship” that is “continuously cut”.

Incomplete code isolation

Therefore, in componentized development, there should be no code coupling between components, and when they need to communicate with each other, use a communication method that uses interfaces and indirect dependencies.

In the actual component development process, isolation is often the most difficult to operate, whether in the face of historical code that is difficult to maintain, or new business that is insufficiently planned in the early stage. However, once the boundaries between the two components are clearly delineated and the codes are isolated from each other, a series of engineering benefits will be brought:

  • Clarify the responsibilities of developers: Since the modification of one module will not directly affect another module, developers responsible for developing different modules only need to be responsible for the agreed interface, and do not need to care about the specific implementation of other components.

  • Reduce testing cost: Similarly, for testers, if the modification of a component can only affect the component itself and the interface of the component, then there is no need for regression testing for components that have not been modified.

  • Improve compilation speed: In actual development, most components can be compiled into binary in advance, and only a few components that change frequently can be kept in source code state.

  • Fault isolation: When a component fails, it can not affect the normal operation of other components.

For the isolation of components, it can be summed up in one sentence: isolating code is not the purpose, isolating changes is the most important!

In addition, isolation is also a prerequisite for component reuse. Only by isolating all implicit impacts of a business component on the outside can it be ensured that the component is safe, reliable, and has no side effects when it is reused by other scenarios.

To what extent should component-level reuse be achieved?

Component-level reuse refers to the reuse of existing business components between different applications. This is often to quickly build new projects and ensure the code consistency of new and old projects to the greatest extent, thereby reducing code maintenance costs. Therefore, the component-based development framework should be able to reserve existing components, and when a new project starts, filter existing components through simple configuration and quickly generate new applications.

Filtering and reuse of components

In actual development, there may be some functional differences when the same component is reused for different applications. Therefore, the componentization framework must also be able to differentially compile the same set of code into component variants with slightly different functions, so as to be reused under different requirements.

Variant reuse of components

Rubik consists of two parts, one is the Rubik Router module that solves low-coupling communication between components, and the other is the Rubik toolchain based on Gradle Plugin, which is responsible for solving component management and dependency management issues.

Rubik Router: Kotlin DSL-based “function” routing

On the basis of dependency inversion and dependency injection, Rubik implements a set of Uri-based routing communication methods. Unlike general page routing or four major component routing, Rubik Router allows Uri and parameters to be navigated to any public component inside the component. Execution of Java or Kotlin functions. The choice of Rubik Router is based on three considerations:

  • Flexibility: In actual development, the boundaries of components are usually not simple page jumps, but may be Api calls or data and instance acquisition. Compared with traditional page routing, “function” routing can be more lightweight meet these needs.

  • Extensibility: “Function” routing has a lower level, and users can extend more usages on the basis of functions.

  • Consistency: For route callers, Rubik Router provides a consistent calling method whether the end point of the route is a function, a page or data.

function routing

In addition, Rubik Router provides a calling method based on Kotlin DSL and a routing declaration method based on meta-annotations, allowing users to call interfaces provided by other components more easily and intuitively.


@RRoute(path = "user") 
fun getUser(id : Int, name : String) : User? { 
    …
}

Register functions to routes with meta-annotations


navigate {
	uri = "app://com.account/user"
	query {
        "id" with 400
        "name" with "zhangsan01" 
	}
    result<User?> { user ->  // 通过泛型指定接收数据类型
        …
    }
} 

Call interfaces provided by other components through Kotlin DSL

Rubik Plugins: Gradle Plugin-based component management and dependency management tools

Rubik gradle plugins provide component definition, version control, maven release, binary dependency and source code dependency switching capabilities, including 4 gradle plugins:

    • Provide the ability to define components globally, and automatically enable plugins such as rubik-context and rubik-root according to the global definition

      rubik plugin project structure

      
      rubik {
          component {
              uri "app://com.cloud-file"  
      		    // uri 是组件的唯一 id,和路由根路径     
              dependencies {    // 组件所依赖的其他组件uri
                  uri ("app://com.local-file" ) 
                  uri ("app://com.upload" )
              }
              source {    // 定义的多种来源
                  project (":lib-cloud-file") 
                  maven {   // 其他组件依赖自己的默认版本
      		        version "0.2.0"
      		        variant "english-debug" 
                  }
              }
          }
          component { … }  // 继续定义下一个组件
      }  

      How components are defined

    • Provide App engineering with the ability to filter components, and filter business components to be packaged into apk according to flavor and version number

    • Provide source code engineering and aar switching capabilities of components

Component filtering and dependency mode switching


rubik {	
	packing { 
        uri ("app://com.cloud-*") {   // 筛选范围,uri表示用uri筛选,支持*匹配任意字符
            projcetMode () // 筛选方式, projcetMode通过工程筛选一些组件
        }
        uri ("app://com.preview-file") {
            mavenMode {    // 筛选方式, mavenMode通过maven依赖aar筛选一些组件                             
                version "0.2.0"  
		        variant "netdisk-english-debug" 
            }
        } 
	       ……
    }
} 

How to filter components

    • Provides the ability to compile business code into aar according to flavor and version number and publish it to maven

    • Provide auxiliary function routing, package the intermediate code into context.jar, and publish to maven according to the version number, and automatically add the intermediate code dependencies of other components to the component according to the global definition

Release and automatic dependency of components and intermediate code

    • Provide a unit test environment for the project

I hope Rubik can help you achieve component development more conveniently, and you are welcome to exchange code and technology. If you think we are doing well, please do not hesitate to star, fork and watch:
https://github.com/baidu/Rubik

#Rubik #Kotlinbased #Android #component #development #framework #open #source #News Fast Delivery

Leave a Comment

Your email address will not be published. Required fields are marked *