While it’s a ClassNotFoundException the “DexPathList” gives away the true reason. So just add multiDexEnabled true like you always did.

If the next adb install ... results in the same error you fell for the same trap I did: Android Studio doesn’t create the .apk file when building via Build | Make Project. Select Build | Build APK instead.

The only surprise is that getTimestamp() doesn’t return a Date but a String. That’s because the value will be bound to a property of type String and Android does no type conversions. Using the Date would result in LoggedErrorException - Cannot find the setter for attribute 'android:text' with parameter type java.util.Date on android.widget.TextView.

By the way: returning an Integer would result in a befuddling android.content.res.Resources$NotFoundException: String resource ID #0x0. That’s because setText() has an overload which takes an Integer and treats it as a resource ID.

And a production app will probably create the SimpleDateFormat instance only once.

3: Create the List Item Layout

The list item layout defines how each of the ListView’s items should look. When Android Studio 3 generates a new layout resource file it looks like this – finally a ConstraintLayout but no traces of data binding:

The most important part isn’t even shown: this layout is called layout_demo_item.xml and will result in a data binding class named LayoutDemoItemBinding once you compile the project.

Had the data model simply returned a Date you’d have to convert it here: @{String.valueOf(demoData.timestamp)}.

Note that IDs are only used for constraints and state management but not needed for data binding.

4: Create the Layout with the ListView using the List Items

Data binding the ListView itself consists of the same steps as creating the list item layout. This example starts from Android Studio’s Basic Activity template which provides this layout in content_main.xml:

The constructor just takes and stores the data. Then getView() is called for each of the ListView’s elements:

i provides the element’s position – why should the parameter’s name give that away?

view is a used view representing a list item layout or null. If it’s a view the method just updates it with the respective data.

viewGroup is the parent of the view to return

If getView() doesn’t get a view to reuse the LayoutDemoItemBinding class previously generated from layout_demo_item.xml creates the proper binding. That gets provided with the data and stored in the resulting view’s tag.

Basic data binding is accompanied by showing how to handle click events on items in lines 41 to 50.

The thing is (ViewGroup) findViewById(R.id.custom_toast_container) will always return null because the Activity has no idea of R.id.custom_toast_container.

But just calling it like View layout = inflater.inflate(R.layout.custom_toast, null); causes a Lint warning “Avoid passing null as the view root …”. That’s usually correct because while the inflater will still do its job the configuration information would get lost which may or may not lead to a messed up layout.

This doesn’t apply to Toasts though and passing null is perfectly OK. So just suppress the Lint warning with @SuppressLint("InflateParams").

Now let’s take a look at this code that customizes Android’s built-in toast on-the-fly:

Lines 14 to 16 are standard code. But Line 17 shows how to access the built-in Toast’s layout. You can manipulate it like any other View object and in this case its background color and opacity are changed – more on that later.

Lines 22 to 31 show how to style the TextView. The straightforward part is setting its position, opacity and size.

The surprising part is that each TextView contains a means to show an image – certainly a debatable architectural decision but that’s another story. Again the code focuses on setting the opacity.

The code not shown is just Android Studio’s Basic Activity template with some color and text adjustments to get a proper background for showing a transparent Toast.

A Drawable’s alpha values work the same but are beween 0 and 255. So toastLoveDrawable.setAlpha(128) results in a half transparent Drawable. Because getBackground() returns a Drawable toastView.getBackground().setAlpha(128) works likewise.

A 3rd option to get transparent output is specifying the background color’s alpha channel. Values are beween 0 and 255 again but they are usually given as a hexadecimal number. And 128 equals 0x80 so toastView.setBackgroundColor(0x80FFFFFF) specifies a half-transparent white background.

The SQL is similar to that saving watchlists with the only twist of avoiding duplicate symbols. DbHelper.updateOrCreateSecurity() first checks whether securityId equals NEW_ITEM_ID. If that’s the case it looks for an existing security with the same symbol. If it finds one it aborts the transaction and returns an error message.

SecurityEditActivity.onOkButtonClick() checks whether DbHelper.updateOrCreateSecurity() returned an non-empty string. If that’s the case it shows a Toast with that string and stays on screen.

Enter a symbol that’s already in use and tap OK – you’ll get a message that you cannot add another security with that symbol and the Edit Security screen stays

