前面的註解處理器教程涉及到的 Tieguanyi 框架現在怎麼樣了?

  • 2020 年 2 月 20 日
  • 筆記

前不久我錄製了一套講解註解處理器的影片,當中用到了一個叫 「Tieguanyin」 的框架的簡化版,這篇文章主要介紹下完整版。

項目是做什麼的?

我們遇到了怎樣的問題

我們先來看個例子:

public class UserActivity extends Activity {        String name;      int age;      String title;      String company;        ...  }

我們有這樣一個 Activity,啟動它,我們需要傳入四個參數,那麼我們通常會怎麼做呢?

Intent intent = new Intent(this, UserActivity.class);  intent.putExtra("age", age);  intent.putExtra("name", name);  intent.putExtra("company", company);  intent.putExtra("title", title);  startActivity(intent);

僅僅是這樣,還不夠,所以我們還需要在 UserActivity 這個類當中去讀取這些值:

Intent intent = getIntent();  this.age = intent.getIntExtra( "age", 0);  this.name = intent.getStringExtra("name");  this.company = intent.getStringExtra("company" );  this.title = intent.getStringExtra("title");

如果你只有這麼一個 Activity 那倒也還好,可是如果你有十個這樣的 Activity 呢?

我們怎麼去解決

其實我們仔細觀察前面的程式碼,就會發現這兩大段傳參和讀參的程式碼,都是模式化的程式碼,我們只需要通過註解處理器來生成就可以了,因此我們給出的解決方法是:

@Builder  public class UserActivity extends Activity {        @Required      String name;        @Required      int age;        @Optional      String title;        @Optional      String company;        ...  }

這樣的話,對於 Java 程式碼,我們會生成 UserActivityBuilder,通過它啟動 UserActivity

UserActivityBuilder.builder(30, "bennyhuo")          .company("Kotliner")          .title("Kotlin Developer")          .start(this);

注意到,我們的 nameage 都是 Required,因此我們生成的 Builder 在構造時必須對他們進行賦值,而其他兩個因為是 Optional,用戶可以根據實際情況選擇性調用。

而對於 Kotlin 來說,我們則選擇為 ContextViewFragment 生成擴展方法,所以我們只需要:

startUserActivity(30, "bennyhuo", "Kotliner", "Kotlin Developer")

需要注意的是,對於 companytitle 這兩個可選的欄位,我們的擴展方法提供了默認參數 null,因此我們可以選擇性提供這些參數的值:

startUserActivity(30, "bennyhuo",  title = "Kotlin Developer")

這些方便快捷的方法幫我們處理了 Intent 傳遞參數的過程,當然,我們也在運行時對 Activity 的聲明周期進行了監聽,在 ActivityonCreate 方法調用時,對這些參數進行了注入,因此:

@Override  protected void onCreate(@Nullable Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      ...      nameView.setText(name);      ...  }

super.onCreate(savedInstanceState); 之後,屬性 name 就已經被合理的初始化了。

Fragment 我們也提供了類似的邏輯。

狀態保存

在一些特定的場景下,例如轉屏時, Activity 或者 Fragment 會被銷毀並重新創建,銷毀前會調用 onSaveInstanceState 來保存狀態。我們同樣通過監聽其生命周期來實現對用戶配置好的屬性的值進行保存,以保證這些屬性在 Activity 或者 Fragment 重新創建時能夠得以恢復。

Activity 轉場

除了提供參數傳遞功能外,還支援通過註解為 Activity 配置 pendingTransition,例如:

@Builder(pendingTransition = PendingTransition(enterAnim = R.anim.fade_in, exitAnim = R.anim.fade_out))  class UserActivity : AppCompatActivity() {      ...  }

這樣每次啟動 UserActivity 時,我們都會在相應的方法當中調用 overridePendingTransition 來設置這些轉場動畫。

SharedElement 元素動畫

從 Android 5.0 開始,系統在 Activity、Fragment、View 之間支援了共享元素動畫,但介面使用起來略顯複雜,因此我們通過對 Activity 或者 Fragment 添加註解,在啟動或者顯示相應的組件時,調用相應的方法來實現共享元素動畫,讓頁面的跳轉更加連貫。

我們支援用戶通過 idtransitionName 來實現元素的關聯。

@Builder(          sharedElements = [SharedElement(sourceId = R.id.openJavaActivity, targetName = "hello")],          sharedElementsWithName = [(SharedElementWithName("button2"))],          sharedElementsByNames= [(SharedElementByNames(source = "button1",target = "button3"))]  )  class DetailsActivity : AppCompatActivity() {      ...  }

Activity 的結果

