├── core ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ └── java │ │ └── com │ │ └── numeron │ │ └── brick │ │ └── core │ │ ├── DaoMethod.kt │ │ ├── InjectKind.kt │ │ ├── Inject.kt │ │ ├── InjectableKind.kt │ │ ├── RetrofitInstance.kt │ │ ├── RoomInstance.kt │ │ └── Brick.kt ├── build.gradle └── proguard-rules.pro ├── annotation ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── com │ │ └── numeron │ │ └── brick │ │ └── annotation │ │ ├── Inject.kt │ │ ├── Provide.kt │ │ ├── Port.kt │ │ ├── Url.kt │ │ ├── RoomInstance.kt │ │ └── RetrofitInstance.kt └── build.gradle ├── compiler ├── .gitignore ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── javax.annotation.processing.Processor │ │ └── java │ │ └── com │ │ └── numeron │ │ └── brick │ │ └── processor │ │ ├── ProvideProcessor.kt │ │ ├── BrickProcessor.kt │ │ ├── InjectProcessor.kt │ │ └── ProvideGenerator.kt └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── plugin ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── gradle-plugins │ │ │ └── brick.properties │ │ └── java │ │ └── com │ │ └── numeron │ │ └── brick │ │ └── plugin │ │ ├── RetrofitTemplate.kt │ │ ├── BrickPlugin.kt │ │ ├── BaseTransform.kt │ │ └── BrickTransform.kt └── build.gradle ├── settings.gradle ├── .gitignore ├── brick.gradle ├── gradle.properties ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /annotation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compiler/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiazunyang/brick2/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/gradle-plugins/brick.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.numeron.brick.plugin.BrickPlugin -------------------------------------------------------------------------------- /compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | com.numeron.brick.processor.BrickProcessor -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':plugin' 2 | include ':core' 3 | include ':compiler' 4 | include ':annotation' 5 | rootProject.name = "Brick" -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/DaoMethod.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | data class DaoMethod(val type: String, val name: String) -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/InjectKind.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | enum class InjectKind { 4 | 5 | Class, 6 | 7 | Room, 8 | 9 | Retrofit; 10 | 11 | } -------------------------------------------------------------------------------- /annotation/src/main/java/com/numeron/brick/annotation/Inject.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.annotation 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Inject -------------------------------------------------------------------------------- /annotation/src/main/java/com/numeron/brick/annotation/Provide.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.annotation 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Provide -------------------------------------------------------------------------------- /annotation/src/main/java/com/numeron/brick/annotation/Port.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.annotation 2 | 3 | @Retention(AnnotationRetention.BINARY) 4 | @Target(AnnotationTarget.CLASS) 5 | annotation class Port(val value: Int) -------------------------------------------------------------------------------- /annotation/src/main/java/com/numeron/brick/annotation/Url.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.annotation 2 | 3 | @Retention(AnnotationRetention.BINARY) 4 | @Target(AnnotationTarget.CLASS) 5 | annotation class Url(val value: String) -------------------------------------------------------------------------------- /annotation/src/main/java/com/numeron/brick/annotation/RoomInstance.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.annotation 2 | 3 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class RoomInstance -------------------------------------------------------------------------------- /annotation/src/main/java/com/numeron/brick/annotation/RetrofitInstance.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.annotation 2 | 3 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class RetrofitInstance -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/Inject.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | data class Inject( 4 | 5 | val variableName: String, 6 | 7 | val variableType: String, 8 | 9 | val owner: String, 10 | 11 | val kind: InjectKind 12 | 13 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 24 19:17:47 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/InjectableKind.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | enum class InjectableKind { 4 | 5 | Class, //需要通过类的构造来创建对象 6 | 7 | Companion, //伴生类 8 | 9 | Object, //object修饰的单例 10 | 11 | Method, //无参的方法 12 | 13 | Getter, //属性的getter 14 | 15 | Field, //一个字段,一般是私有字段 16 | 17 | } -------------------------------------------------------------------------------- /annotation/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | 5 | group = brick_group 6 | version = brick_version 7 | 8 | dependencies { 9 | compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 10 | } 11 | 12 | sourceCompatibility = "1.8" 13 | targetCompatibility = "1.8" 14 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | 5 | group = brick_group 6 | version = brick_version 7 | 8 | dependencies { 9 | api project(':annotation') 10 | compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 11 | implementation 'com.google.code.gson:gson:2.8.6' 12 | } 13 | 14 | sourceCompatibility = "1.8" 15 | targetCompatibility = "1.8" 16 | -------------------------------------------------------------------------------- /brick.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin-kapt' 2 | if(project.plugins.hasPlugin("com.android.application")) { 3 | apply plugin: 'brick' 4 | } 5 | 6 | ext { 7 | brick_version = '0.3.4' 8 | } 9 | 10 | android { 11 | defaultConfig { 12 | javaCompileOptions { 13 | annotationProcessorOptions { 14 | arguments += ['MODULE_NAME': project.name, 'PROJECT_NAME': project.rootProject.name] 15 | } 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation "com.gitee.numeron.brick:annotation:$brick_version" 22 | kapt "com.gitee.numeron.brick:compiler:$brick_version" 23 | } -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/RetrofitInstance.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | data class RetrofitInstance( 4 | 5 | val name: String, 6 | 7 | val owner: String, 8 | 9 | val kind: InjectableKind 10 | 11 | ) { 12 | 13 | fun getInstance(): String { 14 | return when (kind) { 15 | InjectableKind.Object -> "$owner.INSTANCE" 16 | InjectableKind.Class, InjectableKind.Companion -> owner 17 | else -> throw IllegalStateException("Not supported injected method.") 18 | }.let { 19 | "$it.$name()" 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /plugin/src/main/java/com/numeron/brick/plugin/RetrofitTemplate.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.plugin 2 | 3 | import retrofit2.Retrofit 4 | 5 | @Suppress("unused") 6 | class RetrofitTemplate { 7 | 8 | private fun newRetrofit(retrofit: Retrofit, port: Int, url: String?): Retrofit { 9 | if (port > 0) { 10 | val httpUrl = retrofit.baseUrl().newBuilder().port(port).build() 11 | return retrofit.newBuilder().baseUrl(httpUrl).build() 12 | } 13 | if (!url.isNullOrEmpty()) { 14 | return retrofit.newBuilder().baseUrl(url).build() 15 | } 16 | return retrofit 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | 5 | group = brick_group 6 | version = brick_version 7 | 8 | dependencies { 9 | api project(":core") 10 | implementation gradleApi() 11 | implementation localGroovy() 12 | implementation 'commons-io:commons-io:2.7' 13 | implementation 'org.javassist:javassist:3.27.0-GA' 14 | implementation 'com.squareup.retrofit2:retrofit:2.6.2' 15 | implementation "com.android.tools.build:gradle:$gradle_version" 16 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 17 | } 18 | 19 | sourceCompatibility = "1.8" 20 | targetCompatibility = "1.8" 21 | -------------------------------------------------------------------------------- /compiler/build.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.jvm.Jvm 2 | 3 | apply plugin: 'java-library' 4 | apply plugin: 'kotlin' 5 | apply plugin: 'com.github.dcendents.android-maven' 6 | 7 | group = brick_group 8 | version = brick_version 9 | 10 | dependencies { 11 | api project(':core') 12 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 13 | implementation 'me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0' 14 | implementation 'com.squareup:kotlinpoet:1.6.0' 15 | implementation 'com.google.auto:auto-common:0.10' 16 | implementation 'com.bennyhuo.aptutils:aptutils:1.6' 17 | compileOnly files(Jvm.current().getToolsJar()) 18 | } 19 | 20 | sourceCompatibility = "1.8" 21 | targetCompatibility = "1.8" 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/RoomInstance.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | import java.lang.IllegalStateException 4 | 5 | data class RoomInstance( 6 | 7 | val name: String, 8 | 9 | val owner: String, 10 | 11 | val kind: InjectableKind, 12 | 13 | val dao: List) { 14 | 15 | fun getInvoke(type: String): String? { 16 | val daoMethod = dao.find { it.type == type } ?: return null 17 | return when (kind) { 18 | InjectableKind.Object -> "$owner.INSTANCE.$name()" 19 | InjectableKind.Class, InjectableKind.Companion -> "$owner.$name()" 20 | else -> throw IllegalStateException("Not supported injected method.") 21 | }.let { 22 | "$it.${daoMethod.name}()" 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /plugin/src/main/java/com/numeron/brick/plugin/BrickPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.plugin 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.android.build.gradle.LibraryExtension 5 | import com.numeron.brick.core.Brick 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.Task 9 | import org.gradle.api.logging.LogLevel 10 | import org.gradle.api.tasks.Delete 11 | 12 | class BrickPlugin : Plugin { 13 | 14 | override fun apply(project: Project) { 15 | //获取android app扩展 16 | val extension = project.extensions.findByType(AppExtension::class.java) 17 | ?: throw IllegalStateException("module ${project.name} can't apply plugin: brick. only android app.") 18 | //注册Brick插件 19 | extension.registerTransform(BrickTransform(project)) 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | #org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | # AndroidX package structure to make it clearer which packages are bundled with the 16 | # Android operating system, and which are packaged with your app"s APK 17 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 18 | android.useAndroidX=true 19 | # Automatically convert third-party libraries to use AndroidX 20 | android.enableJetifier=true 21 | # Kotlin code style for this project: "official" or "obsolete": 22 | kotlin.code.style=obsolete 23 | org.gradle.daemon=false 24 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/numeron/brick/processor/ProvideProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.processor 2 | 3 | import com.bennyhuo.aptutils.AptContext 4 | import com.google.auto.common.SuperficialValidation 5 | import com.numeron.brick.annotation.Provide 6 | import com.sun.tools.javac.code.Symbol 7 | import javax.annotation.processing.Filer 8 | import javax.annotation.processing.RoundEnvironment 9 | import javax.lang.model.element.ElementKind 10 | import javax.lang.model.element.TypeElement 11 | 12 | class ProvideProcessor { 13 | 14 | fun process(env: RoundEnvironment) { 15 | env.getElementsAnnotatedWith(Provide::class.java) 16 | .filter { 17 | SuperficialValidation.validateElement(it) 18 | } 19 | .filter { 20 | it.kind == ElementKind.CLASS 21 | } 22 | .mapNotNull(TypeElement::class.java::cast) 23 | .forEach { typeElement -> 24 | val classSymbol = typeElement as Symbol.ClassSymbol 25 | classSymbol.members().elements 26 | .first(Symbol::isConstructor) 27 | .let { 28 | val methodSymbol = it as Symbol.MethodSymbol 29 | ProvideGenerator(classSymbol, methodSymbol).generate(AptContext.filer) 30 | } 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/numeron/brick/processor/BrickProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.processor 2 | 3 | import com.bennyhuo.aptutils.AptContext 4 | import com.numeron.brick.annotation.* 5 | import com.numeron.brick.core.Brick 6 | import javax.annotation.processing.AbstractProcessor 7 | import javax.annotation.processing.ProcessingEnvironment 8 | import javax.annotation.processing.RoundEnvironment 9 | import javax.lang.model.SourceVersion 10 | import javax.lang.model.element.* 11 | import javax.tools.Diagnostic 12 | 13 | class BrickProcessor : AbstractProcessor() { 14 | 15 | private var processFlag = true 16 | private lateinit var moduleName: String 17 | private lateinit var projectName: String 18 | 19 | private val supportedAnnotations = setOf( 20 | Inject::class.java, 21 | Provide::class.java, 22 | RoomInstance::class.java, 23 | RetrofitInstance::class.java 24 | ) 25 | 26 | override fun init(processingEnv: ProcessingEnvironment) { 27 | Companion.processingEnv = processingEnv 28 | AptContext.init(processingEnv) 29 | super.init(processingEnv) 30 | moduleName = processingEnv.options["MODULE_NAME"] ?: "" 31 | projectName = processingEnv.options["PROJECT_NAME"] ?: "" 32 | if(moduleName.isEmpty() || projectName.isEmpty()) { 33 | processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, "No configuration annotationProcessorOptions, only supports single module project.") 34 | } 35 | Brick.init(projectName, moduleName) 36 | } 37 | 38 | override fun process(annotations: MutableSet?, roundEnv: RoundEnvironment): Boolean { 39 | if (processFlag) { 40 | InjectProcessor(projectName, moduleName).process(roundEnv) 41 | ProvideProcessor().process(roundEnv) 42 | processFlag = !processFlag 43 | } 44 | return true 45 | } 46 | 47 | override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported() 48 | 49 | override fun getSupportedAnnotationTypes(): MutableSet { 50 | return supportedAnnotations.mapTo(HashSet(), Class<*>::getCanonicalName) 51 | } 52 | 53 | companion object { 54 | 55 | lateinit var processingEnv: ProcessingEnvironment 56 | 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin/src/main/java/com/numeron/brick/plugin/BaseTransform.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.plugin 2 | 3 | import com.android.build.api.transform.* 4 | import com.android.build.gradle.internal.pipeline.TransformManager 5 | import org.apache.commons.codec.digest.DigestUtils 6 | import org.apache.commons.io.FileUtils 7 | import org.gradle.api.Project 8 | import org.gradle.api.logging.LogLevel 9 | import java.io.File 10 | 11 | abstract class BaseTransform(protected val project: Project) : Transform() { 12 | 13 | override fun getInputTypes(): MutableSet { 14 | return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES) 15 | } 16 | 17 | override fun isIncremental(): Boolean = false 18 | 19 | override fun getScopes(): MutableSet { 20 | return TransformManager.SCOPE_FULL_PROJECT 21 | } 22 | 23 | override fun transform(transformInvocation: TransformInvocation) { 24 | 25 | val outputProvider = transformInvocation.outputProvider 26 | 27 | transformInvocation.inputs.forEach { transformInput -> 28 | 29 | transformInput.jarInputs.forEach { jarInput -> 30 | //获取一个新的文件名 31 | var name = jarInput.name 32 | val md5Hex = DigestUtils.md5Hex(jarInput.file.absolutePath) 33 | if (name.endsWith(".jar")) { 34 | name = name.substringBeforeLast('.') 35 | } 36 | //获取输出目录 37 | val destinationJarFile = outputProvider.getContentLocation(name + md5Hex, 38 | jarInput.contentTypes, jarInput.scopes, Format.JAR) 39 | //复制到输入目录 40 | FileUtils.copyFile(jarInput.file, destinationJarFile) 41 | //处理jar 42 | processJar(jarInput.name, destinationJarFile) 43 | } 44 | transformInput.directoryInputs.forEach { directoryInput -> 45 | //复制到输入目录 46 | val destinationDirectory = outputProvider.getContentLocation(directoryInput.name, 47 | directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) 48 | FileUtils.copyDirectory(directoryInput.file, destinationDirectory) 49 | //处理本地class 50 | processDirectory(destinationDirectory) 51 | } 52 | } 53 | onTransformed() 54 | } 55 | 56 | protected fun iLog(message: String, vararg args: Any) { 57 | project.logger.log(LogLevel.INFO, message, *args) 58 | } 59 | 60 | protected fun eLog(message: String, vararg args: Any) { 61 | project.logger.log(LogLevel.ERROR, message, *args) 62 | } 63 | 64 | protected fun wLog(message: String, vararg args: Any) { 65 | project.logger.log(LogLevel.WARN, message, *args) 66 | } 67 | 68 | protected fun File.forEachDeep(block: (File) -> Unit) { 69 | listFiles()?.forEach { 70 | it.forEachDeep(block) 71 | if (it.isFile) { 72 | block(it) 73 | } 74 | } 75 | } 76 | 77 | protected open fun processJar(jarName: String, jarFile: File) = Unit 78 | 79 | protected open fun processDirectory(classPath: File) = Unit 80 | 81 | protected open fun onTransformed() = Unit 82 | 83 | } -------------------------------------------------------------------------------- /core/src/main/java/com/numeron/brick/core/Brick.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.core 2 | 3 | import com.google.gson.GsonBuilder 4 | import com.google.gson.reflect.TypeToken 5 | import java.io.File 6 | 7 | object Brick { 8 | 9 | private const val OUTPUT_PROPERTY = "java.io.tmpdir" 10 | 11 | private const val INJECT_OUTPUT_FILE_NAME = "inject.json" 12 | 13 | private const val ROOM_INSTANCE_OUTPUT_FILE_NAME = "room.json" 14 | 15 | private const val RETROFIT_INSTANCE_OUTPUT_FILE = "retrofit.json" 16 | 17 | private val TEMP_PATH = File(System.getProperty(OUTPUT_PROPERTY), "brick") 18 | 19 | private val gson = GsonBuilder().setPrettyPrinting().create() 20 | 21 | /** 在初始化时,删除缓存里面的brick相关数据,并重新创建缓存目录 */ 22 | fun init(projectName: String, moduleName: String) { 23 | val file = File(TEMP_PATH, projectName + File.separatorChar + moduleName) 24 | file.listFiles()?.map(File::delete) 25 | file.mkdirs() 26 | } 27 | 28 | fun addInject(projectName: String, moduleName: String, injectList: List) { 29 | if (injectList.isEmpty()) return 30 | val injectPath = File(TEMP_PATH, projectName + File.separatorChar + moduleName) 31 | if (!injectPath.exists()) { 32 | injectPath.mkdirs() 33 | } 34 | File(injectPath, INJECT_OUTPUT_FILE_NAME).writeText(gson.toJson(injectList)) 35 | } 36 | 37 | fun getInjectList(projectName: String): List { 38 | return File(TEMP_PATH, projectName).listFiles { file -> 39 | file.isDirectory 40 | }!!.mapNotNull { directory -> 41 | try { 42 | val injectListJson = File(directory, INJECT_OUTPUT_FILE_NAME).readText() 43 | gson.fromJson>(injectListJson, object : TypeToken>() {}.type) 44 | } catch (throwable: Throwable) { 45 | null 46 | } 47 | }.flatten() 48 | } 49 | 50 | fun addRoomInstance(projectName: String, moduleName: String, roomInstanceList: List) { 51 | if (roomInstanceList.isEmpty()) return 52 | val roomPath = File(TEMP_PATH, projectName + File.separatorChar + moduleName) 53 | if (!roomPath.exists()) { 54 | roomPath.mkdirs() 55 | } 56 | File(roomPath, ROOM_INSTANCE_OUTPUT_FILE_NAME).writeText(gson.toJson(roomInstanceList)) 57 | } 58 | 59 | fun getRoomInstance(projectName: String): List { 60 | return File(TEMP_PATH, projectName).listFiles { file -> 61 | file.isDirectory 62 | }!!.mapNotNull { directory -> 63 | try { 64 | val roomInstanceFile = File(directory, ROOM_INSTANCE_OUTPUT_FILE_NAME) 65 | gson.fromJson>(roomInstanceFile.readText(), object : TypeToken>() {}.type) 66 | } catch (e: Throwable) { 67 | emptyList() 68 | } 69 | }.flatten() 70 | } 71 | 72 | fun setRetrofitInstance(projectName: String, retrofitInstance: RetrofitInstance) { 73 | val retrofitPath = File(TEMP_PATH, projectName) 74 | if (!retrofitPath.exists()) { 75 | retrofitPath.mkdirs() 76 | } 77 | val retrofitFile = File(retrofitPath, RETROFIT_INSTANCE_OUTPUT_FILE) 78 | retrofitFile.writeText(gson.toJson(retrofitInstance)) 79 | } 80 | 81 | fun getRetrofitInstance(projectName: String): RetrofitInstance { 82 | return try { 83 | val retrofitFile = File(TEMP_PATH, projectName + File.separatorChar + RETROFIT_INSTANCE_OUTPUT_FILE) 84 | gson.fromJson(retrofitFile.readText(), RetrofitInstance::class.java) 85 | } catch (e: Exception) { 86 | throw IllegalStateException("Annotation @RetrofitInstance not found.") 87 | } 88 | } 89 | 90 | fun clearProjectCache(projectName: String) { 91 | File(TEMP_PATH, projectName).deleteRecursively() 92 | } 93 | 94 | /** 95 | * 当缓存目录下存在此模块的数据时,返回true 96 | * @param projectName String 97 | * @param moduleName String 98 | * @return Boolean 99 | */ 100 | fun isInjectModule(projectName: String, moduleName: String): Boolean { 101 | return File(TEMP_PATH, projectName + File.separatorChar + moduleName).exists() 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brick 2 | 当前最新版本号:[![](https://jitpack.io/v/com.gitee.numeron/brick.svg)](https://jitpack.io/#com.gitee.numeron/brick) 3 | 4 | 多模块示例android工程:[https://github.com/xiazunyang/Wandroid.git](https://github.com/xiazunyang/Wandroid.git) 5 | 6 | ### 介绍 7 | 辅助android开发者搭建基于JetPack组件构建MVVM框架的注解处理框架。通过注解自动生成ViewModel的Factory类、lazy方法等;支持在项目的任意位置注入ROOM的dao层接口与Retrofit库中的api接口。 8 | 9 | ### 特点 10 | `android开发者`可以将`brick`理解为一个轻量级的注入框架,使用非常简单,使用4-6个注解即可工作。`brick`主要在编译期工作, **不会在`App`运行时产生任何额外的性能消耗** ,并且只有1个注解库会打包到你的`android`工程中,**不用担心体积增大**的问题。 11 | 12 | ### 适用范围 13 | 1. 使用`androidx`而非`support`库。 14 | 2. 使用`JetPack`的`ViewModel`组件。 15 | 3. 使用`Retrofit`作为网络请求库。 16 | 4. 使用`ROOM`数据库框架。(可选) 17 | 5. 服务端为多端口、多IP的项目。(可选) 18 | 19 | ### 引入 20 | 21 | 1. 在你的`android`工程的根目录下的`build.gradle`文件中的适当的位置添加以下代码: 22 | ``` 23 | buildscript { 24 | ... 25 | repositories { 26 | ... 27 | maven { url 'https://jitpack.io' } 28 | } 29 | dependencies { 30 | classpath 'com.gitee.numeron.brick:plugin:0.3.4' 31 | } 32 | } 33 | 34 | allprojects { 35 | repositories { 36 | ... 37 | maven { url 'https://jitpack.io' } 38 | } 39 | } 40 | ``` 41 | 2. 在要启用`brick`的模块中找到`build.gradle`文件,在所有的`apply`下面添加一行: 42 | ``` 43 | apply plugin: 'com.android.application' 44 | ... 45 | //添加下面这行 46 | apply from: 'https://gitee.com/numeron/brick/raw/master/brick.gradle' 47 | ``` 48 | 3. 如果想加快编译速度,可以把该脚本下载下来放在与`settings.gradle`相同的文件夹里,然后把`apply`改为: 49 | ``` 50 | apply from: '../brick.gradle' 51 | ``` 52 | 53 | **注:第2步和第3步任选其一即可配置brick。** 54 | ### 使用 55 | 56 | #### **一、 @Provide注解的使用方法:** 57 | 1. 在你编写好的ViewModel子类上添加@Provide注解 58 | ``` 59 | @Provide 60 | class WxAuthorViewModel: ViewModel() { 61 | ... 62 | } 63 | ``` 64 | 2. 有3种方式让`brick`注解处理器开始工作: 65 | * 在`Terminal`终端上输入`gradlew :[ModuleName]:kaptDebugKotlin`运行脚本; 66 | * 在`AndroidStudio`右侧`Gradle`扩展栏中依次找到`[PrjectName] -> [ModuneName] -> Tasks -> other -> kaptDebugKotlin`并双击运行脚本; 67 | * `Ctrl + F9`编译整个项目。 68 | **以上三种方式任选其一即可运行`brick`注解处理器。** 69 | 3. 脚本运行结束后,会生成两个包级方法: 70 | * `lazyWxAuthorViewModel()`扩展方法,在`Activity`或`Fragment`中直接调用即可。 71 | * `getWxAuthorViewModel()`方法,在不方便使用`lazy`方法时,可使用此方法获取`ViewModel`的实例。 72 | **注:`lazyWxAuthorViewModel`方法就是对`getWxAuthorViewModel()`方法的包装。** 73 | 直接使用生成的方法,即可创建对应的`ViewModel`实例: 74 | ``` 75 | private val wxAuthorViewModel by lazyWxAuthorViewModel() 76 | ``` 77 | 或在`onCreate()`之后,通过`getWxAuthorViewModel`创建: 78 | ``` 79 | private lateinit var wxAuthorViewModel: WxAuthorViewModel 80 | 81 | override fun onCreate(savedInstanceState: Bundle?) { 82 | super.onCreate(savedInstanceState) 83 | wxAuthorViewModel = getWxAuthorViewModel(this) 84 | } 85 | ``` 86 | 87 | #### **二、 @Inject注解的使用方法** 88 | 89 | -2. **(必需)** 在获取`Retrofit`实例的方法上添加`@RetrofitInstance`,如: 90 | ``` 91 | @RetrofitInstance 92 | val retrofit: Retrofit by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { 93 | Retrofit.Builder() 94 | .client(okHttpClient) 95 | .baseUrl(WANDROID_BASE_URL) 96 | .addConverterFactory(MoshiConverterFactory.create()) 97 | .build() 98 | } 99 | 100 | val okHttpClient: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { 101 | val logInterceptor = HttpLoggingInterceptor() 102 | logInterceptor.level = HttpLoggingInterceptor.Level.BODY 103 | OkHttpClient.Builder() 104 | .addInterceptor(logInterceptor) 105 | .callTimeout(15, TimeUnit.SECONDS) 106 | .readTimeout(60, TimeUnit.SECONDS) 107 | .writeTimeout(60, TimeUnit.SECONDS) 108 | .connectTimeout(15, TimeUnit.SECONDS) 109 | .build() 110 | } 111 | ``` 112 | **注:`@RetrofitInstance`注解只能标记在`public`修饰的`val`属性上或方法上,`val`属性上或方法可以在`object 单例`或`companion object`中,也可以是包级属性/方法。** 113 | 114 | -1. **(可选)** 在获取`RoomDatabase`实例的属性或方法上标记`@RoomInstance`,如: 115 | ``` 116 | @RoomInstance 117 | val wandroidDatabase: WandroidDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { 118 | Room.databaseBuilder(CONTEXT, WandroidDatabase::class.java, "wandroid.db") 119 | .build() 120 | } 121 | ``` 122 | **注:`@RoomInstance`注解只能标记在`public`修饰的`val`属性上或方法上,`val`属性上或方法可以在`object 单例`或`companion object`中,也可以是包级属性/方法。** 123 | 124 | 0. 假设已有`Retrofit Api`接口和`WxAuthorRepo`类 125 | ``` 126 | interface WxAuthorApi { 127 | @GET("wxarticle/chapters/json ") 128 | suspend fun getWxAuthorList(): List 129 | } 130 | 131 | class WxAuthorRepo { 132 | ... 133 | } 134 | 135 | ``` 136 | 137 | 1. 在WxAuthorRepo中添加`lateinit var`修饰的`WxAuthorApi`字段,并用`@Inject`标记: 138 | ``` 139 | class WxAuthorRepo { 140 | 141 | @Inject 142 | lateinit var wxAuthorApi: WxAuthorApi 143 | 144 | } 145 | ``` 146 | 147 | 2. 在ViewModel中创建`lateinit var`修饰的`WxAuthorRepo`字段,并用`@Inject`标记: 148 | ``` 149 | @Provide 150 | class WxAuthorViewModel: ViewModel() { 151 | @Inject 152 | private lateinit var wxAuthorRepo: WxAuthorRepo 153 | } 154 | ``` 155 | 标记后,继续编写业务代码即可,所有被`@Inject`标记的字段,都会在编译期自动获取或创建实例,无需担心它们在何时被赋值。 156 | **注:虽然是`lateinit var`修饰的字段,但是不要尝试为它们赋值,这会导致致命的错误。** 157 | **注:`@Inject`可以注入的类型只有`Retrofit`的`api`接口和`ROOM`的`dao`接口、以及有无参构造的类。** 158 | 159 | #### **三、 多服务器或多端口的处理方法:** 160 | 假设有另一个Retrofit api接口,它的访问地址或端口与`baseUrl`中的不一样,此时,可以在`Retrofit`的`api`接口上添加`@Port`和`@Url`注解来设置它们的url或port。 161 | 162 | 1. `@Port`的使用: 163 | ``` 164 | @Port(1080) 165 | interface ArticleApi { 166 | 167 | @GET("wxarticle/list/{chapterId}/{page}/json") 168 | suspend fun getArticleList(@Path("chapterId") chapterId: Int, @Path("page") page: Int): Paged
169 | 170 | } 171 | ``` 172 | 添加此注解后,brick会在编译期根据`@RetrofitInstance`注解标记的`Retrofit`实例和`@Port`的端口号,重新创建一个`Retrofit`实例,并使用新的`Retrofit`实例创建`ArticleApi`的实例。 173 | 174 | 2. `@Url`的使用: 175 | ``` 176 | @Url("http://www.wanandroid.com:1080/") 177 | interface ArticleApi { 178 | @GET("wxarticle/list/{chapterId}/{page}/json") 179 | suspend fun getArticleList(@Path("chapterId") chapterId: Int, @Path("page") page: Int): Paged
180 | } 181 | ``` 182 | 与`@Port`的使用基本一致,实现的原理也是一样的。 183 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/numeron/brick/processor/InjectProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.processor 2 | 3 | import com.bennyhuo.aptutils.types.asElement 4 | import com.bennyhuo.aptutils.types.simpleName 5 | import com.numeron.brick.annotation.RoomInstance as ARoom 6 | import com.numeron.brick.core.* 7 | import com.numeron.brick.annotation.Inject as AInject 8 | import com.numeron.brick.annotation.RetrofitInstance as ARetrofit 9 | import com.sun.tools.javac.code.Symbol 10 | import javax.annotation.processing.RoundEnvironment 11 | import javax.lang.model.element.Element 12 | import javax.lang.model.element.ExecutableElement 13 | import javax.lang.model.element.Modifier 14 | import javax.lang.model.element.TypeElement 15 | 16 | class InjectProcessor(private val projectName: String, private val moduleName: String) { 17 | 18 | fun process(env: RoundEnvironment) { 19 | //处理RetrofitInstance注解 20 | retrofitProcess(env) 21 | 22 | //处理RoomInstance注解 23 | roomProcess(env) 24 | 25 | //获取所有被Inject注解标记的类,并输入到inject.json中 26 | Brick.addInject(projectName, moduleName, env.getElementsAnnotatedWith(AInject::class.java).map(::inject)) 27 | 28 | } 29 | 30 | private fun retrofitProcess(env: RoundEnvironment) { 31 | val annotatedElements = env.getElementsAnnotatedWith(ARetrofit::class.java) 32 | 33 | val element = annotatedElements.firstOrNull() ?: return 34 | 35 | val ownerClassSymbol = element.enclosingElement as Symbol.ClassSymbol 36 | 37 | val methodSymbol = ownerClassSymbol.members() 38 | .getElements { 39 | it is Symbol.MethodSymbol && it.returnType.toString() == RETROFIT_CLASS_NAME 40 | } 41 | .firstOrNull() as? Symbol.MethodSymbol 42 | ?: throw IllegalStateException("The method whose return type is Retrofit was not found.") 43 | 44 | val name = methodSymbol.simpleName() 45 | val owner = ownerClassSymbol.toString() 46 | val kind = getKind(ownerClassSymbol) 47 | 48 | Brick.setRetrofitInstance(projectName, RetrofitInstance(name, owner, kind)) 49 | } 50 | 51 | private fun inject(element: Element): Inject { 52 | val variableName = element.simpleName.toString() 53 | 54 | val asElement = element.asType().asElement() 55 | 56 | val variableType = asElement.toString() 57 | 58 | val ownerClassName = element.enclosingElement.toString() 59 | 60 | if (asElement is Symbol.ClassSymbol && asElement.isInterface) { 61 | //如果是接口,则判断是Room的DAO还是Retrofit的API 62 | var isRoomDao = false 63 | var isRetrofitApi = false 64 | 65 | asElement.members() 66 | //获取接口上所有的方法 67 | .getElements { 68 | it is ExecutableElement 69 | } 70 | .forEach { symbol -> 71 | //根据方法上的注解的包名来判断是Room的Dao接口,还是Retrofit的Api接口 72 | val annotationMirrors = symbol.annotationMirrors 73 | isRetrofitApi = !isRoomDao && annotationMirrors.any { 74 | it.toString().contains(RETROFIT_NAME_SPACE) 75 | } 76 | isRoomDao = !isRetrofitApi && annotationMirrors.any { 77 | it.toString().contains(ROOM_NAME_SPACE) 78 | } 79 | } 80 | return when { 81 | isRoomDao -> Inject(variableName, variableType, ownerClassName, InjectKind.Room) 82 | isRetrofitApi -> Inject(variableName, variableType, ownerClassName, InjectKind.Retrofit) 83 | else -> Inject(variableName, variableType, ownerClassName, InjectKind.Class) 84 | } 85 | } 86 | return Inject(variableName, variableType, ownerClassName, InjectKind.Class) 87 | } 88 | 89 | private fun getKind(element: Element): InjectableKind { 90 | if (element is Symbol.ClassSymbol) { 91 | //判断这个类是否是object单例 92 | element.members() 93 | .getElements { 94 | it.name.contentEquals("INSTANCE") 95 | } 96 | .firstOrNull { 97 | it.isStatic && it.modifiers.containsAll(listOf(Modifier.PUBLIC, Modifier.STATIC)) 98 | } 99 | ?.let { 100 | return InjectableKind.Object 101 | } 102 | 103 | //判断这个类是否是Companion对象 104 | val isSubClass = element.enclosingElement is TypeElement 105 | if (isSubClass) { 106 | val outerClass = element.enclosingElement as Symbol.TypeSymbol 107 | val companionMember = outerClass.members().getElementsByName(element.name) 108 | .firstOrNull() 109 | ?: throw IllegalStateException("Annotation @RetrofitInstance only use on object class and companion object.") 110 | if (companionMember is Symbol.VarSymbol) { 111 | val isPublicAndStatic = element.modifiers.containsAll(listOf(Modifier.PUBLIC, Modifier.STATIC)) 112 | if (!isPublicAndStatic) throw IllegalStateException("Annotation @RetrofitInstance only use on companion object.") 113 | return InjectableKind.Companion 114 | } 115 | } 116 | return InjectableKind.Class 117 | } else if (element is Symbol.MethodSymbol) { 118 | val isEmptyArg = element.params.isEmpty() 119 | if (isEmptyArg) { 120 | return if (element.name.startsWith("get")) { 121 | InjectableKind.Getter 122 | } else { 123 | InjectableKind.Method 124 | } 125 | } 126 | } else if (element is Symbol.VarSymbol) { 127 | return InjectableKind.Field 128 | } 129 | throw IllegalStateException() 130 | } 131 | 132 | private fun roomProcess(env: RoundEnvironment) { 133 | env.getElementsAnnotatedWith(ARoom::class.java).map { element -> 134 | 135 | val ownerClassSymbol = element.enclosingElement as Symbol.ClassSymbol 136 | 137 | val methodSymbol = ownerClassSymbol.members() 138 | .getElements { 139 | it is Symbol.MethodSymbol && isSubTypeOfRoomDatabase(it.returnType.asElement()) 140 | } 141 | .firstOrNull() as? Symbol.MethodSymbol 142 | ?: throw IllegalStateException("The method whose return type is Retrofit was not found.") 143 | 144 | val kind = getKind(ownerClassSymbol) 145 | val name = methodSymbol.simpleName() 146 | val owner = ownerClassSymbol.toString() 147 | val daoMethods = getMethods(methodSymbol.returnType.asElement()) 148 | RoomInstance(name, owner, kind, daoMethods) 149 | }.let { 150 | Brick.addRoomInstance(projectName, moduleName, it) 151 | } 152 | } 153 | 154 | private fun isSubTypeOfRoomDatabase(typeSymbol: Symbol.TypeSymbol): Boolean { 155 | val typeName = typeSymbol.toString() 156 | return when { 157 | typeName == ROOM_CLASS_NAME -> true 158 | typeSymbol is Symbol.ClassSymbol -> { 159 | val superTypeSymbol = typeSymbol.superclass.asElement() ?: return false 160 | isSubTypeOfRoomDatabase(superTypeSymbol) 161 | } 162 | else -> false 163 | } 164 | } 165 | 166 | private fun getMethods(typeSymbol: Symbol.TypeSymbol): List { 167 | return typeSymbol.members() 168 | .getElements { 169 | it is ExecutableElement 170 | } 171 | .map { 172 | it as Symbol.MethodSymbol 173 | } 174 | .filter { 175 | //方法的返回值是接口或抽象类 176 | it.returnType.isInterface || it.returnType.asElement().modifiers.contains(Modifier.ABSTRACT) 177 | } 178 | .map { 179 | val type = it.returnType.toString() 180 | val name = it.simpleName() 181 | DaoMethod(type, name) 182 | } 183 | } 184 | 185 | companion object { 186 | 187 | private const val ROOM_NAME_SPACE = "androidx.room" 188 | private const val RETROFIT_NAME_SPACE = "retrofit2.http" 189 | private const val RETROFIT_CLASS_NAME = "retrofit2.Retrofit" 190 | private const val ROOM_CLASS_NAME = "androidx.room.RoomDatabase" 191 | 192 | } 193 | 194 | } -------------------------------------------------------------------------------- /plugin/src/main/java/com/numeron/brick/plugin/BrickTransform.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.plugin 2 | 3 | import com.numeron.brick.annotation.Port 4 | import com.numeron.brick.annotation.Url 5 | import com.numeron.brick.core.* 6 | import javassist.* 7 | import javassist.bytecode.AccessFlag 8 | import org.gradle.api.Project 9 | import java.io.File 10 | import java.util.jar.JarOutputStream 11 | import java.util.zip.ZipEntry 12 | import java.util.zip.ZipFile 13 | 14 | class BrickTransform(project: Project) : BaseTransform(project) { 15 | 16 | private val projectName = project.rootProject.name 17 | 18 | private val moduleName = project.name 19 | 20 | private val classPool = ClassPool() 21 | 22 | private val modulePathList = mutableListOf() 23 | 24 | //获取待注入的元素列表 25 | private val injectMap by lazy { 26 | Brick.getInjectList(projectName).groupBy(Inject::owner).toMutableMap() 27 | } 28 | 29 | private val retrofitInstance by lazy { 30 | Brick.getRetrofitInstance(projectName) 31 | } 32 | 33 | private val roomInstances by lazy { 34 | Brick.getRoomInstance(projectName) 35 | } 36 | 37 | private val newRetrofitMethod by lazy { 38 | classPool.getCtClass("com.numeron.brick.plugin.RetrofitTemplate") 39 | .getDeclaredMethod("newRetrofit") 40 | } 41 | 42 | init { 43 | classPool.appendSystemPath() 44 | classPool.appendClassPath(LoaderClassPath(javaClass.classLoader)) 45 | } 46 | 47 | override fun getName(): String = "Brick" 48 | 49 | override fun processJar(jarName: String, jarFile: File) { 50 | if (jarName.startsWith(':') && Brick.isInjectModule(projectName, jarName.substring(1))) { 51 | //获取并创建jar解压路径 52 | val unzipPath = File(jarFile.parent, jarFile.name.substringBeforeLast('.')) 53 | if (!unzipPath.exists()) unzipPath.mkdirs() 54 | //解压jar 55 | val zipFile = ZipFile(jarFile) 56 | unzip(zipFile, unzipPath) 57 | //添加到池中 58 | classPool.appendClassPath(unzipPath.absolutePath) 59 | //添加到待处理模块列表中 60 | modulePathList.add(unzipPath) 61 | /* 不可以在这里处理,因为处理的顺序与依赖的顺序不一样,先加到列表中,最后再在onTransformed方法中统一处理 */ 62 | } 63 | } 64 | 65 | override fun processDirectory(classPath: File) { 66 | scanInjectModule(classPath) 67 | } 68 | 69 | override fun onTransformed() { 70 | modulePathList.forEach { classPath -> 71 | //处理模块的class 72 | scanInjectModule(classPath) 73 | //重新打包成jar文件并替换原文件 74 | val jarFile = File(classPath.parentFile, classPath.name + ".jar") 75 | zip(classPath, jarFile) 76 | } 77 | //处理完成后,清理缓存文件 78 | //Brick.clearProjectCache(projectName) 79 | } 80 | 81 | private fun scanInjectModule(classPath: File) { 82 | classPool.appendClassPath(classPath.absolutePath) 83 | 84 | classPath.forEachDeep { 85 | //获取此class的相对路径 86 | val classFilePath = it.absolutePath 87 | .substringBeforeLast('.') 88 | .removePrefix(classPath.absolutePath + File.separatorChar) 89 | .replace(File.separatorChar, '.') 90 | prepareInject(classPath.absolutePath, classFilePath) 91 | } 92 | } 93 | 94 | private fun prepareInject(classPath: String, classFilePath: String) { 95 | //从缓存列表中找到对应的注入项 96 | val injectList = injectMap.remove(classFilePath) 97 | if (injectList != null) { 98 | //获取CtClass 99 | var injectClass = classPool.get(classFilePath) 100 | //解冻 101 | val frozen = injectClass.isFrozen 102 | //frozen为false时,则开始注入,否则直接写到文件中 103 | if (!frozen) { 104 | //注入 105 | injectList.forEach { inject -> 106 | injectClass = dispatchInject(injectClass, inject) 107 | } 108 | } 109 | iLog("injected: ${injectClass.name}") 110 | //写入到文件中 111 | injectClass.writeFile(classPath) 112 | } 113 | } 114 | 115 | private fun dispatchInject(ctClass: CtClass, inject: Inject): CtClass { 116 | return when (inject.kind) { 117 | InjectKind.Room -> injectDao(ctClass, inject) 118 | InjectKind.Retrofit -> injectApi(ctClass, inject) 119 | InjectKind.Class -> injectInstance(ctClass, inject) 120 | } 121 | } 122 | 123 | private fun injectApi(ctClass: CtClass, inject: Inject): CtClass { 124 | val injectField = ctClass.getDeclaredField(inject.variableName) 125 | 126 | //移除已存在的、未初始化的字段 127 | ctClass.removeField(injectField) 128 | 129 | //添加final修饰符 130 | injectField.modifiers += Modifier.FINAL 131 | injectField.modifiers = AccessFlag.setPrivate(injectField.modifiers) 132 | 133 | //添加初始化代码,并重新添加到class中 134 | val retrofitGetter = retrofitInstance.getInstance() 135 | //导包 136 | import(retrofitInstance.owner) 137 | 138 | val variableType = inject.variableType 139 | val (url, port) = getUrlAndPort(inject) 140 | if ((!url.isNullOrEmpty() || port > 0) && !hasNewRetrofitMethod(ctClass)) { 141 | ctClass.addMethod(CtMethod(newRetrofitMethod, ctClass, null)) 142 | ctClass.addField(injectField, "newRetrofit($retrofitGetter, $port, $url).create(${variableType}.class);") 143 | } else { 144 | ctClass.addField(injectField, "${retrofitGetter}.create(${variableType}.class);") 145 | } 146 | 147 | //移除掉相关的setter方法 148 | removeSetter(ctClass, injectField.name) 149 | 150 | return ctClass 151 | } 152 | 153 | private fun getUrlAndPort(inject: Inject): Pair { 154 | val apiClass = classPool.getOrNull(inject.variableType) 155 | val annotationUrl = apiClass?.getAnnotation(Url::class.java) as? Url 156 | val url = annotationUrl?.value?.let { 157 | "\"$it\"" 158 | } 159 | val annotationPort = apiClass?.getAnnotation(Port::class.java) as? Port 160 | val port = annotationPort?.value ?: 0 161 | return url to port 162 | } 163 | 164 | private fun removeSetter(ctClass: CtClass, fieldName: String) { 165 | try { 166 | val methodName = "set" + fieldName[0].toUpperCase() + fieldName.substring(1) 167 | val ctMethod = ctClass.getDeclaredMethod(methodName) 168 | ctClass.removeMethod(ctMethod) 169 | } catch (ignore: Throwable) { 170 | } 171 | } 172 | 173 | private fun injectDao(ctClass: CtClass, inject: Inject): CtClass { 174 | val injectField = ctClass.getDeclaredField(inject.variableName) 175 | //移除已存在的、未初始化的字段 176 | ctClass.removeField(injectField) 177 | 178 | //添加final修饰符 179 | injectField.modifiers += Modifier.FINAL 180 | injectField.modifiers = AccessFlag.setPrivate(injectField.modifiers) 181 | 182 | //找到可以注入的ROOM对象和可用的DAO方法 183 | val roomInstance = roomInstances.find { 184 | it.getInvoke(inject.variableType) != null 185 | } ?: throw IllegalStateException("Type:${inject.variableType} not found.") 186 | 187 | //导包 188 | import(roomInstance.owner) 189 | //获取创建的代码 190 | val invoke = roomInstance.getInvoke(inject.variableType)!! 191 | //重新添加字段 192 | ctClass.addField(injectField, "$invoke;") 193 | 194 | //移除掉setter方法 195 | removeSetter(ctClass, injectField.name) 196 | 197 | return ctClass 198 | } 199 | 200 | private fun injectInstance(ctClass: CtClass, inject: Inject): CtClass { 201 | val injectField = ctClass.getDeclaredField(inject.variableName) 202 | //移除旧的字段 203 | ctClass.removeField(injectField) 204 | //修改修饰符 205 | injectField.modifiers += Modifier.FINAL 206 | injectField.modifiers = AccessFlag.setPrivate(injectField.modifiers) 207 | //重新添加字段,并通过默认构造创建默认值。 208 | ctClass.addField(injectField, "new ${injectField.type.name}();") 209 | return ctClass 210 | } 211 | 212 | /** 检查是否已经导包,如果没有,则导入这个包下的类,如果已导入,则忽略 */ 213 | private fun import(classPath: String) { 214 | if (!classPool.importedPackages.asSequence().contains(classPath)) { 215 | classPool.importPackage(classPath) 216 | } 217 | } 218 | 219 | private fun hasNewRetrofitMethod(ctClass: CtClass): Boolean { 220 | return try { 221 | ctClass.getDeclaredMethod("newRetrofit", newRetrofitMethod.parameterTypes) 222 | true 223 | } catch (throwable: Throwable) { 224 | false 225 | } 226 | } 227 | 228 | /** 229 | * 解压jar文件 230 | * @param zipFile ZipFile 要解压的文件 231 | * @param outputPath File 要输入的目录,必需是文件夹 232 | */ 233 | private fun unzip(zipFile: ZipFile, outputPath: File) { 234 | for (zipEntry in zipFile.entries()) { 235 | if (zipEntry.isDirectory) { 236 | continue 237 | } 238 | //获取并创建存放目录 239 | val entryFile = File(outputPath, zipEntry.name.replace('/', File.separatorChar)) 240 | if (!entryFile.parentFile.exists()) entryFile.parentFile.mkdirs() 241 | //通过流复制到存放文件中 242 | zipFile.getInputStream(zipEntry).copyTo(entryFile.outputStream()) 243 | } 244 | } 245 | 246 | /** 247 | * 压缩一个文件夹为jar文件 248 | * @param inputPath File 要压缩的文件夹 249 | * @param outputFile File 压缩后输入的文件,是一个文件 250 | */ 251 | private fun zip(inputPath: File, outputFile: File) { 252 | JarOutputStream(outputFile.outputStream()).use { jarOutputStream -> 253 | inputPath.forEachDeep { file -> 254 | val zipEntry = ZipEntry(file.path.removePrefix(inputPath.path + File.separatorChar)) 255 | jarOutputStream.putNextEntry(zipEntry) 256 | file.inputStream().copyTo(jarOutputStream) 257 | } 258 | } 259 | } 260 | 261 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/numeron/brick/processor/ProvideGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.numeron.brick.processor 2 | 3 | import com.bennyhuo.aptutils.types.asKotlinTypeName 4 | import com.bennyhuo.aptutils.types.packageName 5 | import com.bennyhuo.aptutils.types.simpleName 6 | import com.squareup.kotlinpoet.* 7 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 8 | import com.sun.tools.javac.code.Symbol 9 | import com.sun.tools.javac.code.Type 10 | import me.eugeniomarletti.kotlin.metadata.* 11 | import org.jetbrains.annotations.NotNull 12 | import org.jetbrains.annotations.Nullable 13 | import javax.annotation.processing.Filer 14 | import javax.annotation.processing.ProcessingEnvironment 15 | import javax.lang.model.type.WildcardType 16 | 17 | class ProvideGenerator(private val classSymbol: Symbol.ClassSymbol, methodSymbol: Symbol.MethodSymbol) { 18 | 19 | private val ktClassName = classSymbol.asType().asKotlinTypeName() 20 | 21 | private val parameters: List 22 | 23 | private val propertiesSpec: List 24 | 25 | init { 26 | //获取构造参数中的名字与具体类型 27 | typeMapper.clear() 28 | val metadata = classSymbol.kotlinMetadata 29 | if (metadata is KotlinClassMetadata) { 30 | val classData = metadata.data 31 | val (nameResolver, classProto) = classData 32 | classProto.constructorList 33 | .single { it.isPrimary } 34 | .valueParameterList 35 | .forEach { valueParameter -> 36 | val name = nameResolver.getString(valueParameter.name) 37 | val fqClassName = valueParameter.type.extractFullName(classData).replace("`", "") 38 | typeMapper[name] = fqClassName 39 | } 40 | } 41 | 42 | methodSymbol.params() 43 | .map { 44 | val name = it.simpleName() 45 | val type = asKotlinKnownTypeName(it) 46 | 47 | val parameterSpec = ParameterSpec.builder(name, type).build() 48 | 49 | val propertySpec = PropertySpec.builder(name, type) 50 | .addModifiers(KModifier.PRIVATE) 51 | .initializer(name) 52 | .build() 53 | propertySpec to parameterSpec 54 | } 55 | .unzip() 56 | .let { (propertiesSpec, parameters) -> 57 | this.propertiesSpec = propertiesSpec 58 | this.parameters = parameters 59 | } 60 | } 61 | 62 | fun generate(filer: Filer) { 63 | 64 | val packageName = classSymbol.packageName() 65 | 66 | val fileName = classSymbol.simpleName.toString() + 's' 67 | 68 | val jvmNameAnnotation = AnnotationSpec.builder(JvmName::class) 69 | .addMember("%S", fileName) 70 | .useSiteTarget(AnnotationSpec.UseSiteTarget.FILE) 71 | .build() 72 | 73 | FileSpec.builder(packageName, fileName) 74 | .addFunction(generateLazyFunction()) //kotlin lazy方法 75 | .addFunction(generateGetFunction()) //java provide方法 76 | .addType(generateFactoryClass()) //ViewModelFactory 77 | .addType(generateLazyClass()) //LazyViewModel 78 | .addAnnotation(jvmNameAnnotation) 79 | .build() 80 | .writeTo(filer) 81 | } 82 | 83 | private fun generateLazyFunction(): FunSpec { 84 | 85 | val className = classSymbol.simpleName.toString() 86 | 87 | val lazyParameterizedTypeName = LAZY_INTERFACE_TYPE_NAME.parameterizedBy(ktClassName) 88 | 89 | val parameters = parameters.joinToString(transform = ParameterSpec::name).let { 90 | if (it.isEmpty()) it else ", $it" 91 | } 92 | 93 | return FunSpec.builder("lazy$className") 94 | .receiver(VIEW_MODEL_STORE_OWNER_TYPE_NAME) 95 | .addParameters(this.parameters) 96 | .returns(lazyParameterizedTypeName) 97 | .addStatement("return Lazy$className(this$parameters)") 98 | .build() 99 | } 100 | 101 | private fun generateLazyClass(): TypeSpec { 102 | val simpleName = classSymbol.simpleName.toString() 103 | 104 | val lazyClassName = "Lazy$simpleName" 105 | 106 | val constructorFunSpec = FunSpec.constructorBuilder() 107 | .addParameter("owner", VIEW_MODEL_STORE_OWNER_TYPE_NAME) 108 | .addParameters(parameters) 109 | .build() 110 | 111 | val parameters = parameters.joinToString(transform = ParameterSpec::name) 112 | 113 | val valueGetterFunSpec = FunSpec.getterBuilder() 114 | .beginControlFlow("if(_value == null)") 115 | .addStatement("_value = owner.get$simpleName(${parameters})") 116 | .endControlFlow() 117 | .addStatement("return _value!!") 118 | .build() 119 | 120 | val valuePropertySpec = PropertySpec.builder("value", ktClassName) 121 | .addModifiers(KModifier.OVERRIDE) 122 | .getter(valueGetterFunSpec) 123 | .build() 124 | 125 | val valuePrivatePropertySpec = PropertySpec.builder("_value", ktClassName.copy(true)) 126 | .addModifiers(KModifier.PRIVATE) 127 | .initializer("null") 128 | .mutable() 129 | .build() 130 | 131 | val isInitializedFunSpec = FunSpec.builder("isInitialized") 132 | .addModifiers(KModifier.OVERRIDE) 133 | .returns(Boolean::class.java) 134 | .addStatement("return _value != null") 135 | .build() 136 | 137 | val lazyParameterizedTypeName = LAZY_INTERFACE_TYPE_NAME.parameterizedBy(ktClassName) 138 | 139 | val ownerPropertySpec = PropertySpec.builder("owner", VIEW_MODEL_STORE_OWNER_TYPE_NAME) 140 | .addModifiers(KModifier.PRIVATE) 141 | .initializer("owner") 142 | .build() 143 | 144 | return TypeSpec.classBuilder(lazyClassName) 145 | .addProperty(valuePrivatePropertySpec) 146 | .addProperty(valuePropertySpec) 147 | .addFunction(isInitializedFunSpec) 148 | .primaryConstructor(constructorFunSpec) 149 | .addModifiers(KModifier.PRIVATE) 150 | .addProperty(ownerPropertySpec) 151 | .addProperties(propertiesSpec) 152 | .addSuperinterface(lazyParameterizedTypeName) 153 | .build() 154 | } 155 | 156 | private fun generateGetFunction(): FunSpec { 157 | val simpleName = classSymbol.simpleName 158 | return FunSpec.builder("get$simpleName") 159 | .addParameters(parameters) 160 | .receiver(VIEW_MODEL_STORE_OWNER_TYPE_NAME) 161 | .addStatement("val factory = ${simpleName}Factory(${parameters.joinToString(transform = ParameterSpec::name)})") 162 | .addStatement("return %T(this, factory).get(%T::class.java)", VIEW_MODEL_PROVIDER_TYPE_NAME, ktClassName) 163 | .returns(ktClassName) 164 | .build() 165 | } 166 | 167 | private fun generateFactoryClass(): TypeSpec { 168 | 169 | val suppressAnnotationSpec = AnnotationSpec.builder(Suppress::class.java) 170 | .addMember("%S", "UNCHECKED_CAST") 171 | .build() 172 | 173 | val className = classSymbol.simpleName.toString() 174 | 175 | val factoryClassName = className + "Factory" 176 | 177 | val typeVariableName = TypeVariableName("VM", VIEW_MODEL_TYPE_NAME) 178 | 179 | val parameterizedTypeName = CLASS_TYPE_NAME.parameterizedBy(typeVariableName) 180 | 181 | val constructorSpec = FunSpec.constructorBuilder() 182 | .addParameters(parameters) 183 | .build() 184 | 185 | val createFunSpec = FunSpec.builder("create") 186 | .addModifiers(KModifier.OVERRIDE) 187 | .addParameter("clazz", parameterizedTypeName) 188 | .addAnnotation(suppressAnnotationSpec) 189 | .addTypeVariable(typeVariableName) 190 | .returns(typeVariableName) 191 | .addStatement("return ${className}(${parameters.joinToString(transform = ParameterSpec::name)}) as VM") 192 | .build() 193 | 194 | return TypeSpec.classBuilder(factoryClassName) 195 | .addSuperinterface(VIEW_MODEL_FACTORY_INTERFACE_TYPE_NAME) 196 | .addProperties(propertiesSpec) 197 | .addModifiers(KModifier.PRIVATE) 198 | .primaryConstructor(constructorSpec) 199 | .addFunction(createFunSpec) 200 | .build() 201 | } 202 | 203 | companion object : KotlinMetadataUtils { 204 | 205 | private val CLASS_TYPE_NAME = Class::class.java.asClassName() 206 | private val LAZY_INTERFACE_TYPE_NAME = ClassName("kotlin", "Lazy") 207 | private val VIEW_MODEL_TYPE_NAME = ClassName("androidx.lifecycle", "ViewModel") 208 | private val VIEW_MODEL_PROVIDER_TYPE_NAME = ClassName("androidx.lifecycle", "ViewModelProvider") 209 | private val VIEW_MODEL_STORE_OWNER_TYPE_NAME = ClassName("androidx.lifecycle", "ViewModelStoreOwner") 210 | private val VIEW_MODEL_FACTORY_INTERFACE_TYPE_NAME = ClassName("androidx.lifecycle", "ViewModelProvider", "Factory") 211 | 212 | private const val FUNCTION_PACKAGE = "kotlin.jvm.functions" 213 | 214 | private val typeMapper = mutableMapOf() 215 | 216 | private fun asKotlinKnownTypeName(varSymbol: Symbol.VarSymbol): TypeName { 217 | val varType = varSymbol.type 218 | val notNull = varSymbol.getAnnotation(NotNull::class.java) 219 | val nullable = varSymbol.getAnnotation(Nullable::class.java) 220 | if (varType.isParameterized && varType.toString().startsWith(FUNCTION_PACKAGE)) { 221 | //将泛型中的参数切割成一个String列表 222 | val simpleName = varSymbol.simpleName() 223 | val parameterizedNullState = typeMapper[simpleName] 224 | ?.substringAfter('<') 225 | ?.substringBeforeLast('>') 226 | ?.split(',') 227 | ?: emptyList() 228 | //将类型转成KotlinTypeName,然后根据上面的参数列表判断是否可空 229 | val typeArguments = varType.typeArguments.mapIndexed { index, type -> 230 | val kotlinTypeName = type.asTypeName() 231 | val isNullable = parameterizedNullState.getOrNull(index)?.endsWith('?') ?: false 232 | kotlinTypeName.copy(nullable = isNullable) 233 | } 234 | val returnType = typeArguments.last() 235 | val params = if (typeArguments.size < 2) emptyArray() else typeArguments.subList(0, typeArguments.size - 1).toTypedArray() 236 | return LambdaTypeName.get(null, *params, returnType = returnType).copy(nullable = nullable != null && notNull == null) 237 | } 238 | return varType.asTypeName().copy(nullable = nullable != null && notNull == null) 239 | } 240 | 241 | private fun Type.asTypeName(): TypeName { 242 | val typeMirror = if (this is WildcardType) superBound ?: extendsBound else this 243 | val kotlinTypeName = typeMirror.asKotlinTypeName() 244 | return when (kotlinTypeName.toString()) { 245 | "java.lang.Integer" -> INT 246 | "java.lang.Double" -> DOUBLE 247 | "java.lang.Float" -> FLOAT 248 | "java.lang.Long" -> LONG 249 | "java.lang.Byte" -> BYTE 250 | "java.lang.Short" -> SHORT 251 | else -> kotlinTypeName 252 | } 253 | } 254 | 255 | override val processingEnv: ProcessingEnvironment 256 | get() = BrickProcessor.processingEnv 257 | 258 | } 259 | 260 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------