Assignment 9

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

Topics of discussion

  • Listeners on Listviews

  • Handling Firebase updates, saving the app state

  • Filtering data from the cloud

Adding listeners on Listviews

Apart from being able to show app users their data in the form of a list or grid, it is equally important for users to be able to interact with their own data. For example, they might want to edit it, remove it, or simply create new entries. An excellent example would be the list of emails in your inbox, or images in your gallery. As such, we need to determine what kind of interactions the user may have with each view.

There are two ways to add a listener to a Listview, depending on whether we want to always treat each row item as a whole (i.e. click anywhere inside), or have multiple clickable views inside the row item. On buttons, we already introduced the onClickListener, however, viewholders like Listviews need a different type of listener. We need to know which specific element was selected by the user, so we attach an OnItemClickListener. Some example code is given below. Argument i in the onItemClick method let's you know which element was clicked, and this makes it possible to link this index to an index in the data list of your activity. In the example below, each row corresponds to a payment class stored in a payments list.

Fig.1.

In the situation above, the listener is attached on the Listview items, so a click will be intercepted whenever the user clicks (anywhere) inside one of the two rows in Figure 1. In other words, we cannot differentiate between different areas or views inside the row where the users might have wanted to click. For this option, there is a second solution. Given Figure 2 and the code below we can now have listeners on the two added icons (edit and delete).

listPayments.setOnItemClickListener(new AdapterView.OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {

Toast.makeText(getApplicationContext(), "Clicked on " + payments.get(i), Toast.LENGTH_SHORT).show();

startActivity(new Intent(getApplicationContext(), ViewPaymentActivity.class));

}

});

public class PaymentAdapter extends ArrayAdapter<Payment> {

@Override

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

// ...

final Payment pItem = payments.get(position);

itemHolder.tName.setText(pItem.getName());

// ...

itemHolder.iEdit.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View view) {

// ... edit payment at "position"

}

});

itemHolder.iDelete.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View view) {

// ... delete payment at "position"

}

});

return view;

}

private static class ItemHolder {

// ...

ImageView iEdit, iDelete;

}

}

Fig.2.

Note that if we use listeners defined in the list adapter, we often won't have access to activity methods, so be sure to pass along the Context. For example, clicking on the edit button in a row could start a new activity. For this, the context is needed to call startActivity. If you add listeners both on the row and on specific views inside the row make sure the users understands the difference in functionality. The inner-most listeners will be called first.

Handling Firebase updates. The App State.

Given a Firebase data list, we created the corresponding Listview for the user, and added listeners on rows or on views inside the rows. Populating the list is only one of four possible interactions with the data: (create, read, update, delete), namely read. For the other 3 operations you will often need a new activity offering the appropriate UI. In case the data needed for input or editing is just 2-3 fields, it is often more intuitive to create a dialog/pop-up instead of a full screen activity. To achieve a fully custom UI, with all the functionality behind, the best solution is to create a normal activity, but overwrite it's default theme (which is full screen), and set it to a Dialog theme. This is done by adding the following line in the manifest, for the selected activity.

<activity

android:name=".ui.AddPaymentActivity"

android:theme="@style/Base.Theme.AppCompat.Light.Dialog">

</activity>

Note that by setting the theme to Dialog, the width and height of the activity will be automatically set to wrap_content. Optionally, if the space is to cramped, increase the width manually to, say, 300dp. Make sure you test this value on multiple devices.

This new activity is meant for creating, updating or deleting data, and should have access to two objects: the database reference from the MainActivity, and the selected list object (if editing or deleting). The selected object may be null if a new one is intended to be created. To pass information between multiple activities, it is common in Android development to rely on a Singleton class which holds the relevant state objects of the application; let's call it AppState. The AppState will have one single instance accessible by everyone in the project code, and will store specific objects intended for sharing. Each such object should provide setters and getters as necessary. An example AppState implementation is given below. Interacting with this class is as simple as AppState.get().method(....).

Other methods of passing instances of objects from one activity to another may be: implementing Serializable, Parcelable, an Event-bus or an Intent helper singleton class. Read about them here.

