Objectives

In this version of MyRent, we introduce a range of UI Widgets to evolve the UX into something more useful. These widgets will be 'active', meaning that the host Activity will be intercepting and responding to events the user may generate when interacting with the application.

Preview

At the end of this topic our goal is to have a screen looking something like this:

We will have added:

  • Section titles (LOCATION & STATUS)
  • Button indicating the residence registration date
  • Checkbox: a tick indicating the residence rented.

In the previous step we introduced a listener for the geolocation input (the latitude-longitude string).

In this step we shall:

  • Add functionality to the Residence model, namely:
    • A Date field to indicate when the residence was registered with the MyRent app.
      • This shall be instantiated in the Residence constructor.
    • A boolean to indicate the rented status of the residence, that is whether or not the residence has a tenant.
    • We shall introduce a method to return a suitably formatted version of the Date object in the form of a string.
      • This shall be used as the button label in the STATUS section.
    • Setters and getters for the rented boolean.
  • In MyRentFragment.onCreate add:
    • A listener to capture any change to the rented checkbox status in the UI.
      • Maintain the Residence.rented state up-to-date with any such changes.
    • A method to write the residence registration date to the date button in the UI.

Restructure MyRent

Continue building the MyRent app that you commenced in the previous lab.

Before we start to expand the project, we need to perform some rearranging so it can be extended in an orderly manner.

This is our current application workspace:

Figure 1: Existing structure

.. and this is a version we used in writing this lab:

Figure 2: Structure following refactoring

Here is a detailed description on how this may be achieved:

  • Select gear icon and untick Compact Empty Middle Packages. Observe the change to the package layout.
    Figure 3: Expand empty middle packages
  • Select the myrent package and create a package named activities within org.wit.myrent. Figure 4: Creating new package Figure 5: New package org.wit.myrent.activities
  • Drag and drop MyRentActivity into this new package.
  • Create a package named models within org.wit.myrent. Figure 6: Creating models package
  • Drag and drop Residence into this new package. Figure 7: Refactored layout
  • Ensure that when the refactoring is complete you tick this menu item again resulting in the required layout as shown in Figure 2 above and again here in Figure 8. Figure 8: Structure following refactoring

Layout

Before proceeding make a name use the menu refactor command to rename MyRentActivity to ResidenceAcivity.

  • This change is being made to provide nameing consistency with future classes that we shall be developing.

The previous iteration has one UI control, an EditText.

This iteration shall introduce:

  • 2 section labels (LOCATION & STATUS).
  • A button that will have a label representing the residence registration date and that will be added at runtime.
  • A checkbox which when ticked indicates that the residence has a tenant.

Button

Select activity_myrent.xml, right click and using context menu commands Refactor | Rename change the name to activity_residence.xml.

Open activity_residence.xml.

Remove background, text and hint colouring (if not already done). This was introduced simply to demonstrate how it might be achieved. The refactored file is as follows:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                tools:context="app.myrent.ResidenceActivity" >

  <EditText
      android:id="@+id/geolocation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentRight="true"
      android:hint="@string/geolocation_hint"/>

</RelativeLayout>

A number of different approaches are available to change the layout.

  • Here we shall use a both the graphical display, XML and the outline view.

In graphical display mode drag a Button onto the MyRent canvas and resize using the handles so as to extend full width of the screen as shown in Figure 1.

Figure 1: Drag and drop button onto MyRent canvas and manually resize

Double click Button in Design view and complete the text (blank) and id (registration_date) fields as depicted in Figure 2.

Figure 2: Removing hardwired strings

Here is the resulting strings.xml file:

<resources>

  <string name="app_name">MyRent</string>
  <string name="title_activity_myrent">ResidenceActivity</string>
  <string name="geolocation_hint">52.253456,-7.187162</string>
  <string name="action_settings">Settings</string>
  <string name="registration_date"/>

</resources>

Open activity_residence.xml in Text mode and add this code to the Button element to set the margins:

        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"

Observe in the Design view how button resizes (Figure 3). Figure 3: Margins added to Button

