refactor: merge to BetterAndroid new usage

This commit is contained in:
2025-08-03 23:27:46 +08:00
parent 8990e03e97
commit ee97222bcb
15 changed files with 163 additions and 25 deletions

View File

@@ -59,11 +59,13 @@ libraries:
version-ref: <this>::kotlinpoet version-ref: <this>::kotlinpoet
com.highcapable.betterandroid: com.highcapable.betterandroid:
ui-component: ui-component:
version: 1.0.7 version: 1.0.8
ui-component-adapter:
version: 1.0.0
ui-extension: ui-extension:
version: 1.0.6 version: 1.0.7
system-extension: system-extension:
version: 1.0.2 version: 1.0.3
org.lsposed.hiddenapibypass: org.lsposed.hiddenapibypass:
hiddenapibypass: hiddenapibypass:
version: 6.1 version: 6.1
@@ -74,7 +76,7 @@ libraries:
version: 1.0.1 version: 1.0.1
com.highcapable.pangutext: com.highcapable.pangutext:
pangutext-android: pangutext-android:
version: 1.0.2 version: 1.0.3
androidx.core: androidx.core:
core: core:
version: 1.16.0 version: 1.16.0

View File

@@ -39,15 +39,20 @@ fun KSDeclaration.getSimpleNameString(): String {
fun KSClassDeclaration.isSubclassOf(superType: KSType?): Boolean { fun KSClassDeclaration.isSubclassOf(superType: KSType?): Boolean {
if (superType == null) return false if (superType == null) return false
if (this == superType.declaration) return true if (this == superType.declaration) return true
superTypes.forEach { parent -> superTypes.forEach { parent ->
val resolvedParent = parent.resolve() val resolvedParent = parent.resolve()
// Direct match. // Direct match.
if (resolvedParent == superType) return true if (resolvedParent == superType) return true
val parentDeclaration = resolvedParent.declaration as? KSClassDeclaration val parentDeclaration = resolvedParent.declaration as? KSClassDeclaration
?: return@forEach ?: return@forEach
// Recursively check the parent class. // Recursively check the parent class.
if (parentDeclaration.isSubclassOf(superType)) return true if (parentDeclaration.isSubclassOf(superType)) return true
}; return false }
return false
} }
fun KSClassDeclaration.asType() = asType(emptyList()) fun KSClassDeclaration.asType() = asType(emptyList())

View File

@@ -71,6 +71,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
override fun startProcess(resolver: Resolver) { override fun startProcess(resolver: Resolver) {
Processor.init(logger, resolver) Processor.init(logger, resolver)
val dependencies = Dependencies(aggregating = true, *resolver.getAllFiles().toList().toTypedArray()) val dependencies = Dependencies(aggregating = true, *resolver.getAllFiles().toList().toTypedArray())
resolver.getSymbolsWithAnnotation(HikageViewSpec.CLASS) resolver.getSymbolsWithAnnotation(HikageViewSpec.CLASS)
.filterIsInstance<KSClassDeclaration>() .filterIsInstance<KSClassDeclaration>()
@@ -79,9 +80,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
ksClass.annotations.forEach { ksClass.annotations.forEach {
// Get annotation parameters. // Get annotation parameters.
val (annotation, declaration) = HikageViewSpec.create(resolver, it, ksClass) val (annotation, declaration) = HikageViewSpec.create(resolver, it, ksClass)
performers += Performer(annotation, declaration) performers += Performer(annotation, declaration)
} }
} }
resolver.getSymbolsWithAnnotation(HikageViewDeclarationSpec.CLASS) resolver.getSymbolsWithAnnotation(HikageViewDeclarationSpec.CLASS)
.filterIsInstance<KSClassDeclaration>() .filterIsInstance<KSClassDeclaration>()
.distinctBy { it.qualifiedName } .distinctBy { it.qualifiedName }
@@ -89,14 +92,17 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
ksClass.annotations.forEach { ksClass.annotations.forEach {
// Get annotation parameters. // Get annotation parameters.
val (annotation, declaration) = HikageViewDeclarationSpec.create(resolver, it, ksClass) val (annotation, declaration) = HikageViewDeclarationSpec.create(resolver, it, ksClass)
performers += Performer(annotation, declaration) performers += Performer(annotation, declaration)
} }
} }
processPerformer(dependencies) processPerformer(dependencies)
} }
private fun processPerformer(dependencies: Dependencies) { private fun processPerformer(dependencies: Dependencies) {
val duplicatedItems = performers.groupBy { it.declaration.key }.filter { it.value.size > 1 }.flatMap { it.value } val duplicatedItems = performers.groupBy { it.declaration.key }.filter { it.value.size > 1 }.flatMap { it.value }
require(duplicatedItems.isEmpty()) { require(duplicatedItems.isEmpty()) {
"Discover duplicate @HikageView or @HikageViewDeclaration's class name or alias definitions, " + "Discover duplicate @HikageView or @HikageViewDeclaration's class name or alias definitions, " +
"you can re-specify the class name using the `alias` parameter.\n" + "you can re-specify the class name using the `alias` parameter.\n" +
@@ -109,25 +115,30 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
private fun generateCodeFile(dependencies: Dependencies, performer: Performer) { private fun generateCodeFile(dependencies: Dependencies, performer: Performer) {
val classNameSet = performer.declaration.alias ?: performer.declaration.className val classNameSet = performer.declaration.alias ?: performer.declaration.className
val fileName = "_$classNameSet" val fileName = "_$classNameSet"
val viewClass = performer.declaration.toClassName().let { val viewClass = performer.declaration.toClassName().let {
val packageName = it.packageName val packageName = it.packageName
val simpleName = it.simpleName val simpleName = it.simpleName
val topClassName = if (simpleName.contains(".")) simpleName.split(".")[0] else null val topClassName = if (simpleName.contains(".")) simpleName.split(".")[0] else null
// com.example.MyViewScope // com.example.MyViewScope
// com.example.MyViewScope.MyView // com.example.MyViewScope.MyView
topClassName?.let { name -> ClassName(packageName, name) } to it topClassName?.let { name -> ClassName(packageName, name) } to it
} }
val lparamsClass = performer.annotation.lparams?.let { val lparamsClass = performer.annotation.lparams?.let {
val packageName = it.packageName.asString() val packageName = it.packageName.asString()
val subClassName = it.getSimpleNameString() val subClassName = it.getSimpleNameString()
val simpleName = it.simpleName.asString() val simpleName = it.simpleName.asString()
val topClassName = subClassName.replace(".$simpleName", "") val topClassName = subClassName.replace(".$simpleName", "")
// android.view.ViewGroup // android.view.ViewGroup
// android.view.ViewGroup.LayoutParams // android.view.ViewGroup.LayoutParams
(if (topClassName != subClassName) (if (topClassName != subClassName)
ClassName(packageName, topClassName) ClassName(packageName, topClassName)
else null) to ClassName(packageName, subClassName) else null) to ClassName(packageName, subClassName)
} }
val packageName = "$PACKAGE_NAME_PREFIX.${performer.declaration.packageName}" val packageName = "$PACKAGE_NAME_PREFIX.${performer.declaration.packageName}"
val fileSpec = FileSpec.builder(packageName, fileName).apply { val fileSpec = FileSpec.builder(packageName, fileName).apply {
addFileComment( addFileComment(
@@ -140,6 +151,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
**DO NOT EDIT THIS FILE MANUALLY** **DO NOT EDIT THIS FILE MANUALLY**
""".trimIndent() """.trimIndent()
) )
addAnnotation( addAnnotation(
AnnotationSpec.builder(Suppress::class) AnnotationSpec.builder(Suppress::class)
.addMember("%S, %S, %S", "unused", "FunctionName", "DEPRECATION") .addMember("%S, %S, %S", "unused", "FunctionName", "DEPRECATION")
@@ -150,17 +162,22 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
.addMember("%S", "${classNameSet}Performer") .addMember("%S", "${classNameSet}Performer")
.build() .build()
) )
addImport(DeclaredSymbol.ANDROID_VIEW_PACKAGE_NAME, DeclaredSymbol.ANDROID_VIEW_GROUP_CLASS_NAME) addImport(DeclaredSymbol.ANDROID_VIEW_PACKAGE_NAME, DeclaredSymbol.ANDROID_VIEW_GROUP_CLASS_NAME)
addImport(DeclaredSymbol.HIKAGE_CORE_PACKAGE_NAME, DeclaredSymbol.HIKAGE_CLASS_NAME) addImport(DeclaredSymbol.HIKAGE_CORE_PACKAGE_NAME, DeclaredSymbol.HIKAGE_CLASS_NAME)
// Kotlin's import rule is to introduce the parent class that also needs to be introduced at the same time. // Kotlin's import rule is to introduce the parent class that also needs to be introduced at the same time.
// If a child class exists, it needs to import the parent class, // If a child class exists, it needs to import the parent class,
// and kotlinpoet will not perform this operation automatically. // and kotlinpoet will not perform this operation automatically.
viewClass.first?.let { addImport(it.packageName, it.simpleName) } viewClass.first?.let { addImport(it.packageName, it.simpleName) }
lparamsClass?.first?.let { addImport(it.packageName, it.simpleName) } lparamsClass?.first?.let { addImport(it.packageName, it.simpleName) }
// May conflict with other [LayoutParams]. // May conflict with other [LayoutParams].
lparamsClass?.second?.let { addAliasedImport(it, it.getTypedSimpleName()) } lparamsClass?.second?.let { addAliasedImport(it, it.getTypedSimpleName()) }
addAliasedImport(ViewGroupLpClass, ViewGroupLpClass.getTypedSimpleName()) addAliasedImport(ViewGroupLpClass, ViewGroupLpClass.getTypedSimpleName())
addAliasedImport(HikageLparamClass, HikageLparamClass.getTypedSimpleName()) addAliasedImport(HikageLparamClass, HikageLparamClass.getTypedSimpleName())
addFunction(FunSpec.builder(classNameSet).apply { addFunction(FunSpec.builder(classNameSet).apply {
addKdoc( addKdoc(
""" """
@@ -170,10 +187,12 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
@return [${performer.declaration.className}] @return [${performer.declaration.className}]
""".trimIndent() """.trimIndent()
) )
addAnnotation(HikageableClass) addAnnotation(HikageableClass)
addModifiers(KModifier.INLINE) addModifiers(KModifier.INLINE)
addTypeVariable(TypeVariableName(name = "LP", ViewGroupLpClass).copy(reified = true)) addTypeVariable(TypeVariableName(name = "LP", ViewGroupLpClass).copy(reified = true))
receiver(PerformerClass.parameterizedBy(TypeVariableName("LP"))) receiver(PerformerClass.parameterizedBy(TypeVariableName("LP")))
addParameter( addParameter(
ParameterSpec.builder(name = "lparams", HikageLparamClass.copy(nullable = true)) ParameterSpec.builder(name = "lparams", HikageLparamClass.copy(nullable = true))
.defaultValue("null") .defaultValue("null")
@@ -203,9 +222,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
) )
addStatement("return ViewGroup<${performer.declaration.className}, ${it.simpleName}>(lparams, id, init, performer)") addStatement("return ViewGroup<${performer.declaration.className}, ${it.simpleName}>(lparams, id, init, performer)")
} ?: addStatement("return View<${performer.declaration.className}>(lparams, id, init)") } ?: addStatement("return View<${performer.declaration.className}>(lparams, id, init)")
returns(viewClass.second) returns(viewClass.second)
}.build()) }.build())
}.build() }.build()
// May already exists, no need to generate. // May already exists, no need to generate.
runCatching { runCatching {
fileSpec.writeTo(codeGenerator, dependencies) fileSpec.writeTo(codeGenerator, dependencies)
@@ -239,6 +260,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
ksType.declaration.qualifiedName?.asString() != Any::class.qualifiedName ksType.declaration.qualifiedName?.asString() != Any::class.qualifiedName
} }
var resolvedLparams = lparamsType?.declaration?.getClassDeclaration(resolver) var resolvedLparams = lparamsType?.declaration?.getClassDeclaration(resolver)
when { when {
// If the current view is not a view group but the lparams parameter is declared, // If the current view is not a view group but the lparams parameter is declared,
// remove the lparams parameter. // remove the lparams parameter.
@@ -253,6 +275,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
// set the default type parameter for it. // set the default type parameter for it.
declaration.isViewGroup && resolvedLparams == null -> resolvedLparams = lparamsDeclaration declaration.isViewGroup && resolvedLparams == null -> resolvedLparams = lparamsDeclaration
} }
// Verify layout parameter class. // Verify layout parameter class.
if (resolvedLparams != null) require(resolvedLparams.isSubclassOf(lparamsDeclaration.asType())) { if (resolvedLparams != null) require(resolvedLparams.isSubclassOf(lparamsDeclaration.asType())) {
val resolvedLparamsName = resolvedLparams.qualifiedName?.asString() val resolvedLparamsName = resolvedLparams.qualifiedName?.asString()
@@ -271,17 +294,21 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
val packageName = ksClass.packageName.asString() val packageName = ksClass.packageName.asString()
val className = ksClass.getSimpleNameString() val className = ksClass.getSimpleNameString()
val isViewGroup = ksClass.isSubclassOf(viewGroupDeclaration.asType()) val isViewGroup = ksClass.isSubclassOf(viewGroupDeclaration.asType())
var _alias = alias var _alias = alias
// If no alias name is set, if the class name contains a subclass, // If no alias name is set, if the class name contains a subclass,
// replace it with an underscore and use it as an alias name. // replace it with an underscore and use it as an alias name.
if (_alias.isNullOrBlank() && className.contains(".")) if (_alias.isNullOrBlank() && className.contains("."))
_alias = className.replace(".", "_") _alias = className.replace(".", "_")
_alias = _alias?.takeIf { it.isNotBlank() } _alias = _alias?.takeIf { it.isNotBlank() }
val declaration = ViewDeclaration(packageName, className, _alias, isViewGroup, baseType) val declaration = ViewDeclaration(packageName, className, _alias, isViewGroup, baseType)
// Verify the legality of the class name. // Verify the legality of the class name.
if (!_alias.isNullOrBlank()) require(ClassDetector.verify(_alias)) { if (!_alias.isNullOrBlank()) require(ClassDetector.verify(_alias)) {
"Declares @$tagName's alias \"$_alias\" is illegal.\n${declaration.locateDesc}" "Declares @$tagName's alias \"$_alias\" is illegal.\n${declaration.locateDesc}"
} }
// [ViewGroup] cannot be new instance. // [ViewGroup] cannot be new instance.
require(ksClass != viewGroupDeclaration) { require(ksClass != viewGroupDeclaration) {
"Declares @$tagName's class must not be a directly \"${DeclaredSymbol.ANDROID_VIEW_GROUP_CLASS}\".\n${declaration.locateDesc}" "Declares @$tagName's class must not be a directly \"${DeclaredSymbol.ANDROID_VIEW_GROUP_CLASS}\".\n${declaration.locateDesc}"
@@ -290,6 +317,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
require(ksClass.isSubclassOf(viewDeclaration.asType())) { require(ksClass.isSubclassOf(viewDeclaration.asType())) {
"Declares @$tagName's class must be subclass of \"${DeclaredSymbol.ANDROID_VIEW_CLASS}\".\n${declaration.locateDesc}" "Declares @$tagName's class must be subclass of \"${DeclaredSymbol.ANDROID_VIEW_CLASS}\".\n${declaration.locateDesc}"
} }
return declaration return declaration
} }
} }
@@ -332,9 +360,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
val alias = annotation.arguments.getOrNull<String>("alias") val alias = annotation.arguments.getOrNull<String>("alias")
val requireInit = annotation.arguments.getOrNull<Boolean>("requireInit") ?: false val requireInit = annotation.arguments.getOrNull<Boolean>("requireInit") ?: false
val requirePerformer = annotation.arguments.getOrNull<Boolean>("requirePerformer") ?: false val requirePerformer = annotation.arguments.getOrNull<Boolean>("requirePerformer") ?: false
// Solve the actual content of the annotation parameters. // Solve the actual content of the annotation parameters.
val declaration = Processor.createViewDeclaration(NAME, alias, ksClass) val declaration = Processor.createViewDeclaration(NAME, alias, ksClass)
val resolvedLparams = Processor.resolvedLparamsDeclaration(NAME, resolver, declaration, lparams) val resolvedLparams = Processor.resolvedLparamsDeclaration(NAME, resolver, declaration, lparams)
return HikageViewSpec(resolvedLparams, alias, requireInit, requirePerformer) to declaration return HikageViewSpec(resolvedLparams, alias, requireInit, requirePerformer) to declaration
} }
} }
@@ -363,9 +393,11 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
val alias = annotation.arguments.getOrNull<String>("alias") val alias = annotation.arguments.getOrNull<String>("alias")
val requireInit = annotation.arguments.getOrNull<Boolean>("requireInit") ?: false val requireInit = annotation.arguments.getOrNull<Boolean>("requireInit") ?: false
val requirePerformer = annotation.arguments.getOrNull<Boolean>("requirePerformer") ?: false val requirePerformer = annotation.arguments.getOrNull<Boolean>("requirePerformer") ?: false
// Solve the actual content of the annotation parameters. // Solve the actual content of the annotation parameters.
val resolvedView = view?.declaration?.getClassDeclaration(resolver) ?: error("Internal error.") val resolvedView = view?.declaration?.getClassDeclaration(resolver) ?: error("Internal error.")
val declaration = Processor.createViewDeclaration(NAME, alias, resolvedView, ksClass) val declaration = Processor.createViewDeclaration(NAME, alias, resolvedView, ksClass)
// Only object classes can be used as view declarations. // Only object classes can be used as view declarations.
require(ksClass.classKind == ClassKind.OBJECT) { require(ksClass.classKind == ClassKind.OBJECT) {
"Declares @$NAME's class must be an object.\n${declaration.locateDesc}" "Declares @$NAME's class must be an object.\n${declaration.locateDesc}"
@@ -373,6 +405,7 @@ class HikageViewGenerator(override val environment: SymbolProcessorEnvironment)
require(!ksClass.isCompanionObject) { require(!ksClass.isCompanionObject) {
"Declares @$NAME's class must not be a companion object.\n${declaration.locateDesc}" "Declares @$NAME's class must not be a companion object.\n${declaration.locateDesc}"
} }
val resolvedLparams = Processor.resolvedLparamsDeclaration(NAME, resolver, declaration, lparams) val resolvedLparams = Processor.resolvedLparamsDeclaration(NAME, resolver, declaration, lparams)
return HikageViewDeclarationSpec(resolvedView, resolvedLparams, alias, requireInit, requirePerformer) to declaration return HikageViewDeclarationSpec(resolvedView, resolvedLparams, alias, requireInit, requirePerformer) to declaration
} }

