聲明:本文也在我的微信公眾號 Android程序員(AndroidTrending) 發布。
原文鏈接:Better Android Intents with Dart & Henson
原文作者:Daniel Molinero Reguera
譯文出自:湯濤的簡書
譯者:湯濤
狀態:完成
最近看到這篇文章,感覺不錯,就翻譯了一下。文中提到的 Android Intent 的種種問題,有些也是我之前遇到的一些痛點,項目規模稍大一些后,有些問題會慢慢暴露出來,雖不是非常嚴重,但正是對代碼的精益求精,才是我們不斷進步的源泉,也是我推薦文章的重要標準。作者來自著名的團購鼻祖Groupon公司,相信這篇分享值得大家一看。
Intent 是 Android 生態系統的重要組成部分。他們用來表達一個執行動作,可分為隱式和顯式 Intent。在應用程序內部,所有的 Intent 以一種抽象的方式,一起定義了一個信息傳遞層。在本文中,我們將解釋為什么 Android 創建顯式 Intent 的方式容易出錯,也給大家展示一些有問題的應對方案。最后,我們將介紹一個生成這種信息傳遞層的庫:Dart & Henson,它使用簡單,能方便、快捷與健壯地在你的 Activity 和 Service 之間傳遞信息。
顯示 Intent 需要明確指定組件,常用于在應用內的 Activity 或 Intent 之間傳遞信息,額外的信息通過 extras 提供給目標組件,與 Intent 一起傳遞。比如下面的代碼,創建了一個顯示 Intent 來啟動 Activity:
Intent intent = new Intent(context, DetailActivity.class);
intent.putExtra(EXTRA_ITEM_ID, selectedItem.id);
intent.putExtra(EXTRA_SHOW_MAP, true);
startActivity(intent);
被啟動的 Activity 代碼可能是這樣的:
public class DetailActivity extends Activity {
public static final String EXTRA_ITEM_ID = "extra.item_id";
public static final String EXTRA_SHOW_MAP = "extra.show_map";
private String itemId;
private boolean shouldShowMap;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
itemId = getIntent().getStringExtra(EXTRA_ITEM_ID);
shouldShowMap = getIntent().getBooleanExtra(EXTRA_SHOW_MAP, false);
if (itemId == null) {
throw new IllegalArgumentException("Item Id is required");
}
...
}
...
}
這種機制很好地處理了組件的創建與通信,但仍然有一些問題需要注意:
- 目標組件作為一個實體,對輸入沒有任何控制權。在我們的例子里,itemId 是必需的,但如果沒有傳遞它,DetailActivity 最好的處理方式也只能是拋出異常。
- Intent 的創建(完全)不夠健壯,并沒有對 extra 中的 key 或 value 做任何檢查。
一分預防勝過十分治療
有問題的解決方案
解決這些問題的一個可能的方案是 Intent 工廠模式。它主要由一些工廠方法組成,包含了應用程序里用到的各種 Intent。比如像下面這樣的 Intent 工廠:
public class IntentFactory {
public Intent newDetailActivityIntent(Context context, String itemId, boolean showMap) {
Intent intent = new Intent(context, DetailActivity.class);
intent.putExtra(EXTRA_ITEM_ID, itemId);
intent.putExtra(EXTRA_SHOW_MAP, showMap);
return intent;
}
...
}
然而,這種解決方案有一些局限,并不是一個很好的辦法。
- Intent 工廠是一個集中類,這個類可能會變得很大且復雜。
- 它違背了開放/閉合原則。對修改并沒有關閉,我們將總是需要給每個新的 Activity 添加一個新方法。
- 目標組件應該是唯一知曉參數細節與邏輯的地方。
- 可選參數處理。同一個組件有不同的需求,是否應該寫不同的方法?還是寫一個方法并使用默認值?
- 它會誘使后續的開發人員模仿,進而產生其他的 Intent 工廠,最終演變成大泥球模式,使得代碼越來越糟。
有一個類似的策略可以分散這些工廠方法到各自的目標組件。也就是指,每個組件可以包含一個(或多個)靜態方法,用于生成這些啟動它自身的 Intent。這個辦法可以解決開放/閉合原則的問題,分解 Intent 工廠,也許還可以避免大泥球模式。盡管如此,關于可選參數的問題依然存在。有人說 builder 模式可以?我們自己實現它?...
我選擇用懶惰的人做困難的工作,因為一個懶惰的人會找到簡單的方法完成它。比爾蓋茨
Dart 2 & Henson
Dart 是一個 Android 開源庫。它綁定 Activity 字段到 Intent extra,Butter Knife 也是用類似的方案,關聯 Activity 與 XML 布局中的View。在我們的例子里,它看起來是這樣:
public class DetailActivity extends Activity {
@InjectExtra String itemId;
@Nullable @InjectExtra boolean shouldShowMap;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Dart.inject(this);
...
}
...
}
@InjectExtra 注解聲明了一個同名的 extra key,默認情況下,所有的注解字段都是必需的,如果 extra 沒有提供,會拋出異常。如果想使其可選,需要加上 @Nullable 注解。接下來,只需要調用 Dart.inject 即可自動生成相關代碼。
在 Groupon,我們意識到注解里的那些信息,已經足夠創建我們一直想要的builder模式。因此,我們決定在 Dart 基礎上再進一步:我們做了一個注解處理器,用于生成 Intent builders,這個新模塊叫做 Henson,它集成在 Dart 2 中。
在 DetailActivity 這個例子里,Henson 生成了一個小型的領域特定語言 (DSL),來使得跳轉到 DetailActivity 變得非常容易:
Intent intent = Henson.with(context)
.gotoDetailActivity()
.itemId(selectedItem.id)
.shouldShowMap(true)
.build();
startActivity(intent);
首先是通過 Henson.with(context).gotoXXX() 獲取目標 Activity 或 Service 的 builder。然后,使用自動生成的方法設置必需的 extras, 比如 itemId 是使用 itemId(String str)。之后,用同樣的方式設置可選參數。最后調用 build,你就可以得到一個有效的 Intent,用于啟動你的組件。
這段領域特定語言(DSL)會為所有@InjectExtra 注解標記的字段生成相關類。這相當于一個信息傳遞層,解決了我們創建 Intent 時碰到的那些問題:
- 通過注解,目標組件對 extras 擁有完全的控制權。
- DSL 定義在組件內的一處,如果它有修改,產生的問題都可以在編譯時被發現。
- 沒有違反開放/閉合原則,實際上,我們什么都不需要寫,一切都是自動生成。
- 因為使用了 builder 模式,可選參數很容易實現。
- 還可以自動補全代碼!
完整的示例代碼在這里。
總結
Henson 創建了一個小型的領域特定語言(DSL),可以更加健壯地構建啟動 Activity 與 Service 的 Intent,它允許缺失必需的extra,支持靈活的可選參數,最棒的是,使用 Dart 2 與 Henson,你一行代碼也不必寫了。??
還不趕緊試試?
f2prateek/dart---Extras "injection" Library for Android