Here is the refactored activity_residence.xml. Notice that we are changing the layout type to LinearLayout.

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

  <EditText
      android:id="@+id/geolocation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="@string/geolocation_hint">
  </EditText>

  <Button
      android:id="@+id/registration_date"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginLeft="16dp"
      android:layout_marginRight="16dp"/>

</LinearLayout>

Observe that a button id has now been generated in the R.java file:

Figure 4: R File

Next we shall add a section label and divider immediately before the geolocation node.

  • Use the graphical editor and drag a TextView element into position.
  • Then inspect the code in the xml editor.
    • Change the width to match parent.
    • Change the default android:text to @string/location.
    • Style the TextView with a list separator.
  • In res/values/string.xml add a resource for location as follows here:
    <string name="location">Location</string>

Here is the completed xml node for the location label:

    <TextView
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@string/location"
         style="?android:listSeparatorTextViewStyle"/>

Implement these modifications and inspect the result in the Graphical Layout. You should be presented with that shown in Figure 5:

Figure 5: List separator added

Finally, in this step, add a section label for status.

Here is the xml:

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/status"
        style="?android:listSeparatorTextViewStyle"
        />

Add the referenced string resource status in res/values/strings.xml:

    <string name="status">Status</string>

The result is shown in Figure 6.

Figure 6: Status section label and separator added

We shall continue with the development of the layout in the following steps.

Here is the refactored activity_residence layout:

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

  <TextView
      style="?android:listSeparatorTextViewStyle"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/location"/>

  <EditText
      android:id="@+id/geolocation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:hint="@string/geolocation_hint">
  </EditText>

  <TextView
      style="?android:listSeparatorTextViewStyle"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/status"/>

  <Button
      android:id="@+id/registration_date"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginLeft="16dp"
      android:layout_marginRight="16dp"/>

</LinearLayout>

Add Widgets

We shall now complete remaining work on the layout using the Graphical Layout and the Outline panel.

Here is the the layout at this stage of development.

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

    <!-- LOCATION -->

    <TextView
        style="?android:listSeparatorTextViewStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/location" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:baselineAligned="false"
        android:orientation="horizontal" >

        <!-- Geolocation (GPS Coords) -->

        <EditText
            android:id="@+id/geolocation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/geolocation_hint" >

            <requestFocus />
        </EditText>

    </LinearLayout>

    <!-- STATUS -->

    <TextView
        style="?android:listSeparatorTextViewStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/status" />

    <Button android:id="@+id/registration_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        />

</LinearLayout>

It remains only to add the checkbox.

With the Graphical Layout open, drag a CheckBox from the Form Widgets folder and drop directly underneath the registration_date button in the Outline panel.

Figure 1: Drag and drop CheckBox widget directly underneath registration_date button

Replace android:text attribute with string referenced in strings.xml and change the android:id, all as shown here:

  <CheckBox
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/isrented"
      android:id="@+id/isrented"
      android:checked="false"/>

Add a string resource for the checkbox:

  <string name="isrented">Rented?</string>

Because we have moved MyRentActivity to a new folder, a change to the manifest file is required:

    <activity android:name=".activities.MyRentActivity">

This concludes the layout design and implementation in this step.

  • Result to date shown in Figure 2:

Figure 2: Layout completed

The hierarchical arrangement of the layout is shown here in Figure 3.

  • Notice that the LinearLayout orientation is vertical thus creating a stack of xml nodes comprising the various UI widgets.

Figure 3: Hierarchical arrangement of UI components

Activity & Model Updates

Replace your Residence class with the following:

package org.wit.myrent.models;

import java.util.Date;
import java.util.Random;

public class Residence
{
  public Long id;
  public Long date;

  //a latitude longitude pair
  //example "52.4566,-6.5444"
  private String geolocation;
  public boolean rented;

  public Residence()
  {
    id = unsignedLong();
    date = new Date().getTime();
  }

  /**
   * Generate a long greater than zero
   * @return Unsigned Long value greater than zero
   */
  private Long unsignedLong() {
    long rndVal = 0;
    do {
      rndVal = new Random().nextLong();
    } while (rndVal <= 0);
    return rndVal;
  }

  public void setGeolocation(String geolocation)
  {
    this.geolocation = geolocation;
  }

  public String getGeolocation()
  {
    return geolocation;
  }