View File

@@ -60,6 +60,7 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) { override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
if (node.selector !is UBinaryExpressionWithType) return if (node.selector !is UBinaryExpressionWithType) return
val castExpr = node.selector as UBinaryExpressionWithType val castExpr = node.selector as UBinaryExpressionWithType
visitAndLint(context, castExpr, node) visitAndLint(context, castExpr, node)
} }
@@ -75,11 +76,14 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
) { ) {
// Get the parent node, if it is wrapped with brackets will also be included. // Get the parent node, if it is wrapped with brackets will also be included.
val locationNode = node.uastParent as? UParenthesizedExpression ?: node val locationNode = node.uastParent as? UParenthesizedExpression ?: node
val receiver = parent?.receiver ?: node.operand val receiver = parent?.receiver ?: node.operand
val receiverType = (node.operand as? UArrayAccessExpression)?.receiver?.getExpressionType() ?: return val receiverType = (node.operand as? UArrayAccessExpression)?.receiver?.getExpressionType() ?: return
val receiverClass = receiverType.canonicalText val receiverClass = receiverType.canonicalText
// Filter retains results that meet the conditions. // Filter retains results that meet the conditions.
if (receiverClass != DeclaredSymbol.HIKAGE_CLASS) return if (receiverClass != DeclaredSymbol.HIKAGE_CLASS) return
// Like `hikage["your_id"] as YourView`. // Like `hikage["your_id"] as YourView`.
val exprText = node.sourcePsi?.text ?: return val exprText = node.sourcePsi?.text ?: return
// Like `hikage["your_id"]`. // Like `hikage["your_id"]`.
@@ -88,11 +92,15 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
val receiverNameText = receiverText.split("[")[0] val receiverNameText = receiverText.split("[")[0]
// Like `"your_id"`. // Like `"your_id"`.
val receiverContent = runCatching { receiverText.split("[")[1].split("]")[0] }.getOrNull() ?: return val receiverContent = runCatching { receiverText.split("[")[1].split("]")[0] }.getOrNull() ?: return
val isSafeCast = exprText.contains("as?") || exprText.endsWith("?") val isSafeCast = exprText.contains("as?") || exprText.endsWith("?")
// Like `YourView`. // Like `YourView`.
val castTypeContent = node.typeReference?.sourcePsi?.text?.removeSuffix("?") ?: return val castTypeContent = node.typeReference?.sourcePsi?.text?.removeSuffix("?") ?: return
val replacement = "$receiverNameText.${if (isSafeCast) "getOrNull" else "get"}<$castTypeContent>($receiverContent)" val replacement = "$receiverNameText.${if (isSafeCast) "getOrNull" else "get"}<$castTypeContent>($receiverContent)"
val replaceSuggestion = if (isSafeCast) "Hikage.getOrNull<$castTypeContent>" else "Hikage.get<$castTypeContent>" val replaceSuggestion = if (isSafeCast) "Hikage.getOrNull<$castTypeContent>" else "Hikage.get<$castTypeContent>"
val location = context.getLocation(locationNode) val location = context.getLocation(locationNode)
val lintFix = LintFix.create() val lintFix = LintFix.create()
.name("Replace with '$replacement'") .name("Replace with '$replacement'")
@@ -101,6 +109,7 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
.with(replacement) .with(replacement)
.reformat(true) .reformat(true)
.build() .build()
context.report( context.report(
ISSUE, locationNode, location, ISSUE, locationNode, location,
message = "Can be replaced with safe type cast `$replaceSuggestion`.", message = "Can be replaced with safe type cast `$replaceSuggestion`.",

View File

@@ -70,6 +70,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
override fun visitCallExpression(node: UCallExpression) { override fun visitCallExpression(node: UCallExpression) {
val callExpr = node.sourcePsi as? KtCallExpression ?: return val callExpr = node.sourcePsi as? KtCallExpression ?: return
val method = node.resolve() ?: return val method = node.resolve() ?: return
startLint(callExpr, method) startLint(callExpr, method)
organizeAndReport() organizeAndReport()
} }
@@ -78,6 +79,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
val className = method.containingClass?.qualifiedName ?: "" val className = method.containingClass?.qualifiedName ?: ""
val hasHikageable = method.hasHikageable() val hasHikageable = method.hasHikageable()
val hasLayoutParams = className == DeclaredSymbol.HIKAGE_PERFORMER_CLASS && method.name == "LayoutParams" val hasLayoutParams = className == DeclaredSymbol.HIKAGE_PERFORMER_CLASS && method.name == "LayoutParams"
if (hasHikageable || hasLayoutParams) visitAndLint(callExpr, method) if (hasHikageable || hasLayoutParams) visitAndLint(callExpr, method)
} }
@@ -85,6 +87,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
reports.forEach { reports.forEach {
// Check if the call has been reported before reporting. // Check if the call has been reported before reporting.
if (reportedNodes.contains(it.callExpr)) return@forEach if (reportedNodes.contains(it.callExpr)) return@forEach
val location = context.getLocation(it.callExpr) val location = context.getLocation(it.callExpr)
val lintFix = LintFix.create() val lintFix = LintFix.create()
.name("Delete Call Expression") .name("Delete Call Expression")
@@ -93,6 +96,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
// Delete the call expression. // Delete the call expression.
.with("") .with("")
.build() .build()
context.report(ISSUE, it.callExpr, location, it.message, lintFix) context.report(ISSUE, it.callExpr, location, it.message, lintFix)
reportedNodes.add(it.callExpr) reportedNodes.add(it.callExpr)
} }
@@ -102,28 +106,37 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
val bodyBlocks = mutableMapOf<String, KtExpression>() val bodyBlocks = mutableMapOf<String, KtExpression>()
val parameters = method.parameterList.parameters val parameters = method.parameterList.parameters
val valueArguments = callExpr.valueArgumentList?.arguments ?: emptyList() val valueArguments = callExpr.valueArgumentList?.arguments ?: emptyList()
fun visitValueArg(arg: KtValueArgument) { fun visitValueArg(arg: KtValueArgument) {
val name = arg.getArgumentName()?.asName?.identifier ?: "" val name = arg.getArgumentName()?.asName?.identifier ?: ""
val expr = arg.getArgumentExpression() val expr = arg.getArgumentExpression()
val parameter = parameters.firstOrNull { it.name == name } val parameter = parameters.firstOrNull { it.name == name }
// If the last bit is a lambda expression, then `parameter` must have a lambda parameter defined by the last bit. // If the last bit is a lambda expression, then `parameter` must have a lambda parameter defined by the last bit.
?: if (arg is KtLambdaArgument) parameters.lastOrNull() else null ?: if (arg is KtLambdaArgument) parameters.lastOrNull() else null
val isMatched = parameter?.type?.canonicalText?.matches(DeclaredSymbol.HIKAGE_VIEW_REGEX) == true && val isMatched = parameter?.type?.canonicalText?.matches(DeclaredSymbol.HIKAGE_VIEW_REGEX) == true &&
!parameter.type.canonicalText.contains(DeclaredSymbol.HIKAGE_PERFORMER_CLASS) !parameter.type.canonicalText.contains(DeclaredSymbol.HIKAGE_PERFORMER_CLASS)
if (expr is KtLambdaExpression && isMatched) if (expr is KtLambdaExpression && isMatched)
expr.bodyExpression?.let { bodyBlocks[name] = it } expr.bodyExpression?.let { bodyBlocks[name] = it }
} }
// Get the last lambda expression. // Get the last lambda expression.
val lastLambda = callExpr.lambdaArguments.lastOrNull() val lastLambda = callExpr.lambdaArguments.lastOrNull()
if (lastLambda != null) visitValueArg(lastLambda) if (lastLambda != null) visitValueArg(lastLambda)
valueArguments.forEach { arg -> visitValueArg(arg) } valueArguments.forEach { arg -> visitValueArg(arg) }
bodyBlocks.toList().flatMap { (_, value) -> value.children.filterIsInstance<KtCallExpression>() }.forEach { bodyBlocks.toList().flatMap { (_, value) -> value.children.filterIsInstance<KtCallExpression>() }.forEach {
val expression = it.toUElementOfType<UCallExpression>() ?: return@forEach val expression = it.toUElementOfType<UCallExpression>() ?: return@forEach
val sCallExpr = expression.sourcePsi as? KtCallExpression ?: return@forEach val sCallExpr = expression.sourcePsi as? KtCallExpression ?: return@forEach
val sMethod = expression.resolve() ?: return@forEach val sMethod = expression.resolve() ?: return@forEach
if (sMethod.hasHikageable()) { if (sMethod.hasHikageable()) {
val message = "Performers are not allowed to appear in `${method.name}` DSL creation process." val message = "Performers are not allowed to appear in `${method.name}` DSL creation process."
reports.add(ReportDetail(message, expression)) reports.add(ReportDetail(message, expression))
// Recursively to visit next level. // Recursively to visit next level.
visitAndLint(sCallExpr, sMethod) visitAndLint(sCallExpr, sMethod)
} }

View File

@@ -65,6 +65,7 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
override fun visitMethod(node: UMethod) { override fun visitMethod(node: UMethod) {
val uastBody = node.uastBody as? UBlockExpression ?: return val uastBody = node.uastBody as? UBlockExpression ?: return
val bodyHasHikageable = uastBody.expressions.any { val bodyHasHikageable = uastBody.expressions.any {
when (it) { when (it) {
is UCallExpression -> it.resolve()?.hasHikageable() ?: false is UCallExpression -> it.resolve()?.hasHikageable() ?: false
@@ -73,17 +74,22 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
else -> false else -> false
} }
} }
if (!node.hasHikageable() && bodyHasHikageable) { if (!node.hasHikageable() && bodyHasHikageable) {
val location = context.getLocation(node) val location = context.getLocation(node)
val nameLocation = context.getNameLocation(node) val nameLocation = context.getNameLocation(node)
val message = "Function `${node.name}` must be marked with the `@Hikageable` annotation." val message = "Function `${node.name}` must be marked with the `@Hikageable` annotation."
val functionText = node.asSourceString() val functionText = node.asSourceString()
val hasDoubleSlash = functionText.startsWith("//") val hasDoubleSlash = functionText.startsWith("//")
val replacement = functionRegex.replace(functionText) { result -> val replacement = functionRegex.replace(functionText) { result ->
val functionBody = result.groupValues.getOrNull(0) ?: functionText val functionBody = result.groupValues.getOrNull(0) ?: functionText
val prefix = if (hasDoubleSlash) "\n" else "" val prefix = if (hasDoubleSlash) "\n" else ""
"$prefix@Hikageable $functionBody" "$prefix@Hikageable $functionBody"
} }
val lintFix = LintFix.create() val lintFix = LintFix.create()
.name("Add '@Hikageable' to '${node.name}'") .name("Add '@Hikageable' to '${node.name}'")
.replace() .replace()
@@ -92,6 +98,7 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
.imports(DeclaredSymbol.HIKAGEABLE_ANNOTATION_CLASS) .imports(DeclaredSymbol.HIKAGEABLE_ANNOTATION_CLASS)
.reformat(true) .reformat(true)
.build() .build()
context.report(ISSUE, node, nameLocation, message, lintFix) context.report(ISSUE, node, nameLocation, message, lintFix)
} }
} }

View File

@@ -135,18 +135,23 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
importRefs.getOrNull(1)?.trim() importRefs.getOrNull(1)?.trim()
else null else null
val importPrefix = importRefs[0] val importPrefix = importRefs[0]
val hasPrefix = importPrefix.contains(".") val hasPrefix = importPrefix.contains(".")
val name = if (hasPrefix) val name = if (hasPrefix)
importPrefix.split(".").last() importPrefix.split(".").last()
else importPrefix else importPrefix
val packagePrefix = importPrefix.replace(if (hasPrefix) ".$name" else name, "") val packagePrefix = importPrefix.replace(if (hasPrefix) ".$name" else name, "")
val reference = ImportReference(packagePrefix, name, alias) val reference = ImportReference(packagePrefix, name, alias)
importReferences.add(reference) importReferences.add(reference)
} }
override fun visitCallExpression(node: UCallExpression) { override fun visitCallExpression(node: UCallExpression) {
val callExpr = node.sourcePsi as? KtCallExpression ?: return val callExpr = node.sourcePsi as? KtCallExpression ?: return
val method = node.resolve() ?: return val method = node.resolve() ?: return
startLint(callExpr, method) startLint(callExpr, method)
} }
@@ -165,7 +170,9 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
type.canonicalText == VIEW_GROUP_CLASS_NAME type.canonicalText == VIEW_GROUP_CLASS_NAME
} }
} }
val isTypedViewFunction = typedViewFunctionIndex >= 0 val isTypedViewFunction = typedViewFunctionIndex >= 0
val imports = typeArgumentsText.mapNotNull { typeName -> val imports = typeArgumentsText.mapNotNull { typeName ->
when { when {
// Like `TextView`. // Like `TextView`.
@@ -179,18 +186,24 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
else -> null else -> null
} }
} }
val matchedIndex = builtInWidgets.indexOfFirst { imports.any { e -> e.alias == it || e.name == it } } val matchedIndex = builtInWidgets.indexOfFirst { imports.any { e -> e.alias == it || e.name == it } }
val isBuiltInWidget = matchedIndex >= 0 val isBuiltInWidget = matchedIndex >= 0
if (isTypedViewFunction && isBuiltInWidget) { if (isTypedViewFunction && isBuiltInWidget) {
val widgetName = builtInWidgets[matchedIndex] val widgetName = builtInWidgets[matchedIndex]
val sourceLocation = context.getLocation(callExpr) val sourceLocation = context.getLocation(callExpr)
val sourceText = callExpr.toUElement()?.asSourceString() ?: return val sourceText = callExpr.toUElement()?.asSourceString() ?: return
val callExprElement = callExpr.toUElement() ?: return val callExprElement = callExpr.toUElement() ?: return
// Matchs '>' and like `View<TextView`'s length + 1. // Matchs '>' and like `View<TextView`'s length + 1.
val callExprLength = sourceText.split(">")[0].trim().length + 1 val callExprLength = sourceText.split(">")[0].trim().length + 1
val nameLocation = context.getRangeLocation(callExprElement, fromDelta = 0, callExprLength) val nameLocation = context.getRangeLocation(callExprElement, fromDelta = 0, callExprLength)
// Only replace the first one, because there may be multiple sub-functions in DSL. // Only replace the first one, because there may be multiple sub-functions in DSL.
val replacement = sourceText.replaceFirst(viewExpressionRegex, widgetName) val replacement = sourceText.replaceFirst(viewExpressionRegex, widgetName)
val lintFix = LintFix.create() val lintFix = LintFix.create()
.name("Replace with '$widgetName'") .name("Replace with '$widgetName'")
.replace() .replace()
@@ -199,6 +212,7 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
.imports("$WIDGET_FUNCTION_PREFIX.$widgetName") .imports("$WIDGET_FUNCTION_PREFIX.$widgetName")
.reformat(true) .reformat(true)
.build() .build()
val message = "Can be simplified to `$widgetName`." val message = "Can be simplified to `$widgetName`."
context.report(ISSUE, callExpr, nameLocation, message, lintFix) context.report(ISSUE, callExpr, nameLocation, message, lintFix)
} }