有些情況下我們需要目標 Activity 在結束時回傳一些結果給當前 Activity,例如我們為了修改用戶資訊,需要從 UserActivity 跳轉到 EditUserActivity,編輯完成之後需要把修改後的結果返回給 UserActivity,我們只需要:

@Builder(resultTypes = {@ResultEntity(name = "name", type = String.class),                  @ResultEntity(name = "age", type = int.class),                  @ResultEntity(name = "title", type = String.class),                  @ResultEntity(name = "company", type = String.class)})  public class EditUserActivity extends Activity {      ...  }

這樣我們就可以這樣啟動 EditUserActivity

EditUserActivityBuilder.builder(30, "Kotlin", "bennyhuo", "Kotlin Developer")          .start(this, new EditUserActivityBuilder.OnEditUserActivityResultListener() {              @Override              public void onResult(int age, String company, String name, String title) {                  ... // handle result              }          });

編輯之後這樣返回:

EditUserActivityBuilder.smartFinish(this, 36, "Kotliner","bennyhuo", "Kotlin Dev");

如果是 Kotlin 程式碼,那麼我們還可以使用 Lambda 表達式讓程式碼變得簡單:

startEditUserActivity(36, "Kotliner", "bennyhuo", "Kotlin Dev"){      age, company, name, title ->      ... // handle result  }

值得一提的是,對於在編輯用戶資訊時, UserActivity 的實例因各種原因(例如開發者選項中的」不保留活動「開啟時)被銷毀,從 EditUserActivity 返回時, UserActivity 被重新創建,導致之間的回調(匿名內部類、Lambda 表達式)持有的外部引用失效,進而使回調沒有意義。為了解決這個問題,我會在頁面返回,上一個頁面被重新創建時嘗試替換掉失效的實例以保證回調可以正常使用,其中主要包括:

  1. 外部 Activity 的實例,這個通常沒有問題。
  2. 外部 View 的實例,通常也是回調所在的 Activity 當中的 View,在更新實例時,我們通過 View 的 id 來索引,因此如果布局當中有重複的 id,回調可能將無法更新到正確的實例而產生問題。因此請注意保持 Activity 的布局當中 View 的 id 的唯一性。
  3. 外部 Fragment 的實例,通常也是所在的 Activity 當中的 Fragment,為了保證 Fragment 的唯一性,我使用了 Fragment 未公開的屬性 mWho 來進行索引。

儘管從理論的角度,這個更新實例的方法較為可靠,但畢竟這個功能比較 Tricky,如果大家在使用過程中發現回調調用之後沒有反應,那麼請開 Issue 一起討論解決方案。

屬性名常量

有些情況下,大家在頁面跳轉時不是很方便調用我們生成的方法,那麼這時候為了方便使用,我們也會生成以屬性名為值的常量,方便使用,例如:

public final class UserActivityBuilder {    public static final String REQUIRED_age = "age";    public static final String REQUIRED_name = "name";    public static final String OPTIONAL_company = "company";    public static final String OPTIONAL_title = "title";    ...  }

Fragment 支援

由於從 API 28 開始,Android 廢棄了 android.app.Fragment 相關的 API,轉而推薦使用 support-fragment,同時由於框架本身也需要監聽 Fragment 的生命周期,因此我們對於 android.app.Fragment不予支援,請諒解。

項目如何接入?

倉庫配置:

repositories {      ...      jcenter()      ...  }

依賴配置:

api "com.bennyhuo.tieguanyin:tieguanyin-runtime:$latest_version"  kapt "com.bennyhuo.tieguanyin:tieguanyin-compiler:$latest_version"

如果你不用 Kotlin,那麼 kapt 替換成 annotationProcessor。

最後在 ApplicationonCreate 當中調用:

Tieguanyin.init(this);

即可。

項目狀態

  • 當前最新版本 2.0-beta2
  • 當前項目的 compiler 模組已經使用 Kotlin 重構,程式碼較 1.0 時更緊湊和靈活,部分 Api 也做了一些調整。
  • 為了保證純 Java 用戶的正常使用,runtime 和 annotation 兩個模組將一直使用純 Java 開發。
  • 歡迎大家開 Issue,有空時我就會來迭代一下~

其他相關

  • Apt-Utils:解決了類型在 Java 和 Kotlin 之間的統一性和兼容性問題,提供了註解處理器一些常用的工具方法,尤其適合約時生成 Java 和 Kotlin 程式碼的註解處理器項目。
  • Apt-Tutorials:基於本項目簡化後並錄製的一套註解處理器的教學影片。

為什麼叫這個名字?

因為我比較喜好喝茶,這個框架開發期間主要喝鐵觀音。相應的,之前有一段時間常喝茉莉花,在公司內部做了一套框架被我命名為 "Jasmine"。