  public String getDateString() {
    return "Registered:" + dateString();
  }

  private String dateString() {
    String dateFormat = "EEE d MMM yyyy H:mm";
    return android.text.format.DateFormat.format(dateFormat, date).toString();
  }

}

Note that we have made the fields public for convenience. Also, we have introduced a new date and rented fields into the model.

Change the name of MyRentActivity to ResidenceActivity. This naming convention will prove more intuitive and consistent as you will see in later development iterations.

  • Use the Refactor command: select MyRentActivity and in the context menu Refactor | Rename as shown in Figure 1.

Figure 1: Change activity name using Refactor

In ResidenceActivity introduce 2 new fields to access the new widgets we have just introduced:

  private CheckBox rented;
  private Button   dateButton;

and on OnCreate, we need to initialize these:

    dateButton  = (Button)   findViewById(R.id.registration_date);
    rented  = (CheckBox) findViewById(R.id.isrented);
    rented.setOnCheckedChangeListener(this);

Furthmore, we are going to disable the date button when the activity is created:

    dateButton .setEnabled(false);

Run the app now, and verify that the activity launches without incident.

We would now like to engage the checkbox rented. First, implement the OnCheckedChangeListener interface:

public class ResidenceActivity extends Activity implements TextWatcher, OnCheckedChangeListener
{

This will require the following import:

import android.widget.CompoundButton.OnCheckedChangeListener;

and this is the implementation:

  @Override
  public void onCheckedChanged(CompoundButton arg0, boolean isChecked)
  {
    Log.i(this.getClass().getSimpleName(), "rented Checked");
    residence.rented = isChecked;
  }

This completes the class. Here is the complete code to this stage:

package org.wit.myrent.activities;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;

import org.wit.myrent.R;
import org.wit.myrent.models.Residence;

public class ResidenceActivity extends AppCompatActivity implements TextWatcher, CompoundButton.OnCheckedChangeListener
{
  private EditText geolocation;
  private Residence residence;

  private CheckBox rented;
  private Button dateButton;

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_myrent);

    geolocation = (EditText) findViewById(R.id.geolocation);
    residence = new Residence();

    // Register a TextWatcher in the EditText geolocation object
    geolocation.addTextChangedListener(this);

    dateButton  = (Button)   findViewById(R.id.registration_date);
    rented      = (CheckBox) findViewById(R.id.isrented);

    dateButton .setEnabled(false);

  }

  @Override
  public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
  {

  }

  @Override
  public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
  {

  }

  @Override
  public void afterTextChanged(Editable editable)
  {
    residence.setGeolocation(editable.toString());
  }

  @Override
  public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
  {
    Log.i(this.getClass().getSimpleName(), "rented Checked");
    residence.rented = isChecked;
  }
}

Test

Run the app and check that the layout presented is as expected, something like that shown in Figure 1.

Figure 1: MyRent

Run the app and use the debugger to ensure data is being transmitted to and from the Residence object.

  • Run MyRent app on an emulator.
  • Place a breakpoint at ResidenceActivity.afterTextChanged as shown in Figure 2.
  • Tick the Rented? checkbox.
  • Input a number in the Location input field.
    • The program should halt at the breakpoint.

Examine the state of the variables such as:

  • this
  • editable
  • residence.date
  • residence.geolocation
  • residence.id
  • residence.rented

Press the Run to Cursor toolbar icon to run to completion.

Figure 2: Using debugger to verify data input transmitted to model object

Summary

Here is what we have achieved in this topic:

  • Explored various ways of modifying layout working with the Graphical Layout editor, the XML editor and the Outline view.
  • Added widgets to the layout

    • Section labels and dividers
    • A button to display the date the property was registered
    • A checkbox to indicate whether or not the residence is rented.
  • Added a listener in the controller to detected changes in the UI checkbox state and transmit any state changes to the model Residence object

  • Added a date field to the model and intialized this at the time a residence object created which represents the registration date of the property with the MyRent app.

  • Described how to conduct a simple test using the debugger to verify that the listeners operate correctly and that UI data transmission takes place successfully in both directions between model and UI.

The application at the end of this lab is available for reference here:

https://github.com/wit-ictskills-2016/myrent-01.git