Extracting form models in Android (1)
I have been pursuing code separation from Android activities. In a recent project, I have successfully implemented the traditional "Form Model" mode and want to share my thoughts here.
The basic idea of "Form Model" is to extract the code that processes UI interaction and Data Binding and status persistence to a separate class. This separation is very natural and makes our Activity simple.
I think this field is not very important in Android-data input and forms are not important in most development documents. In many popular social apps, most pictures only show information. There may also be several pictures used to send Weibo messages or messages, but they are not the pain points of applications.
For me, the last two Android applications have a lot of data input work. This is partly because of the medical and financial fields in which the customers are located ). However, we often confuse the "form input" interface-especially when you start to add something, such as editing existing entries, prompting you to discard unsaved changes, and process rotation without clearing all field values.
Using this form model solution reduces bugs, makes the code easier to understand, and makes developers happier.
Search form example
We have a banking application, hoping to have a screen to search for transaction data. There are multiple filtering conditions: the start is a drop-down list of amounts, a keyword field, and an amount range. I hope you can imagine that more filters will be added in the future, and the complexity will surge ).
Instead of heap all the views, click the handler, and the verification logic and data binding code into an Activity, we need to create a SearchForm class to process all the views.
- 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);
- }
- }