ActivityBuilder is a annotation base library using builder pattern to make inner activity communitication more easiely.

Demo

假设我们需要调用一个 EditorActivity,并在 EditorActivity 完成编辑后拿到编辑的文本,同时还要先其传递一个参数用于表示输入框的提示语(hint)

正常情况下,需要这样做:

private static final int REQUEST_SOME_TEXT = 0x2;

  private void requestSomeTextNormalWay() {
    findViewById(R.id.fab).setOnClickListener(
        view -> {
          Intent intent = new Intent(this, EditorActivity.class);
          intent.putExtra("hint", "say something");
          startActivityForResult(intent, REQUEST_SOME_TEXT);
        }
    );
  }

  @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
      case REQUEST_SOME_TEXT:
        if (resultCode == EditorActivityHelper.RESULT_CONTENT) {
          String text = data.getStringExtra("content");
          showToast(text);
        }
    }
  }

通过 ActivityBuilder,你可以把这些代码压缩成一行(You can solve it by one line of code 😆):

private void requestSomeText() {
    findViewById(R.id.fab).setOnClickListener(
        view ->
            EditorActivityBuilder.create(this)
                .hint("say something!")
                .forContent(this::showToast)
                .start()
    );
  }

你主要做的只是为 EditorActivity 添加几个注解,ActivityBuilder 会为你自动生成其他代码(take care the rest of it)

@Builder
@Result(name = "content", parameters = { @ResultParameter(name = "content", type = String.class) })
public class EditorActivity extends AppCompatActivity {
  @BuilderParameter String hint;
  ...
  }

可以在这里看到 EditorActivity 的完整代码。

当我们需要启动一个 Activity 的时候,最大的问题是不知道怎么使用它,通过 Intent 来传递参数有很大的随意性, ActivityBuilder 相当一个合约。

How to use

ActivityBuilder ,的生成代码使用了 lambda 表达式,需要在 build.gradle 加入如下配置:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

具体见 Use Java 8 language features | Android Studio

@Builder

使用 Builder 注解 Activity,如

@Builder
public class ${ActivityName} extends AppCompatActivity {

那么 ActivityBuilder 将会在相同的包下面为你生成 ${ActivityName}Builder${ActivityName}Helper 两个类。

使用 ${ActivityName}Builder

可以通过 ${ActivityName}Builder#create 方法获得 Activity Builder 实例。即便没有任何参数,${ActivityName}Builder 还有一些默认的配置方法。

主要有三个:

  • forCancel(Consumer<Intent>) 用于处理 RESULT_CANCEL 的回调
  • forOk(Consumer<Intent>) 用于处理 RESULT_OK 的回调
  • result(BiConsumer<Integer, Intent>)onActivityResult 相同

注意 Consumer 不同于 RxJava 的 Consumer,Intent 是可能为空的。

然后通过 start 方法来最终启动 Activity。start 方法会根据有没有回调而自动选择用 startActivty 还是 startActivityForResult 来启动 Activity

使用 ${ActivityName}Helper

${ActivityName}Helper 供所注解的 Activity 使用。Helper 所有方法都是 package 的。可以用 BuildUtil.createHelper(ActivityName) 来获取一个新的 ${ActivityName}Helper 实例。

@Builder
public class ${ActivityName} extends AppCompatActivity {
  EditorActivityHelper mHelper;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mHelper = BuilderUtil.createHelper(this);
  }
}

BuilderUtil.createHelper 会自动为 Activity 注入参数,注入过程需要获取 Activity 的 Intent 实例,所以必须得在 onCreate 中调用。如果不想自动注入。可以直接通过 new ${ActivityName}Helper(this) 构建实例,并调用 inject 方法注入参数。 ${ActivityName}Helper 还有另外两个 helper 方法:

  • save(Bundle) 用于 Activity#onSaveInstanceState 中保存需要保存的参数
  • restore(Bundle) 用于在 Bundle 恢复保存的参数

@BuilderParameter

@BuilderParameter 可用于 Activity 中的任何字段,当然字段不能是 private 的。因为这个字段需要依靠外部类 Helper 来注入。

@Builder
public class ${ActivityName} extends AppCompatActivity {
  @BuilderParameter String title; // can't no be private
}

每添加一个 BuilderParameter,${ActivityName}Builder 都会生成一个相应的 setter 支持链式调用的方法

public ${ActivityName}Builder<A> title(String title) {
  getIntent().putExtra("title");
  return this;
}

注意,必须要在调用 Helper 的 inject 方法之后,这个参数才是可用的,比如在 mHelper = BuilderUtil.createHelper(this); 后 title 才有值。

支持传递任何类型

我们知道用 Intent 来传递对象只支持部分特定的类型,而 BuilderParameter 没有这样的限制。BuilderParameter 的默认策略是这样的,支持用 Intent 传递的对象则用 Intent 传递。不支持的对象则直接传递引用。 可以通过配置 transmit 来控制传递对象的方法,比如强制使用引用传递字符串:

@Builder
public class ${ActivityName} extends AppCompatActivity {
  @BuilderParameter(transmit = TransmitType.Ref) String title;
  @BuilderParameter Object obj;
}

可以看到 setter 方法变了:

public ${ActivityName}Builder<A> title(String title) {
  getRefMap().put("title",title);
  return this;
}

public ${ActivityName}Builder<A> obj(Object obj) {
  getRefMap().put("obj", obj);
  return this;
}

key