View File

@@ -31,7 +31,7 @@ import android.content.res.loader.AssetsProvider
import android.content.res.loader.ResourcesProvider import android.content.res.loader.ResourcesProvider
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import com.highcapable.betterandroid.system.extension.tool.SystemVersion import com.highcapable.betterandroid.system.extension.tool.AndroidVersion
import com.highcapable.betterandroid.ui.extension.view.inflateOrNull import com.highcapable.betterandroid.ui.extension.view.inflateOrNull
import com.highcapable.betterandroid.ui.extension.view.layoutInflater import com.highcapable.betterandroid.ui.extension.view.layoutInflater
import com.highcapable.hikage.core.R import com.highcapable.hikage.core.R
@@ -116,12 +116,12 @@ internal object XmlBlockBypass {
private val resolver = object : MemberProcessor.Resolver() { private val resolver = object : MemberProcessor.Resolver() {
override fun <T : Any> getDeclaredConstructors(declaringClass: Class<T>): List<Constructor<T>> = override fun <T : Any> getDeclaredConstructors(declaringClass: Class<T>): List<Constructor<T>> =
SystemVersion.require(SystemVersion.P, super.getDeclaredConstructors(declaringClass)) { AndroidVersion.require(AndroidVersion.P, super.getDeclaredConstructors(declaringClass)) {
HiddenApiBypass.getDeclaredMethods(declaringClass).filterIsInstance<Constructor<T>>().toList() HiddenApiBypass.getDeclaredMethods(declaringClass).filterIsInstance<Constructor<T>>().toList()
} }
override fun <T : Any> getDeclaredMethods(declaringClass: Class<T>): List<Method> = override fun <T : Any> getDeclaredMethods(declaringClass: Class<T>): List<Method> =
SystemVersion.require(SystemVersion.P, super.getDeclaredMethods(declaringClass)) { AndroidVersion.require(AndroidVersion.P, super.getDeclaredMethods(declaringClass)) {
HiddenApiBypass.getDeclaredMethods(declaringClass).filterIsInstance<Method>().toList() HiddenApiBypass.getDeclaredMethods(declaringClass).filterIsInstance<Method>().toList()
} }
} }
@@ -143,6 +143,7 @@ internal object XmlBlockBypass {
fun init(context: Context) { fun init(context: Context) {
// Context may be loaded from the preview and other non-Android platforms, ignoring this. // Context may be loaded from the preview and other non-Android platforms, ignoring this.
if (context.javaClass.name.endsWith("BridgeContext")) return if (context.javaClass.name.endsWith("BridgeContext")) return
init(context.applicationContext.applicationInfo) init(context.applicationContext.applicationInfo)
} }
@@ -151,11 +152,12 @@ internal object XmlBlockBypass {
* @param info the application info. * @param info the application info.
*/ */
private fun init(info: ApplicationInfo) { private fun init(info: ApplicationInfo) {
if (SystemVersion.isLowOrEqualsTo(SystemVersion.P)) return if (AndroidVersion.isAtMost(AndroidVersion.P)) return
if (isInitOnce) return if (isInitOnce) return
val sourceDir = info.sourceDir val sourceDir = info.sourceDir
xmlBlock = when { xmlBlock = when {
SystemVersion.isHighOrEqualsTo(SystemVersion.R) -> AndroidVersion.isAtLeast(AndroidVersion.R) ->
// private static native long nativeLoad(@FormatType int format, @NonNull String path, // private static native long nativeLoad(@FormatType int format, @NonNull String path,
// @PropertyFlags int flags, @Nullable AssetsProvider asset) throws IOException; // @PropertyFlags int flags, @Nullable AssetsProvider asset) throws IOException;
ApkAssetsClass.resolve() ApkAssetsClass.resolve()
@@ -166,7 +168,7 @@ internal object XmlBlockBypass {
parameters(Int::class, String::class, Int::class, AssetsProvider::class) parameters(Int::class, String::class, Int::class, AssetsProvider::class)
modifiers(Modifiers.NATIVE) modifiers(Modifiers.NATIVE)
}?.invokeQuietly(FORMAT_APK, sourceDir, PROPERTY_SYSTEM, null) }?.invokeQuietly(FORMAT_APK, sourceDir, PROPERTY_SYSTEM, null)
SystemVersion.isHighOrEqualsTo(SystemVersion.P) -> AndroidVersion.isAtLeast(AndroidVersion.P) ->
// private static native long nativeLoad( // private static native long nativeLoad(
// @NonNull String path, boolean system, boolean forceSharedLib, boolean overlay) // @NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
// throws IOException; // throws IOException;
@@ -180,18 +182,20 @@ internal object XmlBlockBypass {
}?.invokeQuietly(sourceDir, false, false, false) }?.invokeQuietly(sourceDir, false, false, false)
else -> error("Unsupported Android version.") else -> error("Unsupported Android version.")
} as? Long? ?: error("Failed to create ApkAssets.") } as? Long? ?: error("Failed to create ApkAssets.")
blockParser = XmlBlockClass.resolve() blockParser = XmlBlockClass.resolve()
.processor(resolver) .processor(resolver)
.optional() .optional()
.firstConstructorOrNull { .firstConstructorOrNull {
if (SystemVersion.isHighOrEqualsTo(36)) if (AndroidVersion.isAtLeast(AndroidVersion.BAKLAVA))
parameters(AssetManager::class, Long::class, Boolean::class) parameters(AssetManager::class, Long::class, Boolean::class)
else parameters(AssetManager::class, Long::class) else parameters(AssetManager::class, Long::class)
}?.let { }?.let {
if (SystemVersion.isHighOrEqualsTo(36)) if (AndroidVersion.isAtLeast(AndroidVersion.BAKLAVA))
it.createQuietly(null, xmlBlock, false) it.createQuietly(null, xmlBlock, false)
else it.createQuietly(null, xmlBlock) else it.createQuietly(null, xmlBlock)
} ?: error("Failed to create XmlBlock\$Parser.") } ?: error("Failed to create XmlBlock\$Parser.")
isInitOnce = true isInitOnce = true
} }
@@ -208,8 +212,10 @@ internal object XmlBlockBypass {
*/ */
fun createViewAttrs() = context.layoutInflater.inflateOrNull<HikageAttrsView>(R.layout.layout_hikage_attrs_view)?.attrs fun createViewAttrs() = context.layoutInflater.inflateOrNull<HikageAttrsView>(R.layout.layout_hikage_attrs_view)?.attrs
as? XmlResourceParser? ?: error("Failed to create AttributeSet.") as? XmlResourceParser? ?: error("Failed to create AttributeSet.")
return if (SystemVersion.isHighOrEqualsTo(SystemVersion.P)) {
return if (AndroidVersion.isAtLeast(AndroidVersion.P)) {
if (!isInitOnce) return createViewAttrs() if (!isInitOnce) return createViewAttrs()
require(blockParser != null) { "Hikage initialization failed." } require(blockParser != null) { "Hikage initialization failed." }
newParser?.copy()?.of(blockParser) newParser?.copy()?.of(blockParser)
?.invokeQuietly<XmlResourceParser>(resId) ?.invokeQuietly<XmlResourceParser>(resId)

View File

@@ -167,7 +167,9 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
if (parent != null && attachToParent) { if (parent != null && attachToParent) {
val parentId = generateRandomViewId() val parentId = generateRandomViewId()
provideView(parent, parentId) provideView(parent, parentId)
}; newPerformer(lpClass, parent, attachToParent, context).apply(performer) }
newPerformer(lpClass, parent, attachToParent, context).apply(performer)
} }
/** /**
@@ -317,11 +319,13 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
*/ */
private fun <V : View> createView(viewClass: Class<V>, id: String?, context: Context): V { private fun <V : View> createView(viewClass: Class<V>, id: String?, context: Context): V {
val attrs = createAttributeSet(context) val attrs = createAttributeSet(context)
val view = createViewFromFactory(viewClass, id, context, attrs) ?: getViewConstructor(viewClass)?.build(context, attrs) val view = createViewFromFactory(viewClass, id, context, attrs) ?: getViewConstructor(viewClass)?.build(context, attrs)
if (view == null) throw PerformerException( if (view == null) throw PerformerException(
"Create view of type ${viewClass.name} failed. " + "Create view of type ${viewClass.name} failed. " +
"Please make sure the view class has a constructor with a single parameter of type Context." "Please make sure the view class has a constructor with a single parameter of type Context."
) )
provideView(view, id) provideView(view, id)
return view return view
} }
@@ -334,16 +338,20 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
private fun <V : View> getViewConstructor(viewClass: Class<V>) = private fun <V : View> getViewConstructor(viewClass: Class<V>) =
viewConstructors[viewClass.name] as? ViewConstructor<V>? ?: run { viewConstructors[viewClass.name] as? ViewConstructor<V>? ?: run {
var parameterCount = 0 var parameterCount = 0
val twoParams = viewClass.resolve() val twoParams = viewClass.resolve()
.optional(silent = true) .optional(silent = true)
.firstConstructorOrNull { parameters(Context::class, AttributeSet::class) } .firstConstructorOrNull { parameters(Context::class, AttributeSet::class) }
val onceParam = viewClass.resolve() val onceParam = viewClass.resolve()
.optional(silent = true) .optional(silent = true)
.firstConstructorOrNull { parameters(Context::class) } .firstConstructorOrNull { parameters(Context::class) }
val constructor = onceParam?.apply { parameterCount = 1 } val constructor = onceParam?.apply { parameterCount = 1 }
?: twoParams?.apply { parameterCount = 2 } ?: twoParams?.apply { parameterCount = 2 }
val viewConstructor = constructor?.let { ViewConstructor(it, parameterCount) } val viewConstructor = constructor?.let { ViewConstructor(it, parameterCount) }
if (viewConstructor != null) viewConstructors[viewClass.name] = viewConstructor if (viewConstructor != null) viewConstructors[viewClass.name] = viewConstructor
viewConstructor viewConstructor
} }
@@ -358,15 +366,20 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
private fun <V : View> createViewFromFactory(viewClass: Class<V>, id: String?, context: Context, attrs: AttributeSet): V? { private fun <V : View> createViewFromFactory(viewClass: Class<V>, id: String?, context: Context, attrs: AttributeSet): V? {
val parent = performers.firstOrNull()?.parent val parent = performers.firstOrNull()?.parent
var processed: V? = null var processed: V? = null
factories.forEach { factory -> factories.forEach { factory ->
val params = PerformerParams(id, attrs, viewClass as Class<View>) val params = PerformerParams(id, attrs, viewClass as Class<View>)
val view = factory(parent, processed, context, params) val view = factory(parent, processed, context, params)
if (view != null && view.javaClass isNotSubclassOf viewClass) throw PerformerException( if (view != null && view.javaClass isNotSubclassOf viewClass) throw PerformerException(
"HikageFactory cannot cast the created view type \"${view.javaClass}\" to \"${viewClass.name}\", " + "HikageFactory cannot cast the created view type \"${view.javaClass}\" to \"${viewClass.name}\", " +
"please confirm that the view type you created is correct." "please confirm that the view type you created is correct."
) )
if (view != null) processed = view as? V? if (view != null) processed = view as? V?
}; return processed }
return processed
} }
/** /**
@@ -379,6 +392,7 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
val (requireId, viewId) = generateViewId(id) val (requireId, viewId) = generateViewId(id)
view.id = viewId view.id = viewId
views[requireId] = view views[requireId] = view
return requireId return requireId
} }
@@ -395,12 +409,16 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
*/ */
fun doGenerate(id: String): Int { fun doGenerate(id: String): Int {
val generateId = View.generateViewId() val generateId = View.generateViewId()
if (viewIds.contains(id)) throw PerformerException("View with id \"$id\" already exists.") if (viewIds.contains(id)) throw PerformerException("View with id \"$id\" already exists.")
viewIds[id] = generateId viewIds[id] = generateId
return generateId return generateId
} }
val requireId = id ?: generateRandomViewId() val requireId = id ?: generateRandomViewId()
val viewId = doGenerate(requireId) val viewId = doGenerate(requireId)
return requireId to viewId return requireId to viewId
} }
@@ -456,6 +474,7 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
*/ */
internal inline fun requireNoPerformers(name: String, block: () -> Unit) { internal inline fun requireNoPerformers(name: String, block: () -> Unit) {
val viewCount = views.size val viewCount = views.size
block() block()
if (views.size != viewCount) throw PerformerException( if (views.size != viewCount) throw PerformerException(
"Performers are not allowed to appear in $name DSL creation process." "Performers are not allowed to appear in $name DSL creation process."
@@ -472,13 +491,16 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
private fun include(delegate: Delegate<*>, context: Context, embedded: Boolean): Hikage { private fun include(delegate: Delegate<*>, context: Context, embedded: Boolean): Hikage {
val hikage = delegate.create(context) val hikage = delegate.create(context)
if (!embedded) return hikage if (!embedded) return hikage
val duplicateId = hikage.viewIds.toList().firstOrNull { (k, _) -> viewIds.containsKey(k) }?.first val duplicateId = hikage.viewIds.toList().firstOrNull { (k, _) -> viewIds.containsKey(k) }?.first
if (duplicateId != null) throw PerformerException( if (duplicateId != null) throw PerformerException(
"Embedded layout view IDs conflict, the view id \"$duplicateId\" is already exists." "Embedded layout view IDs conflict, the view id \"$duplicateId\" is already exists."
) )
viewIds.putAll(hikage.viewIds) viewIds.putAll(hikage.viewIds)
views.putAll(hikage.views) views.putAll(hikage.views)
performers.addAll(hikage.performers) performers.addAll(hikage.performers)
return hikage return hikage
} }
@@ -544,9 +566,11 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams) val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams)
val view = createView(classOf<V>(), id, context) val view = createView(classOf<V>(), id, context)
view.layoutParams = lpDelegate.create() view.layoutParams = lpDelegate.create()
requireNoPerformers(classOf<V>().name) { view.init() } requireNoPerformers(classOf<V>().name) { view.init() }
startProvide(id, classOf<V>()) startProvide(id, classOf<V>())
addToParentIfRequired(view) addToParentIfRequired(view)
return view return view
} }
@@ -587,10 +611,12 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams) val lpDelegate = LayoutParams.from(current, lpClass, parent, lparams)
val view = createView(classOf<VG>(), id, context) val view = createView(classOf<VG>(), id, context)
view.layoutParams = lpDelegate.create() view.layoutParams = lpDelegate.create()
requireNoPerformers(classOf<VG>().name) { view.init() } requireNoPerformers(classOf<VG>().name) { view.init() }
startProvide(id, classOf<VG>()) startProvide(id, classOf<VG>())
addToParentIfRequired(view) addToParentIfRequired(view)
newPerformer<LP>(view).apply(performer) newPerformer<LP>(view).apply(performer)
return view return view
} }
@@ -625,10 +651,12 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
id: String? = null id: String? = null
): View { ): View {
val view = context.layoutInflater.inflate(resId, parent, attachToRoot = false) val view = context.layoutInflater.inflate(resId, parent, attachToRoot = false)
startProvide(id, view.javaClass, view) startProvide(id, view.javaClass, view)
lparams?.create()?.let { view.layoutParams = it } lparams?.create()?.let { view.layoutParams = it }
provideView(view, id) provideView(view, id)
addToParentIfRequired(view) addToParentIfRequired(view)
return view return view
} }
@@ -645,14 +673,17 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
): VB { ): VB {
val viewBinding = ViewBinding<VB>().inflate(context.layoutInflater, parent, attachToParent = false) val viewBinding = ViewBinding<VB>().inflate(context.layoutInflater, parent, attachToParent = false)
val view = viewBinding.root val view = viewBinding.root
startProvide(id, view.javaClass, view) startProvide(id, view.javaClass, view)
if (view.parent != null) throw ProvideException( if (view.parent != null) throw ProvideException(
"The ViewBinding($view) already has a parent, " + "The ViewBinding($view) already has a parent, " +
"it may have been created using layout root node <merge> or <include>, cannot be provided." "it may have been created using layout root node <merge> or <include>, cannot be provided."
) )
lparams?.create()?.let { view.layoutParams = it } lparams?.create()?.let { view.layoutParams = it }
provideView(view, id) provideView(view, id)
addToParentIfRequired(view) addToParentIfRequired(view)
return viewBinding return viewBinding
} }
@@ -670,11 +701,14 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
id: String? = null id: String? = null
): View { ): View {
if (view.parent != null) throw ProvideException("The view $view already has a parent, cannot be provided.") if (view.parent != null) throw ProvideException("The view $view already has a parent, cannot be provided.")
startProvide(id, view.javaClass, view) startProvide(id, view.javaClass, view)
val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams) val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams)
view.layoutParams = lpDelegate.create() view.layoutParams = lpDelegate.create()
provideView(view, id) provideView(view, id)
addToParentIfRequired(view) addToParentIfRequired(view)
return view return view
} }
@@ -708,13 +742,17 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
): Hikage { ): Hikage {
val view = hikage.root val view = hikage.root
startProvide(id, view.javaClass, view) startProvide(id, view.javaClass, view)
val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams) val lpDelegate = LayoutParams.from(current = this@Hikage, lpClass, parent, lparams, view.layoutParams)
if (view.parent != null) throw ProvideException( if (view.parent != null) throw ProvideException(
"The Hikage layout root view $view already has a parent, cannot be provided." "The Hikage layout root view $view already has a parent, cannot be provided."
) )
view.layoutParams = lpDelegate.create() view.layoutParams = lpDelegate.create()
provideView(view, id) provideView(view, id)
addToParentIfRequired(view) addToParentIfRequired(view)
return hikage return hikage
} }
@@ -783,6 +821,7 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
*/ */
private fun <V : View> startProvide(id: String?, viewClass: Class<V>, view: V? = null) { private fun <V : View> startProvide(id: String?, viewClass: Class<V>, view: V? = null) {
provideCount++ provideCount++
if (provideCount > 1 && (parent == null || !attachToParent)) throw ProvideException( if (provideCount > 1 && (parent == null || !attachToParent)) throw ProvideException(
"Provide view ${view?.javaClass ?: viewClass}(${id?.let { "\"$it\""} ?: "<anonymous>"}) failed. ${ "Provide view ${view?.javaClass ?: viewClass}(${id?.let { "\"$it\""} ?: "<anonymous>"}) failed. ${
if (parent == null) "No parent view group found" if (parent == null) "No parent view group found"
@@ -900,6 +939,7 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
superclass() superclass()
}?.invokeQuietly<ViewGroup.LayoutParams>(it) }?.invokeQuietly<ViewGroup.LayoutParams>(it)
} ?: lparams } ?: lparams
return wrapped return wrapped
// Build a default. // Build a default.
?: lpClass.createInstanceOrNull(LayoutParamsWrapContent, LayoutParamsWrapContent) ?: lpClass.createInstanceOrNull(LayoutParamsWrapContent, LayoutParamsWrapContent)
@@ -912,12 +952,15 @@ class Hikage private constructor(private val factories: List<HikageFactory>) {
*/ */
fun create(): ViewGroup.LayoutParams { fun create(): ViewGroup.LayoutParams {
if (bodyBuilder == null && wrapperBuilder == null) throw PerformerException("No layout params builder found.") if (bodyBuilder == null && wrapperBuilder == null) throw PerformerException("No layout params builder found.")
return bodyBuilder?.let { return bodyBuilder?.let {
val lparams = ViewLayoutParams(lpClass, it.width, it.height, it.matchParent, it.widthMatchParent, it.heightMatchParent) val lparams = ViewLayoutParams(lpClass, it.width, it.height, it.matchParent, it.widthMatchParent, it.heightMatchParent)
current.requireNoPerformers(lparams.javaClass.name) { it.body(lparams) } current.requireNoPerformers(lparams.javaClass.name) { it.body(lparams) }
lparams lparams
} ?: wrapperBuilder?.let { } ?: wrapperBuilder?.let {
val lparams = it.delegate?.create() ?: it.lparams val lparams = it.delegate?.create() ?: it.lparams
createDefaultLayoutParams(lparams) createDefaultLayoutParams(lparams)
} ?: throw PerformerException("Internal error of build layout params.") } ?: throw PerformerException("Internal error of build layout params.")
} }

View File

@@ -47,6 +47,7 @@ abstract class HikagePreview(context: Context, attrs: AttributeSet? = null) : Fr
@CallSuper @CallSuper
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
removeAllViews() removeAllViews()
build().create(context, parent = this) build().create(context, parent = this)
} }

