Android中提取表单模型

移动开发 Android
Form Model”的基本思想是,把处理UI交互以及数据绑定和状态保持的代码提取到单独的类中。这种分离非常自然,并且让我们的Activity变得简单。我认为在Android中这个领域不太被关注——在大多数的开发文档中数据录入和表单不是重点。在很多流行的社交应用程序中,大多数的画面只是显示信息;可能也有几个画面用于发微博或者消息,但不是应用的痛点。

我一直追求从Android活动中分离代码。在最近的一个项目中,我成功的实现了传统的”Form Model”模式,想在此分享我的感想。

“Form Model”的基本思想是,把处理UI交互以及数据绑定和状态保持的代码提取到单独的类中。这种分离非常自然,并且让我们的Activity变得简单。

我认为在Android中这个领域不太被关注——在大多数的开发文档中数据录入和表单不是重点。在很多流行的社交应用程序中,大多数的画面只是显示信息;可能也有几个画面用于发微博或者消息,但不是应用的痛点。

对我来说,上两个Android应用有特别多的数据录入工作。部分原因是因为所处的领域(医疗、金融)和客户(更贴近于企业应用而不是创业)。但我 们经常把“表单输入”界面搞得一片混乱——特别是当开始添加东西的时候,比如编辑现有的条目,提示丢弃未保存的更改,以及处理旋转而不会清除所有字段值。

使用这种表单模型方案会减少bug,让代码更容易理解,开发者也会变得更快乐。

搜索表单示例

我们有一个银行应用程序,希望有一个画面来搜索交易数据。有多个过滤条件:开始是一个金额下拉列表,一个关键字字段和一个金额范围。(希望你可以想象在未来将会增加更多的这类过滤器,复杂性会激增)。