public class AppState {

private static AppState singletonObject;

public static synchronized AppState get() {

if (singletonObject == null) {

singletonObject = new AppState();

}

return singletonObject;

}

// reference to Firebase used for reading and writing data

private DatabaseReference databaseReference;

// current payment to be edited or deleted

private Payment currentPayment;

public DatabaseReference getDatabaseReference() {

return databaseReference;

}

public void setDatabaseReference(DatabaseReference databaseReference) {

this.databaseReference = databaseReference;

}

public void setCurrentPayment(Payment currentPayment) {

this.currentPayment = currentPayment;

}

public Payment getCurrentPayment() {

return currentPayment;

}

}

With the helper activity created, and the appropriate UI implemented, the necessary object state should be constructed locally and then passed to Firebase via it's API. To create or update an object, use a map containing values under keys with appropriate names, and call updateChildren on the exact node that has to be updated. The following code prepares a Payment object for serialization: it saves the name, cost ans type of the payment and sends the value to the real-time database via the update method. Note that the full path to the node is used: child("wallet").child(timestamp), where timestamp is the key of the payment node.

Map<String, Object> map = new HashMap<>();

map.put("name", name);

map.put("cost", Double.parseDouble(cost));

map.put("type", type);

AppState.get().getDatabaseReference().child("wallet").child(timestamp).updateChildren(map);

To delete an object, use the setValue method with a null argument, or the removeValue method. See the code below as a suggestion. Additionally, the example method below also finishes the activity in which it runs. This might be needed in case a dialog was opened just for this specific task. A call to finish() destroys the current activity (implies calling pause, stop, destroy) and returns to the previous activity on the stack; it is equivalent to pressing the Back button.

Note: The system calls onDestroy after it has already called onPause and onStop in all situations except one: when you call finish from within the onCreate method. In some cases, such as when your activity operates as a temporary decision maker to launch another activity, you might call finish from within onCreate to destroy the activity. In this case, the system immediately calls onDestroy without calling any of the other lifecycle methods.

private void delete(String timestamp) {

AppState.get().getDatabaseReference().child("wallet").child(timestamp).removeValue();

// finishes the current activity and returns to the last activity on the stack

finish();

}

