从 startActivity 谈谈 APT 的应用

本文通过对 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");

这里我们就会发现两个问题:

  1. 传值不安全

这几个变量我们分别定义了几个魔法值,如果我们想跳转到这个页面需要到详情页找到 getIntent() 调用的地方,并获取到这些 key 来传值,如果是两个开发人员分别负责这两个页面,极大的降低了开发效率以及增加了许多沟通成本。

当然在我们现在的开发中,这些 key 会在目的 Activity 定义为常量,来提高安全性,但是这样我们依然需要查看目标文件这些变量的定义。

  1. 逻辑分散

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;

/**
* Start DetailActivity.
*
* @param context context
* @param id id
* @param userId userID
* @param title detail content
*/
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",
"测试标题"
);

解决的问题:

  1. 降低了调用者的调用成本,通过注释的方式也降低了沟通成本。
  2. 使传参和取参的逻辑聚合到了一个页面,提高了可维护性。

未解决的问题:

  1. 在目标 Activity 中获取参数逻辑分散的问题,如果新增或者修改字段甚至还要多修改一个地方。
  2. 需要自己写 start() 函数,比较繁琐。

再次思考

经过以上优化后,我们还存在两个问题

  1. 参数获取问题

    1. 获取参数 → 给成员变量赋值
  2. 需要手动写 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() {
// TODO call server...
}

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}

还有路由框架 ARouter:

// Declare a field for each parameter and annotate it with @Autowired
@Route(path = "/test/activity")
public class Test1Activity extends Activity {
@Autowired
public String name;
@Autowired
int age;
@Autowired(name = "girl") // Map different parameters in the URL by name
boolean boy;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this);

// ARouter will automatically set value of fields
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 解决页面跳转传参问题

  1. 写出要生成的代码

首先我们需要写出 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;

// 非必传参数可以通过以参数为名的方法传入
/**
* @param title 详情内容
*/
public DetailActivityBuilder title(String title) {
this.title = title;
return this;
}

// 必传参数需要通过 builder 方法创建出来
/**
* @param id
* @param userId 用户ID
*/
public static DetailActivityBuilder builder(long id, String userId) {
DetailActivityBuilder builder = new DetailActivityBuilder();
builder.id = id;
builder.userId = userId;
return builder;
}

// 将参数存到 Intent 中,方便跳转获取
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", "");
}
}
}

// 可以将传入的参数保存到 Bundle 中,以方便屏幕旋转或其他配置发生改变时恢复
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());
}
}

// NewIntent 情况
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());
}
}

// 启动目标 Activity
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);
}
}
  1. 定义注解

@Builder 注解

根据目标 Activity 来生成对应的 Builder 类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Builder {
}

@Extra 注解

打在参数上的注解,用来执行解析和注入工作,类似于 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 "";
}
  1. 引入 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) {
// TODO 处理并生成 Builder 代码
// 收集 Activity 类信息
// 收集参数(打上 @Extra 注解的成员变量)信息
// 根据所收集的信息生成代码文件
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() 方法。

  1. 使用 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()

上文我们处理了页面传参的情况,另外还有一种情况需要我们考虑,就是如何获取页面返回的参数,相同的,返回参数的情况也会遇到同样的问题,所以页面返回的参数也需要注解处理器帮我们处理一下。

  1. 定义 @ResultField 注解

@Retention(RetentionPolicy.CLASS)
public @interface ResultField {
String name();

Class<?> type();
}

在打 @Builder 注解的时候,可以传入返回值的名称和类型:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Builder {
ResultField[] resultFields() default {};
}
// 返回类型为 String 的 color 参数
@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);
}

// 启动 Activity 解析返回参数
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;
}

// 目标 Activity 通过此方法返回结果
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 注解,来规范我们的返回结果,使用方式如下:
// 返回类型为 String 的 color 参数
@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;
// TODO do something...
}
}
  1. 适配 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;
}

// 我们定义输入为目标Builder,输出为返回结果
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 中注册 Activity 生命周期的回调
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 来生成一些扩展函数,例如:

  1. 启动页面方法的扩展函数

// DetailActivityBuilder.kt
// 通过可控参数的方式来处理非必传参数
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", "测试标题")
  1. Activity Result API 扩展函数

// ColorSelectActivityBuilder.kt
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)
}
  1. finish 扩展函数

public fun KotlinActivity.finish(testResult: String): Unit {
KotlinActivityBuilder.finish(this, testResult)
}

调用方式如下:

finish("success")

开发 IDEA 插件支持快速跳转到目标 Activity

当我们使用原来的方法进行开发时,在 Android Studio 里边很方便的就可以跳转到目标 Activity:

DetailActivity.start(
this,
123456L,
"100008",
"测试标题"
);

e2c813e9-7e8e-42e0-9919-766c2ed4c0b3

但是我们使用注解处理器生成的 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<*>? {
// 找到 builder 方法,添加标记
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
// 获取目标 Activity 全类名
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 中可以本地安装打包好的插件:

c4ff6396-9426-4bf5-ba23-c0e696060665

e5528a01-fd05-451e-8983-e1b787263654

效果如下:

4ef8b522-e23f-4182-aebf-caf44d2db88f

TODO

  • 支持通过路由来处理无依赖关系的组件间的页面跳转
  • 支持 KSP

结语

通过启动页面传参和处理返回值的这个点,我们大概了解了注解处理器的使用,以及可以解决开发中的哪些问题,可以解放我们的双手,从而提高我们的开发效率。

我们也可以从 ButterKnife、ARouter 和 Dagger 等优秀的框架学习注解处理器的更为进阶的使用,以及他们是如何通过这个工具来解决我们开发过程中的一些问题的。重要的是思路,以及我们可以在开发过程中发现怎样的问题,并善用工具去解决这些问题。

附上源码地址:https://github.com/qihuan92/ActivityStarter

评论