默认情况下,BuilderParameter 所用的 key 就是其变量名。key 并没什么作用,不会暴露给调用者,但是如果出现冲突的话,可以通过 key 来配置其他 key。

keep

keep,表示参数会在 Helper#restore Helper#save 方法中进行保存和恢复。默认情况下是 false。而且 keep 只对能通过 Bundle 保存的对象生效。

@Result

@Result 注解可以用于 Activity 类,用于注解方法,两种途径都可以达到同样目的,如下注解类的例程:

@Builder
@Result(name = "content", parameters = { @ResultParameter(name = "content", type = String.class) })
public class EditorActivity extends AppCompatActivity {
}

和如下注解方法的例程,最终生成的代码是一样的:

@Builder
public class EditorActivity extends AppCompatActivity {
@Result void resultContent(String content){}
}

@Result 方法的命名,需要满足正则表达式 result(?<name>[A-Z][\w]*),如上面的方法 content 就会被当成这个 Result 的名称。

为什么 @Result 注解要有两种用法,主要的原因就是无法用注解来表示参数化类型或原生类型,所以只能通过方法声明来达到目的:

@Builder
public class EditorActivity extends AppCompatActivity {
@Result void resultSelected(int index, ArrayList<User> data){}
}

方法体可以为空,也可以不为空,比如调用 mHelper.resultSelected(index, data)。注解处理器不关心方法的实现,只解析方法的声明。

Helper

每个 Result ,Helper 都会为其一个常量和生成两个方法:

public class EditorActivityHelper {
  public static final int RESULT_CONTENT = Activity.RESULT_FIRST_USER + 1;
  ...
  void resultContent(String content) {
    Intent intent = new Intent();
    intent.putExtra("content",content);
    activity.setResult(RESULT_CONTENT,intent);
  }

  void finishContent(String content) {
    resultContent(content);
    activity.finish();
  }
}

然后在 Activity 中可以这样用:

@Override public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
    case R.id.action_ok:
      // set the content to result and finish activity
      mHelper.finishContent(mBinding.editText.getText().toString());
      return true;
  }
  ...
}

Builder

对于 Builder 来说,每个 Result 也会生成两个方法:

public class EditorActivityBuilder<A extends Activity> extends BaseActivityBuilder<EditorActivityBuilder<A>, A>{
  ...
  public EditorActivityBuilder<A> forContent(Consumer<String> contentConsumer) {
    getConsumer().contentConsumer = (activity, content) -> contentConsumer.accept(content);
    return this;
  }

  public EditorActivityBuilder<A> forContent(BiConsumer<A, String> contentConsumer) {
    getConsumer().contentConsumer = contentConsumer;
    return this;
  }
  ...
}

然后便可以这样使用 EditorActivityBuilder.create(this).forContent(System.out::println).start(),一行代码完成启动 Activity 并处理 onActivityResult 的回调。

为什么要有两个回调,见 {lambda 的引用问题},现在先来说说 Result Parameter

Result Parameter

每个 Result 可以有一个或多个 Parameter 也可以没有 Parameter,比如

@Builder
@Result(name = "delete")
public class UserDetailActivity extends AppCompatActivity {
}

相应的 Builder 方法:

public UserDetailBuilder<A> forDelete(Runnable deleteConsumer)

Result 是可以支持多参数的,但自带的 Callback 只有 3 个,分别是ConsumerBiConsumerTriConsumer.如果参数数量超过自带的 Consumer , ActivityBuilder 会自动创建新的 Consumer

@Result
public void resultAbcd(String a, String b, String c, String d) 

ActivityBuilder 会为我们创建新的 Consumer:

package info.dourok.esactivity.function;

public interface Consumer4<T0, T1, T2, T3> {
  void accept(T0 t0, T1 t1, T2 t2, T3 t3);
}

package info.dourok.esactivity.function;

public interface Consumer5<T0, T1, T2, T3, T4> {
  void accept(T0 t0, T1 t1, T2 t2, T3 t3, T4 t4);
}

相应的 Builder 方法

public ${ActivityName}Builder<A> forAbcd(Consumer4<String, String, String, String> abcdConsumer) {...}
public ${ActivityName}}Builder<A> forAbcd(Consumer5<A, String, String, String, String> abcdConsumer) {...}

TransmitType

@BuilderParameter 一样,默认情况下,能通过 Intent 传递的对象类型则通过 Intent 传递,其他对象则直接传递引用。但也可以配置不同的 TransmitType,对于 Result 方法来说,需要引入一个新注解 @Transmit 来配置方法参数

@Result public void resultText(@Transmit(TransmitType.REF) String name){}

多个 Result

一个 Activity 是可以有多个 Result 的,用方法声明配合 @Result 注解可以轻易实现多个 Result,只要方法名不一样便可以。但是 @Result 注解类,低于 java 8,是不能对同一目标使用多个相同注解,这时可以用 @ResultSet 来实现:

@Builder
@ResultSet(results = { 
              @Result(name = "date",parameters = {@ResultParameter(name = "date", type = Long.class)}),
              @Result(name = "text",parameters = {
                                     @ResultParameter(name = "ids", type = ArrayList.class),
                                     @ResultParameter(name = "name", type = Character.class)})})
public class SomeActivity extends AppCompatActivity {

}

当然还是注解方法更简洁:

@Result void resultDate(Long date){}
@Result void resultText(ArrayList ids, Character name){}

Lambda 引用的问题

lambda 实际上是可以理解为匿名内部类。