View File

@@ -42,6 +42,7 @@ dependencies {
implementation(com.highcapable.kavaref.kavaref.core) implementation(com.highcapable.kavaref.kavaref.core)
implementation(com.highcapable.kavaref.kavaref.extension) implementation(com.highcapable.kavaref.kavaref.extension)
implementation(com.highcapable.betterandroid.ui.component) implementation(com.highcapable.betterandroid.ui.component)
implementation(com.highcapable.betterandroid.ui.component.adapter)
implementation(com.highcapable.betterandroid.ui.extension) implementation(com.highcapable.betterandroid.ui.extension)
implementation(com.highcapable.betterandroid.system.extension) implementation(com.highcapable.betterandroid.system.extension)
implementation(androidx.core.core.ktx) implementation(androidx.core.core.ktx)

View File

@@ -25,7 +25,7 @@
package com.highcapable.hikage.extension.betterandroid.ui.component.adapter package com.highcapable.hikage.extension.betterandroid.ui.component.adapter
import android.view.ViewGroup import android.view.ViewGroup
import com.highcapable.betterandroid.ui.component.adapter.CommonAdapterBuilder import com.highcapable.betterandroid.ui.component.adapter.BaseAdapterBuilder
import com.highcapable.hikage.core.Hikage import com.highcapable.hikage.core.Hikage
import com.highcapable.hikage.core.base.HikagePerformer import com.highcapable.hikage.core.base.HikagePerformer
import com.highcapable.hikage.core.base.Hikageable import com.highcapable.hikage.core.base.Hikageable
@@ -51,26 +51,26 @@ import com.highcapable.hikage.extension.betterandroid.ui.component.adapter.viewh
* hikage.get<TextView>("text").text = "Item ${entity.name} of ${position + 1}" * hikage.get<TextView>("text").text = "Item ${entity.name} of ${position + 1}"
* } * }
* ``` * ```
* @see CommonAdapterBuilder.onBindItemView * @see BaseAdapterBuilder.onBindItemView
* @receiver [CommonAdapterBuilder]<[E]> * @receiver [BaseAdapterBuilder]<[E]>
* @param Hikageable the performer body. * @param Hikageable the performer body.
* @return [CommonAdapterBuilder]<[E]> * @return [BaseAdapterBuilder]<[E]>
*/ */
@JvmOverloads @JvmOverloads
fun <E> CommonAdapterBuilder<E>.onBindItemView( fun <E> BaseAdapterBuilder<E>.onBindItemView(
Hikageable: HikagePerformer<ViewGroup.LayoutParams>, Hikageable: HikagePerformer<ViewGroup.LayoutParams>,
viewHolder: (hikage: Hikage, entity: E, position: Int) -> Unit = { _, _, _ -> } viewHolder: (hikage: Hikage, entity: E, position: Int) -> Unit = { _, _, _ -> }
) = onBindItemView(Hikageable(performer = Hikageable), viewHolder) ) = onBindItemView(Hikageable(performer = Hikageable), viewHolder)
/** /**
* Create and add view holder from [Hikage.Delegate]. * Create and add view holder from [Hikage.Delegate].
* @see CommonAdapterBuilder.onBindItemView * @see BaseAdapterBuilder.onBindItemView
* @receiver [CommonAdapterBuilder]<[E]> * @receiver [BaseAdapterBuilder]<[E]>
* @param delegate the delegate. * @param delegate the delegate.
* @return [CommonAdapterBuilder]<[E]> * @return [BaseAdapterBuilder]<[E]>
*/ */
@JvmOverloads @JvmOverloads
fun <E> CommonAdapterBuilder<E>.onBindItemView( fun <E> BaseAdapterBuilder<E>.onBindItemView(
delegate: Hikage.Delegate<*>, delegate: Hikage.Delegate<*>,
viewHolder: (hikage: Hikage, entity: E, position: Int) -> Unit = { _, _, _ -> } viewHolder: (hikage: Hikage, entity: E, position: Int) -> Unit = { _, _, _ -> }
) = onBindItemView(HikageHolderDelegate(delegate), viewHolder) ) = onBindItemView(HikageHolderDelegate(delegate), viewHolder)

View File

@@ -28,6 +28,7 @@ class DemoApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
} }
} }

View File

@@ -52,9 +52,11 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView { setContentView {
var username = "" var username = ""
var password = "" var password = ""
CoordinatorLayout( CoordinatorLayout(
lparams = LayoutParams(matchParent = true) lparams = LayoutParams(matchParent = true)
) { ) {

View File

@@ -31,6 +31,7 @@ abstract class BaseActivity : AppViewsActivity() {
@CallSuper @CallSuper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
PanguTextFactory2.inject(this) PanguTextFactory2.inject(this)
} }
} }