So this video will be a challenge to fix
a bug in the calculator app, so we're
gonna start by demonstrating that bug. So
you can see that I've got the calculator
app running on an emulator. Now the way
that this app works, the arithmetic
operation keys will cause the previous
calculation to be performed, and then
become the next operation to be
performed. So this is done by storing
the operation in the pendingOperation
variable - as we saw when writing the code
in previous videos. Now if you
accidentally press an incorrect
operation key when the input is empty,
you can correct it by pressing the
correct one - and you can see the
operation change on the screen as I
press each one. So to see the bug, I'm
starting afresh so I don't need to clear
anything, but just to make sure, I'm going
to press, to clear rather, the pending
operation by pressing equals. Then I'm
going to enter 57 equals, then minus 6,
and then multiply. So the result of 51 is
displayed, and the next number that I
enter should multiply 51. But, before
entering another number, I'm going to
rotate the device. So I'm gonna go into
landscape mode. Now if I want to multiply
51 by 2, I should just be able to do 2
equals - so 2 equals. Now that didn't go well.
So the problem is that although the
contents of the EditText widget
survive a rotation, none of the app's
variables do. So the activity is
destroyed and recreated when the device
is rotated, remember, so the pending
Operation variable is reset to
equals. Alright, so the challenge is to
fix the bug, by storing and retrieving
the app's state when the device is rotated.
Now if you're not sure how to do this, or
to how to approach it, review the last
videos in section 5, where I discussed
the Activity Lifecycle, and where we
saved the contents of the TextView for
the Button Click App. Alright, so pause
the video here, and when you've had a go
at that and you're ready to come back
and see the
solution, you can start the
video and I'll go through and do that. So
pause the video now.
Alright, so let's look at the solution.
So the methods we need to Override to
store state are onSaveInstanceState
and onRestoreInstanceState. And as
a reminder, here's the Activity Lifecycle
slide from section five.
Now onSaveInstanceState is called if
needed, when Android's about to destroy
the Activity - as it does when the
orientation changes. So now that we've
created a separate layout for landscape
mode, the reason why Android destroys and
recreates the activity may make more
sense now. And it's because it's an entire new
layout that has to be applied, and the
easiest and safest way to do that is to
just start again. So with all these
challenges, there's often more than one
way to solve them. The real test here is
Does it work?
and definitely not Does your code look
the same as mine? Alright, so state can
be restored in either onCreate or on
RestoreInstanceState, and I'm going to
use onRestoreInstanceState. But if
your solution used onCreate, that's fine.
The reason I prefer not to restore state
in there - in onCreate - is mainly
because it's a good idea to keep the
code in a separate method, just to keep
things easier to understand. In addition,
if you try to use the Bundle in
onCreate, you have to check that it isn't
null.
Now if there was no saved state, then
Bundle will be null there, whereas on
RestoreInstanceState is only ever
called if there is a valid bundle.
Alright, so let's go back to Android
Studio for my solution to fixing this
bug.
Alright, so a bundle contains a list of
pairs of keys and values, so we need keys
to store the pendingOperation and
operand1 variables under, to enable us to
get them back again. Now because we'll be
using the same keys in two different
places, I'm gonna store them in two constants.
Now unlike Java, where everything must
exist inside a class, Kotlin allows
variables to be declared at the top level
of the file, outside of the class. In fact,
for val constraints, it insists on this.
So if I tried to put this next piece of
code that I'm about to type, inside the
MainActivity class, we'd actually get
an error. Alright, so let's go back up
to the top of MainActivity, and we want
to put this before the class definition. So
I'm gonna start on line 10, and I'm going
to type private const val, and in
uppercase, STATE_PENDING
_OPERATION is equal to, and in
double quotes, PendingOperation. Then
private Const val STATE_
OPERAND1 equal to, double quotes
Operand1.
Now the actual contents of those strings
can be pretty much anything, but there's
a limit to how big a bundle can be, so
don't go using incredibly long strings.
And just in case it wasn't obvious when
we discussed this earlier, data is saved
in a Bundle as key/value pairs. So the
keys we'll be using are these two
constants; STATE_PENDING
_OPERATION and STATE_
OPERAND1. Now when we want to get
the value for operand1, we use the key
STATE_OPERAND1 to
retrieve it, and similarly for the
pending operation. Now saving the
variables is just a case of overriding
onSaveInstanceState using the
shortcut Ctrl-O, or you can get a list of
methods to override from Generate on
the Code menu. Now I'm going to put the
overridden methods at the end of
the code, but before the final curly
brace. I'm gonna go right down to the bottom, just down
here, and I'm going to do a Ctrl-O, and we
want onSaveInstanceState. So I'm just
going to type the first few letters -
onSave. There it is there, onSaveInstance
State, so I'm just going to press
Enter. But before I do that, do be
careful here, because there's two
versions of this function. We want the
one that takes a single argument,
and unfortunately, that appears further
down the list than the one that we don't
want - the one that I've actually
currently highlighted. So I'm going to
use the down arrow key to move to the
second definition. So you can see the
second definition there, has only got
just the one argument - outState, which is a
Bundle. Alright, I'm gonna press Enter. And
there's a slight oddity about this
function, to do with the interoperability
with Java, and I'm gonna come back to
that in a minute. But first we need to
add our variables to the bundle, so the
overridden function is going to become
as follows; so we're gonna leave the
first call to super.onSave
InstanceState. And then we're going to
append to that, if parentheses operand
one is not equal to null, in code block.
We're going to do outState.put
Double parentheses, and it's going to be
STATE_OPERAND1 comma
space and operand1 and bang bang - so
two exclamation marks. Now we actually get
an error there - we'll talk about that
shortly. And then I'm going to put on the
line after, outside the closing code
block, outState.putString and STATE
_PENDING_OPERATION comma
space pendingOperation. So notice that
we have to check, firstly, that operand
1 isn't null, otherwise the program would
crash if the device is rotated, before
any numbers had been entered. Alright,
so we've got this error at the moment, on
line 113. And if we hover over that;
Only safe or none-null asserted calls are
allowed on a nullable receiver of type
Bundle.
Now if you haven't got that error, please
keep watching and I'm going to explain
why you haven't. So the problem is that
the outState parameter is of type
Bundle? which is a nullable
Bundle. Now Kotlin doesn't allow method
calls to be made on objects that could
be null. That's a classic problem that
causes Null Pointer Exceptions, and
Kotlin actually makes sure we don't get
those anymore - or at least
it does its best. We can still get them
if we go using the bang bang operator,
which we shouldn't, for example.
So using bang bang is one possible
solution. So in other words, I could come
back up to here, two exclamation marks -
the bang bang. But what we're doing is
asserting that outState won't be null.
Can we be really sure that it can't be
null? Well actually, yes we can. The reason
is that onSaveInstanceState is only
called if there's a Bundle to save. In
fact, you may well not have that error.
Google are adding the @NonNull
annotations to the Android framework
code all the time. And if they mark out
State as NonNull, then Kotlin will be able to
use a non-nullable Bundle type, instead
of this Bundle? that it's
using as the argument on line 110. So
bang bang is one option, but Kotlin also
provides another operator, that I think's
more appropriate here. So let's go
back and replace the double bang with a
safe call operator, which is the question
mark. So we're gonna go back and delete
that - the bang bang - and replace that with
the question mark. So that also fixes the
error, and actually is a safer way to
deal with this. So that applies probably
everywhere that you're accessing a
property or calling a method, on an
object that's of a nullable type. So
whereas bang bang asserts that the
object can't be null, and throws an
exception if it is, the safe call
operator won't actually make the call if
the object is null. So in our code here,
putDouble just won't be called if out
State is null. So that line will just do
nothing. So that's a safer solution than
the double bang operator, and I suggest
you try to use the safe call operator in
these situations. Now it doesn't really
matter here, because we know that out
State won't be null, but safe call can be
used even when you can't be sure. And
we'll see that used again throughout the
course so, you'll get a feel for when
it's a good way to deal with nullable
objects. Okay, so there's a third approach
that can be used here. Now before I
change the code to use it, I should
really point out that you need to
be very careful when using this approach.
So don't go using this approach
indiscriminately.
You need to be very sure that you
understand
what you're doing. So what I'm going
to do is modify the function signature,
so that it accepts a non-null Bundle
argument. So the way to do that is to get
rid of the question mark from the Bundle
type, and once I've done that, I can
actually get rid of the safe, the safe
call operator, like so. Now I can't
predict the future, but it wouldn't
surprise me if this function signature
gets generated automatically, before too
long. It may even be the code that you've
got, if you're watching this video
sometime after I recorded it. As I said,
you do need to be careful when using
this, though. If you're absolutely sure
that outState really will be an
non-nullable type, then this is perfectly
valid. But changing the function
signatures of overridden methods mustn't
be done without fully understanding what
you're doing. So this is the approach I'm
going to take in this app. Now one reason
is that you'll already find code like
this out there. People are already doing
this, and although I haven't yet found an
example of Google doing it, with onSave
InstanceState, there may well be
examples by the time you watch this
video. Now restoring the variable is done
by reading it from a bundle, and I'm also
going to update the display so that the
user can see which operation is pending.
Once again, though, be careful, because
there's an overloaded version of the on
RestoreInstanceState method too. So we
want the one that accepts a single
Bundle argument, and I'm just going to
generate it, just after this onSave
InstanceState function. Let's go ahead
and do that, doing a search for onRestore
and notice we've got the two definitions
there. We actually want the top one - the one
that's just got the Bundle argument and
not the second argument. So I'm going to
press ENTER there. And what I'm going to
do here is take the same approach as for
onSaveInstanceState, and change the
parameter to be a non-nullable Bundle. So
we're going to get rid of the question
mark on the type. Alright, so after the
super.onRestoreInstanceState line,
we need to restore our two properties
from the Bundle,
and then display the operation in its
TextView. So I'm going to start by
typing operand1 is equal to saved
InstanceState.getDouble, and the
argument's gonna be STATE
_OPERAND1 for our const. And
then pendingOperation equals saved
InstanceState.getString, and the
argument is STATE_PENDING
_OPERATION, our constant definition. Then
lastly, as I mentioned, we're going to
display the operation. So it's going to
be displayOperation.text is equal to
pendingOperation. Now that looks fairly
straightforward, but it's important to
check the documentation for any methods
that you use, so that you really
understand how they behave. Now we can do
that in Android Studio, by clicking on
the function that we're interested in,
and using Ctrl-Q on Windows or Linux, and
on a Mac, it's actually Ctrl-J. So we're gonna go
ahead and do that - and this is for the get
Double. And we're gonna come up here and
press my key, and there's the
documentation that's popped up. Again,
it's a Ctrl-Q on Windows or Linux, or
Ctrl-J on a Mac. So the documentation is
short, but it does state that the
function will return 0.0 if the key
doesn't exist in the Bundle. Well
actually, it says or 0.0 if no mapping of
the desired type exists for the given
key, which essentially, means the same
thing. So if a key with the value of
STATE_OPERAND1 doesn't
exist in the Bundle, or if it does exist
but its value isn't a Double, then
operand1 will be given the value 0.0.
Well, that's not actually quite what we
want. If we haven't stored operand1,
that's because it was null, not because it
was zero. So we do want it to have the value
of null after calling getDouble, if it
doesn't exist. Now we can provide a
default value to the getDouble function,
but unfortunately, that value can't be null.
So the way that I suggest coping with
storing values that may be null in a
Bundle, is to also store a Boolean, to
indicate if there's a value there or not.
And that's probably easier to see than to
describe, so let's get to the code.
So firstly, we need to define another
constant for the key, so I'll go right up
to the top of the file to do that,
remembering that the definition is above
the class definition. This is going to be
private const val. This one we'll call
STATE_OPERAND1_STORED is equal to,
and we'll call this one Operand1
_Stored. So next we need to
change the onSaveInstanceState
function slightly, so let's go down to
the bottom and do that. Now we need to
add some code, so we've got this
code on line 113 checking, to see if
operand1 is not equal to null, and then
we've got it putting a Double there. But what
we're going to do after that line, is add
a new line to put outState.put
Boolean. Then we're going to put STATE_
OPEERAND1_STORED comma true.
So that's all we need to do for the
onSaveInstanceState function.
Now if operand1 was stored, we can check
the value stored under the key STATE
_OPERAND1_STORED.
And if that's true, then we can call get
Double, otherwise operand1 should be
null. So let's update our onRestore
InstanceState function. And we're going
to make a change here, so after the super
call, we're going to put a call here.
We're going to put - type some code - if
savedInstanceState.getBoolean
STATE_OPERAND1_
STORED comma space false. I'm going to
open a code block. And what I'm going to
do is put in this line here, this operand
1 equals savedInstanceState.get
Double. That will be in the if code block,
and we'll also have an else here, else,
another code block, operand1 is equal to
null. So now on line 122, we call the get
Boolean method, to retrieve the value
stored with the key STATE_
OPERAND1_STORED. The call to
getBoolean includes a default value of
false, so if the key isn't present, the
condition will be false.
So only if we saved true with that key,
will line 123 be executed. Otherwise,
operand1 will be set to null, as you
can see on line 125. Now we can lift that
assignment out of the if, just like we
did earlier. If I click over here, I can
click on that, and then the light bulb
comes up and I can click on that. And notice
that I only get that operation when I
did click on that in the gutter. Notice
there, that when I do that, if is
underlined there. You see that just
briefly, so, we could also have just clicked
if to see that come up. We can come back
over here again. There's the light bulb -
click on that, and lift ssignment out of if.
And you can see the code's been changed
for us. And that should now do it for us.
So, let's actually run the app again and
just check that it's actually working, as
we suspect it will, or hope that it will.
Alright, so I'm going to start by doing
equals 57, equals minus 6, times. Now I'm
going to rotate like I did last time, and
then I'm going to type 2 equals. This
time, we got the answer 102 so that's
now working fine. So our calculator now
behaves as expected, even when the
orientation's changed. Now this particular
app has very little state - there's just
the contents of the two EditText widgets,
the content of the displayOperation
TextView, and operand1. Now the two
EditText widgets are saved and
restored for us. Android knows that users
may type text into EditText widgets -
that's what they're there for, after all -
so the Activity's onSaveInstanceState
and onRestoreInstanceState methods,
are taking care of the EditText widgets.
So that's why it's important, looking
back at the code, to leave these super
dot on calls,
super.savedInstanceState on line
112, and super.on
RestoreInstanceState on line 121. So
it's important to leave those in when we
override the onSaveInstanceState and
onRestoreInstanceState functions. So all
we had to do was store and retrieve
the contents of
the TextView, and operand1. This was
complicated slightly, because we're
allowing operand1 to be null, and
Kotlin can't store null in a Bundle. So
we had to use that additional flag, to
let us decide whether operand1 should
be null, when we try to restore it. So
there's still room for improvement, but
before we change the way our Calculator
works, we're going to look at another
method that Kotlin provides, for getting
references to widgets. And we'll do that
in the next video.