Enter a new symbol and select one or more watchlists

Tap OK – the Edit Security screen closes and the Manage Securities screen shows the new security – without its name because no quotes were downloaded yet

Also tap OK in the Manage Securities screen to close it

Navigate to one of the watchlists that include the new security – there is no report for it because no quotes were downloaded yet

Tap refresh – DbTradeAlert shows a complete report for the new security

Optional: commit changes

2.5 Add Edit Functionality for Existing Securities

Editing a security – symbol read-only

Editing securities is implemented like editing watchlists and I’ll skip repeating the explanation. One difference is that SecurityEditActivity.onCreate() calls symbolEditText.setEnabled(false) for existing securities. And DbHelper.readSecurity() of course has to join the Quote table for its Name field.

When the user taps OK the security and its connection to watchlists are saved like for a new security. If the user entered a duplicate symbol the error message from DbHelper is shown and the screen stays open.

And finally WatchlistsManagementActivity.onActivityResult() receives a resultCode of RESULT_OK and refreshes is list of securities if this was an addition – as it shows only the symbols and these are immutable there is no need for a refresh after updating a security.

Give it a try:

Open the Manage Securities screen

Tap Edit on one of the securities to show the Edit Security screen – it says “Edit Security”

Change one of the fields or the watchlists that will include the security

Tap OK

Also tap OK in the Manage Securities screen

Check if the changes were applied correctly

Optional: commit changes

2.6 Add Delete Functionality for Securities

The functionality to delete securities uses the same pattern as for editing them – for explanations see deleting watchlists.

Deleting the data is straightforward – DbHelper.deleteSecurity() first deletes the security’s quote, then the records connecting the security to any watchlists and after that deletes the security itself. Of course it wraps everything in a transaction. The log entries look like this if the security was in a single watchlist:
… V/DbHelper: deleteSecurity(): securityId = 6
… V/DbHelper: deleteSecurity(): result of db.delete() from quote = 1
… V/DbHelper: deleteSecurity(): result of db.delete() from securities_in_watchlists = 1
… V/DbHelper: deleteSecurity(): result of db.delete() from security = 1
… D/DbHelper: deleteSecurity(): success!

Deleting a security

To try it:

Open the Manage Securities screen

Tap Delete on one of the securities

Tap Ok in the confirmation dialog – note that it displays the security’s name to avoid any mishaps

2. Add Securities Management

Securities management works exactly the same way watchlists management does. And adding securities management will follow the exact same steps adding watchlist management used. So let’s just fast forward through this.

2.1 Add the Manage Securities Screen

You need a new empty activity named “SecuritiesManagementActivity” with 3 Buttons, a ListView, and a TextView similar to activity_watchlists_management.xml.

And you need code for the Cancel and OK buttons and to set the screen’s title in onCreate() like in WatchlistsManagementActivity.java.

And finally you need a new menu item in menu_watchlist_list.xml.

Then you need to extend WatchlistListActivity to start the new activity in onOptionsItemSelected() and retrieve its result in onActivityResult().

Manage Securities screen – list of watchlists empty

Now try out the additions:

Start the app

In its overflow menu tap “Manage Securities”

The “Manage Securities” screen appears

Note the hint about adding a new security when there is none

Note that there is no menu but a distinct title for the screen

In the “Manage Securities” screen tap either OK or Cancel – tapping “New” will crash the app

Optional: commit the changes

2.2 Fill List of Existing Securities

To list the existing securities DbTradeAlert needs a layout to show each security. It’s added like layout_watchlists_management_detail with one more TextView to show the security’s symbol.

Manage Securities screen listing securities

The next step will be using an adapter to marry the ListView with its cursor and its detail layout. So create a new class named “SecuritiesManagementCursorAdapter” extending CursorAdapter like you did for WatchlistsManagementCursorAdapter.

For explanations see filling the list of existing watchlists.

Finally connect SecuritiesManagementCursorAdapter to securitiesListView in WatchlistsManagementActivity.onCreate() – to fill the list of securities DbHelper.getAllSecuritiesAndMarkIfInWatchlist() will be reused.

Test it: the securities show up in the Manage Securities screen. Optionally check in the changes.

2.3 Add an Activity to Edit Securities

