Assignment 8

Welcome to week six of Mobile Systems and Applications! (cs.upt.ro/~alext/msa/lab8)

Topics of discussion

  • Handling data lists in Android

  • Custom adapters

  • Synchronizing a Firebase data list with a listview

Working with lists of data in Android

Until now we have introduced the Spinner (drop-down item list) as a view capable of holding a variable number of identical elements, namely a list holder. Showing lists to Android users is one of the fundamental features most apps must handle, and they must handle it right. Good examples of showing lists in very popular apps are: emails in your Gmail or Yahoo app's inbox, personal notes in Evernote, messages in WhatsApp, apps displayed in Google Play, posts on your Facebook wall etc. There are many aspects about performance and looks to discuss about showing lists.

Apart from the Spinner, we will mention and use ListView and GridView. ListView is a view group that displays a list of scrollable items. The list items are automatically inserted to the list using an adapter that pulls content from a source such as an array or database query and converts each item result into a view that's placed into the list. GridView is a view group that displays items in a two-dimensional, scrollable grid. The grid items are automatically inserted to the layout using a list adapter. Example code for initializing such views and setting up adapters is given below.

The basic steps for initialization are:

  • First, prepare an array or list of data that we want to display in the list. In our assignment we have List<String> payments. This array may be empty when it is attached to the adapter, but not null!

  • Initialize the listView reference from the activity UI with findViewById.

  • Create an adapter which can handle the type of data in the list (String, Person, MonthlyExpense, Payment). If we have a non-primitive class (other than String), we have to overwrite toString and return the string intended to be shown (e.g. person's first + last names, month's name, or payment amount). If we want to display more than a set of characters (i.e. a complex list item that could contain an icon, a button, more text fields etc.) then we need to create our own custom adapter.

  • Attach the adapter to the listView.

  • Then, at runtime, anytime a new element is added or removed, we need only to update the array of data, then call notifyDataSetChanged on the adapter to update the UI.

// data to be displayed

List<String> payments = new ArrayList<>();

payments.add("45.25");

payments.add("5.50");

payments.add("15.60");

// initialize list view from activity layout

ListView listPayments = (ListView) findViewById(R.id.listPayments);

// create adapter for payments list

ArrayAdapter<String> listAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, payments);

// set adapter to list view

listPayments.setAdapter(listAdapter);

// update list: add one element, then remove element at index 2

payments.add("10.25");

payments.remove(2);

// update UI

listAdapter.notifyDataSetChanged();

The xml definition for list and grid views is given below. Some notable tags for the views are: divider and dividerHeight="4dp" for the listView. These define a vertical spacing of 4dp between rows using no color at all (transparent). For the gridView we have some typical tags: columnWidth defines the fixed width of columns, horizontalSpacing and verticalSpacing define the amount of empty space between items, numColumns can specify the number of columns to try to fit on the horizontal (or can be left automatic with auto_fit), and stretchMode which defines the scaling rule on the horizontal axis (may be columnWidth - columns are stretched equally; spacingWidth - spaces in between are stretched; none - nothing is stretched).

<ListView

android:id="@+id/listView"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_above="@+id/lBottom"

android:layout_below="@+id/tStatus"

android:divider="@android:color/transparent"

android:dividerHeight="4dp"/>

<GridView

android:id="@+id/gridView"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:columnWidth="90dp"

android:gravity="center"

android:horizontalSpacing="10dp"

android:numColumns="auto_fit"

android:stretchMode="columnWidth"

android:verticalSpacing="10dp"/>

Custom adapters

To be able show a more complex list to users, we almost always need to implement custom adapters for the used list or gridView. An example of relying on a simple ArrayAdapter versus implementing a custom adapter is suggested in Figure 1.

Fig.1.

Apart from the obvious aesthetic effect of the custom adapter on the ride side of Figure 1, the most notable difference is that the array adapter on the left side can only display one single thing: a string. We can make it larger, italic, blue or underlined, but it will just remain a line of text. There is no extra code needed to implement the view on the left side, we just need to overwrite toString in the model class used for displaying. To achieve the view on the right side, we need to begin by designing the item layout (an icon image, multiple text views with custom positioning).