Task #1

  • Based on the Smart Wallet application from last week, implement the functionality to also create, update and delete payments from your MainActivity.

  • First, create a new activity called AddPaymentActivity with a suggested layout as in Figure 3. To make it looks like a Dialog at runtime, make sure you change the theme to android:theme="@style/Base.Theme.AppCompat.Light.Dialog" in the manifest file. The activity needs an Edittext for the payment name (description), an Edittext (inputType="numberDecimal") for the cost, a Spinner for the payment type, a Textview for the timestamp, and a Save (create or update) and a Delete button.

    • Fig.3.

  • Programatically, in AddPaymentActivity you will need to initialize the views, set an adapter for the Spinner and populate it with the predefined type names. If the AppState contains a payment that is non-null, then it means a row was selected by the user in the MainActivity and you should update the views with the data encapsulated by that payment. If the AppState's payment is null, it means the user pressed the floating action button to create a new payment, so the views will be left empty.

                • setTitle("Add or edit payment");

                • // ui

                • eName = (EditText) findViewById(R.id.eName);

                • eCost = (EditText) findViewById(R.id.eCost);

                • sType = (Spinner) findViewById(R.id.sType);

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

                • // spinner adapter

                • String[] types = PaymentType.getTypes();

                • final ArrayAdapter<String> sAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, types);

                • sAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

                • sType.setAdapter(sAdapter);

                • // initialize UI if editing

                • payment = AppState.get().getCurrentPayment();

                • if (payment != null) {

                • eName.setText(payment.getName());

                • eCost.setText(String.valueOf(payment.getCost()));

                • tTimestamp.setText("Time of payment: " + payment.timestamp);

                • try {

                • sType.setSelection(Arrays.asList(types).indexOf(payment.getType()));

                • } catch (Exception e) {

                • }

                • } else {

                • tTimestamp.setText("");

                • }

  • In case we create a new payment, we need to check all entries in the UI and generate a timestamp once the user clicks on Save. To generate a timestamp in the needed format, use the code below. In case we update a payment, we need to check the the new entered data is consistent, then use the existing timestamp to update the value in Firebase. Note, do not create a new timestamp in this second scenario. If we want to delete a payment, then the received payment from the AppState has to be non-null. Just get it's timestamp and call removeValue on the Firebase node corresponsing to that timestamp. You may use a code skeleton for click events like the one below.

                • public static String getCurrentTimeDate() {

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

                • Date now = new Date();

                • return sdfDate.format(now);

                • }

                  • case R.id.bSave:

                  • if (payment != null)

                  • save(payment.timestamp);

                  • else

                  • save(AppState.getCurrentTimeDate());

                  • break;

                  • case R.id.bDelete:

                  • if (payment != null)

                  • delete(payment.timestamp);

                  • else

                  • Toast.makeText(this, "Payment does not exist", Toast.LENGTH_SHORT).show();

                  • break;

  • Next, coming back to the MainActivity, attach a listener to the Listview used for payments. The listener should be added in the onCreate method and may look like in the code below. Notice that before starting the other activity, you must set the selected payment object in the AppState.

                • listPayments.setOnItemClickListener(new AdapterView.OnItemClickListener() {

                • @Override

                • public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {

                • AppState.get().setCurrentPayment(payments.get(i));

                • startActivity(new Intent(getApplicationContext(), AddPaymentActivity.class));

                • }

                • });

  • Finally, if the user simply clicks on the floating action button to add a new payment, make sure you start the same AddPaymentActivity activity you created in this lab, but set the payment stored in the AppState to null.

                • case R.id.fabAdd:

                • AppState.get().setCurrentPayment(null);

                • startActivity(new Intent(this, AddPaymentActivity.class));

                • break;

  • Homework: Add a filtering functionality for the data received from Firebase. Namely, each payment has a date when it was created, so let's only show the payments for the current month. Additionally, use the bPrevious and bNext buttons at the bottom of the activity to advance through the months of the calendar. For this, first filter out the data in onChildAdded, like below. Rely on an enum called Month for helper methods.

                  • @Override

                  • public void onChildAdded(DataSnapshot dataSnapshot, String s) {

                  • try {

                  • if (currentMonth == Month.monthFromTimestamp(dataSnapshot.getKey())) {

                  • Payment payment = dataSnapshot.getValue(Payment.class);

                  • payment.timestamp = dataSnapshot.getKey();

                  • payments.add(payment);

                  • adapter.notifyDataSetChanged();

                  • tStatus.setText("Found " + payments.size() + " payments for " + Month.intToMonthName(currentMonth) + ".");

                  • }

                  • } catch (Exception e) {

                  • }

                  • }

                  • public enum Month {

                  • January, February, March, April, May, June, July, August, September, October, November, December;

                  • public static int monthNameToInt(Month month) {

                  • return month.ordinal();

                  • }

                  • public static Month intToMonthName(int index) {

                  • return Month.values()[index];

                  • }

                  • public static int monthFromTimestamp(String timestamp) {

                  • // 2016-11-02 14:15:16

                  • int month = Integer.parseInt(timestamp.substring(5, 7));

                  • return month - 1;

                  • }

                  • }

  • The currentMonth parameter should be a class member which is initialized in onCreate from either shared preferences or from the current time.

                • In onCreate:

                • currentMonth = prefs.getInt(TAG_MONTH, -1);

                • if (currentMonth == -1)

                • currentMonth = Month.monthFromTimestamp(AppState.getCurrentTimeDate());

  • When a user clicks on the bPrevious and bNext buttons, decrease or increase the currentMonth by 1, and make sure it remains within [0, 11]. When clicking on the buttons, also save the new value of the current month in shared preferences. The trick in refreshing the list view and listeners is to simply recreate the activity, as if we were starting the app with a different current month. To do this, just call the recreate method.

                • In clicked:

                • prefs.edit().putInt(TAG_MONTH, currentMonth).apply();

                • recreate();

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.

Have a fruitful week.

Guanabana fruit

Guanabana popularly known as Soursop, is a native fruit of Central and South America. It is also grown as a commercial fruit in some parts of Africa and south East Asia. The fruit appears green in color with total mass of 6.8kg. Soursop consist white pulp that is rich in vitamin C, vitamin B1 and vitamin B2. The pulp is used to make candies, fruit drinks and ice cream flavorings.

http://www.fruitsinfo.com/guanabana-fruit.php