Add a new empty activity named “SecurityEditActivity”. It hosts a lot of controls and I’ll just show the resulting layout:

Edit Security layout

The first step after creating the layout is to provide a handler for SecuritiesManagementActivity’s New button. The pattern is exactly like in WatchlistsManagementActivity:

Create an intent pointing to SecurityEditActivity.class

Add an Extra named SecurityEditActivity.SECURITY_ID_INTENT_EXTRA with the security ID of DbHelper.NEW_ITEM_ID to the intent

Again this code works exactly like that for editing a watchlist – just with a lot more fields.

Finally DbHelper needs getAllWatchlistsAndMarkIfSecurityIsIncluded() which works like explained for DbHelper.getAllSecuritiesAndMarkIfInWatchlist().

Run the app again:

Open the Manage Securities screen

Tap New: the new Edit Security screen shows up listing all watchlists and ready for input

again no menu but a distinct title of “Add Security”

Tap Cancel – tapping OK will crash the app

Optional: commit changes

Adding a new security

The screen shows a security’s fields: at the top is the symbol field. Below that you see value and date for base and maximum price, targets, and the notes field. Note that the dates have to be entered according to the device’s locale. While Android provides a DatePickerDialog I found it too much hassle to implement and connect it to the EditTexts.

At the bottom of the screen is a list of all watchlists with those checked that contain this security.

Note that that I closed the automatically displayed on-screen keyboard for the screenshot so it doesn’t obstruct the activity’s screen.

The method extracts the (new) watchlist’s name and the securities to include in it from the controls and passes it to DbHelper.updateOrCreateWatchlist(). After that it sets the activity’s result to RESULT_OK and closes the screen.

For a new watchlist updateOrCreateWatchlist() first inserts it into the watchlist table and then creates the connection to its securities in the securities_in_watchlists table. All of this is done in a transaction of course. We’ll ignore the code to update existing watchlists for now.

Now WatchlistManagementActivity needs code to deal with tapping OK in WatchlistEditActivity:

Tap New to show the Edit Watchlist screen – it says “Create Watchlist”

Enter a name and select one or more securities

Tap OK

Also tap OK in the Manage Watchlists screen

DbTradeAlert shows the new watchlist in the rightmost tab

Optional: commit changes

What’s missing now is only functionality to edit and delete existing watchlists.

1.5 Add Edit Functionality for Existing Watchlists

Implementing functionality to edit watchlists is a bit involved because the button for it (and the one for deleting watchlists) isn’t in an activity like all the previous buttons. Instead, a ListView hosts a list of views and each view contains both a button for deleting and one for editing the watchlist it represents. For that reason the button’s click handler goes into the ListView’s CursorAdapter class which connects it in newView():

The code creates an OnClickListener instance from an anonymous class and provides an onClick() handler for it. The view representing the watchlist is passed as a parameter to onClick() and the WatchlistManagementDetailViewHolder instance connected to that view provides access to the watchlist’s Id and WatchlistsManagementActivity’s Context (see highlighted lines).

A necessary change for that was to add a Context field to WatchlistManagementDetailViewHolder. That makes it possible to access the WatchlistsManagementActivity instance inside editButtonClickListener.

The code then creates an intent to show an WatchlistEditActivity and provides the watchlist’s Id in its extras so WatchlistEditActivity knows which watchlist to load. When starting the activity the code uses WatchlistEditActivity.UPDATE_WATCHLIST_REQUEST_CODE to signal that it’s about editing an existing watchlist.

Actually WatchlistEditActivity.onCreate() only checks if watchlistId isn’t DbHelper.NewItemId to decide if that’s an edit:

When the user taps OK the watchlist and its connection to securities are saved like for a new watchlist. And finally WatchlistsManagementActivity.onActivityResult() receives a resultCode of RESULT_OK and refreshes its list of watchlists to show a possibly changed name – again this was already implemented for creating new watchlists.

Give it a try:

Open the Manage Watchlists screen

Tap Edit on one of the watchlists to show the Edit Watchlist screen – it says “Edit Watchlist”

Change the name or the securities to include

Tap OK

Also tap OK in the Manage Watchlists screen

Check if the changes were applied correctly – either in the main screen or by using the Edit Watchlist screen again

Optional: commit changes

