Android中提取表單模型
我一直追求從Android活動(dòng)中分離代碼。在最近的一個(gè)項(xiàng)目中,我成功的實(shí)現(xiàn)了傳統(tǒng)的”Form Model”模式,想在此分享我的感想。
“Form Model”的基本思想是,把處理UI交互以及數(shù)據(jù)綁定和狀態(tài)保持的代碼提取到單獨(dú)的類中。這種分離非常自然,并且讓我們的Activity變得簡(jiǎn)單。
我認(rèn)為在Android中這個(gè)領(lǐng)域不太被關(guān)注——在大多數(shù)的開發(fā)文檔中數(shù)據(jù)錄入和表單不是重點(diǎn)。在很多流行的社交應(yīng)用程序中,大多數(shù)的畫面只是顯示信息;可能也有幾個(gè)畫面用于發(fā)微博或者消息,但不是應(yīng)用的痛點(diǎn)。
對(duì)我來說,上兩個(gè)Android應(yīng)用有特別多的數(shù)據(jù)錄入工作。部分原因是因?yàn)樗幍念I(lǐng)域(醫(yī)療、金融)和客戶(更貼近于企業(yè)應(yīng)用而不是創(chuàng)業(yè))。但我 們經(jīng)常把“表單輸入”界面搞得一片混亂——特別是當(dāng)開始添加?xùn)|西的時(shí)候,比如編輯現(xiàn)有的條目,提示丟棄未保存的更改,以及處理旋轉(zhuǎn)而不會(huì)清除所有字段值。
使用這種表單模型方案會(huì)減少bug,讓代碼更容易理解,開發(fā)者也會(huì)變得更快樂。
搜索表單示例
我們有一個(gè)銀行應(yīng)用程序,希望有一個(gè)畫面來搜索交易數(shù)據(jù)。有多個(gè)過濾條件:開始是一個(gè)金額下拉列表,一個(gè)關(guān)鍵字字段和一個(gè)金額范圍。(希望你可以想象在未來將會(huì)增加更多的這類過濾器,復(fù)雜性會(huì)激增)。
我們沒有把所有的視圖、單擊處理程序,驗(yàn)證邏輯和數(shù)據(jù)綁定的代碼堆到一個(gè)Activity中,而是要?jiǎng)?chuàng)建一個(gè) SearchForm類來處理這一切。
- public class SearchForm extends LinearLayout {
- @InjectView(R.id.account)
- private Spinner mAccountSpinner;
- private AccountAdapter mAccountAdapter;
- @InjectView(R.id.keyword)
- private EditText mKeywordField;
- @InjectView(R.id.min_amount)
- private CurrencyEditText mMinAmountField;
- @InjectView(R.id.max_amount)
- private CurrencyEditText mMaxAmountField;
- public SearchFormModel(Context context, AttributeSet attrs) {
- super(context, attrs);
- setup(context);
- }
- private void setup(Context context) {
- LayoutInflater.from(context).inflate(R.layout.search_form, this, true);
- ButterKnife.inject(this); // <3 @JakeWharton
- mAccountAdapter = new AccountAdapter(context);
- mAccountSpinner.setAdapter(mAccountAdapter);
- }
- public initialize(List<Account> accounts) {
- mAccountAdapter.setItems(accounts);
- }
- public String getKeywords() {
- return mKeywordField.getText().toString();
- }
- public void setKeywords(String keywords) {
- mKeywordField.setText(keywords);
- }
- public MoneyAmount getMinimumAmount() {
- return mMinAmountField.getAmount();
- }
- public void setMinimumAmount(double amount) {
- mMinmountField.setAmountFromDouble(amount);
- }
- public MoneyAmount getMaximumAmount() {
- return mMaxAmountField.getAmount();
- }
- public void setMaximumAmount(double amount) {
- mMaxAmountField.setAmountFromDouble(amount);
- }
- public Account getSelectedAccount() {
- return mAccountSpinner.getSelectedItem();
- }
- public boolean validate() {
- clearErrors();
- boolean isValid = true;
- if (!isValidAmountRange()) {
- isValid = false;
- mMinAmountField.setError("Invalid range");
- mMaxAmountField.setError("Invalid range");
- }
- return isValid;
- }
- private boolean isValidAmountRange() {
- return getMinimumAmount() <= getMaximumAmount();
- }
- private void clearErrors() {
- mMinAmountField.setError(null);
- mMaxAmountField.setError(null);
- }
- public SearchParameters buildParameters() {
- return new SearchParameters(getSelectedAccount(),
- getKeywords(),
- getMinimumAmount(),
- getMaximumAmount());
- }
- public void persist(Bundle outState) {
- outState.putInt("SELECTED_ACCT_INDEX", mAccountSpinner.getSelectedItemPosition());
- }
- public void restore(Bundle bundle) {
- int accountPosition = bundle.getInt("SELECTED_ACCT_INDEX");
- mAccountSpinner.setSelection(accountPosition, false);
- }
- }
#p#
我們創(chuàng)建了一個(gè)類,繼承自LinearLayout(或者FrameLayout,由你的喜好決定)。它允許把相關(guān)的控件組織到一個(gè)布局中,我們將填充布局,設(shè)置列表視圖并為金額列表創(chuàng)建一個(gè)適配器。
我們把Android控件封裝到getter和setter方法中——這可能會(huì)有些爭(zhēng)議,但我認(rèn)為它使SearchForm擁有更好的公共API。我們有一個(gè)方法來驗(yàn)證用戶的輸入,并根據(jù)需要提供錯(cuò)誤信息。 buildParameters()方法做了一些數(shù)據(jù)綁定工作并返回業(yè)務(wù)對(duì)象。結(jié)尾的兩個(gè)方法使用了Android onSaveInstanceState中的Bundle,以處理自定義配置的更改(注意,大多數(shù)的原始UI控件會(huì)自行處理持久化)。
這是個(gè)一百行左右的代碼,大部分還不錯(cuò)。這個(gè)類中所有內(nèi)容似乎都屬于“搜索表單”對(duì)象,對(duì)未來的特性有良好的功能擴(kuò)展點(diǎn)(日期范圍過濾器、支出與存款過濾器、只用支票等)。我們有意避免處理如何獲取數(shù)據(jù),把它留給了其他更適合的地方處理這些邏輯代碼。
活動(dòng)中的代碼是什么樣的呢?
- public class TransactionSearchActivity extends BaseActivity {
- @InjectView(R.id.search_form)
- private SearchForm mForm;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.transaction_search);
- setTitle("Search Your Transactions");
- mForm.initialize(mAccounts); // fetch accounts via API/DB/etc
- if (savedInstanceState != null) {
- mForm.restore(savedInstanceState);
- }
- }
- @Override
- public boolean onOptionsItemSelected(MenuItem menu) {
- switch (menu.getItemId()) {
- case R.id.action_submit_search:
- onSubmitSearch();
- return true;
- }
- return super.onOptionsItemSelected(menu)
- }
- private void onSubmitSearch() {
- if (mForm.validate()) {
- // Do your magic, post to an API/DB/etc
- // You have access to the domain object with mForm.buildParameters()
- }
- }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.search_menu, menu);
- return super.onCreateOptionsMenu(menu);
- }
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mForm.persist(outState);
- }
- }
我們的Activity在XML布局文件中包含了一個(gè) **標(biāo)簽,并且只處理高層面的用戶交互(點(diǎn)擊動(dòng)作欄中的提交按鈕),并協(xié)調(diào)獲取和存儲(chǔ)數(shù)據(jù)。繁重的UI控制和表單邏輯都委托給了 **SearchForm。
Activity的代碼在50行左右——其中大部分是處理框架中生命周期和菜單創(chuàng)建的樣板代碼。
總體印象
一旦涉及到API或數(shù)據(jù)庫(kù),事情總是會(huì)變得更復(fù)雜。但總體來講,通過把表單特定的邏輯和視圖相關(guān)內(nèi)容移出活動(dòng),代碼變得更容易理解。
我可以為 SearchForm編寫大量的Robolectric測(cè)試代碼而且不會(huì)帶來與活動(dòng)生命周期有關(guān)的問題。我可以為表單的交互、動(dòng)作欄、后端編寫測(cè)試代碼而不用考慮邊界。當(dāng)為表單添加新過濾條件時(shí),可以避免對(duì)活動(dòng)做任何的更改(類似于設(shè)計(jì)模式中的開/閉原則)。
對(duì)比其他框架(從其他開發(fā)人員的角度來說),Android中數(shù)據(jù)綁定功能很弱。這種設(shè)計(jì)似乎還差點(diǎn)什么,因?yàn)楹虯ndroid的類耦合的過于緊 密,依賴于方法的調(diào)用順序(initialize()方法應(yīng)在validate()方法之前調(diào)用)——盡管如此,但我認(rèn)為對(duì)于“所有內(nèi)容混在一起的 Activity”來說是一種改進(jìn)。
隨著表單模型越來越復(fù)雜,你可能要考慮把驗(yàn)證邏輯提取到一個(gè)單獨(dú)的對(duì)象中,并且把自定義視圖功能移動(dòng)到自己的控件中(就像我們例子中的 CurrencyEditText)。此外,為了更好的為用戶服務(wù),也可以考慮把復(fù)雜的表單拆分成為多步驟向?qū)А?/p>
我們發(fā)現(xiàn)這種模式可以成功的清理亂糟糟的表單代碼,建議嘗試一下。我把代碼模式稍微規(guī)范了一下,并創(chuàng)建了一個(gè)小的基類,以減少樣板代碼,可以隨意的使用。
譯文鏈接:http://blog.jobbole.com/73195/
原文鏈接:mdswanson
本文鏈接:http://blog.jobbole.com/73195/