As such, the steps to implement the view presented above are the following:

  • Data model: know what kind of data you want to show in the list. Each item in a listView is indirectly mapped to an item in a Java List<?>.

  • Create xml layout: design how one item (row) in the list looks like. This is done by creating a custom xml in the res/layouts folder and then designing it as any activity layout.

  • Extend the base adapter class: create your own adapter by overwriting the getView method, to inflate your custom layout. The adapter links the object array with the view in the user interface.

  • Create, populate and attach adapter: add the corresponding code in your activity class to link each of the above items.

Let's detail each of the above steps by implementing today's laboratory task.

Task #1

  • Using last week's Smart Wallet application, create a new empty activity and set it as the launcher activity of the application (hint: edit the manifest file).

  • Design your activity to contain a status Textview, a listView for displaying payments taken retrieved Firebase, a button (Floating action button) to quickly add new payments, and two utility buttons at the bottom of the layout. Your layout could look like the one in Figure 2. To create this layout, see his week's code examples. To get any icons specially designed for material design widgets visit materialdesignicons.com. Tip: search for "add", "remove", "edit" icons and download the white 48x48 .png files into your drawable folder. If this is too much, get the icons here.

    • Fig.2.

  • Having the Firebase data structure displayed Figure 3 under the wallet node, create a Payment model class in your project. See the code examples for the payment class. Our class has to simply be able to map the three fields received in a data snapshot: cost (double), name (String) and type of payment (String). Additionally, we want to store the time of the payment, which is retrieved as the key of the snapshot (String).

    • Fig.3.

  • Design the row item layout to be displayed in the listView. Based on the information a payment holds (time, name, cost, type) we can create something similar to the layout in Figure 4. To create this layout, go to the res/layout folder in the project overview, right click on layout > new > XML > New layout XML, and name the file item_payment.xml. You can find a suggestive implementation of this item here. Notice that the example code uses a cardView instead of a usual layout. For the code to compile, make sure you add the material design dependencies.

    • Fig.4.

                  • dependencies {

                  • compile 'com.android.support:cardview-v7:23.0.+'

                  • compile 'com.android.support:recyclerview-v7:23.0.+'

                  • compile 'com.android.support:design:23.0.+'

                  • }

  • Implement a PaymentAdapter in a new package named "ui". This adapter needs to overwrite the getView method which returns a view of each row to be displayed. Each row needs to inflate the item_payment layout file, then populate its views with data taken from the array of payments. A layout inflater is an activity specific mechanism which can load any xml file into a layout at runtime. It is used to link xml's to custom views. The adapter is implemented as below. Notice that we make use of the View Holder pattern for caching created views. To better understand this concept read about smooth scrolling and about ViewHolder. In short, this design pattern enables you to access each list item view without the need for the look up, avoiding frequent calls of findViewById during ListView scrolling.

                • public class PaymentAdapter extends ArrayAdapter<Payment> {

                • private Context context;

                • private List<Payment> payments;

                • private int layoutResID;

                • public PaymentAdapter(Context context, int layoutResourceID, List<Payment> payments) {

                • super(context, layoutResourceID, payments);

                • this.context = context;

                • this.payments = payments;

                • this.layoutResID = layoutResourceID;

                • }

                • @Override

                • public View getView(int position, View convertView, ViewGroup parent) {

                • ItemHolder itemHolder;

                • View view = convertView;

                • if (view == null) {

                • LayoutInflater inflater = ((Activity) context).getLayoutInflater();

                • itemHolder = new ItemHolder();

                • view = inflater.inflate(layoutResID, parent, false);

                • itemHolder.tIndex = (TextView) view.findViewById(R.id.tIndex);

                • itemHolder.tName = (TextView) view.findViewById(R.id.tName);

                • itemHolder.lHeader = (RelativeLayout) view.findViewById(R.id.lHeader);

                • itemHolder.tDate = (TextView) view.findViewById(R.id.tDate);

                • itemHolder.tTime = (TextView) view.findViewById(R.id.tTime);

                • itemHolder.tCost = (TextView) view.findViewById(R.id.tCost);

                • itemHolder.tType = (TextView) view.findViewById(R.id.tType);

                • view.setTag(itemHolder);

                • } else {

                • itemHolder = (ItemHolder) view.getTag();

                • }

                • final Payment hItem = payments.get(position);

                • itemHolder.tIndex.setText(String.valueOf(position + 1));

                • itemHolder.tName.setText(hItem.getName());

                • itemHolder.lHeader.setBackgroundColor(PaymentType.getColorFromPaymentType(hItem.getType()));

                • itemHolder.tCost.setText(hItem.getCost() + " LEI");

                • itemHolder.tType.setText(hItem.getType());

                • itemHolder.tDate.setText("Date: " + hItem.timestamp.substring(0, 10));

                • itemHolder.tTime.setText("Time: " + hItem.timestamp.substring(11));

                • return view;

                • }

                • private static class ItemHolder {

                • TextView tIndex;

                • TextView tName;

                • RelativeLayout lHeader;

                • TextView tDate, tTime;

                • TextView tCost, tType;

                • }

                • }

  • As an extra touch, the header color of each list item should change based on the type of the payment. Include the PaymentType class in your model package. Its public utility method is used in the adapter to set the header color as such: itemHolder.lHeader.setBackgroundColor(PaymentType.getColorFromPaymentType(hItem.getType())). One long line of code! The type of the payment, taken as a lowercase string, is passed to getColorFromPaymentType which returns an integer color code. This code is given to the setBackgroundColor method of the lHeader relative layout, which is the header itself. See the cyan area in Figure 4.

  • Finally, in your activity onCreate method link everything together. Implement the ChildEventListener and in the method onChildAdded instantiate each new received data snapshot to a new Payment class and add it to the payments list. For safety, put the code inside onChildAdded in a try-catch block, in case we receive broken (i.e. wring format) data from Firebase.

                  • // firebase

                  • private DatabaseReference databaseReference;

                  • private int currentMonth;

                  • private List<Payment> payments = new ArrayList<>();

                  • @Override

                  • protected void onCreate(Bundle savedInstanceState) {

                  • super.onCreate(savedInstanceState);

                  • setContentView(R.layout.activity_main6);

                  • tStatus = (TextView) findViewById(R.id.tStatus);

                  • bPrevious = (Button) findViewById(R.id.bPrevious);

                  • bNext = (Button) findViewById(R.id.bNext);

                  • fabAdd = (FloatingActionButton) findViewById(R.id.fabAdd);

                  • listPayments = (ListView) findViewById(R.id.listPayments);

                  • final PaymentAdapter adapter = new PaymentAdapter(this, R.layout.item_payment, payments);

                  • listPayments.setAdapter(adapter);

                  • // setup firebase

                  • final FirebaseDatabase database = FirebaseDatabase.getInstance();

                  • databaseReference = database.getReference();

                  • databaseReference.child("wallet").addChildEventListener(new ChildEventListener ...);

                  • // ...

                  • }

  • After putting all blocks together, the app should look like this at runtime:

    • Fig.5.

  • Homework: Add functionality to the floating action button. Namely, when it is clicked, a new activity should be launched offering the user the possibility to create a new payment. As such, provide input views for payment name (string), type (perhaps a spinner with predefined categories?) and cost (numeric decimal). When the new payment is approved by the user (e.g Save/Ok), remember to also take the current time and store a new entry to Firebase. To take the exact timestamp used for our current Firebase format, use the following code. Plus one activity (+1) point for the homework. Homework may only be presented individually, on your smartphone or emulator, with the code running on your PC.

                • public static String getCurrentTimeDate() {

                • SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

                • Date now = new Date();

                • return sdfDate.format(now);

                • }

Have a fruitful week.

Pineberry fruit

Pineberry is considered to be smaller than strawberry and said to have the same genetic as strawberry with 15 to 23mm. When the fruits ripen, it becomes white with red seeds. Pine berries start life as green berries, and slightly turn into white. The fruit produced by pineberry plants is very aromatic and has sweet flavor just like pineapple. Pineberries are produced on a very small scale in Europe and Belize. Pineberries are grown in very large and commercial glasshouses.

http://www.fruitsinfo.com/pineberry-hybrid-fruits.php