G'day everyone, welcome back.
Back in Android Studio and we're going to create an AppDialog class that we can use,
whenever we need to show a confirmation dialog.
Right click the package, select New, Kotlin File/Class.
Let's call it AppDialog, and to save ourselves some typing, select Class.
We're going to extend AppCompatDialogFragment, so we'll start by adding our usual tag,
and then adding the extension.
We know we're going to have to use callbacks, so let's have a think about the interface that we're going to define.
We're going to use a dialog in a number of different places.
We've already decided to show it when our user deletes a task
but it could also be useful to prompt the user, if they close an AddEditFragment without saving any changes.
A bit later, we'll be storing the timings for the tasks,
and we'll give the user a chance to delete old records.
That's a third place we'll use our dialog.
Let's think about what we can do with our dialog.
There are really only two things that can happen, once we've shown our dialog.
The user can accept whichever action we're proposing, or they can refuse it.
There's a third option as well.
The user could close a dialog without responding to it at all, by using the back button, for example.
To fully implement everything that we can do in the dialog, our interface needs to define three functions.
We'll start with a comment to remind us what we're going to be doing,
and then we'll define our functions.
That would let the activity respond to anything that the user could do with our dialog.
In this app, we're only interested in responding when a user confirms an action.
I'll leave the other two functions, but I'll comment them out.
If you decide you want to implement them, then you can do so,
but remember that everything using this AppDialog class, will need to implement all the functions in the interface,
even if it doesn't want to respond to the dialog being cancelled.
So what are those two parameters in the function?
Well, we're going to use this dialog class in a number of different situations,
and it's quite possible that a single activity might want to confirm deletion of a record,
and confirm exiting from the Edit operation.
The activity's callback function will be called by both instances of the dialog,
and it needs to be able to tell which dialog is responding.
When an Activity creates its dialogs, it'll give each one a unique ID.
It can then test this ID in the callback function, so that it knows which dialog the code is responding to.
The second parameter in our interface functions, is the bundle that was passed when the dialog fragment was created.
I won't explain about that just yet, because it's far easier to understand when you see it being used.
Then you'll understand the problem that passing it back is designed to solve.
So we'll come back to that shortly.
Our DialogFragment is a Fragment, and we've seen that a fragment's constructor can't have any parameters.
If we want to pass any values into DialogFragment, we use a bundle,
just like we did with the AddEditFragment class.
Values are stored in a bundle as a key / value pair,
and in AddEditActivity, we use the class name as the key.
In this DialogFragment, we're going to provide more than one value.
I'll define a few constants for the keys.
That way, the classes using our dialog don't have to specify the exact strings, and the keys are used consistently.
The next bit of code's very similar to what we did in AddEditFragment.
We need a field to store the object that we'll be calling back,
and we set it to the calling Activity in the onAttach functions.
I'll add the field,
then use Android Studio to generate the onAttach and onDetach functions, using Ctrl O.
And of course, we'll add the logging,
Be careful when choosing the onAttach function from the dialog.
There's a deprecated onAttach that takes an Activity argument.
We want the one that takes a Context.
We should check that the calling activity does implement the required interface,
and we'll raise an exception if it doesn't.
We did the same thing in AddEditFragment, and this is pretty standard code.
If you're going to cast an object to an interface type,
then it's a good idea to check that it can be of that type first.
If it isn't, the error will be spotted by the programmer, you, pretty quickly.
without having to wait until the callback functions are called.
I'm going to use a slightly more complicated test in onAttach.
We could use our dialog from an Activity, or we could use it from a Fragment.
If we call it from an Activity, the context will be fine, and we've seen that before.
However, if we use the dialog from a Fragment, the context will be the Fragment's Activity,
and in that case, we don't want to call back the Activity.
Fortunately, the Android framework supports what we're trying to do.
Fragments have a parent fragment, that refers to the fragment that created them.
That'll make sense when you see the code.
We'll first try and cast the parent fragment to be a DialogEvents.
If there is no parent fragment, then it'll be null, and we'll get a typecast exception.
If that happens, we should have a context, the Activity, and we'll try to cast that to be a DialogEvents.
If that fails, then the Activity doesn't implement the interface.
And if our attempt to cast parent fragment fails, that means the fragment doesn't implement the interface.
In both cases, we'll throw an exception.
The onDetach function is the same as in AddEditFragment.
We make sure that we won't attempt to call back functions, if the Activity's been destroyed.
We'll be testing for null when we come to call the interface functions.
For a dialog fragment, we normally set everything up in the onCreateDialog function.
I'll get Android Studio to generate the function for us, after the onAttach function.
The code's pretty much copied from the Android doc we were looking at earlier.
I've used variables for the ID's and messages,
and we've got errors because a few variables haven't been initialled yet,
but we'll do that in a moment
We're using an AlertDialog, rather than a Date or Time Picker,
so we create a new AlertDialogBuilder that will build the dialog for us.
We need to provide some basic information for the dialog.
The message to be displayed, for example,
and the text to appear on the two buttons.
But this is pretty much boilerplate code,
and you'll generally start all your dialog fragments with something like this to begin with.
Each of the AlertDialogBuilder functions returns the instance that it was called on,
so we can chain the function calls together,
which we've seen done a few times now.
Our code uses the setMessage function to tell the Builder what text to display,
and the setPositiveButton and setNegativeButton functions to set the text on the buttons,
and add the onClick listener to them.
These listeners are very similar to the button onClick listeners that we've used before.
Instead of a reference to the button, we get a reference to the dialog in the first parameter.
We're not interested in anything about the actual dialog object that the builder creates for us,
so we'll ignore this first parameter.
It can be useful in a custom dialog, though, because it allows us to call the dialog's cancel function.
We'll discuss that later.
The second parameter, which, is the button that was clicked,
or the position of an item, if we were allowing our dialog to allow one of a list of options to be chosen.
We're not doing that, so we'll ignore that parameter as well.
In fact, we'll get some suggestions for improving those calls, once we've corrected the errors.
I've included the code that you'd use, if you want to respond to the negative button, as comments in the code.
You'll have to include an onClick listener when setting the buttons,
but it can be empty, as our negative button listener is here.
Okay, our dialog would be a bit limited if it always displayed the same message.
We'll be passing in the message that we want to display, in the arguments bundle,
in the same way as we passed task information to the AddEditFragment.
We'll also want the buttons to reflect what the dialog's all about,
so we pass in text for the two buttons.
I've already mentioned the dialogue ID, and that will appear in our bundle.
We'll start by retrieving the bundle that was provided, when this dialog fragment was created, and extract the values from it.
I've already created the constants that we'll use as keys, for retrieving the values of the bundle.
As long as there is a bundle,
we can use those keys to retrieve the values.
Our DialogFragment relies on being provided with at least a message to display,
and our callback interface requires a dialogue ID.
So if arguments is null, we'll raise an exception
If our dialog's used without these being provided, that exception will indicate what the programmer is doing wrong,
which is helpful, as we're not writing any documentation.
That exception will be thrown as soon as the dialog is displayed.
It'll be picked up even before the app enters formal testing.
It's intended to help programmers, and users of the app will never see it.
Okay, I've made the text for the buttons optional.
If the text isn't provided for the two buttons, they'll default to showing OK, and Cancel.
One thing you may have noticed,
is that I'm talking about text, but I'm retrieving int values, resource IDs, for the positive and negative button captions.
It's a good idea to store all strings that users will see in string resources.
So we're passing the string resource IDs in our bundle, instead of the strings themselves.
The setMessage, setPositiveButton and setNegativeButton functions can accept either a string or an int argument.
The functions have been overloaded in the DialogBuilder class.
If you want to work with strings instead, then you can.
Control click on the setMessage function
to view the source,
and you'll see the two overloaded functions; one taking an int ID, and the other a CharSequence.
The Java String class implements this CharSequence interface,
so a String is a CharSequence.
If you're going to use string resources, and you should,
then it makes sense to extract the resources as late as possible.
There's not much point extracting the strings in MainActivity, if this dialog is never shown.
That's why I'm passing the resource IDs, rather than the strings.
The reason we don't do that for the message,
is because that may have to be built up by combining a resource string with some other values,
such as a task name or task ID.
Our dialog doesn't know anything about tasks,
so we have to let the calling program extract the string resource, and add any extra values that it needs.
It is likely that the message can contain variable text, but it's quite unlikely that the button's caption would.
Now that we've finished the code, all the errors have gone,
except for our oc and cancel resource IDs that we're using as the defaults.
Rather than editing the res/value/strings.xml file, click on ok,
then drop down from the light bulb that appears.
We want the first option, Create string value resource 'ok'.
The resource name's filled in for us.
All we need to do is to provide the resource value, OK.
We can do the same thing for the cancel.
This time, the resource value is Cancel.
Right, let's end this video here. And I'll see you in the next one.