Montag, 10. Februar 2014

BARACUS from Scratch - Part 6 - Automatic Form Validation (Chapter Two of Two)

Table of Contents

In this tutorial, we are going to apply the newly learned knowledge and start creating a form including validation. This tutorial is based on the part one of the automatic form validation chapter. So You should check out that, first.


Previous Tutorial : Automatic Form Validation (Chapter one of two)

Download the sourcecode for this tutorial from github

Step 1 : We need a form!

To validate a form, we first need a form. Therefore, we are going to add a customer_editor.xml to the res/layout files. For the beginning, we only place a horizontal layout and the labels and the save button to the xml :


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/customerFirstName"
            android:id="@+id/customerFirstNameLabel" android:layout_gravity="center"/>

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/customerLastName"
            android:id="@+id/customerNameLabel" android:layout_gravity="center"/>

    <Button
            android:id="@+id/btnSave"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/saveCustomer"
            android:onClick="btnSaveClicked"
            />

</LinearLayout>

Do not forget to add the labels to the strings.xml :

...
    <string name="customerFirstName">First Name</string>
    <string name="customerLastName">Last Name</string>
    <string name="saveCustomer">save customer</string>
...

So far, so good. Now let's add the Text Edit Fields.

Step 2 : Adding the fields

In order to validate fields, we learned, that we need to add vaildateable fields. These are brought in by the framework. Please notice, that the app:validatedBy denotes the named constraint which is applied to the field:

<net.mantucon.baracus.ui.ConstrainedEditText android:id="@+id/txtCustomerFirstName"
                                                 android:layout_width="fill_parent"
                                                 android:layout_height="wrap_content"
                                                 app:validatedBy="stringNotEmpty"
            />

and
<net.mantucon.baracus.ui.ConstrainedEditText android:id="@+id/txtCustomerLastName"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              app:validatedBy="stringNotEmpty"  />

The stringNotEmpty-Constraint is a framework built-in. Later, we are going to build Our own custom validator.

Step 3 : Form Class

After we have added the fields, we need to create the java class for this form. I chose an Activity for this, but You also can use a Fragment. In order to have luxury goods like automatic form validation, dependency injection and so on, BARACUS introduces the ManagedFragment and the ManagedActivity class. Notice, ManagedFragment also is needed, if Your application supports device rotation (internally, the device calls the constructor of the Fragment class and this causes all injected by BARACUS to be nullified; this problem is solved by the ManagedFragment).

There is the form class :

package net.mantucon.baracus;

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.TextView;
import net.mantucon.baracus.annotations.Bean;
import net.mantucon.baracus.application.ApplicationContext;
import net.mantucon.baracus.context.ManagedActivity;
import net.mantucon.baracus.dao.CustomerDao;
import net.mantucon.baracus.model.Customer;
import net.mantucon.baracus.validation.ValidationFactory;

/**
 * Created by marcus on 03.02.14.
 */
public class CustomerEditorActivity extends ManagedActivity {

    TextView lastName; // sic.
    TextView firstName;
    Customer customer;

    @Bean
    CustomerDao customerDao;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.customer_editor);
        enableFocusChangeBasedValidation();

        lastName = (TextView) findViewById(R.id.txtCustomerLastName);
        firstName = (TextView) findViewById(R.id.txtCustomerFirstName);


        // If we edit an existing customer, load the customer data
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            Long customerId = extras.getLong("customerId");
            customer = customerDao.getById(customerId);
            lastName.setText(customer.getLastName());
            firstName.setText(customer.getFirstName());
        } else {
            customer = new Customer();
        }

    }

    public void btnSaveClicked(View v) {
        if (validate()) { // re-validate, is it ok?
            customer.setFirstName(firstName.getText().toString());  // ok : take data, write it back into DB.
            customer.setLastName(lastName.getText().toString());
            customerDao.save(customer);
            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)); // go back to caller
            finish();
        } else {
            Toast.makeText(this, R.string.insufficientData, Toast.LENGTH_LONG).show();
        }
    }
}

Pretty easy, huh? So this is all we need. If You violate one of the declared constraint an automatic notification happens to the form - the constraint violating form components become highlighted and a toast is displayed.

Step 4 : Wiring the dialogs


OK, now let's wire this activity to the caller Activity. I want a Customer -> BankAccount expandable List, wiring the long touch on the customer as the open action for the customer editor activity.

To dangle the stuff, at first we modify the main XML. We add  the ExpandableListView to Our main :


...
<expandablelistview android:groupindicator="@null" android:id="@+id/expandableListView" android:layout_height="match_parent" android:layout_width="match_parent"/>
...

Next, we need the layout description for the group and the child item.
entry_list_group_item.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="55dip"
              android:orientation="vertical" >


    <TextView
            android:id="@+id/tvGroup"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>


</LinearLayout>

and entries_list_child_item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="55dip"
              android:orientation="horizontal"  android:selectAllOnFocus="true" >

    <TextView
            android:id="@+id/entryName"
            android:layout_width="150dip"
            android:layout_height="wrap_content"
            android:textSize="14dip"/>

    <TextView
            android:id="@+id/entryAccount"
            android:layout_width="175dip"
            android:layout_height="wrap_content"
            android:textSize="14dip"/>


</LinearLayout>

To manage an ExpandableList View, we are going to need a suitable Adapter. This adapter is derived from BaseExpandableListAdapter :
package net.mantucon.baracus;

