本文通过对 Activity 启动聊起,从这个小需求谈一谈注解处理器框架(APT)的应用。
背景 在 Android 开发中,Activity 是页面的载体,由于我们还未完全使用单 Activity 这种开发架构,所以会有多个 Activity 分别来承载这些页面,这中间就会涉及到页面的跳转传值问题,我们一般会采用系统提供给我们的 startActivity()
来启动页面,通过传入 Intent 参数来处理页面的传值逻辑。
遇到的问题 举个简单的例子,我们现在有一个详情页面 DetailActivity:
public class DetailActivity extends AppCompatActivity { private long id; private String userId; private String title; }
其中有三个参数,如果我们想跳转到这个 Activity 并传参,一般会按照这种方式:
Intent intent = new Intent (this , DetailActivity.class);intent.putExtra("id" , 123456L ); intent.putExtra("userId" , "100008" ); intent.putExtra("title" , "测试" ); startActivity(intent);
然后 DetailActivity 需要按照这样的方式来获取参数:
Intent intent = getIntent();id = intent.getLongExtra("id" , 0L ); userId = intent.getStringExtra("userId" ); title = intent.getStringExtra("title" );
这里我们就会发现两个问题:
传值不安全
这几个变量我们分别定义了几个魔法值,如果我们想跳转到这个页面需要到详情页找到 getIntent()
调用的地方,并获取到这些 key 来传值,如果是两个开发人员分别负责这两个页面,极大的降低了开发效率以及增加了许多沟通成本。
当然在我们现在的开发中,这些 key 会在目的 Activity 定义为常量,来提高安全性,但是这样我们依然需要查看目标文件这些变量的定义。
逻辑分散
public class DetailActivity extends AppCompatActivity { public static final String EXTRA_ID = "id" ; public static final String EXTRA_USER_ID = "userId" ; public static final String EXTRA_TITLE = "title" ; private long id; private String userId; private String title; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_detail); Intent intent = getIntent(); id = intent.getLongExtra(EXTRA_ID, 0L ); userId = intent.getStringExtra(EXTRA_USER_ID); title = intent.getStringExtra(EXTRA_TITLE); } }
从上边的代码可以看出,为了获取从外部传过来的参数,需要好几块代码来处理,如果增加和修改参数,都可能会导致改错或者漏改的情况,并且也需要告知调用者具体的改动,从而一定程度上增加了沟通成本。
优化方案 针对于以上的两个问题,其实我们项目中已经很大程度上的处理了这个问题,来降低可维护以及沟通的成本,就是在目标 Activity 中添加一个 start()
静态函数,通过传入 Context
和页面参数来启动目标页面:
public class DetailActivity extends AppCompatActivity { public static final String EXTRA_ID = "id" ; public static final String EXTRA_USER_ID = "userId" ; public static final String EXTRA_TITLE = "title" ; private long id; private String userId; private String title; public static void start (Context context, long id, String userId, String title) { Intent intent = new Intent (context, DetailActivity.class); intent.putExtra(EXTRA_ID, id); intent.putExtra(EXTRA_USER_ID, userId); intent.putExtra(EXTRA_TITLE, title); context.startActivity(intent); } @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_detail); Intent intent = getIntent(); id = intent.getLongExtra(EXTRA_ID, 0L ); userId = intent.getStringExtra(EXTRA_USER_ID); title = intent.getStringExtra(EXTRA_TITLE); } }
那么我们现在来对比一下调用者启动页面的代码:
Intent intent = new Intent (this , DetailActivity.class);intent.putExtra("id" , 123456L ); intent.putExtra("userId" , "100008" ); intent.putExtra("title" , "测试标题" ); startActivity(intent); DetailActivity.start( this , 123456L , "100008" , "测试标题" );
解决的问题:
降低了调用者的调用成本,通过注释的方式也降低了沟通成本。
使传参和取参的逻辑聚合到了一个页面,提高了可维护性。
未解决的问题:
在目标 Activity 中获取参数逻辑分散的问题,如果新增或者修改字段甚至还要多修改一个地方。
需要自己写 start()
函数,比较繁琐。
再次思考 经过以上优化后,我们还存在两个问题
参数获取问题
获取参数 → 给成员变量赋值
需要手动写 start()
静态函数
注入参数 针对于第一点,其实我们可以将参数注入,可以看到一些框架早就有了解决方案,比如历史悠久的 ButterKnife:
class ExampleActivity extends Activity { @BindView(R.id.user) EditText username; @BindView(R.id.pass) EditText password; @BindString(R.string.login_error) String loginErrorMessage; @OnClick(R.id.submit) void submit () { } @Override public void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this ); } }
还有路由框架 ARouter:
@Route(path = "/test/activity") public class Test1Activity extends Activity { @Autowired public String name; @Autowired int age; @Autowired(name = "girl") boolean boy; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); ARouter.getInstance().inject(this ); Log.d("param" , name + age + boy); } }
其实我们可以借鉴这两个框架,来实现我们的参数注入,代码大概就是下面这样:
public class DetailActivity extends AppCompatActivity { @Extra long id; @Extra(description = "用户ID") String userId; @Extra(required = false, description = "详情内容") String title; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); ActivityStarter.inject(this ); } }
在参数上打上注解,方便注入对应的值,
可以选择必传参数和非必传参数
可以添加描述内容,方便生成注释
我们了解到这两个框架都是用到了注解处理器 (APT,Annotation Processing Tool),这是一种处理注解的工具,确切的说它是 javac 的一个工具,它用来在编译时 扫描和处理注解。注解处理器以 Java 代码 (或者编译过的字节码)作为输入,生成 .java 文件 作为输出。简单来说就是在编译期,通过注解生成**.java**文件。
那么获取参数部分的代码可以通过注解处理器来帮我们生成。
样板代码 接下来就是第二个问题,其实我们发现每个页面的 start()
方法就是重复的样板代码,既然我们都用上了 APT,那么这个问题也就解决了,可以使用 APT 来帮助我们生成 start()
方法。
使用 APT 解决页面跳转传参问题
写出要生成的代码
首先我们需要写出 APT 帮我们生成的代码,以此做为模板,再通过 JavaPoet、KotlinPoet 帮我们生成对应的代码。
此处我们还是以 DetailActivity 为例来讲,此处需要注意的一个点是,页面的传参和方法传参都一样有必传参数 和非必传参数 ,而且参数可能会有很多,这里我们可以考虑使用 Builder 模式来创建启动类:
@Generated public final class DetailActivityBuilder { public static final String EXTRA_ID = "id" ; public static final String EXTRA_TITLE = "title" ; public static final String EXTRA_USER_ID = "userId" ; private long id; private String title; private String userId; public DetailActivityBuilder title (String title) { this .title = title; return this ; } public static DetailActivityBuilder builder (long id, String userId) { DetailActivityBuilder builder = new DetailActivityBuilder (); builder.id = id; builder.userId = userId; return builder; } public Intent getIntent (Context context) { Intent intent = new Intent (context, DetailActivity.class); intent.putExtra(EXTRA_ID, id); intent.putExtra(EXTRA_TITLE, title); intent.putExtra(EXTRA_USER_ID, userId); return intent; } public static void inject (Activity instance, Bundle savedInstanceState) { if (instance instanceof DetailActivity) { DetailActivity typedInstance = (DetailActivity) instance; if (savedInstanceState != null ) { typedInstance.id = BundleUtils.<Long>get(savedInstanceState, "id" , null ); typedInstance.title = BundleUtils.<String>get(savedInstanceState, "title" , "" ); typedInstance.userId = BundleUtils.<String>get(savedInstanceState, "userId" , "" ); } } } public static void saveState (Activity instance, Bundle outState) { if (instance instanceof DetailActivity) { DetailActivity typedInstance = (DetailActivity) instance; Intent intent = new Intent (); intent.putExtra("id" , typedInstance.id); intent.putExtra("title" , typedInstance.title); intent.putExtra("userId" , typedInstance.userId); outState.putAll(intent.getExtras()); } } public static void processNewIntent (DetailActivity activity, Intent intent) { processNewIntent(activity, intent, true ); } public static void processNewIntent (DetailActivity activity, Intent intent, Boolean updateIntent) { if (updateIntent) { activity.setIntent(intent); } if (intent != null ) { inject(activity, intent.getExtras()); } } public void start (Context context) { Intent intent = getIntent(context); if (!(context instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); } public void start (Context context, Bundle options) { Intent intent = getIntent(context); if (!(context instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent, options); } public void start (Activity activity, int requestCode) { Intent intent = getIntent(activity); activity.startActivityForResult(intent, requestCode); } public void start (Activity activity, int requestCode, Bundle options) { Intent intent = getIntent(activity); activity.startActivityForResult(intent, requestCode, options); } public static void finish (Activity activity) { ActivityCompat.finishAfterTransition(activity); } }
定义注解
@Builder 注解 根据目标 Activity 来生成对应的 Builder 类。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Builder {}
打在参数上的注解,用来执行解析和注入工作,类似于 ARouter 的 @Autowire
注解:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface Extra { String value () default "" ; boolean required () default true ; String stringValue () default "" ; char charValue () default '0' ; byte byteValue () default 0 ; short shortValue () default 0 ; int intValue () default 0 ; long longValue () default 0 ; float floatValue () default 0f ; double doubleValue () default 0.0 ; boolean booleanValue () default false ; String description () default "" ; }
引入 APT
首先我们创建一个 compiler 的 Java Module,创建我们的注解处理器并继承于 AbstractProcessor
public class ActivityBuilderProcessor extends AbstractProcessor { @Override public synchronized void init (ProcessingEnvironment processingEnv) { super .init(processingEnv); AptContext.getInstance().init(processingEnv); } @Override public Set<String> getSupportedAnnotationTypes () { Set<String> types = new LinkedHashSet <>(); types.add(Builder.class.getCanonicalName()); types.add(Extra.class.getCanonicalName()); return types; } @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return false ; } }
注意:ActivityBuilderProcessor
需要在 META-INF 中声明:
# ActivityStarter/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor io.github.qihuan92.activitystarter.compiler.ActivityBuilderProcessor
实际上,javac 是利用 ServiceLoader
加载注册文件,从而得到了 APT 实现类的类名,进而执行 Processor 中的 process()
方法。
使用 JavaPoet 生成代码
JavaPoet 是 square 推出的开源 java 代码生成框架,提供 Java Api 生成 .java 源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。
关键类说明:
class
说明
JavaFile
用于构造输出包含一个顶级类的Java文件
TypeSpec
生成类,接口,或者枚举
MethodSpec
生成构造函数或方法
FieldSpec
生成成员变量或字段
ParameterSpec
用来创建参数
AnnotationSpec
用来创建注解
在 JavaPoet 中,JavaFile
是对 .java 文件的抽象,TypeSpec
是类/接口/枚举的抽象,MethodSpec
是方法/构造函数的抽象,FieldSpec
是成员变量/字段的抽象。这几个类各司其职,但都有共同的特点,提供内部 Builder
供外部更多更好地进行一些参数的设置以便有层次的扩展性的构造对应的内容。
另外,它提供 $L(for Literals),$S(for Strings),$T(for Types),$N(for Names) 等标识符,用于占位替换。
有了 JavaPoet,就可以很方便的将我们上边的 Builder 代码构建出来了,包括其中的常量、构造器、传参方法、注入方法等部分。
这部分就涉及到 JavaPoet API 的调用了,在此不再赘述这部分的内容,详见 ActivityBuilderProcessor.java 。
处理 startActivityForResult()
上文我们处理了页面传参的情况,另外还有一种情况需要我们考虑,就是如何获取页面返回的参数,相同的,返回参数的情况也会遇到同样的问题,所以页面返回的参数也需要注解处理器帮我们处理一下。
定义 @ResultField
注解
@Retention(RetentionPolicy.CLASS) public @interface ResultField { String name () ; Class<?> type(); }
在打 @Builder
注解的时候,可以传入返回值的名称和类型:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Builder { ResultField[] resultFields() default {}; } @Builder(resultFields = @ResultField(name = "color", type = String.class)) public class ColorSelectActivity extends AppCompatActivity { toolbar.setNavigationOnClickListener(view -> { ColorSelectActivityBuilder.finish(this , colorItem.getColor()) }); }
需要注解处理器生成如下代码:
public static class Result { public int resultCode; public String color; } public void start (Activity activity, int requestCode) { Intent intent = getIntent(activity); activity.startActivityForResult(intent, requestCode); } public static Result obtainResult (int resultCode, Intent intent) { Result result = new Result (); result.resultCode = resultCode; if (intent != null ) { Bundle bundle = intent.getExtras(); result.color = BundleUtils.<String>get(bundle, "color" ); } return result; } public static void finish (Activity activity, String color) { Intent intent = new Intent (); activity.setResult(Activity.RESULT_OK, intent); intent.putExtra("color" , color); ActivityCompat.finishAfterTransition(activity); }
使用:
通过定义 @ResultField
注解,来规范我们的返回结果,使用方式如下:
@Builder(resultFields = @ResultField(name = "color", type = String.class)) public class ColorSelectActivity extends AppCompatActivity { toolbar.setNavigationOnClickListener(view -> { Intent intent = new Intent (); intent.putExtra("color" , colorItem.getColor()); setResult(RESULT_OK, intent); finish(); ColorSelectActivityBuilder.finish(this , colorItem.getColor()) }); }
ColorSelectActivityBuilder.builder(currentColor) .start(this , REQUEST_CODE_SELECT_COLOR); @Override protected void onActivityResult (int requestCode, int resultCode, @Nullable Intent data) { super .onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_SELECT_COLOR) { if (data != null ) { String color = data.getStringExtra("color" ); } ColorSelectActivityBuilder.Result result = ColorSelectActivityBuilder.obtainResult(resultCode, data); String color = result.color; } }
适配 Activity Result API
由于 onActivityResult()
耦合性严重的问题,谷歌官方已经不再推荐我们使用 onActivityResult()
了,推出了 Activity Result API:
虽然所有 API 级别的 Activity
类均提供底层 startActivityForResult()
和 onActivityResult()
API,但我们强烈建议您使用 AndroidX Activity 和 Fragment 中引入的 Activity Result API。
Activity Result API 提供了用于注册结果、启动结果以及在系统分派结果后对其进行处理的组件。
我们从文档了解到 Activity Result API 支持我们继承 ActivityResultContract 来实现自己的 Contract,从而自定义输入和输出 ,所以我们可以根据对于的注解生成 ResultContract 类,来帮我们处理返回结果。
public static ActivityResultLauncher<ColorSelectActivityBuilder> registerForActivityResult ( @NonNull ActivityResultCaller resultCaller, @NonNull ActivityResultCallback<Result> callback) { return resultCaller.registerForActivityResult(new ResultContract (), callback); } public static class Result { public int resultCode; public String color; } public static class ResultContract extends ActivityResultContract <ColorSelectActivityBuilder, Result> { @NonNull @Override public Intent createIntent (@NonNull Context context, ColorSelectActivityBuilder input) { return input.getIntent(context); } @Override public Result parseResult (int resultCode, @Nullable Intent intent) { return obtainResult(resultCode, intent); } }
调用方式如下:
private final ActivityResultLauncher<ColorSelectActivityBuilder> launcher = ColorSelectActivityBuilder.registerForActivityResult(this , result -> { if (result.resultCode == RESULT_OK) { String color = result.color; btnSelectColor.setBackgroundColor(Color.parseColor(color)); Toast.makeText(this , "选中颜色: " + color, Toast.LENGTH_SHORT).show(); currentColor = color; } }); launcher.launch(ColorSelectActivityBuilder.builder(currentColor));
这样我们就可以更方便的借助 Activity Result API 来帮助我们处理页面返回值了。
还有哪些可以优化的? 使用切面编程简化 inject()
我们每一个打上 @Builder
的目标 Activity 都需要在 onCreate()
函数来进行参数的注入,这部分我们可以使用切面编程的方式,在 Application 中注册一个 Activity 生命周期的监听器来反射调用目标 ActivityBuilder 的 inject()
方法,以此来进一步简化代码。
application.registerActivityLifecycleCallbacks(new StarterActivityLifecycleCallbacks ()); class StarterActivityLifecycleCallbacks implements Application .ActivityLifecycleCallbacks { @Override public void onActivityCreated (@NonNull Activity activity, @Nullable Bundle bundle) { performInject(activity, bundle); } @Override public void onActivitySaveInstanceState (@NonNull Activity activity, @NonNull Bundle bundle) { performSaveState(activity, bundle); } private void performInject (Activity activity, Bundle savedInstanceState) { try { if (savedInstanceState == null ) { Intent intent = activity.getIntent(); if (intent == null ) { return ; } savedInstanceState = intent.getExtras(); } BuilderClassFinder.findBuilderClass(activity).getDeclaredMethod("inject" , Activity.class, Bundle.class).invoke(null , activity, savedInstanceState); } catch (Exception e) { Log.w("ActivityStarter" , e); } } private void performSaveState (Activity activity, Bundle outState) { try { BuilderClassFinder.findBuilderClass(activity).getDeclaredMethod("saveState" , Activity.class, Bundle.class).invoke(null , activity, outState); } catch (Exception e) { Log.w("ActivityStarter" , e); } } }
Kotlin 扩展函数 由于我们项目中越来越多的业务使用 Kotlin,我们都知道 Kotlin 的扩展函数可以简化我们的代码,可以提升一定的开发效率。
可以使用 KotlinPoet 来生成一些扩展函数,例如:
启动页面方法的扩展函数
fun Context.startDetailActivity (id: Long , userId: String , title: String ? = null ) { val builder = DetailActivityBuilder.builder(id, userId) .title(title) builder.start(this ) }
在 Activity 中调用方式如下:
startDetailActivity(123456L , "999999" , "测试标题" )
Activity Result API 扩展函数
fun AppCompatActivity.registerForColorSelectActivityResult ( callback: (ColorSelectActivityBuilder .Result ) -> Unit ) : ActivityResultLauncher<ColorSelectActivityBuilder> { return ColorSelectActivityBuilder.registerForActivityResult(this ) { callback(it) } } fun ActivityResultLauncher<ColorSelectActivityBuilder> .launch (color: String ) { val builder = ColorSelectActivityBuilder.builder(currentColor) launch(builder) }
调用方式如下:
private val launcher = registerForColorSelectActivityResult { if (it.resultCode == RESULT_OK) { currentColor = it.color } } binding.btnSelectColor.setOnClickListener { launcher.launch(currentColor) }
finish 扩展函数
public fun KotlinActivity.finish (testResult: String ) : Unit { KotlinActivityBuilder.finish(this , testResult) }
调用方式如下:
开发 IDEA 插件支持快速跳转到目标 Activity 当我们使用原来的方法进行开发时,在 Android Studio 里边很方便的就可以跳转到目标 Activity:
DetailActivity.start( this , 123456L , "100008" , "测试标题" );
但是我们使用注解处理器生成的 Builder 类时,则无法很方便的直接跳转到目标 Activity,所以需要开发插件来支持。
在 IDEA 插件开发中支持添加 LineMarker,可以通过此标记来进行一些操作。
插件的关键代码如下:
class NavigationLineMarker : LineMarkerProviderDescriptor (), GutterIconNavigationHandler<PsiElement> { companion object { const val GENERATED_ANNOTATION_NAME = "io.github.qihuan92.activitystarter.annotation.Generated" const val NOTIFY_SERVICE_NAME = "ActivityStarter Plugin Tips" const val NOTIFY_TITLE = "Road Sign" const val NOTIFY_NO_TARGET_TIPS = "No destination found or unsupported type." val logger = Logger.getInstance(NavigationLineMarker::class .java) } private val navigationOnIcon = IconLoader.getIcon("/icon/ic_jump.png" ) override fun getName () = "ActivityStarter Location" override fun getLineMarkerInfo (element: PsiElement ) : LineMarkerInfo<*>? { if (isNavigationCall(element)) { return LineMarkerInfo( element, element.textRange, navigationOnIcon, Pass.UPDATE_ALL, null , this , GutterIconRenderer.Alignment.LEFT ) } return null } override fun navigate (e: MouseEvent ?, element: PsiElement ?) { if (element == null ) { return } if (element is PsiCallExpression) { val method = element.resolveMethod() ?: return val parent = method.parent val activityName = (parent as PsiClass).qualifiedName?.dropLast("Builder" .length) ?: return logger.info("activityName: $activityName " ) val fullScope = GlobalSearchScope.allScope(element.project) val findClass = JavaPsiFacade.getInstance(element.project).findClass(activityName, fullScope) NavigationItem::class .java.cast(findClass).navigate(true ) return } Notifications.Bus.notify( Notification( NOTIFY_SERVICE_NAME, NOTIFY_TITLE, NOTIFY_NO_TARGET_TIPS, NotificationType.WARNING ) ) } private fun isNavigationCall (psiElement: PsiElement ) : Boolean { if (psiElement is PsiCallExpression) { val method = psiElement.resolveMethod() ?: return false val parent = method.parent if (method.name == "builder" && parent is PsiClass) { logger.info("builder caller: ${parent.name} " ) if (parent.hasAnnotation(GENERATED_ANNOTATION_NAME)) { return true } } } return false } }
在 Android Studio 中可以本地安装打包好的插件:
效果如下:
TODO
支持通过路由来处理无依赖关系的组件间的页面跳转
支持 KSP
结语 通过启动页面传参和处理返回值的这个点,我们大概了解了注解处理器的使用,以及可以解决开发中的哪些问题,可以解放我们的双手,从而提高我们的开发效率。
我们也可以从 ButterKnife、ARouter 和 Dagger 等优秀的框架学习注解处理器的更为进阶的使用,以及他们是如何通过这个工具来解决我们开发过程中的一些问题的。重要的是思路,以及我们可以在开发过程中发现怎样的问题,并善用工具去解决这些问题。
附上源码地址:https://github.com/qihuan92/ActivityStarter