1.6 Add Delete Functionality for Watchlists

The functionality to delete watchlists in WatchlistManagementCursorAdapter uses the same pattern as for editing them:

If you aren’t used to Java the code for deleteButtonClickListener probably looks somewhat weird due to the anonymous classes and the builder pattern. The code itself is like the code to edit a watchlist but with an added twist: the user needs to tap OK in a confirmation dialog to actually delete a watchlist.

Creating that AlertDialog uses the builder pattern / a fluent API / method chaining. That’s syntactical sugar claiming to make the code more readable. Or maybe it makes it even less readable – you decide.

The setPositiveButton() method’s 2nd parameter is yet another OnClickListener and again it is provided as an anonymous class that only implements an onClick() handler. That handler determines the watchlist’s ID and passes it to DbHelper.deleteWatchlist(). After that it initiates a refresh of the list from which the watchlist was deleted.

WatchlistManagementCursorAdapter avoids referencing WatchlistsManagementActivity because that activity already uses the CursorAdapter. And because deleting a watchlist doesn’t show a new activity it can’t trigger WatchlistsManagementActivity.onActivityResult(). Again, time to send a local broadcast.

In WatchlistsManagementActivity a receiver for that broadcast has to be implemented, registered, and unregistered. Note that in lifecycle methods like onPause() and onResume() you always call super first.

Deleting the data is straightforward – DbHelper.deleteWatchlist() first deletes the records connecting the watchlist to any securities and after that deletes the watchlist itself. Of course it wraps everything in a transaction. The log entries look like this if the watchlist showed 2 securities:
… V/DbHelper: deleteWatchlist(): watchlistId = 4
… V/DbHelper: deleteWatchlist(): result of db.delete() from securities_in_watchlists = 2
… V/DbHelper: deleteWatchlist(): result of db.delete() from watchlist = 1
… D/DbHelper: deleteWatchlist(): success!

Deleting a watchlist

To try it:

Open the Manage Watchlists screen

Tap Delete on one of the watchlists

Tap Ok in the confirmation dialog – note that it displays the watchlist’s name to avoid any mishaps

By now DbTradeAlert does what it’s supposed to do – inform the user about triggered signals. What’s missing is the ability to manage its entities:

Add, edit and delete securities

Add, edit and delete watchlists

For both entities the adding and deleting will be started in one activity and the actual editing will be done in a second activity.

Managing watchlists is easier so let’s start with this.

1. Add Watchlists Management

The first step will be to create the Watchlists Management screen and add code to invoke and dismiss it. In the Watchlists Management screen users see a list of all their watchlists and can Tap Edit or New to go to the – not yet existing – Edit Watchlist screen. They can also Tap Delete to get rid of an unused watchlist.

1.1 Add the Watchlists Management Screen

Add a new empty activity named “WatchlistsManagementActivity” (notice plural). After that add 3 Buttons, a ListView, and a TextView to activity_watchlists_management.xml:

As the OK button shows Android provides translations for general strings like “OK”, “Cancel” or “New” that it will automatically use depending on the device’s language setting. But internationalizing an app properly is a huge effort which doesn’t make sense for DbTradeAlert and so I use hard-coded strings. The rest of the layout should look familiar by now.

Android Studio will automatically add the activity to AndroidManifest.xml.

Then add code for the Cancel and OK buttons and extend onCreate() to set the screen’s title. Using “android:label” in “layout/activity_watchlists_management.xml” would not work because “android:label” in “AndroidManifest.xml” will overrule it. setTitle() does the trick.

In onOptionsItemSelected() you start the activity by calling the superclass’ startActivityForResult() method with an intent specifying the activity’s class. That method also takes an ID that will be passed back to the superclass’ onActivityResult() method once the user has clicked OK or Cancel in the activity. Regardless of resultCode onActivityResult() will initiate a refresh of watchlistListPagerAdapter because while the user may have tapped Cancel in the Manage Watchlists screen he may have OK’d changes in the Edit Watchlist screen.

Not informing watchlistListPagerAdapter about this will result in an exception: “IllegalStateException: The application’s PagerAdapter changed the adapter’s contents without calling PagerAdapter#notifyDataSetChanged!”

To make this possible watchlistListPagerAdapter’s declaration needs to be moved to class level.