import android.app.Activity;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
import net.mantucon.baracus.application.ApplicationContext;
import net.mantucon.baracus.model.BankAccount;
import net.mantucon.baracus.model.Customer;
import net.mantucon.baracus.signalling.DataChangeAwareComponent;

import java.util.ArrayList;
import java.util.List;

/**
 * List Adapter Class for displaying accounts
 */
public class AccountExpandListAdapter extends BaseExpandableListAdapter {

    private Activity context;       // we need this one to inflate the detail rows
    private List<Customer> groups;  // customer master rows

    public AccountExpandListAdapter(Activity context, List<Customer> groups) {
        this.context = context;
        this.groups = groups;
    }

    /**
     * @param groupPosition - the customer row index
     * @param childPosition - the account row index
     * @return child object from group x, child y
     */
    public Object getChild(int groupPosition, int childPosition) {
        List<BankAccount> entries = groups.get(groupPosition).getAccounts();
        ArrayList<BankAccount> chList = new ArrayList<BankAccount>(entries);
        return chList.get(childPosition);
    }

    /**
     * @param groupPosition
     * @param childPosition
     * @return child Id which is equal to our position
     */
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    /**
     * create the child view for a child on a certain position
     *
     * @param groupPosition - the customer row index
     * @param childPosition - the account row index
     * @param isLastChild
     * @param view - the view (the child view, can be null)
     * @param parent - the parent view (the customer)
     * @return the view
     */
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View view,
                             ViewGroup parent) {

        final BankAccount child = (BankAccount) getChild(groupPosition, childPosition);

        if (view == null) {
            LayoutInflater infalInflater = (LayoutInflater) context.getSystemService(context.LAYOUT_INFLATER_SERVICE);
            view = infalInflater.inflate(R.layout.entries_list_child_item, null);
        }
        final TextView entryName = (TextView) view.findViewById(R.id.entryName);
        entryName.setText(child.getBankName().toString());


        final TextView entryValue = (TextView) view.findViewById(R.id.entryAccount);
        entryValue.setText(child.getIban());

        return view;
    }

    public int getChildrenCount(int groupPosition) {
        ArrayList<BankAccount> chList = new ArrayList<BankAccount>(groups.get(groupPosition).getAccounts());

        return chList.size();

    }

    public Object getGroup(int groupPosition) {
        return groups.get(groupPosition);
    }

    public int getGroupCount() {
        return groups.size();
    }

    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    /**
     * creat the customer view
     * @param groupPosition
     * @param isLastChild
     * @param view
     * @param parent
     * @return the customer view
     */
    public View getGroupView(int groupPosition, boolean isLastChild, View view,
                             ViewGroup parent) {
        Customer group = (Customer) getGroup(groupPosition);
        if (view == null) {
            LayoutInflater inf = (LayoutInflater) context.getSystemService(context.LAYOUT_INFLATER_SERVICE);
            view = inf.inflate(R.layout.entries_list_group_item, null);
        }
        TextView tv = (TextView) view.findViewById(R.id.tvGroup);
        tv.setText(group.getLastName()+", "+group.getFirstName());
        return view;
    }

    public boolean hasStableIds() {
        return true;
    }

    public boolean isChildSelectable(int arg0, int arg1) {
        return true;
    }

}

Finally, we dangle the stuff into the HelloAndroidActivity. So the onCreate() method is extended like this :


...
     expandableListView = (ExpandableListView) findViewById(R.id.expandableListView);

     fillTable();
...

/**
     * fill the data trable
     */
    private void fillTable() {

        final List<Customer> customers = customerDao.loadAll();
        AccountExpandListAdapter adapter = new AccountExpandListAdapter(this, new ArrayList<Customer>(customers));

        expandableListView.setAdapter(adapter);

        expandableListView.setLongClickable(true);

        expandableListView.setClickable(true);
        // Handle the long click; hold the customer long to open the CustomerEditor Activity
        expandableListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                Intent intent = new Intent(HelloAndroidActivity.this, CustomerEditorActivity.class);
                ArrayList<Customer> chList = new ArrayList<Customer>(customers);
                Customer c = chList.get(position);

                intent.putExtra("customerId", c.getId()); // pass the customer ID as parameter to the activity
                HelloAndroidActivity.this.startActivity(intent);
                return false;
            }
        });
    }
...
Notice, there is currently no refresh done automatically. Therefore, we modify the onClick method by simply adding the fillTable() call :


    public void onButtonTestClicked(View v) {
        ...
        // refill the data table
        fillTable();
    }

Ok, to add customers, we will have to add a "new" button at last. Therefore we extend the main_layout.xml and insert a button (Do not forget to add a label in the strings.xml!)
<Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="New"
                android:id="@+id/btnNew" android:layout_alignRight="@+id/button" android:layout_alignParentTop="true"
                android:layout_marginTop="39dp"
                android:onClick="onButtonNewClicked"/>

To finish the dialog, simply add the onClick event handler to the activity :
public void onButtonNewClicked(View view) {
        Intent intent = new Intent(HelloAndroidActivity.this, CustomerEditorActivity.class);
        HelloAndroidActivity.this.startActivity(intent);
    }

Conclusion

Using the BARACUS framework built-in form validation technique, message routing and error handling becomes really easy. You have learned how to wire validators to UI components and how to perform pre-saving validation. Because we used the ManagedActivity's focusChangeBasedValidation, the components are validated every time we change the widget.

-