我们没有把所有的视图、单击处理程序,验证逻辑和数据绑定的代码堆到一个Activity中,而是要创建一个 SearchForm类来处理这一切。

 

  1. public class SearchForm extends LinearLayout { 
  2.   
  3.   @InjectView(R.id.account) 
  4.   private Spinner mAccountSpinner; 
  5.   private AccountAdapter mAccountAdapter; 
  6.   
  7.   @InjectView(R.id.keyword) 
  8.   private EditText mKeywordField; 
  9.   
  10.   @InjectView(R.id.min_amount) 
  11.   private CurrencyEditText mMinAmountField; 
  12.   
  13.   @InjectView(R.id.max_amount) 
  14.   private CurrencyEditText mMaxAmountField; 
  15.   
  16.   public SearchFormModel(Context context, AttributeSet attrs) { 
  17.     super(context, attrs); 
  18.     setup(context); 
  19.   } 
  20.   
  21.   private void setup(Context context) { 
  22.     LayoutInflater.from(context).inflate(R.layout.search_form, thistrue); 
  23.   
  24.     ButterKnife.inject(this); // <3 @JakeWharton 
  25.   
  26.     mAccountAdapter = new AccountAdapter(context); 
  27.     mAccountSpinner.setAdapter(mAccountAdapter); 
  28.   } 
  29.   
  30.   public initialize(List<Account> accounts) { 
  31.     mAccountAdapter.setItems(accounts); 
  32.   } 
  33.   
  34.   public String getKeywords() { 
  35.     return mKeywordField.getText().toString(); 
  36.   } 
  37.   
  38.   public void setKeywords(String keywords) { 
  39.     mKeywordField.setText(keywords); 
  40.   } 
  41.   
  42.   public MoneyAmount getMinimumAmount() { 
  43.     return mMinAmountField.getAmount(); 
  44.   } 
  45.   
  46.   public void setMinimumAmount(double amount) { 
  47.     mMinmountField.setAmountFromDouble(amount); 
  48.   } 
  49.   
  50.   public MoneyAmount getMaximumAmount() { 
  51.     return mMaxAmountField.getAmount(); 
  52.   } 
  53.   
  54.   public void setMaximumAmount(double amount) { 
  55.     mMaxAmountField.setAmountFromDouble(amount); 
  56.   } 
  57.   
  58.   public Account getSelectedAccount() { 
  59.     return mAccountSpinner.getSelectedItem(); 
  60.   }  
  61.   
  62.   public boolean validate() { 
  63.     clearErrors(); 
  64.     boolean isValid = true
  65.   
  66.     if (!isValidAmountRange()) { 
  67.       isValid = false
  68.       mMinAmountField.setError("Invalid range"); 
  69.       mMaxAmountField.setError("Invalid range"); 
  70.     } 
  71.   
  72.     return isValid; 
  73.   } 
  74.   
  75.   private boolean isValidAmountRange() { 
  76.     return getMinimumAmount() <= getMaximumAmount(); 
  77.   } 
  78.   
  79.   private void clearErrors() { 
  80.     mMinAmountField.setError(null); 
  81.     mMaxAmountField.setError(null); 
  82.   } 
  83.   
  84.   public SearchParameters buildParameters() { 
  85.     return new SearchParameters(getSelectedAccount(), 
  86.                                 getKeywords(), 
  87.                                 getMinimumAmount(), 
  88.                                 getMaximumAmount()); 
  89.   } 
  90.   
  91.   public void persist(Bundle outState) { 
  92.     outState.putInt("SELECTED_ACCT_INDEX", mAccountSpinner.getSelectedItemPosition()); 
  93.   } 
  94.   
  95.   public void restore(Bundle bundle) { 
  96.     int accountPosition = bundle.getInt("SELECTED_ACCT_INDEX"); 
  97.     mAccountSpinner.setSelection(accountPosition, false); 
  98.   } 

 #p#

我们创建了一个类,继承自LinearLayout(或者FrameLayout,由你的喜好决定)。它允许把相关的控件组织到一个布局中,我们将填充布局,设置列表视图并为金额列表创建一个适配器。

我们把Android控件封装到getter和setter方法中​——这可能会有些争议,但我认为它使SearchForm拥有更好的公共API。我们有一个方法来验证用户的输入,并根据需要提供错误信息。 buildParameters()方法做了一些数据绑定工作并返回业务对象。结尾的两个方法使用了Android onSaveInstanceState中的Bundle,以处理自定义配置的更改(注意,大多数的原始UI控件会自行处理持久化)。

这是个一百行左右的代码,大部分还不错。这个类中所有内容似乎都属于“搜索表单”对象,对未来的特性有良好的功能扩展点(日期范围过滤器、支出与存款过滤器、只用支票等)。我们有意避免处理如何获取数据,把它留给了其他更适合的地方处理这些逻辑代码。

活动中的代码是什么样的呢?

 

  1. public class TransactionSearchActivity extends BaseActivity { 
  2.   
  3.   @InjectView(R.id.search_form) 
  4.   private SearchForm mForm; 
  5.   
  6.   @Override 
  7.   public void onCreate(Bundle savedInstanceState) { 
  8.     super.onCreate(savedInstanceState); 
  9.   
  10.     setContentView(R.layout.transaction_search); 
  11.     setTitle("Search Your Transactions"); 
  12.   
  13.     mForm.initialize(mAccounts); // fetch accounts via API/DB/etc 
  14.   
  15.     if (savedInstanceState != null) { 
  16.       mForm.restore(savedInstanceState); 
  17.     } 
  18.   } 
  19.   
  20.   @Override 
  21.   public boolean onOptionsItemSelected(MenuItem menu) { 
  22.     switch (menu.getItemId()) { 
  23.       case R.id.action_submit_search: 
  24.         onSubmitSearch(); 
  25.         return true
  26.     } 
  27.   
  28.     return super.onOptionsItemSelected(menu) 
  29.   } 
  30.   
  31.   private void onSubmitSearch() { 
  32.     if (mForm.validate()) { 
  33.       // Do your magic, post to an API/DB/etc 
  34.       // You have access to the domain object with mForm.buildParameters() 
  35.     } 
  36.   } 
  37.   
  38.   @Override 
  39.   public boolean onCreateOptionsMenu(Menu menu) { 
  40.     getMenuInflater().inflate(R.menu.search_menu, menu); 
  41.     return super.onCreateOptionsMenu(menu); 
  42.   } 
  43.   
  44.   @Override 
  45.   protected void onSaveInstanceState(Bundle outState) { 
  46.     super.onSaveInstanceState(outState); 
  47.   
  48.     mForm.persist(outState); 
  49.   } 

 

我们的Activity在XML布局文件中包含了一个 **标签,并且只处理高层面的用户交互(点击动作栏中的提交按钮),并协调获取和存储数据。繁重的UI控制和表单逻辑都委托给了 **SearchForm。

Activity的代码在50行左右——其中大部分是处理框架中生命周期和菜单创建的样板代码。

总体印象

一旦涉及到API或数据库,事情总是会变得更复杂。但总体来讲,通过把表单特定的逻辑和视图相关内容移出活动,代码变得更容易理解。

我可以为 SearchForm编写大量的Robolectric测试代码而且不会带来与活动生命周期有关的问题。我可以为表单的交互、动作栏、后端编写测试代码而不用考虑边界。当为表单添加新过滤条件时,可以避免对活动做任何的更改(类似于设计模式中的开/闭原则)。

对比其他框架(从其他开发人员的角度来说),Android中数据绑定功能很弱。这种设计似乎还差点什么,因为和Android的类耦合的过于紧 密,依赖于方法的调用顺序(initialize()方法应在validate()方法之前调用)——尽管如此,但我认为对于“所有内容混在一起的 Activity”来说是一种改进。

随着表单模型越来越复杂,你可能要考虑把验证逻辑提取到一个单独的对象中,并且把自定义视图功能移动到自己的控件中(就像我们例子中的 CurrencyEditText)。此外,为了更好的为用户服务,也可以考虑把复杂的表单拆分成为多步骤向导。

我们发现这种模式可以成功的清理乱糟糟的表单代码,建议尝试一下。我把代码模式稍微规范了一下,并创建了一个小的基类,以减少样板代码,可以随意的使用

译文链接:http://blog.jobbole.com/73195/

原文链接:mdswanson

本文链接:http://blog.jobbole.com/73195/

翻译: 伯乐在线 - lum

 

责任编辑:chenqingxiang 来源: 伯乐在线
相关推荐

2023-11-29 11:30:17

PDF语言模型

2023-11-15 13:04:30

Python提取表格

2021-05-13 23:54:12

DockerDockerfile镜像

2020-07-08 07:54:03

PythonPDF数据

2022-11-23 10:31:54

2019-09-29 09:08:41

Python数据库Google

2022-08-24 15:57:17

图片轮廓

2016-01-26 11:08:54

2021-09-04 23:45:40

机器学习语言人工智能

2022-09-29 15:39:10

服务器NettyReactor

2019-09-04 11:09:38

物联网数据边缘

2013-04-01 11:14:56

IT大数据网络信息化

2023-04-27 07:06:09

Categraf夜莺

2021-03-16 09:00:00

深度学习人工智能传感器

2021-03-15 21:50:22

Linux提取文本GUI工具

2023-08-16 17:44:38

2021-08-16 11:51:16

微软Windows 365Azure

2021-03-10 10:20:06

Linux文本命令

2020-05-08 11:12:58

恶意软件PC安全终端安全

2020-10-30 11:02:16

物联网人工智能机器学习
点赞
收藏

51CTO技术栈公众号