Like for the previously created lists you’ll need an adapter to marry the ListView with its cursor and its detail layout. Only this time it will be more involved because of the edit and delete actions provided by the layout.

Create a new class named “WatchlistManagementCursorAdapter” extending CursorAdapter (android.support.v4.widget.CursorAdapter). Let Android Studio create the missing methods and a constructor with “Context context, Cursor c, boolean autoRequery” parameters. The next step is to extend the class like shown below including the private WatchListManagementDetailViewHolder class:

As always the ViewHolder’s job is to save the time required for finding controls with findViewById(). newView() creates a new WatchListManagementDetailViewHolder instance, stores references to the controls in it and saves it to the View’s tag. After that bindView() is called and uses those references to update the control’s data. Because bindView() is called way more often than newView() the time savings from storing references to the views add up. Even more important: bindView() is called when the screen updates and that’s where split seconds count.

Finally connect WatchlistManagementCursorAdapter to watchListsListView in WatchlistsManagementActivity.onCreate(). Again, the pattern should be familiar by now.

Now start the app and test again: its watchlists show up in the Manage Watchlists screen – be aware that tapping the Edit or Delete buttons will crash the app. Again a good time to check in the changes.

A prerequisite for working New and Edit buttons is yet another new activity: one to edit a watchlist’s details. So let’s create that activity.

1.3 Add an Activity to Edit Watchlists

Add a new empty activity named “WatchlistEditActivity” (notice singular). The layout needs 2 Buttons, a ListView, a TextEdit, and 3 TextViews. That ends up being 250 lines of xml so I’ll not post it – just get the code from GitHub or look at the screenshot below.

The first step is to provide code for WatchlistsManagementActivity’s New button:

The pattern around startActivityForResult() is as used before. But this time an ID is transferred in the intent’s extras so the target activity knowns which watchlist to work on. As onNewButtonClick() starts the creation of a new watchlist it transfers a special ID that DbTradeAlert uses to signal a new item.

WatchlistEditActivity.onCreate() checks the watchlist ID from the intent’s extras. If it’s the special one for a new watchlist it empties the Name TextView and sets the screen’s title appropriately. Finally it calls refreshSecuritiesList() which updates securitiesListView with a cursor containing all the securities from the database and marks those securities with DbHelper.IS_SECURITY_IN_WATCHLIST_ALIAS == 1 (see below). Of course the cursor cannot be closed as it has been passed over to the SimpleCursorAdapter.

This is the SQL generated by DbHelper.getAllSecuritiesAndMarkIfInWatchlist():

SELECT tmp._id AS _id, tmp.symbol AS symbol, q.name AS name, MAX(tmp.isInWatchlist) AS isSecurityInWatchlist
FROM (
SELECT _id, symbol, 1 AS isInWatchlist
FROM security s
LEFT JOIN securities_in_watchlists siwl ON security_id = _id
WHERE siwl.watchlist_id = -1
UNION ALL
SELECT _id, symbol, 0 AS isInWatchlist
FROM security s
) AS tmp
LEFT OUTER JOIN quote q ON q.security_id = tmp._id
GROUP BY tmp._id, tmp.symbol, name
ORDER BY tmp.symbol ASC

The SQL contains an outer and 2 inner SELECTs. The first inner SELECT lists all the securities that are in the watchlist – none in this case as this is a new watchlist – and sets isInWatchlist to 1 for them. The second inner SELECT lists all the securities no matter if they are included in any watchlist and sets isInWatchlist to 0 for them.

Both lists of securities are then combined into a new temporary list by a UNION. The temporary list will have two items for each security in the watchlist – one with isInWatchlist == 0 and one with isInWatchlist == 1. By selecting only the one with MAX(tmp.isInWatchlist) into the final list it will contain a single item for each security and those in the watchlist will have isInWatchlist == 1.

New Edit Watchlist screen

Run the app again:

Open the Manage Watchlists screen

Tap New:

The new WatchlistEditActivity shows up listing all securities and ready to receive a name

Again no menu but a distinct title

Tap Cancel – tapping OK will crash the app

Optional: commit changes

Note that that I closed the automatically displayed on-screen keyboard so it doesn’t obstruct the activity’s screen.

For now watchlist management is all form but no function. Time to make it operational.