Practical Jetpack Compose
Practical Jetpack Compose
Form
When buil ing apps, it’s o ten that we’ll be wor ing with some kind of user a count
- mea ing that we’ll need to provide a way for users to cr ate a new a count or ac-
cess an e is ing one. The abi ity to be able to achieve this will be provided through
an a the ti tion screen, provi ing a way for users to sign-up or sign-in u ing one
of the met ods that your a pli tion o fers. While many apps o fer a the ti tion
via third-party sites (such as s cial a counts), a co mon r quir ment is email au-
the ti tion - not only does this d couple users a counts from third-party sites, but
it also o fers a co mon a the ti tion met od for co nec ing with your a pli a-
tion.
With this in mind, we’re g ing to build out an email a the ti tion screen us-
ing je pack co pose. While an email form isn’t vis ally the most e qui ite e pe i-
ence to be buil ing, it o fers a co le tion of ways for us to think about state man-
1
e
n
u
n
t
c
a
f
m
n
x
d
h
c
t
a
m
d
m
r
x
f
f
o
u
t
p
l
x
o
n
d
c
a
n
c
e
l
a
c
c
u
f
h
k
f
c
r
m
n
u
u
n
e
c
e
m
n
t
a
c
e
a
e
f
x
t
a
u
c
s
s
n
p
c
c
x
a
c
r
When it comes to this screen, we need to co ure and handle se e al di fe ent
things.
- Pe sist the state for the entered email and pas word
- Handle the state change when the a the ti tion mode toggle bu ton is
pressed
2
r
r
n
t
p
b
t
e
n
n
r
c
c
l
t
w
r
u
n
fi
y
c
u
n
a
e
fi
s
g
o
n
n
c
a
s
t
v
r
f
r
We can see here that when it comes to the a the ti tion screen, there is more to it
than a mi i al form that is used for cr de tial entry. The ma ag ment of state and
r co po tion of the UI based on those state changes gives us some good found-
tions to really start to e plore the concept of state wit in Je pack Co pose.
3
a
e
m
s
n
i
m
x
e
n
u
n
c
a
h
t
n
e
m
D ing the A the ti tion
State
Wit in our a the ti tion form, we’re g ing to be dea ing with many di fe ent
pieces of state that d pict how the UI is g ing to be di played to the user. This
state will be used to co pose our UI, a lo ing us to co trol how our co po ables
look and b have to the user. Wit in our UI we’re g ing to need to show the fo low-
ing things:
- A title that will tell the user that they need to sign-in or sign-up
- A bu ton that will a low the user to tri ger the a the ti tion ow. This
bu ton will also be di abled when there is no co tent i put into the
- A bu ton that will a low the user to toggle between sign-in and sign-up
With the above set out, we can start to see that there are g ing to be se e al
pieces of state that we need to hold. With that in mind, let’s start to take the above
stat ments and build a class that will re re ent the state of our co po able UI.
// AuthenticationState.kt
4
a
t
e
h
e
n
n
e
o
t
t
fi
s
e
e
fi
fi
n
s
u
n
r
c
l
n
n
d
t
a
m
fi
c
l
e
a
l
s
m
u
e
n
c
n
e
t
h
c
s
a
l
o
g
u
n
p
w
o
i
o
s
s
o
u
n
n
d
n
n
s
l
n
c
a
h
c
o
a
fl
m
p
m
s
f
s
v
l
r
r
data class AuthenticationState(
and pas word. B cause this is som thing that the user will enter and will be dis-
played wit in our UI, each of these is g ing to re re ent a piece of our state. For
this, we’ll go ahead and add two string re e ences to our state class.
some r quir ments for that pas word. We’ll re re ent these r quir ments in the
form of an enum - this a lows us to e force the length and co tent of the pas word
that is entered.
For each of the r quir ments we’re g ing to want to show a me sage, this will be
used to co m ni ate what the r quir ment is to the user. For this, we’ll need to
5
g
d
d
e
s
h
m
e
l
l
u
e
e
c
e
n
e
l
s
e
o
e
n
o
s
e
o
f
r
e
g
n
p
s
p
s
n
s
e
fi
n
e
s
e
o
e
d
n
s
<string name="password_requirement_characters">
At least 8 characters
</string>
<string name="password_requirement_capital">
At least 1 upper-case letter
</string>
<string name="password_requirement_digit">
At least 1 digit
</string>
We’ll then a just the Pas wor R quir ment to co tain a l bel in the form of a
string r source i teger, se ting a r source for each of the r quir ment enums val-
Wit in the state, we’ll need the cu rent Pas wor R quir ments that are sa i ed
so that we can co m ni ate this to the user. For this rea on, we’ll add a new pass-
wor R quir ments eld to our state that re re ents a list of r quir ments that
6
h
d
e
e
d
e
e
i
n
m
u
fi
c
fi
s
t
d
e
r
e
e
s
p
d
s
e
n
s
e
e
a
e
e
e
t
s
fi
Mo e ling the a the ti tion mode
The above gives us the key parts of the state that we need to a low the user to sign-
up or sign-in to our a pli tion. Ho ever, we’re g ing to be d ing a little more
than that! B cause a user can sign-up and sign-in to our a pli tion, we need to al-
low the form to be toggle-able between these two modes. To be able to re re ent
this in our state we’re g ing to start by d ing a new enum that will be used to
Now we can go ahead and add this to our state - this will then a low us to ea ily
toggle between these two modes and have our UI r co pose whene er the s lec-
to be able to handle the state when a user reaches the point of pe for ing those
o e tions. While we won’t be hi ting an API in our UI e ample, in the real world
this would be an asy chro ous o e tion - mea ing that we would have to wait a
7
p
g
r
i
a
d
d
e
h
l
l
n
p
p
o
s
c
n
a
r
p
u
t
r
w
a
e
d
e
fi
n
n
n
c
a
e
o
m
x
p
fl
c
l
a
o
l
r
v
o
m
p
e
s
s
second or two for it to co plete. In this sce ario, we would want to show a pro-
gress i di a or to the user so that they know a r quest is ta ing place. We’ll re res-
ent this in our state by adding a loa ing ag to the state class.
sponse that would come back from our API. In the case of su cess, we would na ig-
ate onto the next part of our a pli tion, but things might not a ways go as
planned. In these cases, we’ll want to show an e ror to the user, us ally in the form
of the e ror that has come back from the API. To a low for this, we’ll add a new eld
to our state class which will re re ent if an e ror has o curred and at the same time,
8
n
n
d
r
c
t
l
d
r
e
m
p
s
r
p
d
c
a
fl
r
n
m
e
r
l
c
k
c
u
l
p
fi
v
)
d lete this co tent. These elds the selves will also start blank, so in both of these
states, we don’t want the user to be able to su mit the form. We also want to block
the form from b ing su mi ted if the pas word r quir ments have not been sa is-
ed. To handle these cases we’ll add a new fun tion to our state - this will r turn a
boolean value that re re ents whet er the form co tent is va id. We’re u ing a
fun tion for this so that the place that has the a cess to this state can ea ily check if
the cu rent state a lows the user to pr ceed, i stead of adding a ot er value wit in
our state.
9
fi
e
c
r
d
l
n
e
n
l
p
b
s
fi
t
m
l
h
o
s
n
b
c
c
e
n
n
e
fi
n
l
h
s
e
s
h
t
With that in place, we now have a class that can be used to re re ent the state of
our UI. Over the next few se tions of this chapter, we’ll uti ise this state when build-
ing out our UI, mod f ing its va ues as i te a tions with co po ables take place,
10
g
r
e
m
s
i
i
y
c
e
fl
l
n
r
c
l
m
p
s
s
Cr a ing the A the ti tion
Vie Mo el
Now that we have the state mo elled for our A the ti tion screen, we can start
thin ing about the Vie Mo el that will be used to ma age that state and provide a
11
k
e
r
w
s
t
w
d
d
d
n
r
u
u
n
n
n
c
a
c
a
Se ting up the Vie Mo el
B fore we can get sta ted here, we’re g ing to add a new d pen ency to our pro-
ject that will give us a cess to the A droid L f cycle Vie Mo el class:
implementation "androidx.lifecycle:lifecycle-viewmodel-
compose:2.4.0"
💡 You aren’t r quired to use a Vie Mo el when wor ing with co pose. For the
sake of these e e cises, it helps us to keep things simple and fo low an a proach
Next, we’ll cr ate a new Vie Mo el, called A the ti tio Vie Mo el.
// AuthenticationViewModel.kt
class AuthenticationViewModel : ViewModel() {
This Vie Mo el is g ing to need to hold a re e ence to the state of our screen. For
this we’re g ing to uti ise Stat Flow - this a lows us to cr ate a state-hol er ob-
ser able ow that will emit the d fault state we provide to it, along with any up-
dates that o cur du ing its lif time. Here we’ll cr ate a new Mu abl Stat Flow in-
stance, provi ing a re e ence to our A the ti tio State class as the d fault
12
e
v
t
w
fl
c
e
o
d
e
d
e
x
r
e
r
o
c
r
f
l
r
a
w
e
l
i
e
d
e
n
w
w
u
o
d
i
u
n
e
f
l
e
r
c
d
n
a
c
w
a
n
k
d
e
n
e
t
w
l
d
e
m
d
e
p
d
e
With this in place, we now have a Stat Flow re e ence that is hol ing a re e ence to
our a the ti tion state, in tia ising it with a new i stance of the state class and r ly-
13
u
n
e
c
a
i
l
n
t
e
f
r
n
d
f
r
e
M ni la ing state u ing events
While this state is now in place, we need to start thin ing about the di fe ent ways
in which it can be m ni lated - whene er som thing is changed in our UI (text ed-
ited, bu ton pressed), we’ll want to u date the state wit in our Vie Mo el so that
For when this is the case, we’re g ing to mo el some events that can be
triggered in our co po able UI and in turn these events will be used to m ni late
the state wit in the view mo el. This a lows us to have a single way of our co pos-
able UI co m ni a ing with the Vie Mo el, rather than nee ing to pass the e tire
Vie Mo el or many re e ences to se a ate fun tions which could be used to trig-
ger state changes. I stead, we can pass a single fun tion re e ence to our co pos-
able UI which can then be used to tri ger these events in the Vie Mo el. For these
events we’re g ing to need to de ne di fe ent types that can be triggered, so we’ll
// AuthenticationEvent.kt
sealed class AuthenticationEvent {
14
w
a
b
t
d
m
p
h
u
u
o
e
c
m
t
n
a
t
e
p
f
fl
s
u
r
d
fi
w
o
g
p
p
l
v
r
f
d
r
s
p
e
c
s
d
c
k
h
f
d
r
w
w
d
f
d
r
a
m
m
p
u
n
Han ling a the ti tion mode tog-
gling
With this sealed class in place, we can start to think about u ing it to re re ent the
di fe ent events that can o cur. We’re g ing to start by han ling the sce ario where
the a the ti tion mode can be toggled between sign-up and sign-in. For this,
With this event in place, this can now be triggered from our co po able UI to
cause a state change wit in our view mo el. For that to ha pen though, we need
to write this l gic i side of our Vie Mo el. We’ll start here by cr a ing a new func-
tion that will be used to ‘ ip’ the cu rent a the ti tion mode - switc ing it
With this code above, we take the cu rent a the ti tion mode wit in our a then-
ti tion state and set it to the o po ite value. We then use this to copy our e is ing
15
c
f
a
r
u
e
d
n
c
o
a
n
u
h
c
fl
p
n
w
s
r
e
c
o
d
u
r
a
d
u
u
n
c
a
c
n
a
c
a
n
d
p
s
e
t
m
h
n
p
s
s
h
x
u
t
a the ti tion state, se ting the a the ti tion mode to r ect the newly ca cu-
lated value. When this is done, the new value will be emi ted to the o ser er of our
💡 The copy fun tion in Ko lin co ies the e is ing class re e ence, r pl cing
any va ues that have been provided as a g ments to the fun tion.
Now that we have this fun tion avai able to handle the state change of the au-
the ti tion mode, we need to go ahead and add su port for tri ge ing it from
ou side of our Vie Mo el. For this, we’ll add a new fun tion to our Vie Mo el that
}
}
This fun tion takes an A the ti tio Event re e ence and then can use that to
tri ger the d sired ou come, based on the event which has been provided to it. So
in this case of the a the ti tion mode toggle event, we’re g ing to tri ger the
While we’re not g ing to i pl ment the call to handl Event u til the next se tion
of this chapter, our co po able UI will be able to make the fo lo ing call to tri ger
viewmodel.handleEvent(
AuthenticationEvent.ToggleAuthenticationMode
16
u
g
t
n
u
n
c
l
e
a
c
c
u
a
n
l
c
f
e
a
w
r
c
o
w
c
a
u
m
t
d
u
t
n
n
m
c
s
u
c
t
a
n
e
e
n
c
a
u
p
c
l
m
c
a
n
n
r
c
u
a
n
x
t
e
fl
f
r
e
c
p
t
e
c
f
fl
l
r
n
o
w
g
b
e
w
r
v
a
g
d
c
g
l
)
er events that we want to tri ger from ou side of our Vie Mo el. If we take a look
at the a the ti tion state mo el then we can see the ot er pieces of the state that
we need to m ni late. Two key pieces of this state are the email a dress and
pas word, these will re re ent the data that has been i put by the user and will
need to be r e ted in our state whene er they change. For this, we’re g ing to
add two more events to our A the ti tion Event sealed class.
Here, we’ve a ded an Emai Changed event, along with a Pas wor Changed
event. These will a low the UI to tri ger these events whene er the i put of the
email and pas word elds are changed. For that to be po sible, we’ll go ahead and
i pl ment some fun tions in our Vie Mo el that a low for this state change to be
r e ted. We’ll start with a fun tion, u dat mail, that will simply be used to up-
17
m
e
fl
s
e
c
u
d
u
n
e
s
fl
c
d
n
a
a
f
c
c
p
r
a
l
u
fi
c
n
p
s
g
u
d
l
c
fi
n
g
c
w
a
p
v
t
d
e
E
d
l
h
n
w
s
d
v
k
s
n
d
d
o
uiState.value = uiState.value.copy(
email = email
)
}
When it comes to u da ing the pas word, we’ll need to cr ate a ot er new func-
tion, u dat Pas word, that will be used to u date the pas word re e ence wit in
our state.
Ho ever, whene er the pas word is u dated we’ll a ways want to u date the valid-
ity of the pas word r quir ments. For this, we’re g ing to cr ate a new list that con-
sists of Pas wor R quir ments, for which we’ll add re e ences for the r quire-
ments that have been met. This list can then be set on our state so that the UI la er
can be aware of the r quir ments that have been sa i ed. When buil ing this list
- When the length of the pas word is grea er than 7, this means that the
mi i um pas word length has been met. This means that we can add
if (password.length > 7) {
requirements.add(PasswordRequirements.EIGHT_CHARACTERS)
}
- When the pas word co tains an u pe case cha a ter, this means a oth-
er one of our mi i um r quir ments has been met. In this case, we’ll
18
n
w
m
t
s
p
fi
s
e
s
e
s
s
d
s
e
v
s
s
d
n
e
d
m
e
p
e
e
e
e
t
e
n
e
e
e
e
t
s
s
e
t
.
s
p
p
P
r
T
R
t
_
p
C
T
o
r
l
c
t
v
s
fi
r
f
e
e
r
s
f
r
n
p
f
h
n
r
d
n
e
y
h
if (password.any { it.isUpperCase() }) {
requirements.add(PasswordRequirements.CAPITAL_LETTER)
}
- F nally, if the pas word co tains any d git, this means that this r quire-
ment has been sa i ed. In this case, we’ll add the Pas wor R quire-
if (password.any { it.isDigit() }) {
requirements.add(PasswordRequirements.NUMBER)
}
With this l gic now de ned, we can slot this into our u dat Pas word fun tion
and a sign the re ult to the pas wor R quir ments wit in our A the ti a-
With these email and pas word fun tions in place, these can now be triggered via
an event whene er the i put is changed for either of those elds. We can then go
ahead and add these fun tion calls to our handl Event fun tion - now when the
19
i
t
n
s
o
M
f
v
r
t
s
s
s
fi
fi
n
c
s
n
s
c
d
e
i
e
e
s
p
h
fi
d
c
e
e
s
e
u
n
c
c
event is triggered ou side of the Vie Mo el, the state can be u dated based on
cr de tials, na u ally, the next step would be to a low the UI to tri ger a the ti a-
tion. For this event we don’t need to send any data from the UI - this is b cause the
entered email a dress and pas word are already r e ted wit in our A the ti a-
tion State. So here we can simply add a ot er event type to our A the ti tion
20
e
n
r
d
s
d
t
d
r
t
u
s
n
w
u
c
a
n
d
n
c
h
a
l
e
fl
c
g
h
p
g
u
e
u
u
n
c
c
n
n
a
c
c
object Authenticate: AuthenticationEvent()
With this in place, we can next add a fun tion that this event will be used to tri ger.
For this e ample, we won’t be tri ge ing a ne work r quest, but i stead will be re-
spon ing to the a the ti tion r quest from the UI and r ec ing this via the load-
With this fun tion in place, we now have som thing that will si late the ne work
r quest ta ing place wit in our a pli tion. We can now also add this event type to
our handl Event fun tion, a lo ing the A the ti ate event to be triggered and
21
e
d
p
x
k
e
c
h
t
u
c
n
h
u
c
a
l
n
w
c
w
e
a
p
g
c
d
r
a
c
u
t
e
n
c
e
e
fl
t
m
u
n
t
g
Han ling a the ti tion e rors
With the above in place, we have a Vie Mo el that can handle the di fe ent re-
quired state pro e ties and events that a low the user to enter their cr de tials,
toggle between a the ti tion modes and then pr ceed with tri ge ing the au-
Ho ever, we still have a nal pro erty from our A the ti tion State to handle -
and that is the e ror. This pro erty d faults to null wit in our state, so in that case,
our UI won’t need to di play any kind of e ror. But when this is not null, the UI will
re re ent that e ror in some way and then also provide a way for it to be di missed
from view. So that we can si late this sce ario and then ma age the e pe ted
After we have emi ted the loa ing state we’ll add a delay to si late a ne work
r quest, fo lowed by r mo ing the loa ing status and emi ting an e ror me sage in
withContext(Dispatchers.Main) {
uiState.value = uiState.value.copy(
isLoading = false,
error = “Something went wrong!”
)
}
}
}
You’ll n tice here that we’re ho ping between coroutine di patc ers - while we’re
not ma ing a real ne work r quest there, this is to si late a real sce ario and en-
22
e
p
w
n
u
s
c
a
k
o
d
n
l
c
a
o
r
r
p
i
t
u
r
t
e
n
u
s
fi
c
u
v
a
e
m
p
d
n
u
p
p
n
c
e
d
c
a
w
l
r
c
n
d
u
m
o
n
h
u
r
c
a
t
s
m
n
u
h
g
r
n
r
f
e
t
s
x
s
r
n
c
sure that the asy chro ous work and live data emi sions ha pen u ing the e pec-
ted di patc ers. With this in place, the e ror will be emi ted as part of the a then-
ti tion state for the UI to r ect. While we haven’t cr ated the UI yet, this e ror will
be co posed in the form of an alert di log - this means that we will also need to
be able to di miss this di log. To su port this, the e ror of our state will need to be
cleared so that the UI is r co posed the alert di log is not a part of the co po i-
tion. So that this can be cleared from our state from our UI, we’re g ing to add a
With this in place, we are now able to r ceive e ror di missal events from our UI
la er, mea ing that we also need to i pl ment the state change for when this oc-
curs. Here we’ll cr ate a new fun tion that will be used to simply clear the e ror
23
c
y
a
m
s
u
n
n
h
s
c
a
n
e
n
n
a
e
e
fl
m
c
r
p
r
D
m
a
r
e
e
a
r
s
r
e
s
t
p
s
o
r
r
m
u
x
s
The last thing to do here is tri ger this fun tion whene er the E ro ismissed event
is triggered. For this, we’ll add a nal check to our handl Event when clause to trig-
With this i pl me ted, we are now ma aging the state of our a the ti tion
screen and provi ing the r quired entry points for our UI la er to m ni late the
state based on user i te a tion. Our view mo el is now ready to be plugged into a
co po able UI, which we’ll cr ate in the next se tion of this chapter!
24
m
s
m
c
e
d
n
n
r
c
e
g
e
fi
c
n
d
c
v
e
y
r
r
D
a
u
p
u
n
c
a
Cr a ing the A the ti tion
UI
With the Vie Mo el and state ma ag ment all in place, we’re ready to move on
and start i pl men ing the co po able UI for our a the ti tion screen. When
we’re ished buil ing this UI, we’re g ing to end up with som thing that looks
25
fi
e
n
l
m
w
w
t
e
d
d
t
m
n
s
e
o
u
n
u
n
c
c
a
a
e
This UI will give our users a screen that can be used to log in to an a pli tion - giv-
ing the o tion of pe for ing either a sign-in or sign-up o e tion. While buil ing
this UI we’ll dive into the sp ci cs of how the co po ables can be co gured,
along with adding some nice touches to i prove the User E pe ence of our Au-
26
n
c
a
p
r
m
e
fi
m
m
s
p
r
x
a
r
i
p
c
a
n
fi
d
Se ting up the entry point
B fore we can get start buil ing our pr ject, we’re g ing to need to add a couple
of d pen e cies that we’re g ing to need. We’ll start here by adding these to the
The cu rent r lease of this book is buil ing against 1.1.0 of co pose - be sure to
27
e
e
t
.
r
m
d
n
e
t
fi
s
d
o
w
o
r
d
o
o
m
With these a ded to our pr ject, we’re now ready to start buil ing out our UI.
We’re g ing to start here by buil ing the a cess point to our fe ture - this is how
the me saging fe ture will in tially be co posed wit in our user i te face.
Here we’ll b gin by buil ing a root co po able fun tion, A the ti tion
that will be used to house all of our Co po able UI for the A the ti tion screen.
For this, we’ll cr ate a new Ko lin le called A the ti tion.kt (to keep our
A the ti tion:
// Authentication.kt
@Composable
fun Authentication() { }
This co po able is g ing to be the entry point to our A the ti tion screen - so
we don’t want this fun tion to have to take any a g ments. The point that is na i at-
ing to a the ti tion will be able to just co pose this fun tion, and this co pos-
able will handle everything else. While you won’t see an thing vis al just yet, you’ll
block of the acti ity that was cr ated through the pr ject wi ard. Then as we build
out the pr ject, we’ll be able to vis a ise the A the ti tion when ru ning the
pr ject.
Wit in this root level co po able we’re g ing to want to force our a pli tion
theme on the co po ables that are co tained i side of it, so we’ll add a M te i-
@Composable
fun Authentication() {
MaterialTheme {
}
}
28
u
l
o
m
h
n
s
s
m
u
o
o
c
m
a
s
m
n
e
d
c
a
v
e
m
s
a
s
o
c
r
m
e
d
n
u
o
r
i
s
a
n
e
t
d
l
c
u
fi
a
l
n
m
m
m
o
c
m
s
s
m
u
e
r
u
n
t
u
s
h
n
o
n
c
y
c
c
a
u
a
c
z
h
u
u
n
d
m
n
c
a
u
a
n
n
r
s
p
c
a
c
t
a
c
n
a
a
n
m
v
c
r
g
💡 Wra ping your co po able hie archy in a theme a lows all co po ents to be
co sis ently styled. In most cases, this can ha pen at the highest point of co po i-
tion - even wit in the se Co tent fun tion of the pa ent acti ity that is co po ing
the UI.
This now means that for any of the co po ables that are co posed in the con-
tent block of our M te a Theme, these will be themed a cor ing to the co ors
Sa ing that though, we don’t cu rently have any co po ables that are g ing to
make up the co tent of our a the ti tion form. We’ll go ahead and cr ate a new
co po able fun tion here, A the ti tio Co tent. The di fe ence here is that
this co po able is g ing to be r spon ible for co po ing our UI based on the
state of the screen, mea ing that this fun tion is g ing to take some form of a gu-
ment. B cause it’s co po ing UI based on our state, it’s also g ing to need to
propa ate any events so that the state can be u dated a cor ingly. For this rea on,
it’s also g ing to need to be able to handle the A the ti tio Event types that
we pr v ously de ned. We’ll need to de ne two a d tio al a g ments for our com-
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
means that the pa ent who is co po ing the child can co trol the co po tion to
some e tent. This also helps to keep your co po able fun tions re-u able across
your UI.
29
y
n
m
s
e
g
t
m
p
s
x
i
e
o
s
t
h
s
n
c
fi
c
r
a
e
m
o
m
r
n
t
e
i
s
s
l
l
n
u
u
e
h
r
m
e
n
n
r
c
m
s
c
d
a
c
i
fi
a
fi
s
c
s
p
n
m
p
n
u
d
s
o
m
m
i
r
n
l
s
n
c
s
c
n
a
m
c
c
r
d
v
u
f
d
n
r
o
m
m
m
s
n
o
e
s
m
s
i
m
s
r
s
l
s
This co po able will take an A the ti tio State that re re ents the state
of our screen, along with an event han ler that a lows us to pass up A the ti a-
So, why can’t this just be the entry point to our a the ti tion screen? One
clear thing here is that this d couples the co po able with b ing co cerned
about how the state is provided - passing in the state via an a g ment makes it sim-
pler, as in, it gets passed a state and co poses UI based on it. This also makes it
much eas er to write tests our co po able - b cause we can simply pass it a state
o ject and pe form a se tions based on that, rather than nee ing to si late user
With this co po able de ned, we can then hop up to the root A the ti a-
tion that we de ned and co pose the A the ti tio Co tent wit in our
theme block.
@Composable
fun Authentication() {
MaterialTheme {
AuthenticationContent(
modifier = Modifier.fillMaxWidth(),
authenticationState = ...,
handleEvent = ...
)
}
}
Things aren’t quite sa i ed here yet though, we need to provide both the state
and event han ler to our A the ti tio Co tent. We’ll be u ing our A then-
g ments, so we’ll rst need to r trieve an i stance of this. So that we can a cess
this i for tion, we’re g ing to need to o tain an i stance to this Vie Mo el in-
side of our co po able. Here we’ll use the vie Mo el fun tion from the l fe-
30
c
b
u
c
n
a
n
m
m
n
i
a
s
m
n
r
d
m
w
r
s
fi
d
fi
s
s
t
s
fi
o
r
k
fi
u
u
m
e
n
e
m
e
o
n
c
a
s
d
c
m
a
x
n
b
n
u
n
e
c
n
e
m
l
w
n
i
u
n
d
s
c
a
n
c
c
a
r
n
c
p
d
u
s
n
s
u
e
t
u
s
n
m
w
u
n
n
u
c
h
d
c
i
c
cycle-vie mo el-co pose pac age. This will r trieve an i stance of the d sired
We can then use this Vie Mo el wit in our root co po able for our r quired a gu-
ments.
pass the value of this emi sion as the state re e ence to our co po able.
- For event han ling, our Vie Mo el co tains a fun tion that matches the
r quir ments of our lambda fun tion a g ment. We can pass this func-
Event.
@Composable
fun Authentication() {
val viewModel: AuthenticationViewModel = viewModel()
MaterialTheme {
AuthenticationContent(
modifier = Modifier.fillMaxWidth(),
authenticationState =
viewModel.uiState.collectAsState().value,
handleEvent = viewModel::handleEvent
)
}
}
With this in place, we have a co po able fun tion that will house the co po ables
ma ing up our a the ti tion UI. As you might r me ber from when we cr ated
the A the ti tio State, there’s a lot to take into a count when it comes to the UI.
While there is plenty of space for there to be more co ple ity and co d tions to
31
e
k
w
u
e
f
d
f
r
n
r
w
c
a
e
l
d
d
t
r
u
n
c
t
n
s
m
c
w
s
a
d
w
w
m
m
c
k
d
d
h
s
s
r
l
r
n
u
s
f
m
c
r
x
s
e
e
m
c
w
c
s
m
s
m
d
n
x
m
s
e
m
n
i
s
e
e
r
think about in r gards to the state, we have plenty to think about here when it
comes to buil ing the a t al UI. When it comes to this, we’re g ing to need to
• A Pa ent Box to hold the di fe ent co po ables that make up our a the ti tion
screen
• An a the ti tion form co po able that will hold the i put elds, bu tons
32
o
r
h
u
n
a
n
c
d
a
e
c
m
t
n
c
m
g
u
s
m
i
f
s
r
s
u
p
d
m
n
c
s
a
s
t
a
r
s
n
fi
o
u
t
n
c
a
With the above in mind, we can start buil ing out our Co po able UI to rep-
re ent our a the ti tion screen. We’re g ing to build a co le tion of co po able
fun tions, all of which can be plugged t get er to cr ate the co plete screen.
33
s
c
u
n
c
a
o
o
d
h
e
m
l
c
s
m
m
s
D ing the Pa ent Co tai er
We b gin at the start of the above i lu tr tion, the pa ent Box co po able. Be-
cause our UI can show three di fe ent child co po ables, we need a co tai er to
house those. Ot er than the a the ti tion form, we’re g ing to be di pla ing
34
e
e
fi
n
o
h
n
c
t
f
u
r
r
n
l
c
a
s
a
m
n
s
s
r
n
o
n
m
s
n
s
n
y
ent) or an alert di log (which will be shown over the top of the a the ti tion
form), we need these to be placed in a co tai er to a low su port for these di fer-
ent sce ar os. The Box co po able provides su port for the alig ment of child
co po ables, as well as the abi ity to show ove la ping co po ables - which
// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier
) {
Box {
}
}
Here we’ve de ned an empty Box, which is enough to a low us to di play child
co po ables i side of it. Ho ever, we need to provide some pro e ties to have
the Box ll the e tire avai able space on-screen, while also provi ing alig ment for
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Here we use the fil Ma Size mo er to have the Box ll all of the avai able
space on the screen (both the width and height wit in its pa ent), along with u ing
the co ten lig ment a g ment to align all of its chi dren in the ce ter of the
Box.
35
m
m
n
l
s
s
n
fi
i
r
t
A
n
fi
n
n
a
s
l
l
x
m
r
u
w
s
l
d
i
fi
n
n
p
r
h
p
l
l
l
fi
r
p
m
d
p
n
s
u
r
s
n
n
n
c
a
l
s
f
Di play a Pr gress State
B fore we go ahead and start sho ing co tent to users, we’re g ing to think about
the state that o curs b for hand - which is when a loa ing i di a or will be dis-
played to users. We’re g ing to start here by u ing the i Loa ing pro erty from
our A the ti tio State re e ence. U ing this we’re either g ing to want to show a
36
e
s
u
n
c
a
c
n
e
o
o
f
e
r
w
s
n
s
d
s
o
n
d
o
c
t
p
pr gress i di a or or go on to di play co tent to the user. To handle these di fe ent
sce ar os, we’ll start by adding an if stat ment that checks the status of this loa ing
ag.
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
if (authenticationState.isLoading) {
} else {
}
}
With this in place, we can now d cide whet er to co pose the loa ing state or the
co tent state. Here we’re just g ing to tackle the loa ing state of our UI, so we’ll go
ahead and uti ise one of the avai able pr gress i di a or co po ables - the Cir-
c la Pr gres I di a or.
if (authenticationState.isLoading) {
CircularProgressIndicator()
} else {
itely while it is di played on the screen. This is ne for our r quir ments, as we’re
just g ing to show it on screen u til the co tent is loaded. If you need to di play a
sp ci c pr gress value on the i di a or, you can provide this pr gress value to the
gress.
37
fl
u
c
o
e
n
m
n
t
r
fi
o
i
s
o
n
o
m
c
l
t
s
s
n
c
t
n
o
n
c
e
t
o
n
e
s
n
l
m
c
n
t
e
n
o
n
n
h
m
c
t
r
fi
n
u
d
m
c
r
t
o
n
p
m
e
s
s
n
o
s
c
e
d
t
r
n
s
f
d
r
fi
Di pla ing the L gin Co tent
Even though now we have a pr gress i di a or in place for our l gin screen, this
isn’t som thing we’re g ing to be di pla ing u til the user has triggered the au-
the ti tion ow. So that we can have this be triggered, we’re now g ing to go
38
n
s
c
a
e
fl
y
o
o
o
s
n
y
c
t
n
n
o
o
ahead and build out the a the ti tion form UI. We’ll need to start here by cr a ing
a new co po able fun tion that will be used to house our a the ti tion form.
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
)
I side of this, we’re g ing to start here by d ing a Column co po able - our au-
is most a pr pr ate for this. When co po ing this Column, we’ll pass the Mo fi-
er re e ence that was provided to our A the ti tio Form co po able to ap-
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
}
}
Loo ing at the design of the screen, we’re also g ing to want all of the child com-
po ables to be p s tioned in the ce ter h r zon ally. For this we’ll uti ise the h ri-
zon a lig ment a g ment, provi ing Cente H r zon ally as the value to
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
39
n
s
n
k
t
f
c
l
a
r
A
p
m
o
n
s
n
i
o
i
n
o
o
r
c
u
u
n
p
c
a
s
n
d
m
r
u
o
s
r
e
i
m
fi
c
n
n
t
r
s
o
c
a
o
i
n
m
u
t
m
n
n
m
c
a
s
l
s
d
e
i
o
t
}
}
With this in place, we now have a Column that will be used to house the co tents
40
o
n
Adding the A the ti tion Title
We’ll start rst by adding the title for our a the ti tion form - this will di play a
hea er that will state that the user can either sign in or sign up, d pen ing on the
cu rent state of the screen. So that we can di play this title, we’ll need to b gin by
41
r
d
fi
u
g
n
c
u
s
fi
a
n
c
a
e
d
e
s
<string name="label_sign_in_to_account">
Sign In to your account
</string>
<string name="label_sign_up_for_account">
Sign Up for an account
</string>
We’re not g ing to use these just yet, but at least they’re now in place for when it
comes to slo ting them into our UI. So that we can start buil ing out our a the ti a-
tion title, we’ll need to cr ate a new co po able fun tion, A the ti tion-
Title.
// AuthenticationTitle.kt
@Composable
fun AuthenticationTitle(
modifier: Modifier = Modifier
)
And so that this knows what title needs to di play, it’s g ing to need to know the
cu rent A the ti tio Mode of the screen - which we’ll pass in as an a g ment
@Composable
fun AuthenticationTitle(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)
Next, we’ll build out a mi i al Text co po able u ing a string r trieved from one
of the pr v ously a ded string r sources. The A the ti tio Mode re e ence we
have d picts whet er the user is cu rently sig ing-in or sig ing-up. Based on this
mode we want to set the title of the screen, so it is clear to the user whet er they
are sig ing-in or sig ing-up. U ing the provided A the ti tio Mode we’ll set
@Composable
42
r
e
n
e
e
m
u
i
o
t
s
n
d
c
h
a
n
c
n
n
e
m
s
e
r
m
m
m
s
s
s
n
u
s
s
u
n
c
c
o
n
a
d
n
c
a
n
u
e
n
n
f
u
c
r
r
h
a
u
n
c
fun AuthenticationTitle(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Text(
text = stringResource(
if (authenticationMode == AuthenticationMode.SIGN_IN)
{
R.string.label_sign_in_to_account
} else {
R.string.label_sign_up_for_account
}
)
)
}
Our title will now be di pla ing a string based on the A the ti tio Mode that
43
c
s
y
u
n
c
a
n
B cause this Text co po able is the title of the screen, we’re g ing to want to style
the text a cor ing to the theme of our a pli tion. Here we’re g ing to a ply a
fon Size to our Text co po able that feels a bit more ting for a title. We’ll do
this by passing the value of 24.sp to the fon Size a g ment of the co po able.
Text(
text = stringResource(
if (authenticationState.authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.label_sign_in_to_account
} else {
R.string.label_sign_up_for_account
}
),
fontSize = 24.sp
44
e
t
c
d
m
s
m
s
p
t
c
a
r
u
fi
t
o
o
m
s
p
)
We’ll also a just the fon Weight of our co po able - this will help to make it
stand out a bit more at the top of our UI. We don’t want this to be too bold when
di played, so we’ll a ply the weight as the Fon Weight Black value.
Text(
text = stringResource(
if (authenticationState.authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.label_sign_in_to_account
} else {
R.string.label_sign_up_for_account
}
),
fontSize = 24.sp,
fontWeight = FontWeight.Black
)
45
s
d
p
t
m
t
s
.
Now that our title is styled, we can go ahead and co pose it wit in our UI. B fore
we go ahead and co pose it i side of our A the ti tio Form, we need to en-
sure there is a cess to an A the ti tio Mode re e ence that our title can use.
We’re g ing to be passing this from our state, so we’ll need to rst add this as an
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)
46
r
u
o
c
u
m
n
c
u
a
n
n
n
c
a
m
n
u
s
n
m
f
c
a
r
n
fi
h
e
Fo lowed by passing this into our A the ti tio Form fun tion at the point of
co po tion.
// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
...
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
authenticationMode =
authenticationState.authenticationMode
)
...
}
Ho ping back over to our A the ti tio Form co po able, we can then com-
pose our A the ti tio Title and pass in the r quired A the ti tion-
Mode re e ence that is now a ces ible from the a the ti tion form.
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
AuthenticationTitle(
authenticationMode = authenticationMode
)
}
}
47
m
l
p
s
f
i
r
u
n
c
a
n
u
c
s
n
u
c
a
n
n
c
a
u
n
n
m
e
c
a
s
c
u
n
c
a
To cr ate a bit of vis al sp cing at the top of our UI, we’ll also add a Spacer com-
po able, sized with a height of 32dp via the use of the height mo er.
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode
)
}
}
With this in place, the title for our a the ti tion form is now b ing co posed
wit in our UI - this title is then co posed based on the cu rent A the ti tion-
48
s
h
e
h
u
a
m
u
n
c
a
r
u
d
e
i
fi
n
c
m
a
49
Cr a ing the i put co tai er
With our title now in place, we can go ahead and start pu ting t get er the area
that will be used to hold the co po ents for the form i put area. As seen in the
end goal of our design, and the co po able stru ture in the di gram above, this is
50
e
t
n
m
m
n
s
n
c
n
n
t
a
o
h
all g ing to be co tained wit in a Card co po ent. The Card is a co po able
used to hold child co po ents in a Card shaped co tai er, o ten e e ated from
We’re still g ing to be wor ing wit in the A the ti tio Form co po able
that we pr v ously de ned, co tin ing co po tion from where we last a ded the
A the ti tio Title. B fore we add our Card, we’re g ing to start by adding
a ot er Spacer co po able, this time b neath our A the ti tio Title de-
cla tion - this is so that our Card does not end up pressed up right against the
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode
)
Spacer(modifier = Modifier.height(40.dp))
}
}
With this in place, we can now add the d cla tion for our Card co po able.
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
51
n
u
t
r
a
h
o
r
n
e
c
o
a
i
n
n
m
fi
s
m
s
k
n
e
h
n
u
h
e
m
e
u
r
m
a
s
i
n
n
c
a
n
u
n
n
o
n
f
c
a
m
m
l
s
n
v
m
d
s
s
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode
)
Spacer(modifier = Modifier.height(40.dp))
Card {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
}
}
}
}
The Card co po able only has one r quired a g ment, which is the co tent of
the co po able. U der the hood, the Card uti ises a Su face which in turn uses a
Box to co tain the co tent that we provide it with. Ho ever, we are g ing to be
stac ing co tent ve ti ally - som thing that we can’t achieve from r l ing on the
Box here. We’ll co pose a Column wit in the co tent of our Card co po able,
which will a low us to then co pose our chi dren i side of the Column, achie ing
the re ult of ve ti ally stacked co po ables. When adding this we’ll also a ply
some sty ing so that the Column has some pa ding a plied to it, along with align-
ing its chi dren h r zon ally in the ce ter via the use of its h r zon a lig ment
a g ment.
Next, we’re g ing to add some co straints to our Card u ing mo ers. As
seen in the design, we want the Card to ll the ma i um width of our screen, but
with some sp cing around the ou side so that it isn’t pres ing against the edges of
52
r
u
k
s
m
l
l
n
s
n
l
m
a
o
r
o
s
c
m
i
n
r
c
n
t
m
e
t
m
n
n
e
s
h
fi
l
d
l
r
n
u
n
x
m
p
r
w
s
o
s
i
t
d
e
l
i
fi
A
y
o
m
n
n
s
p
v
the screen. To ll the width of the area we’ll use the fil Ma Width mo er, along
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
}
}
Cu rently, our card won’t look like much, but we’d be able to see som thing like
53
r
l
w
d
fi
r
d
i
fi
p
o
i
t
d
l
x
m
s
d
e
i
fi
By d fault the Card uses an e e tion value of 1dp, which we can see doesn’t make
our card too vi ible on the su face bac ground. Here we’re g ing to i crease this
to 4dp by ove ri ing the d fault value by passing in our own value via the e e a-
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
We can see now that our Card has grea er pa ding a plied to it.
54
e
r
u
r
s
d
e
l
r
v
a
k
t
d
p
o
n
l
v
With this in place, we now have an area that is co gured for our form i put area.
At this point, our A the ti tio Form co po able should look som thing like
the fo lo ing.
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode
)
55
l
w
u
n
c
a
n
m
s
n
fi
e
n
Spacer(modifier = Modifier.height(40.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
}
}
}
}
56
Di pla ing the Email A dress I put
Field
Now that we have the Card in place to hold our i put form, we can now think
about mo ing ahead with adding the rst of our i put elds - the Email I put. We’ll
57
s
v
y
e
t
m
s
fi
l
d
n
n
n
fi
n
n
// EmailInput.kt
@Composable
fun EmailInput(
modifier: Modifier = Modifier
)
For the email i put, we’re g ing to e plore the use of the Tex Field co po able,
@Composable
fun EmailInput(
modifier: Modifier = Modifier
) {
TextField(modifier = modifier)
}
You’ll n tice at this point the IDE will give you a war ing, that’s b cause the Text-
Field r quires two a g ments for the fun tion to be sa i ed. These are the value
(that re re ents the cu rent value to be di played in the tex eld) and the value
change han ler (triggered when the i put changes and used to u date the com-
po able state). We’ll need to sa i fy these b fore we can co ti ue, so let’s start by
Wit in our a the ti tion state, we have an email a dress pro erty, which rep-
re ents the cu rently entered email a dress from the user. We’re g ing to need to
use this piece of state for our email i put eld - so we’ll need to pass it into our
Emai I put co po able and a sign it to the value a g ment of the Tex field.
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?
) {
TextField(
modifier = modifier,
value = email ?: ""
)
58
o
s
s
h
s
l
o
p
n
e
s
d
u
r
n
m
e
n
c
r
a
s
r
r
u
o
t
r
s
s
u
m
x
d
n
n
s
c
fi
s
e
c
d
n
r
t
u
s
fi
n
t
t
p
n
fi
e
o
p
m
t
s
}
When present, this will be shown as the cu rent i put value in our email a dress
eld and if this state changes, then the Tex Field value will be u dated to r ect
that change. Ho ever, we are not cu rently provi ing a way for our state to be
an a g ment in the form of a lambda fun tion that a lows us to hoist the latest
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit
)
We can then use this lambda to tri ger the state hois ing, provi ing a fun tion call
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email ?: "",
onValueChange = { email ->
onEmailChanged(email)
}
)
}
This means that our email a dress will be passed up and out of our co po able
fun tion, a lo ing us to u date the state of our screen when it comes to i ple-
59
fi
h
c
t
r
u
l
n
w
w
e
e
m
p
o
s
i
d
m
g
s
v
r
o
t
c
r
c
n
d
c
t
l
fi
d
p
m
m
c
d
e
m
e
s
fl
At this point, we have an email i put eld that is b ing di played i side of our
co po able fun tion. It’s very mi i al, but it’s enough to a low user i put.
At the m ment out i put eld looks a little blank, and it isn’t too clear what the in-
put eld is to be used for. To add some cla ity here, we’re g ing to uti ise the la-
bel a g ment to provide a co po able that will act as a l bel. We’re g ing to want
this to simply read “Email A dress”, so we’ll need to add a new string r source to
our pr ject:
60
m
fi
r
o
s
u
o
c
a
n
fi
d
m
n
n
m
s
fi
r
n
e
fi
a
s
l
o
n
n
o
l
e
<string name="label_email">Email Address</string>
We can then go ahead and co pose a Text co po able for the l bel of our
Tex Field, passing this string r source as the co tent of that co po able.
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email ?: "",
onValueChange = { email ->
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id = R.string.label_email)
)
}
)
}
With this in place, our Tex Field is now di pla ing a l bel that adds cla ity for
61
t
fi
t
m
e
s
n
y
m
s
a
m
s
a
r
E fo cing the su po ted nu ber of lines
If we have a little play with the i put eld, we might n tice that if we provide an in-
put that e ceeds the length of the i put eld, the text will start to e pand onto mul-
tiple lines.
62
n
r
x
p
n
r
n
fi
fi
m
o
x
We don’t want this as it’s not an e pe ted b h viour for this kind of form. We can
x this by uti ising the singl Line a g ment of the co po able, a lo ing us to
e force all entered text to r main on a single line - the co po able will a low h ri-
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email ?: "",
onValueChange = { email ->
63
fi
n
t
v
g
a
l
e
e
x
c
r
u
e
a
m
m
s
s
n
l
w
l
o
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id = R.string.label_email)
)
},
singleLine = true
)
}
We can see now that with this b ing e forced, our Tex Field b haves in a much
64
x
c
e
n
t
e
Adding some ico graphy
Our Email I put is fee ing good at this point, but we’re g ing to add a small piece
of vis al de o tion by uti ising the leadi con. This a g ment a lows us to
provide a co po able that will be di played at the start of the i put eld. For this,
we’re g ing to use an Icon co po able to di play an email icon - this won’t serve
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email,
onValueChange = { email ->
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id = R.string.label_email)
)
},
singleLine = true,
leadingIcon = {
Icon(
imageVector = Icons.Default.Email,
contentDescription = null
)
}
)
}
B cause this is just a vis al de o tion, we don’t need to provide a co ten De-
scri tion for our Icon. The email i put eld also has a l bel that d scribes the
pu pose of the eld, so we can rely on that for d scri ing the co po ent to the
65
e
r
p
u
o
r
n
c
m
r
a
fi
s
h
l
u
n
l
o
e
m
c
r
a
s
s
n
u
fi
n
c
s
g
r
I
a
e
b
o
a
r
u
n
m
fi
e
l
n
n
t
user. With this in place, we can now see an email icon is di played at the start of the
i put eld.
Now that our email i put is co plete, we can go ahead and co pose it wit in our
UI. B fore we go ahead and co pose it i side of our A the ti tio Form, we
need to e sure there is a cess to both an email and on mai Change re e ence
that the co po able can use. We’re g ing to be passing this from our state, so
we’ll need to rst add these as a g ments to our A the ti tio Form co pos-
able.
66
n
m
e
fi
n
m
s
fi
s
n
c
m
m
r
u
n
o
n
fi
u
n
u
s
E
c
a
n
l
m
c
n
a
n
f
h
m
r
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
onEmailChanged: (email: String) -> Unit
)
Along with then passing this into our A the ti tio Form fun tion at the point
where it is b ing co posed. For the email eld we can pass the email a dress ref-
e ence from wit in our screen state, while for the on mai Change we’ll need to
tri ger an A the ti tio Event by u ing our handl Event lambda. When
cal ing this, we’ll uti ise the Emai Changed event type - i sta t a ing a re e ence by
provi ing the email a dress that is passed through the cal back.
// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
...
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
authenticationMode =
authenticationState.authenticationMode,
email = authenticationState.email,
onEmailChanged = { email ->
handleEvent(
AuthenticationEvent.EmailChanged(email))
}
)
...
}
67
r
g
l
d
e
u
h
n
l
m
d
c
a
n
l
u
s
fi
n
c
a
E
n
n
l
e
l
n
i
t
c
d
f
r
Ho ping back over to our A the ti tio Form co po able, we can then com-
pose our Emai I put and pass in the r quired email and on mai Changed ref-
e ences that are now a ces ible from the a the ti tion form.
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
email: String?,
authenticationMode: AuthenticationMode,
onEmailChanged: (email: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode)
Spacer(modifier = Modifier.height(40.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email ?: "",
onEmailChanged = onEmailChanged
)
}
}
}
}
68
r
p
l
n
c
s
u
n
c
a
e
u
n
n
c
a
m
s
E
l
We’re g ing to make a small tweak here to cu to ise how the Emai I put is
co posed wit in our UI. When cr a ing the co po able, we a ded su port for an
o tio al mo er. We’re g ing to uti ise this a g ment so that we can force the
email i put eld to ll the avai able width, which is the width of the pa ent Column.
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email ?: "",
onEmailChanged = onEmailChanged
)
With this in place, the email i put eld for our a the ti tion form is now b ing
co posed wit in our UI - the co tent of this is then co posed based on the cur-
rent email that is wit in our state, which in turn is then u dated via the use of our
69
p
m
m
E
n
n
l
o
fi
d
i
h
h
fi
l
fi
h
o
l
l
n
c
x
n
e
fi
t
l
d
i
fi
m
r
s
u
u
s
m
n
m
c
p
a
d
r
p
l
n
e
70
Di pla ing the Pas word I put Field
Now that we have our email i put eld, we’re g ing to want to cr ate a si i ar
co po ent, e cept this time for the entry of a pas word. A lot of this co po ent is
start by d pli a ing what we have so far, a ap ing it for pas word entry.
71
o
m
s
n
u
c
y
x
t
n
e
i
fi
s
d
e
t
s
o
m
n
s
s
e
m
o
n
m
l
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String,
onPasswordChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = password,
onValueChange = {
onPasswordChanged(it)
},
singleLine = true,
label = {
Text(text = stringResource(id =
R.string.label_password)
)
}
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
}
)
}
time in the form of a pas word and o Pas wor Changed cal back.
- We now call this o Pas wor Changed whene er the pas word entry
has changed
- The leadi Ion for our co po able is uti ising the Lock icon
We are also u ing a slightly di fe ent l bel for our i put eld, which will need to be
72
d
n
s
g
g
n
s
f
s
r
f
e
f
m
r
r
d
s
r
n
a
fi
u
s
e
l
d
n
v
fi
n
m
n
l
s
s
<string name="label_password">Password</string>
At this point we’ll have a simple i put eld that can be used for our pas word:
Now that we have the si i a i ies i pl me ted, we can go ahead and think about
the things that make our i put eld slightly di fe ent from the email i put.
One co mon thing in pas word i put elds is the abi ity to toggle between vi i il-
i ies of the entered pas word. We’re g ing to i pl ment this for our Pas wor In-
put co po able, but to do so we’re g ing to need some form of state that a lows
73
t
g
m
m
s
s
s
m
n
s
l
r
t
fi
n
n
s
m
b
fi
o
e
o
fi
l
n
f
m
r
e
l
n
s
s
d
l
s
b
our co po able to know how the pas word eld should be co posed. We’ll need
to add a piece of mu able state to our co po able, i Pas wor Hi den. We’ll de-
fault this to false for s cu ity rea ons, fo lowed by wra ping this in r me ber so
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = password ?: "",
onValueChange = {
onPasswordChanged(it)
},
singleLine = true,
label = {
Text(text = stringResource(id =
R.string.label_password)
)
}
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
}
)
}
74
m
s
r
t
e
r
s
e
m
s
m
l
s
i
fi
s
s
p
s
m
d
d
e
m
💡 Not all pieces of our state need to be d clared at a glo al level. This pas word
vi i i ity state is sp ci c to this co po able fun tion, so cou ling this here is some-
With this state in place, we can now uti ise this to co pose our UI. The rst
thing we’re g ing to do is uti ise the traili con of the Tex Field co pos-
able. We’ll start by co po ing a new Icon, whose co tent will d pend on the cur-
rent state of i Pas wor Hi den When the pas word is b ing hi den we want to
show an icon that i di ates the vi i i ity is di abled, while on the ot er hand, we
want to i di ate that the pas word is cu rently vi ible. For now, we’ll use a null
co ten D scri tion, as we’ll be i pl men ing that piece of l gic shortly.
Icon(
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)
So that this icon is i trac able by the user, we’re g ing to want to e able click
events. U ing the clic able mo er we can toggle the i Pas wor Hi den
state so that when the icon is clicked, this state ag is ipped to the o po ite value.
Icon(
modifier = Modifier.clickable {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff
)
This means that now when our Icon is clicked, the state ag will be ipped and our
75
s
n
b
l
t
n
s
e
c
o
e
s
p
m
e
s
n
n
fi
m
c
k
d
t
s
d
e
l
s
fl
m
s
d
b
m
i
fi
l
s
e
l
r
e
s
t
n
fl
c
g
s
I
s
fl
o
n
m
fl
e
b
p
s
o
e
t
d
fl
s
p
h
n
s
d
fi
m
s
d
With this in place, we have a fun tio ing icon that can be i te a ted with by the
user. Ho ever, at this point are icon isn’t very a ces ible - the click event is in place
has no form of d scri tion, mea ing that a ces i i ity se vices will not be aware of
the pu pose of this co po ent. What we’ll do here is uti ise the o Clic L bel of
the clic able mo er so that we can provide a d scri tion based on the cu rent
i Pas wor Hi den state. We’ll need to start here by adding two new string re-
76
s
r
s
w
k
d
d
e
d
i
g
fi
p
m
n
e
n
c
n
c
c
s
b
l
s
e
p
l
r
n
r
n
c
k
a
r
With these in place, we can now a ply a l bel to our click mo er. We’ll uti ise
the stri R source co po able fun tion here to provide a string r source
Icon(
modifier = Modifier.clickable(
onClickLabel = if (isPasswordHidden) {
stringResource(id =
R.string.cd_show_password)
} else stringResource(id =
R.string.cd_hide_password)
) {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)
With this d scri tion a plied, we now have a co pleted Icon co po able that
can be slo ted into the traili con block of our Tex Field.
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String) -> Unit
) {
var isPasswordHidden by remember {
mutableStateOf(true)
}
TextField(
modifier = modifier,
value = password ?: "",
singleLine = true,
onValueChange = {
onPasswordChanged(it)
},
leadingIcon = {
77
n
t
g
e
e
p
r
p
m
n
s
g
I
p
c
a
m
t
d
i
fi
m
s
e
l
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
},
trailingIcon = {
Icon(
modifier = Modifier.clickable(
onClickLabel = if (isPasswordHidden) {
stringResource(id =
R.string.cd_show_password)
} else stringResource(id =
R.string.cd_hide_password)
) {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)
},
label = {
Text(text = stringResource(id =
R.string.label_password))
}
)
}
While we’ve i pl me ted the fun tio a ity to now toggle this state ag, it’s not be-
ing used yet to a fect the vi i i ity of the pas word eld co tent. For these sce ari-
os, the Tex Field co tains a vis a Tran for tion pro erty that can be used to
provide a class that can provide a tran for tion to the i put co tent. Co pose
comes with a pas word tran for tion out of the box in the form of the Pass-
wor Vis a Tran for tion class, which can be provided for the vis al-
78
d
s
u
t
m
l
a
m
f
e
s
s
n
n
p
m
a
s
b
s
l
u
m
c
l
a
n
s
l
s
m
s
a
m
a
fi
p
n
n
n
fl
m
u
n
Here we’re g ing to want to provide the Pas wor Vis a Tran for tion
when the pas word should be masked, and Vis a Tran for tion.None oth-
e wise.
visualTransformation = if (isPasswordHidden) {
PasswordVisualTransformation()
} else VisualTransformation.None
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String) -> Unit
) {
var isPasswordHidden by remember {
mutableStateOf(true)
}
TextField(
modifier = modifier,
value = password ?: "",
singleLine = true,
onValueChange = {
onPasswordChanged(it)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
},
trailingIcon = {
Icon(
modifier = Modifier.clickable(
onClickLabel = if (isPasswordHidden) {
stringResource(id =
R.string.cd_show_password)
} else stringResource(id =
R.string.cd_hide_password)
79
r
o
s
t
m
s
s
u
d
l
u
s
l
m
a
s
m
a
) {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)
},
label = {
Text(text = stringResource(id =
R.string.label_password))
}
)
}
When to gling this, we will now be able to see the pas word co tent i ping from
vi ible to masked.
80
s
g
s
n
fl
p
Now that our pas word i put is co plete, we can go ahead and co pose it wit in
our UI. B fore we go ahead and co pose it i side of our A the ti tio Form,
we need to e sure there is a cess to both a pas word and o Pas wor Changed
re e ence that the co po able can use. We’re g ing to be passing this from our
state, so we’ll need to rst add these as a g ments to our A the ti tio Form
co po able.
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
81
f
m
r
s
e
n
s
m
fi
n
s
c
m
m
r
u
n
s
o
u
u
n
n
n
s
m
c
c
a
a
d
n
n
h
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
)
Along with then passing this into our A the ti tio Form fun tion at the point
where it is b ing co posed. For the pas word we can pass the pas word re er-
ence from wit in our screen state, while for the o Pas wor Changed we’ll need to
tri ger an A the ti tio Event by u ing our handl Event lambda. When
cal ing this, we’ll uti ise the Pas wor Changed event type - i sta t a ing a re er-
ence by provi ing the email a dress that is passed through the cal back.
// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
...
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
authenticationMode =
authenticationState.authenticationMode,
email = authenticationState.email,
password = authenticationState.password,
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(
AuthenticationEvent.PasswordChanged(it))
}
)
...
}
82
g
l
u
e
h
d
n
l
m
c
a
n
d
s
d
u
s
s
n
n
c
a
s
n
e
d
n
c
l
n
i
s
t
f
f
Ho ping back over to our A the ti tio Form co po able, we can then com-
pose our Pas wor I put and pass in the r quired pas word and o Pas word-
Changed re e ences that are now a ces ible from the a the ti tion form. Here
we’ll also add a Spacer co po able so that there is some vis al space between
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode)
Spacer(modifier = Modifier.height(40.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email ?: "",
onEmailChanged = onEmailChanged
83
p
f
s
r
s
d
n
n
m
u
fi
s
n
c
c
a
s
n
e
m
s
u
s
n
u
c
a
n
s
)
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
password = password ?: "",
onPasswordChanged = onPasswordChanged
)
}
}
}
}
We’re g ing to make a small tweak here to cu to ise how the Pas wor I put is
co posed wit in our UI. When cr a ing the co po able, we a ded su port for an
o tio al mo er. We’re g ing to uti ise this a g ment so that we can force the
pas word i put eld to ll the avai able width, which is the width of the pa ent
Column. For this, we’ll uti ise the fil Ma Width mo er.
PasswordInput(
modifier = Modifier.fillMaxWidth(),
password = password ?: "",
onPasswordChanged = onPasswordChanged
)
With this in place, the pas word i put eld for our a the ti tion form is now be-
ing co posed wit in our UI - the co tent of this is then co posed based on the
cu rent pas word that is wit in our state, which in turn is then u dated via the use
84
p
r
m
s
n
m
o
n
n
s
d
s
i
h
fi
fi
h
d
l
fi
s
o
h
e
n
t
l
l
n
l
fi
x
c
s
m
r
m
u
s
d
u
i
fi
n
c
m
a
d
p
s
p
d
n
r
85
Han ling Ke board A tions
When i te ac ing with mu tiple i put elds, you can provide a good user e pe i-
ence by a lo ing the elds to be na i ated through by u ing bu tons that are
provided on the ke board. In A droid d ve o ment these have a ways been re-
ferred to as IME a tions, som thing that we still have a cess to in Je pack Com-
pose u der the name of Ke board O tions. In our a the ti tion screen we’re go-
tent
- Na i tion from the email a dress eld to the pas word eld
These o tions t get er will a low the user to na i ate and su mit the form, seam-
lessly co ple ing the a the ti tion ow without nee ing to sp ci ally i te act
with the UI co po ents via touch. To add these fun tio a i ies we’ll start by cus-
to ising the o tions provided by the email a dress Text Field. For this, we’ll use
the Ke board o tions class to sp cify the ke board type that is to be used for the
email i put eld. This means that if su po ted, our ke board will be laid out spe-
ci ally for email i put (sho ing the ‘@‘ sy bol and cli board o tions).
TextField(
…,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email
)
)
86
fi
m
c
b
v
s
y
n
n
g
n
p
a
m
d
m
s
r
l
a
fi
w
t
t
m
p
o
p
l
n
c
n
y
h
fi
u
y
l
y
w
y
n
l
e
d
c
a
n
n
e
e
p
p
fi
fl
fi
v
p
s
g
d
m
e
r
y
l
c
fi
d
p
l
v
g
u
s
c
p
y
d
x
o
n
c
n
fi
c
c
l
a
t
s
b
p
n
e
l
fi
t
c
t
n
x
r
r
Once the i put of the email a dress has been co pleted, the user is g ing to want
to co ti ue to the next i put eld for pas word entry. At this point we’re g ing to
want to a low this to be done u ing the ke board, so we’ll add an IME a tion to our
TextField(
…,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Email
)
)
We’ll be able to see now that the pr v ously shown ‘r turn’ icon has now been re-
87
y
n
n
l
n
p
n
d
fi
s
e
i
y
s
c
m
e
o
c
o
With this now di played wit in the ke board, we’re g ing to want to add some
form of han ling so that the i te a tion with the IME a tion tri gers an event for
the user. For this, we’re g ing to uti ise the ke boa A tions pro erty of our Text-
Field co po able, provi ing an i stance of the Ke boa A tions class to handle
the r quired IME a tion. This class a lows us to provide fun tion han lers for any
r quired ke board a tions, which in turn will be triggered when the co re pon ing
im A tion is i te a ted with. When the o Next a tion is i te a ted with, we’re
g ing to want to change the cu rent f cus on the screen - ta ing this from the cur-
rently f cused email a dress text eld, r ques ing f cus on the pas word text eld.
To i pl ment this b h viour we’re g ing to need to uti ise the F cus-
Requester class. This can be a signed to a co poser via a mo er, and then
88
e
o
e
m
e
c
o
e
m
y
d
s
n
s
r
c
c
c
d
e
a
o
d
h
n
r
s
fi
r
n
l
c
l
o
y
e
o
n
y
t
m
r
o
d
y
c
c
o
c
r
d
c
k
c
n
l
g
p
r
c
s
d
i
fi
d
r
o
s
fi
d
used els where to r quest the f cus on the gi en co po able. We’ll start here by
AuthenticationTitle(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode
)
Spacer(modifier = Modifier.height(40.dp))
val passwordFocusRequester = FocusRequester()
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) { ... }
With this de ned, we’re now g ing to need to a sign it to our pas word text eld.
This can be done u ing the f cu Requester mo er, which a lows for the Fo-
a signed to.
TextField(
…,
modifier = modifier.focusRequester(passwordFocusRequester)
)
With this F cu Requester now in place we’re g ing to need to tri ger the f cus
r quest, this is g ing to ha pen when the user i te acts with the next IME a tion
that we’ve e abled via the ke board o tions. While we’ve provided this o tion, we
haven’t provided any form of han ler for it - this is done u ing the ke boa Ac-
tions pro erty of the Tex Field. This pro erty takes a Ke boa A tions in-
stance, used to provide han lers for each of the avai able IME a tions. In our case
for the email a dress text eld, we’re just g ing to provide an i pl men tion for
TextField(
89
e
s
e
s
fi
n
n
e
o
p
n
fi
f
c
r
d
s
o
f
s
e
r
o
fi
p
t
d
y
o
s
o
o
d
s
p
e
o
p
v
s
n
o
o
d
r
i
fi
l
m
s
s
s
y
m
m
c
l
fi
s
r
e
g
d
s
y
c
p
t
a
r
d
c
fi
o
…,
keyboardActions = KeyboardActions(
onNext = {
}
)
)
At this point though, we don’t have an thing that we can tri ger from wit in this
o Next block. We could pass the f cus r quester re e ence into the Emai I put
co po able, but it’ll make for a clea er (and more tes able) co po able if we pass
this event up to the pa ent co po able. For this, we’ll add a new a g ment-less
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit,
onNextClicked: () -> Unit
)
Wit in the o Next block of our Ke boa A tions i stance we’re g ing to trig-
TextField(
…,
keyboardActions = KeyboardActions(
onNext = {
onNextClicked()
}
)
)
We’re now g ing to need to hop over to the pa ent co po able and i pl ment
this r quired o Nex Clicked a g ment. Wit in this i pl men tion, we’re then
g ing to use our F cu Requester to tri ger the r sues F cus() fun tion.
90
n
o
m
h
e
s
n
r
u
n
o
t
n
o
t
s
r
m
m
s
r
u
o
s
y
n
y
g
c
r
e
d
c
h
r
e
f
t
n
r
m
m
t
e
o
s
g
m
t
a
s
r
o
u
c
m
l
h
e
n
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
When this fun tion call is triggered, the f cus will be r que ted for our pas word
Tex Field, which is the co po able that our F cu Requester is a tached to. If
the r quest is su ces ful, this Tex Field will come into f cus, a lo ing the user to
move between the Email A dress and Pas word text elds without nee ing to
As well as u ing IME a tions to na i ate between i put elds, there is a range of
ot er events which can be handled. For e ample, we can also use them to su mit
forms that the user has entered data into - this is done in the form of the Done IME
A tion. This can be handled in a very si i ar way to the Next a tion that we just
handled, i stead of provi ing the Im A tion Done a tion type to our Ke boar-
O tions - a sig ing this to our Pas wor I put Tex Field.
TextField(
…,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
)
)
We’ll be able to see now that the pr v ously shown ‘r turn’ icon has now been re-
91
d
c
h
p
t
p
u
e
n
n
t
s
r
s
c
c
n
s
c
d
m
d
b
s
t
v
e
g
s
s
e
m
i
c
m
o
x
s
d
l
s
n
.
o
n
s
e
t
c
e
fi
o
fi
s
c
l
c
w
t
y
d
s
b
We then again need to provide a Ke boa A tions i pl men tion to our tex eld,
this time i pl men ing the o Done han ler. In most cases the pas word eld will
be vi ited after the email a dress eld, mea ing that the form will be co plete and
TextField(
…,
keyboardActions = KeyboardActions(
onDone = {
}
)
)
92
s
m
e
t
r
d
n
u
fi
n
y
c
d
r
d
n
c
e
m
n
e
t
a
s
m
fi
t
fi
At this point though, we don’t have an thing that we can tri ger from wit in this
o Done block. For this, we’ll add a new a g ment-less lambda a g ment to our
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String,
onPasswordChanged: (email: String) -> Unit,
onDoneClicked: () -> Unit
)
With that in mind, wit in this han ler, we’ll use our o Don Clicked() fun tion to
TextField(
…,
keyboardActions = KeyboardActions(
onDone = {
onDoneClicked()
handleEvent(AuthenticationEvent.Authenticate)
}
)
)
We’re now g ing to need to hop up to the pa ent co po able and i pl ment this
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onDoneClicked = {
93
e
n
g
m
s
n
o
l
e
c
h
r
u
d
y
r
u
r
m
n
s
e
g
r
m
u
e
c
h
)
Even though we’ve now i pl me ted this cal back, we want to tri ger the a then-
ti tion ow but we don’t have any way of tri ge ing that yet. To be able to achieve
this, we’ll need to add an a g ment to our A the ti tio Form co po able -
this is g ing to work in the same way as the email + pas word change han lers.
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String?,
password: String?,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onAuthenticate: () -> Unit
)
We’ll then also need to hop up again into the A the ti tio Co tent co pos-
able so that we can sa i fy this new r quired a g ment. At this point wit in the Au-
the ti tio Co tent.kt le, we have a cess to the handl Event fun tion -
we’re now g ing to uti ise this to tri ger the A the ti ate event.
// AuthenticationContent.kt
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
authenticationMode = authenticationState.authenticationMode,
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(AuthenticationEvent.Authenticate)
94
c
a
n
o
c
fl
a
o
n
n
t
l
s
m
r
e
fi
u
n
g
e
g
c
u
u
l
r
u
u
r
n
n
n
c
c
s
a
c
a
n
n
e
g
n
m
h
d
s
c
u
m
}
)
Ho ping back down into the A the ti tio Form.kt le, we can now uti ise
the onA thenti ate fun tion that is passed into our A the ti tio Form
co po able. Here we can di ectly pass this to our Pas wor I put co po able,
this is b cause the fun tion si n tures match - so there’s no need for us to re-im-
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onDoneClicked = onAuthenticate
)
Now, when the IME A tion for the pas word eld is pressed, the a the ti tion
ow will be triggered. One nal thing we can do here for the pas word eld is
for the Email A dress text eld, this will a low the ke board to be cu to ised
TextField(
…,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password
)
)
When co pared the email i put type that we de ned for our email a dress text
eld, we can see here now that the keys avai able on the ke board di fer slightly,
f cu ing on nu bers and le ters, r mo ing the use of some sp cial cha a ters and
95
fl
fi
o
e
p
m
p
s
s
e
u
m
p
y
m
l
d
c
c
c
n
c
t
fi
n
fi
r
g
u
a
e
y
e
n
v
s
c
a
p
l
l
fi
n
fi
n
s
y
fi
u
d
y
m
e
n
l
n
s
c
u
a
f
d
r
m
c
s
n
fi
n
m
c
s
a
l
96
Pas word R quir ments
At this point, our user can enter their cr de tials into our a the ti tion form.
When we de ned some of the bus ness l gic for our state, we de ned an enum,
Pas wor R quir ments. Wit in our state class, we also de ned a list of these
97
s
s
d
e
fi
e
e
h
e
i
o
e
n
fi
u
fi
n
c
a
Pas wor R quir ments, a lo ing us to e force ce tain r quir ments on the
This means that the pas word must co tain a ca i al le ter, a nu ber and be at
least 8 cha a ters long. While we have these r quir ments de ned, we need to let
the user know about them - so we’re g ing to cr ate a co po able that di plays
the r quir ments du ing sign-up, mar ing the r quir ments vis ally as sa i ed
We’re g ing to start here by loo ing at the co po able that will be used to re res-
ent each of the r quir ments that we’ve stated in our enum.
98
e
s
e
t
o
d
e
r
e
s
c
e
e
r
e
e
e
s
l
e
w
k
e
k
o
n
e
m
n
e
m
e
e
p
s
e
t
s
e
r
t
m
e
fi
s
u
m
e
s
t
p
s
fi
For this co po able, we’re g ing to di play a simple UI co po ent that will dis-
play a l bel for the r quir ment, along with an icon that will i di ate if this r quire-
ment has been sa i ed. We’ll start by cr a ing a new co po able fun tion that
@Composable
fun Requirement(
modifier: Modifier = Modifier
)
99
a
m
e
s
t
s
e
d
fi
i
fi
e
o
r
u
s
e
t
m
m
n
s
c
n
c
e
We me tioned above about the two r quir ments for this co po able fun tion -
the me sage to be di played, along with whet er the r quir ment is cu rently sa is-
ed. We’ll add more two a g ments - a string r source that will be used for the la-
bel of our r quir ment co po able, along with a boolean ag to su port the sa is-
ed status.
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
)
With these a g ments in place, we can start to build out the co tent of our com-
po able. Here we’ll add a Row co po able so that we can la out the chi dren h ri-
zon ally next to one a ot er, along with adding some mo ers to cu to ise the
b h viour + di play of co tent. We’ll chain from the mo f er that is passed into
the co po able fun tion, u ing the pa ding mo er to add some pa ding to our
co po able. We’ll also use the ve tica lig ment a g ment to align the chil-
dren in the ve ti al ce ter - b cause we’re sho ing an icon with a text l bel to the
side of it, we want these to be aligned on the Y axis which can be achieved via the
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier = modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
100
fi
fi
e
m
s
t
a
m
s
s
n
s
e
n
r
r
s
u
c
e
.
c
s
n
n
h
m
n
r
r
s
u
e
r
s
m
c
r
s
d
e
l
A
e
h
w
e
n
d
i
fi
e
d
r
i
fl
u
d
e
i
i
y
fi
m
n
s
p
d
r
s
a
l
m
c
o
t
t
}
We’ll now go ahead and add an Icon co po able which will be used to show a
Check icon u ing the Ico s.D fault.Check icon from the co pose pac age.
We’ll use the size mo er to x this size to 12.dp, along with se ting null as the
co tent d scri tion. While we will be u ing this to si n fy the cu rent status of the
r quir ment, we’re g ing to f cus on the a ces i i ity of this co po ent after the
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null
)
}
}
💡 It’s i por ant to r me ber that null should only be used in cases where a de-
scri tion is not a pli able for the co po able. This could be either b cause the
co po able is purely de o a ive, or b cause you are provi ing a d scri tion some
With our icon now in place, let’s add the l bel that will d scribe what the re-
quir ment is. For this, we’ll use the Text co po able and a sign the me sage
@Composable
101
e
h
n
m
p
e
d
a
e
s
m
e
m
t
s
p
s
p
e
c
o
d
i
fi
c
c
m
n
r
t
o
fi
r
e
u
r
m
e
s
m
s
m
a
c
s
m
s
s
b
s
l
g
i
e
d
r
s
m
r
t
m
e
e
n
p
e
s
k
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null
)
Text(
text = message
)
}
}
Cu rently, we’ll have som thing that looks like this co posed:
102
r
e
m
We can i prove the a pea ance of this slightly by ma ing two small tweaks. We’ll
rst add a Spacer co po able between the Icon and Text to cr ate some vis al
space (u ing the width mo er to set the width of this as 8dp). Next, we’ll over-
ride the d fault fon Size of the Text co po able - the text size is cu rently quite
big in the scree shot above, so we’ll use 12sp so that this feels a bit more in style
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
103
fi
s
m
e
n
t
m
p
s
r
d
i
fi
m
s
k
e
r
u
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = message),
fontSize = 12.sp
)
}
}
💡 When mod f ing the fon Size, it’s i por ant to not make the font too small - it
still needs to r main rea able for users. It can be hel ful in these cases to use the
t p graphy va ues from the theme to help pr vent these i sues from o cu ring.
We can see now, that things look a little bit be ter here.
104
y
o
e
l
i
y
d
t
m
t
e
t
p
s
c
r
Cu rently, we have this sa i fied ag wit in our co po able, but we’re not do-
ing an thing with it. We’re g ing to uti ise this here to vis ally re re ent the status
of the r quir ment - which we’ll do by u ing a co or for the icon and text based on
whet er the r quir ment is sa i ed. Here we’ll use the theme of our a pli tion to
cr ate a co or re e ence.
We use the primary co or in our sa i ed sce ario, as this will stand out to the user
du ing the a the ti tion pr cess. For the case where the pas word does not meet
the r quir ments, we copy the o Su face co or from our theme, mod f ing the
105
e
r
r
e
h
y
e
e
l
u
e
e
f
n
r
e
c
a
l
t
o
o
s
t
s
fi
n
t
fl
s
fi
r
l
s
h
n
l
l
m
u
s
s
p
s
p
i
c
y
a
a pha value so that the co or a pears slightly faded out in our UI. U ing this co or
re e ence, we can then a ply this to the Icon u ing the tint a g ment, along with
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val tint = if (satisfied) {
MaterialTheme.colors.onSurface
} else MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null,
tint = tint
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = message),
fontSize = 12.sp,
color = tint
)
}
}
With this in place, we now have a co po able that will di play a r quir ment,
along with the ing it to re re ent the cu rent state of the sa i ed ag.
106
p
l
f
r
y
m
a
s
p
p
l
s
p
l
m
r
r
u
s
s
t
s
s
fi
r
u
m
fl
s
s
e
e
l
I pro ing the r quir ment a ces i i ity
While we have the above co po able fun tion in place to co pose a r quire-
ment, we can i prove things here when it comes to the use of a ces i i ity ser-
vices. Cu rently, the l bel of the r quir ment will be read - but we are r l ing on a
very a ces ible, so we’ll a ply some mod tions to i prove things here. What
we’ll do is add a d scri tion to the r quir ment, so that when the a ces i i ity ser-
vice is d scri ing the el ment, it can be d scribed whet er or not the r quir ment
is cu rently sa i ed.
107
m
l
r
c
p
e
r
v
s
s
b
t
t
s
m
a
fi
e
a
p
e
e
g
p
i
m
e
e
s
h
e
e
e
e
i
fi
e
c
c
a
c
e
s
b
m
h
l
t
s
fi
m
c
c
e
e
s
s
y
b
b
l
l
e
e
We’ll start here by adding two new string r sources to our strin s.xml re-
source le.
<string name="password_requirement_satisfied">
%s, satisfied
</string>
<string name="password_requirement_needed">
%s, needed
</string>
We use %s as a plac hol er for a string that we will be r pl cing it with, this will be
the l bel for the r quir ment. This means that the a ces i i ity se vices will de-
scribe this as “At least 8 cha a ters, sa i ed” or “At least 8 cha a ters, needed”.
Wit in our R quir ment co po able we can now build our d scri tion by ac-
ces ing this r source via the use of the stri R source co po able fun tion.
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val requirementStatus = if (satisfied) {
stringResource(id =
R.string.password_requirement_satisfied, message)
} else {
stringResource(id =
R.string.password_requirement_not_satisfied, message)
}
}
You’ll n tice here that we pass the me sage a g ment from our co po able func-
tion as an a g ment to the stri R source fun tion. This is b cause we are us-
ing the %s plac hol er, so any a g ments that are provided here will be used as
the r plac ment. With this in place, we can now go ahead and a ply the se-
mantics mo er to our Row co po able. When d ing this we’ll want to set the
108
s
h
a
e
fi
o
e
r
e
e
d
u
i
fi
e
e
e
e
d
e
d
r
m
c
n
r
m
s
g
u
e
s
s
t
s
fi
e
n
g
r
e
u
c
o
c
e
a
s
m
b
l
e
s
e
r
m
g
c
r
p
p
s
c
merg De cen ants ag as true (as we don’t need the child co po ables to be
d scribed i d vid ally), along with se ting the text s mantics as the r quire-
men Status that we ge e ated above. This pro erty r quires the A no ated-
String type, so we’ll i sta t ate an i stance by provi ing our r quir ment-
Status.
Row(
modifier = Modifier.padding(6.dp)
.semantics(mergeDescendants = true) {
text = AnnotatedString(requirementStatus)
},
verticalAlignment = Alignment.CenterVertically
)
B cause we’re now se ting the s mantics for our Row and me ging the co tent of
the co tai er, we can go ahead and clear the s mantics on the child Text co pos-
able, u ing the clea An Se S mantics mo er to do so. This will avoid any de-
scri tions from b ing d pli ated with the s mantics tree.
Text(
modifier = Modifier.clearAndSetSemantics { },
text = message,
fontSize = 12.sp,
color = tint
)
With this in place, we now have an u dated co po able fun tion that has im-
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val tint = if (satisfied) {
MaterialTheme.colors.primary
109
e
e
p
t
e
n
s
c
s
n
n
s
i
d
b
e
l
u
r
fl
t
p
u
n
d
n
c
r
t
n
i
e
e
t
n
p
e
d
e
i
fi
p
m
s
e
d
e
r
c
m
e
s
n
e
t
e
n
m
} else MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
val requirementStatus = if (satisfied) {
stringResource(id =
R.string.password_requirement_satisfied, message)
} else {
stringResource(id =
R.string.password_requirement_not_satisfied, message)
}
Row(
modifier = Modifier.padding(6.dp)
.semantics(mergeDescendants = true) {
text = AnnotatedString(requirementStatus)
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null,
tint = tint
)
Spacer(modifier = Modifier.width(8.dp))
Text(
modifier = Modifier.clearAndSetSemantics { },
text = message,
fontSize = 12.sp,
color = tint
)
}
}
At this point, we now have a R quir ment co po able fun tion, but we’re not
buil ing these wit in our UI. We’ll start here by d ing a new co po able func-
tion, Pas wor R quir ments, that takes a list of sa i ed r quir ments - we have
110
d
d
s
d
e
h
e
e
e
e
e
m
e
fi
n
t
s
s
fi
e
c
e
m
s
this value wit in our state, so this will simply be provided from there when the time
Wit in this fun tion, we’ll also co pose a Column, as we’re g ing to be show-
@Composable
fun PasswordRequirements(
modifier: Modifier = Modifier,
satisfiedRequirements: List<PasswordRequirements>
) {
Column(modifier = modifier) {
}
}
Pas wor R quir ments enum, with each value ha ing a l bel pro erty in the
form of a string r source. That means that wit in this co po able we can simply
loop through the va ues of the Pas wor R quir ments enum, co po ing a Re-
quir ment u ing the cu rent item in the loop. Here we’ll use the l bel pro erty
to r trieve a string that can be passed for the me sage a g ment of the R quire-
ment, along with u ing the Pas wor R quir ments re e ence to check whet er
@Composable
fun PasswordRequirements(
modifier: Modifier = Modifier,
satisfiedRequirements: List<PasswordRequirements>
) {
Column(
modifier = modifier
) {
PasswordRequirements.values().forEach { requirement ->
Requirement(
message = stringResource(
id = requirement.label),
111
m
h
e
s
e
e
s
r
d
e
c
m
e
h
s
e
c
e
e
e
s
t
l
r
e
e
r
e
t
s
s
fi
m
s
d
m
e
d
n
s
e
m
e
h
s
o
e
v
r
f
m
u
r
o
a
s
e
i
m
a
p
s
e
fi
p
h
satisfied = satisfiedRequirements.contains(
requirement
)
)
}
}
}
With this, we can now co pose a co le tion of r quir ments and their cu rent sa is-
ed status.
112
fi
m
l
c
e
e
r
t
Co po ing the R quir ment items
With the Pas wor R quir ments co po able now in place, we’re g ing to want
to slot this into our UI. Ho ever, we’re only g ing to want to show this to the user
when they are sig ing up - a user who has signed in will know their pas word and
have a va id pas word, so this va i tion wit in the UI does not make too much
sense. We could simply co pose this based on our A the ti tio Mode re er-
ence wit in our screen state, but i stead, we’re g ing to uti ise the A i ate Vis-
i i ity co po able to a i ate our co po able in and out, based on its vi ible
ag. This means we could de ne, anA i ate Vi i i ity co po able, se ting
value, and provide our Pas wor R quir ments co po able as the co tent.
// AuthenticationForm.kt
AnimatedVisibility(
visible = authenticationMode == AuthenticationMode.SIGN_UP
) {
PasswordRequirements(...)
}
This would mean that when the cu rent A the ti tio Mode wit in our state is
not equal to SIGN_UP, the Pas wor R quir ments would a i ated out of view -
a i a ing into view when that state is toggled by the user (som thing that we have
While the above would work, we need to co pose it wit in our UI. For the Au-
the ti tio Form co po able fun tion, we already have a cess to an A then-
ti tio Mode re e ence - we’ll just need to add a new a g ment to the fun tion
//AuthenticationForm.kt
@Composable
113
fl
n
b
c
m
l
n
a
m
m
s
t
c
e
h
a
n
l
fl
s
m
n
s
n
s
s
s
n
d
f
r
e
m
s
w
m
n
s
e
s
e
s
m
d
fi
r
s
d
e
n
l
e
r
d
e
d
a
c
m
e
n
e
m
e
m
u
s
m
e
h
o
s
d
n
o
u
s
c
m
a
b
u
n
l
h
s
n
r
c
l
n
a
u
n
c
e
m
c
m
a
n
h
n
s
d
n
o
m
n
s
u
d
c
s
t
f
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
)
We’ll then also need to hop over to our A the ti tio Co tent.kt le to
mod fy the co po tion of our A the ti tio Form co po able. We’ll need to
pass a value for the co plete Pas wor R quir ments - the state re e ence that
we are u ing for the ot er a g ments here has the cu rent r quir ments wit in it,
// AuthenticationContent.kt
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
}
)
Hea ing back over to our A the ti tio Form co po able, we can now com-
pose our A i ate Vi i i ity and Pas wor R quir ments co po ables.
Here we’ll also add a Spacer co po able so that there is some vis al space
between UI co po ents.
114
d
i
s
n
m
m
m
s
n
i
d
m
h
s
b
u
r
l
d
u
u
n
m
s
c
n
a
s
d
c
a
e
n
s
u
n
e
d
n
e
m
c
r
a
m
s
e
e
n
s
n
e
f
m
r
u
s
fi
h
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))
AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}
}
115
With this in place, we now have our Pas wor R quir ments co posed wit in
our a the ti tion UI. When sig ing up, e te ing a pas word will now mod fy the
116
e
u
e
n
c
a
s
n
r
s
t
n
s
fi
r
d
e
e
e
s
e
m
i
h
Tri ge ing the A the ti tion ow
In our UI the user can now enter their email a dress and pas word, but they can’t
yet tri ger the a the ti tion ow to a low them to pr ceed in our app. To make
this po sible, we’re now g ing to add a bu ton that a lows them to pe form this au-
117
g
g
s
r
u
n
c
a
o
fl
u
l
t
n
d
c
a
l
o
s
fl
r
the ti tion ow u ing the cr de tials that they have entered into the form. While
this is po sible u ing the IME a tion that we a ded in the last se tion, a bu ton to
tri ger this ow would be e pe ted by a lot of users. We’ll start here by cr a ing a
new co po able fun tion, A the ti tio Bu ton with a d fault Mo f er ar-
g ment.
// AuthenticationButton.kt
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier
)
Next, we’ll add a Bu ton co po able, this r quires two of its pro e ties to be
provided - an o Click cal back han ler and a co po able that re re ents the
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier
) {
Button(
modifier = modifier
) {
}
}
As it is, this Bu ton isn’t too much use to us as it’s not sho ing or tri ge ing any-
thing. To change this we’ll add a co po able for the body of the bu ton, this will
re re ent either a “Sign in” or “Sign up” me sage, d pen ing on the cu rent au-
the ti tio Mode from our a the ti tion state re e ence. We’ll need to start by
118
u
p
g
n
n
s
c
c
a
a
m
s
n
fl
s
fl
t
t
n
s
s
c
t
e
x
l
u
e
u
m
c
c
n
n
n
s
m
c
a
c
d
a
s
n
s
t
d
e
t
f
m
r
e
s
d
w
e
c
p
g
t
p
d
r
i
s
r
r
e
i
t
t
<string name="action_sign_in">Sign In</string>
So that we know what one of these strings to show wit in our bu ton, the co pos-
able fun tion is g ing to need to know what A the ti tio Mode is cu rently
s le ted. For this, we’re g ing to need to pass this into our co po able fun tion.
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)
Next, we’ll use an if stat ment check to handle what the me sage should be de-
pen ing on the state, then set this as the co tent of a Text co po able wit in the
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Button(
modifier = modifier
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_sign_in
} else {
R.string.action_sign_up
}
)
)
}
}
With this Text in place, we’ll now be able to see a bu ton on-screen whose body
re re ents the cu rent a the ti tion mode that is set wit in our state.
119
e
p
d
c
s
c
t
r
o
u
e
o
n
c
a
n
u
n
h
t
h
c
a
m
s
m
n
t
s
s
c
h
r
m
At this point, we can now think about han ling the o Click tri ger from our But-
ton. What we want to do here is tri ger an event that will start the a the ti tion
ow - si i ar to how else we have handled events in this pr ject, we’re g ing to al-
low the pa ent co po able to handle this event for us. With this in mind, we’ll add
a lambda fun tion a g ment to our co po able fun tion, onA thenti ate. We’ll
then want to tri ger this lambda wit in the o Click a g ment of our Bu ton
co po able.
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
onAuthenticate: () -> Unit
) {
120
fl
m
s
m
l
r
c
g
m
r
u
s
g
h
m
d
s
n
c
n
r
o
u
u
g
u
c
o
n
c
t
a
Button(
modifier = modifier,
onClick = {
onAuthenticate()
}
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_sign_in
} else {
R.string.action_sign_up
}
)
)
}
}
Now when our Bu ton is pressed, the a the ti tion ow will be triggered and the
cr de tials from our Tex Fields will be used du ing this pr cess. Ho ever, these
elds might not a ways co tain va id data - we have an e abl A thenti tion
pro erty wit in our a the ti tion state re e ence. While this only checks if both
the email a dress and pas word are not empty, this helps to avoid the ow b ing
triggered when data might not have been entered yet. We can handle this via the
Bu ton by di abling the bu ton from b ing i te a ted with when the form co tent is
not va id. For this we’ll use the e abled pro erty of the Bu ton, a sig ing theen-
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
enableAuthentication: Boolean,
onAuthenticate: () -> Unit
) {
Button(
modifier = modifier,
121
fi
e
t
p
e
n
l
u
d
s
h
c
a
l
t
u
t
n
n
t
s
c
a
n
l
e
u
n
f
n
p
r
c
r
a
c
r
fl
n
t
o
e
u
s
w
n
fl
c
n
a
e
onClick = {
onAuthenticate()
},
enabled = enableAuthentication
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_sign_in
} else {
R.string.action_sign_up
}
)
)
}
}
122
Now that our bu ton co po able is i pl me ted, we can go ahead and co pose
it wit in our A the ti tio Form co po able. We’ll need to start by adding a
new a g ment to our A the ti tio Form co po able, this will be e abl Au-
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
enableAuthentication: Boolean,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
123
h
r
c
u
a
u
t
n
u
c
m
a
s
n
n
c
a
m
n
m
e
s
n
m
s
n
m
e
)
We’ll then also need to hop over to our A the ti tio Co tent.kt le to
mod fy the co po tion of our A the ti tio Form co po able. We’ll need to
pass a value for the e abl A thenti tion - our state re e ence that we are
u ing for the ot er a g ments here has the cu rent e abled state wit in it, so we’ll
// AuthenticationContent.kt
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
enableAuthentication = authenticationState.isFormValid(),
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(AuthenticationEvent.Authenticate)
}
)
Hea ing back over to our A the ti tio Form co po able, we can now com-
pose our A the ti tio Bu ton. Here we’ll also add a Spacer co po able so
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
124
s
d
i
u
m
h
n
s
i
c
r
u
a
n
u
n
e
u
u
t
u
n
c
n
a
c
a
c
a
n
m
u
r
n
n
n
n
m
c
a
m
s
n
s
f
r
n
h
m
s
fi
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))
AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}
Spacer(modifier = Modifier.height(12.dp))
AuthenticationButton(
enableAuthentication = enableAuthentication,
authenticationMode = authenticationMode,
onAuthenticate = onAuthenticate
)
125
}
}
With this in place, our A the ti tio Bu ton is now b ing co posed i side of
our a the ti tion form - this now a lows the user to tri ger the a the ti tion ow.
126
u
n
c
a
u
n
c
a
l
n
t
g
e
u
m
n
c
a
n
fl
To gling the A the ti tion mode
With all of the above in place, our user can pe form a the ti tion u ing the
provided elds. Ho ever, the UI only cu rently su ports the d fault a the ti tion
type - which is cu rently co gured to be sign in. This means that if the user does
127
g
fi
r
w
n
fi
u
r
n
c
a
p
r
u
e
n
c
a
u
n
s
c
a
not cu rently have an a count, they won’t be able to cr ate one u ing our a then-
ti tion form. In this se tion we’re g ing to go ahead and de ne a Bu ton that al-
lows the user to toggle between sign up and sign in, r co po ing our a the ti a-
tion form to r ect the cu rent mode. We’ll start here by cr a ing a new co pos-
able fun tion, Toggl A thenti tio Mode with a d fault Mo f er a gu-
ment.
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
)
With this co po able fun tion in place, we can build out the co tent r quired to
di play our toggle bu ton. We’re g ing to start by d ing the use of a Su face,
which act as the co tai er for our toggle co po able. The Su face co po able
will co pose the provided body i side of a Box co po able. It will also theme it-
self use the su face co or from the a pli tion theme, which is what we want to
be a plied to our se tings item in terms of sty ing. This saves us from u ing a Box
co po able and a pl ing a co le tion of sty ing ourselves when this co po ent
already e ists to do it for us. When co po ing this, we’ll a ply the mo er from
the a g ment of our co po able fun tion, along with ove ri ing the d fault e ev-
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
elevation = 8.dp
) {
}
}
128
a
c
s
a
m
p
r
r
m
u
s
c
x
m
e
fl
r
r
s
p
n
t
e
t
y
c
m
c
n
u
l
c
r
s
l
c
n
c
a
o
o
c
p
m
n
c
s
a
m
l
l
s
m
e
fi
e
n
e
s
r
m
e
d
p
e
fi
t
s
r
n
s
d
i
e
t
s
e
d
i
m
u
i
m
fi
r
u
m
n
s
l
n
r
c
Next, we’re g ing to add a bu ton to our Su face, and for this, we will use the
pac age which a lows us to co pose a at bu ton that di plays some co po able
co tent for its body. When co po ing a Tex Bu ton we will need to provide an
o Click han ler and some co tent body to be di played wit in the co po able
bu ton
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier
.padding(top = 16.dp),
elevation = 8.dp
) {
TextButton(
onClick = {
}
) {
}
}
}
Now that we have the co po able de ned, we need to go ahead and po late
these pro e ties of the co po able. B fore we add this co po able, we’ll go
ahead and add some a d tio al r sources to be used for the body of the co pos-
ables.
<string name="action_need_account">
Need an account?
</string>
<string name="action_already_have_account">
Already have an account?
</string>
129
n
n
t
k
t
t
p
r
d
o
m
l
s
d
m
i
m
n
m
s
m
n
t
s
e
s
m
fi
fl
e
s
t
r
t
t
s
s
h
m
m
s
m
m
m
p
a
s
s
u
r
i
Next, we’ll use the co tent pro erty of the Tex Bu ton so that we can see some
form of vis al re ult on our screen. Now we can add a Text co po able to the
body of our Tex Bu ton, this will di play the co tent of a string r source, de-
pen ing on the cu rent a the ti tion mode re re e ted by our state. B fore we
can do this though, we’ll need to know the A the ti tio Mode which should
be used when co po ing this co po able. We’ll add this as an a g ment for our
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)
With this in place, we can now uti ise this to co pose the bu ton co tent. I side of
our bu ton, we’re g ing to co pose a Text co po able, se ting the co tent
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Surface(
modifier = modifier
.padding(top = 16.dp),
elevation = 8.dp
) {
TextButton(
onClick = {
}
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
130
m
d
s
t
u
c
s
t
m
r
o
t
n
s
u
u
n
n
p
m
c
l
a
m
c
a
s
s
n
u
m
t
p
m
n
n
s
t
n
c
s
a
r
t
n
t
m
r
n
u
e
s
e
n
n
R.string.action_need_account
} else {
R.string.action_already_have_account
}
)
)
}
}
}
F nally, we use need to handle the o Click cal back for our Tex Bu ton. When
the bu ton is clicked we want to tri ger an event that will change the a the ti tion
mode for the state of our screen. - this will be an event in the form of Toggl Au-
thenti tio Mode, which we cr ated in an earl er se tion. Again we’ll want to
pass this event up to the pa ent, so we’ll need to add a new lambda a g ment to
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
toggleAuthentication: () -> Unit
)
When this event is triggered, our Vie Mo el will ip the value of our a the ti a-
tion mode, a lo ing us to switch between the sign in and sign up state. So this will
o cur, we’ll go ahead and tri ger this wit in the o Click han ler for our Text-
Bu ton co po able.
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
toggleAuthentication: () -> Unit
) {
Surface(
modifier = modifier
.padding(top = 16.dp),
131
i
c
t
m
t
c
a
m
s
l
n
w
s
c
r
g
g
e
n
w
h
d
l
fl
i
n
c
d
t
u
t
r
u
u
n
c
n
e
a
c
elevation = 8.dp
) {
TextButton(
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(8.dp),
onClick = {
toggleAuthentication()
}
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_need_account
} else {
R.string.action_already_have_account
}
)
)
}
}
}
Wit in our UI, this toggle bu ton was pushed right to the bo tom of the screen -
this isn’t som thing that can be co gured wit in the pro e ties of the pa ent
132
h
n
e
n
t
n
fi
h
p
t
r
r
Co po ing the Toggle Bu ton
Now that our bu ton is i pl me ted, we can go ahead and co pose it wit in our
A the ti tio Form co po able. We’ll need to start by adding a new a gu-
ment to our A the ti tio Form co po able, this will be o Toggl Mode in
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
133
u
m
n
c
s
a
u
t
n
n
c
m
a
c
m
e
n
s
n
m
t
s
m
n
e
h
r
completedPasswordRequirements: List<PasswordRequirements>,
enableAuthentication: Boolean,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onToggleMode: () -> Unit
)
We’ll then also need to hop over to our A the ti tio Co tent.kt le to
mod fy the co po tion of our A the ti tio Form co po able. We’ll need to
pass an a g ment for the o Toggl Mode - for this, we’ll i pl ment a lambda func-
tion that will be used to tri ger handl Event. For this call, we’re g ing to need to
pass an A the ti tio Event, which we’ll do so in the form of the Toggl Au-
thenti tio Mode type. When this is triggered and handled by our Vie Mo el,
the cu rent A the ti tio Mode will be toggled to the o po ite value and emit-
// AuthenticationContent.kt
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
enableAuthentication = authenticationState.isFormValid(),
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(AuthenticationEvent.Authenticate)
},
onToggleMode = {
handleEvent(
134
i
r
c
a
r
u
u
u
m
n
n
n
s
c
i
a
c
a
n
g
n
n
u
e
n
e
c
a
u
n
n
c
a
m
m
p
e
n
s
s
n
o
w
fi
e
d
AuthenticationEvent.ToggleAuthenticationMode)
}
)
Hea ing back over to our A the ti tio Form co po able, we can now com-
pose our A the ti tio Bu ton u ing the e is ing a the ti tio Mode ref-
e ence, along with the now provided o Toggl Mode lambda fun tion.
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onToggleMode: () -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))
135
r
d
u
n
c
a
n
u
t
n
c
s
n
a
n
e
x
t
m
u
s
n
c
c
a
n
AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}
Spacer(modifier = Modifier.height(12.dp))
AuthenticationButton(
enableAuthentication = enableAuthentication,
authenticationMode = authenticationMode,
onAuthenticate = onAuthenticate
)
ToggleAuthenticationMode(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode,
toggleAuthentication = {
onToggleMode()
}
)
}
}
We’ll n tice here now that our Toggl A thenti tio Mode co po able is
136
o
t
u
n
e
u
c
a
n
c
a
t
n
m
s
m
s
As per the design, we want the Toggl A thenti tio Mode to be pushed
against the bo tom of the pa ent co po able. Between these bu tons we e sen-
tially want a large amount of white space - this white spice needs to ll the avail-
able space between these two bu tons, which in e fect will push our toggle bu ton
to the bo tom of our UI. For this, we’re still g ing to use the Spacer co po able for
cr a ing space between these two co po ables, with the a d tion of the weight
mo er.
Spacer(modifier = Modifier.weight(1f))
A pl ing a weight of 1f to this Spacer will cause it to take up all of the avai able
height to it wit in the Column - which in this case will be all of the space between
the A the ti ate and toggle bu tons. We are not a pl ing any weigh ing to any
137
p
e
d
t
i
y
fi
u
t
n
c
h
t
r
t
t
m
m
s
e
s
o
u
f
p
c
a
y
n
d
i
t
m
fi
t
s
s
l
t
ot er co po ables so that the r mai ing weight here does not need to be di trib-
uted between mu tiple co po ables. If we were to a sign some weigh ing to one
of those bu tons also, we would see a di fe ent re ult as the weight would b come
di tri uted. But b cause we are only a sig ing weight here to the Spacer co pos-
// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onToggleMode: () -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))
138
s
h
b
m
t
s
e
l
l
m
s
e
t
n
s
f
n
r
s
s
t
e
m
s
AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}
Spacer(modifier = Modifier.height(12.dp))
AuthenticationButton(
enableAuthentication = enableAuthentication,
authenticationMode = authenticationMode,
onAuthenticate = onAuthenticate
)
Spacer(modifier = Modifier.weight(1f))
ToggleAuthenticationMode(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode,
toggleAuthentication = {
onToggleMode()
}
)
}
}
With this change, we can now see the toggle bu ton is pushed to the bo tom of the
pa ent co po able, via the u age of the Spacer co po able with its a signed
weight.
139
r
m
s
s
t
m
s
t
s
With these co po ables now a ded, our a the ti tion form is fun tio ally com-
plete - o fe ing a way for users to both sign up and sign in to our a pli a-
tion.
140
f
r
m
s
d
u
n
c
a
c
n
p
c
Di pla ing A the ti tion E rors
With everything we’ve done pr v ously in this se tion, the user can enter some cre-
de tials and pe form either a sign-in or sign-up o e tion. While this o e tion can
su ceed and a low the user to co ti ue into our a pli tion, som times that re-
141
c
n
s
y
l
r
u
e
i
n
n
n
c
a
c
p
r
p
a
c
a
r
e
p
r
a
quest may fail. In these cases, we’ll want to r ect this state to the user so they
know som thing has gone wrong. Our a the ti tion state has an e ror pro erty
wit in it, so we’ll be able to use this pro erty to di play an alert di log to the user.
Wit in our A the ti tio State class there is an e ror pro erty, which is
used to d pict that an e ror has o curred du ing the a the ti tion pr cess. While
this e ror might be set du ing the a the ti tion ow, we’re not cu rently uti ising
this wit in our UI to co m ni ate this to the user. To handle this sce ario, we’re
ing to take this e ror value and co pose it wit in our UI.
// AuthenticationErrorDialog.kt
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
)
Wit in this co po able fun tion, we’re then g ing to co pose an Aler Di log.
There are two Aler Di log co po able fun tions avai able, we’re g ing to use
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
},
confirmButton = {
},
142
o
h
h
h
r
h
e
e
e
u
m
r
n
s
l
t
w
c
a
m
r
a
m
r
u
c
n
s
c
m
c
r
m
u
u
s
u
p
n
u
n
c
r
a
n
e
o
c
c
h
fl
a
c
s
a
fl
n
u
r
m
l
r
n
r
c
a
a
p
a
r
r
o
n
o
t
a
l
p
title = {
},
text = {
}
)
}
As we can see here, there are a co le tion of a g ments that we need to provide to
missed, which we’ll need to use to tri ger an u date to our state so that
- text: the co po able to be used for the co tent body of the di log
With that in mind, let’s start buil ing out the co po ables for each of these a gu-
ments. We’ll start with the title, which we’ll need to start by adding a new string to
<string name="error_title">Whoops</string>
Then u ing the stri R source co po able fun tion, we’ll co pose a Text
co po able provi ing this string for the text a g ment. B cause we’re wor ing
with a title here, we’ll ove ride the d fault fon Size of the co po able and as-
sign a value of 18sp - this is so that is styled la ger than the me sage of the di log.
// AuthenticationErrorDialog.kt
143
fi
m
n
a
d
m
fi
s
s
i
s
fi
m
e
g
s
s
s
t
m
m
m
d
s
s
m
s
e
c
p
n
g
m
a
n
e
r
fi
s
d
l
l
c
g
e
m
n
p
n
s
s
n
r
r
a
p
t
m
u
r
u
s
s
c
e
a
n
t
s
e
s
m
a
m
s
a
k
r
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
},
confirmButton = {
},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
}
)
}
At this point, we can see this text b ing used for the title of our di log.
144
e
a
Next, we’ll use a ot er Text co po able, but this time for the text a g ment of
the Aler Di log co po able. Here we will simply provide the e ror that is
passed to our co po able fun tion for the text a g ment of the co po able.
// AuthenticationErrorDialog.kt
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
},
145
t
a
n
m
h
s
m
s
c
m
s
r
u
m
r
r
s
u
confirmButton = {
},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
Text(
text = error
)
}
)
}
With this in place, we can now see the title of our Aler Di log b ing a co pan-
146
r
s
t
a
e
c
m
Next, we need to co pose the bu ton that will be used to di miss the Aler Dia-
log - this will be provided u ing the bu ton a g ment of our co po able. We’ll
rst start by co po ing a Tex Bu ton to be used for this a tion. This a lows us to
co pose a at bu ton that is vis ally re re e ted as some text. The di fe ence with
this, when co pared to a Text co po able, is that the Tex Bu ton has the ex-
pe ted touch ta get si ing of an i trac able co po ent, ma ing it a ces ible to all
of our users.
There are two r quired a g ments for the Tex Bu ton - the o Click cal back
and the co tent, which is used to provide the co po able body of our bu ton.
TextButton(
onClick = {
147
fi
m
c
n
fl
m
m
e
r
t
s
m
z
r
u
s
t
u
n
t
t
m
t
p
s
t
s
n
t
m
r
m
t
u
n
s
k
c
s
t
n
t
m
c
f
s
l
r
s
l
t
t
}
) {
We’ll start by co po ing the body of our bu ton, for which we’ll need to add a new
<string name="error_action">OK</string>
We can then use this to co pose a Text co po able for the co tent of our but-
ton.
TextButton(
onClick = {
}
) {
Text(text = stringResource(id = R.string.error_action))
}
Next up we’ll need to add some a tion to our o Click cal back. When our bu ton
is clicked we’re simply g ing to want to di miss the di log, so we’ll need to com-
m ni ate this a tion back up to the pa ent so that our state can be u dated and in
turn, the di log will no longer be co posed. For this, we’ll need to add a new
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
)
With this lambda in place, we can now tri ger this from wit in the o Click of our
// AuthenticationErrorDialog.kt
148
e
u
t
c
t
r
u
a
c
m
m
s
s
g
o
m
m
fi
s
c
m
r
c
g
s
t
m
n
s
a
l
h
n
n
p
t
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
},
confirmButton = {
TextButton(
onClick = {
dismissError()
}
) {
Text(text = stringResource(
id = R.string.error_action))
}
},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
Text(
text = error
)
}
)
}
At this point, we’ll now have a vis ally co plete e ror di log that di plays the title,
149
s
c
u
m
r
a
s
The last thing to do here is to add some i pl men tion to the onDi mi s-
Request lambda body of the Aler Di log. In the pr v ous step, we a ded the
di mi E ror a g ment to our co po able fun tion, so we’ll now want to trig-
// AuthenticationErrorDialog.kt
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
150
s
s
s
r
h
r
u
s
s
s
t
m
a
s
m
e
c
t
e
a
i
d
s
s
dismissError()
},
buttons = {
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
TextButton(
onClick = {
dismissError()
}
) {
Text(text = stringResource(
id = R.string.error_action))
}
}
},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
Text(
text = error
)
}
)
}
With the co po able in place, we’ll now want to co pose this wit in our UI. One
thing to note here is that we want to co d tio ally co pose this, so it will only be
co posed when there is an e ror present wit in our state. When pe for ing com-
wit in a ko lin let block, a lo ing us to a sert the nu la i ity of the e ror pro erty.
151
m
s
h
i
t
m
s
l
w
r
m
s
i
n
s
i
h
n
u
m
l
m
b
n
l
c
a
n
h
r
r
r
m
r
p
a
This way the block will only be run if e ror is not null, so our co po able will only
// AuthenticationContent.kt
We can slot this into our A the ti tio Co tent co po able, provi ing the
e ror from our a the ti tion state for the e ror a g ment, along with i ple-
men ing the di mi E ror lambda. Wit in this lambda we’ll want to uti ise the
handl Event lambda that is provided to our co po able fun tion, u ing the Er-
ro ismissed A the ti tio Er or to tri ger our state b ing u dated and
r mo ing the e ror (and in turn, the alert di log will no longer be co posed).
// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
if (authenticationState.isLoading) {
CircularProgressIndicator()
} else {
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
152
e
r
r
D
t
v
e
m
r
s
u
u
s
s
n
n
r
c
a
c
u
a
n
n
c
r
r
a
h
a
n
g
r
n
m
s
r
u
m
s
c
m
e
m
s
s
p
d
l
m
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
enableAuthentication =
authenticationState.isFormValid(),
onEmailChanged = {
handleEvent(
AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(
AuthenticationEvent
.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(
AuthenticationEvent.Authenticate)
},
onToggleMode = {
handleEvent(AuthenticationEvent
.ToggleAuthenticationMode)
}
)
authenticationState.error?.let { error ->
AuthenticationErrorDialog(
error = error,
dismissError = {
handleEvent(
AuthenticationEvent.ErrorDismissed)
}
)
}
}
}
}
At this point we now have an Aler Di log that will be co posed wit in our UI
whene er the state re re ents an e ror state, a lo ing us to co m ni ate any au-
153
n
c
v
a
r
p
s
r
t
a
l
w
m
m
u
c
h
154
Wra ping Up
Throug out this pr ject, we’ve now fully i pl me ted our a the ti tion fe ture -
a lo ing our users to sign-up or sign-in to our a pli tion, with the abi ity to toggle
between the two a the ti tion modes. With this to gling, we’ve been able to ex-
155
l
w
h
p
u
o
n
c
a
m
e
p
n
c
g
a
u
n
c
a
l
a
plore the co po tion of di fe ent states and co po ents wit in our UI, on top of
With all of this in place, we’ll want to e sure these co po ents r main fun tio al
wit in our app. In the next chapter, we’re g ing to e plore wri ing aut mated UI
156
h
n
e
m
m
s
i
s
f
r
n
o
u
m
n
n
x
m
c
a
n
h
t
e
o
c
n
Testing the A the ti tion UI
Now that we’ve built our A the ti tion screen, we’re g ing to take a look at how
we can write tests for our co po ables. We’re g ing to be wri ing some i stru-
men tion tests u ing the co pose ui-test-j nit pac age - a lo ing us to ver fy that
B fore we can get sta ted with our tests, we’re g ing to need to add a couple
androidTestImplementation(
"androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation(
"androidx.compose.ui:ui-test-manifest:$compose_version")
We’re also g ing to need to add mocks to our test - this a lows us to ea ily provide
mock re e ences to any liste ers that are provided to our co po able fun tions, al-
androidTestImplementation(
"org.mockito.kotlin:mockito-kotlin:3.2.0")
androidTestImplementation("org.mockito:mockito-android:3.12.4")
With these in place, we now have a cess to the r quired rules and fun tio a ity that
a low us to test our co po able UI. Ho ever, alon side these d pen e cies, we’re
also g ing to need to add some rules to our build gradle le that will x some
of the co pi tion e rors that we’d cu rently see when tr ing to run our tests. Here
we’ll add some pac agi O tions that will e clude ce tain pac ages from the
a ded d pen e cies. We won’t dive too much into this concept and it’s us ally de-
pen ant on the ve sions of d pen e cies that are b ing used, so this may be re-
157
l
d
e
w
d
d
t
a
o
m
f
e
m
e
r
s
fi
o
l
a
d
s
e
n
s
r
r
k
d
m
i
r
s
n
p
n
s
g
u
n
m
t
e
p
m
n
c
s
d
c
a
r
u
n
r
o
w
c
n
u
e
x
o
n
g
v
o
k
x
.
e
x
c
o
y
c
l
r
c
l
m
a
w
fi
e
s
t
k
d
c
n
s
n
c
u
fi
l
i
n
android {
packagingOptions {
exclude "**/attach_hotspot_windows.dll"
exclude "META-INF/AL2.0"
exclude "META-INF/LGPL2.1"
exclude "META-INF/licenses/ASM"
}
}
158
Se ting up the test class
We’ll next start by cr a ing a new class, A the ti tio Test - this class will be used
class AuthenticationTest {
Tes Rule class - this is what we’re g ing to use to set the co po able co tent on
screen, a lo ing us to pe form i te a tions and a se tions from wit in our tests.
@get:Rule
val composeTestRule = createComposeRule()
When u ing this rule, we don’t need to sp cify any form of acti ity for our co pos-
ables to be launched in, the test rule will handle that for us. So u ing this rule we
will set the co po able co tent to be co posed, the test will then launch a host
acti ity which will be used to co pose our provided co tent i side of.
159
n
t
v
n
t
s
l
w
m
f
r
s
e
t
r
n
n
m
r
o
c
o
fi
u
e
m
n
s
c
f
a
r
r
n
n
m
n
v
s
h
s
m
o
e
n
m
Tes ing the A the ti tion Co pos-
able
At the root of our fe ture is the A the ti tion co po able. What makes this
di fe ent from our lower level co po ables (such as each UI co po ent co pos-
able), is that this co po able a lows us to i te act with el ments to tri ger state
u dates and r co po tion of our UI. This means that with the tests for the Au-
the ti tion co po able we can a sert not only that the e pe ted UI co pon-
ents are di played, but also that i te a tions with them re ult in the e pe ted state
changes.
With that in mind, we’re g ing to start with some tests to e sure that the e pe ted
state is co posed when the UI is i te a ted with. Wit in the A the ti tion
co po able the user can toggle between the sign-in + sign-up mode, so we’ll start
with some tests that a sert the co po tions that are d pen ant on these di fe ent
modes.
To start with, the title of the a the ti tion screen is d pen ing on the a then-
ti tion mode re re e ted wit in our state. So here, we’re g ing to write some
tests to a sert that the co tent of the title co rectly r ects the state of our screen.
By d fault our screen is in the sign in state, so we’re g ing to write a test to a sert
To write this rst test we’ll use the @Test a not tion and cr ate a new fun tion
to test that the Sign In title is di played by d fault wit in our UI.
160
c
p
f
a
m
n
r
e
t
t
s
c
s
a
s
m
fi
e
p
m
m
u
m
s
a
s
n
s
s
i
s
n
o
n
u
u
s
h
c
l
m
a
n
m
n
u
c
r
n
n
a
s
s
c
s
i
n
r
c
n
c
e
c
n
r
a
a
a
r
e
h
fl
e
e
o
m
h
s
d
n
e
e
s
d
x
m
o
m
u
c
n
x
n
g
u
c
c
x
c
m
a
f
m
s
c
r
@Test
fun Sign_In_Title_Displayed_By_Default() {
I side of this test, we’re g ing to need to start by se ting the co po able co tent
that is to be di played on screen for us to a sert against. Here we’ll use the test rule
that we pr v ously de ned, along with its se Co tent fun tion. This fun tion takes
posed on screen for our tests. B cause we’re wan ing to test the A the ti tion
ahead and pass the A the ti tion co po able fun tion for this co po able
a g ment.
@Test
fun Sign_In_Title_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
}
While we aren’t yet pe for ing any a se tions, ru ning this test will launch an activ-
ity that di plays the co tent of our A the ti tion co po able. With this now be-
ing di played, we can next pe form the r quired a se tions to e sure that the Sign
In title is b ing di played wit in our co po able UI. We’ll do this by uti ising the
o Nod Wit Text fun tion from our test rule re e ence.
The o Nod Wit Text fun tion can be used to lo ate a co po able that is dis-
pla ing the text that we have provided to the fun tion. Co po ables will be loc-
ated in the form of a s man ic node - b cause our co po ables are re re e ted
via s mantics, in our tests we are g ing to be lo a ing nodes wit in our s man ic
tree. In this case, this is done u ing the o Nod Wit Text fun tion, which will re-
161
n
r
n
m
y
u
m
e
n
s
e
s
s
s
e
e
e
h
i
s
h
s
c
fi
c
r
u
n
e
fi
m
o
c
n
t
h
r
r
c
s
a
e
u
o
s
u
r
m
e
e
e
n
n
l
m
s
i
t
c
s
w
a
e
s
f
n
n
c
c
r
s
c
c
t
t
h
t
m
r
m
c
m
s
c
fi
s
m
s
c
n
s
m
h
s
u
c
p
m
l
n
e
s
c
s
n
a
n
t
turn us with a S manti Nod I te a tion re e ence to pe form a se tions
against.
@Test
fun Sign_In_Title_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_in_to_account)
)
}
For this test we want to a sert that this node is b ing di played wit in our com-
posed UI, so we’re g ing to go ahead and uti ise the a ser I Di played func-
tion. This is one of the a se tions avai able on the S manti Nod I te a tion
class, a lo ing us to a sert whet er this node is b ing di played on the screen.
@Test
fun Sign_In_Title_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_in_to_account)
).assertIsDisplayed()
}
If you run this test wit in your IDE, you’ll not only see the UI spin up i side of the
co ne ted device / em la or, but the tests should also be passing due to the re-
162
n
c
l
w
e
e
o
s
h
m
u
s
c
s
t
s
r
h
e
h
n
l
r
c
l
e
e
f
e
r
s
s
s
c
t
s
s
r
e
s
h
n
n
r
s
c
r
Alon side the Sign In title b ing di played by the d fault Sign In a the ti a-
tion mode, the Need A count a count should also be co posed wit in our UI. For
these tests, we’re g ing to be able to r use a lot of the same code, with the key dif-
fe ence here b ing that we need to a sert the co po tion of the ac-
@Test
fun Need_Account_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
)
.assertIsDisplayed()
}
With these two tests in place, we are now able to a sert that the e pe ted co pos-
ables for the Sign In A the ti tion Mode are b ing co posed. But what ha pens
if the user toggles the A the ti tion Mode? In this sce ario, we know that tog-
gling to the Sign Up mode will change the title and toggle me sages - so we’re go-
ing to write some tests to a sert these r co po tions are triggered when this
toggle o curs.
For this, we’re g ing to start by tes ing that the title is changed to the Sign Up
title when the A the ti tion Mode toggle is clicked. Just like the last test, we’ll
@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
composeTestRule.setContent {
Authentication()
163
r
g
c
t
c
o
u
e
o
n
n
u
c
c
a
u
n
e
c
s
n
a
c
c
p
a
t
m
s
e
e
s
n
m
e
s
s
i
e
m
m
n
m
s
s
i
x
h
u
c
n
c
m
p
}
}
Now that we have some form of state b ing co posed into our UI, we’re g ing to
want to tri ger the title change - this will be done by clic ing the A the ti tion
Mode toggle bu ton, which switches our screen from the sign-in to sign-up state.
For this i te a tion we’re g ing to use the pe for Click fun tion, this is a ges-
ture a tion that is avai able on the S manti Nod I te a tion class, a lo ing
@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
composeTestRule.setContent {
Authentication()
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
).performClick()
}
Once this i te a tion has taken place, it is e pe ted that the a the ti tion mode
will be toggled. When this o curs, the title of the screen should switch to re re ent
the Sign Up mode. So with this test, we want to a sert that the e pe ted Sign Up
title is co posed. Here we’ll match the a se tion that we used for the title in the
pr v ous test, e cept this time we’ll check for our R.string.l bel_sign_up_-
@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
composeTestRule.setContent {
Authentication()
}
164
e
i
c
c
r
n
m
g
n
r
c
r
x
c
t
e
l
s
o
c
e
e
e
fi
s
x
c
r
s
r
m
c
s
e
m
n
r
k
c
u
c
a
x
n
c
u
c
a
n
p
l
o
c
a
w
s
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
).performClick()
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_up_for_account)
).assertIsDisplayed()
}
With this test in place, we’re now able to a sert that our title is r co posed a cord-
ingly when the a the ti tion mode is toggled. When this toggle o curs, we will
also e pect the a the ti tion bu ton to be r co posed to r ect the Sign Up ac-
tion, as o posed to Sign In. For this, we’re g ing to start with a test that looks very
able, fo lowed by u ing the pe for Click fun tion to i te act with the bu ton
@Test
fun Sign_Up_Button_Displayed_After_Toggle() {
composeTestRule.setContent {
Authentication()
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
).performClick()
}
165
m
l
x
l
p
e
u
u
i
u
s
n
n
c
c
n
a
a
c
a
r
t
m
s
m
o
e
m
c
u
n
e
n
fl
r
e
c
m
a
c
c
m
t
Now, this is clicked, our a the ti tion bu ton should be sho ing the Sign Up ac-
tion. We’re g ing to want to a sert this to e sure this is the case, so we’ll need to
start by adding a tag to the a the ti tion bu ton co po able. We’ll cr ate a new
o ject, Tags, and de ne a new tag that can be a signed to our a the ti tion but-
ton. We’re also g ing to be i te ac ing with the a the ti tion toggle across a
//Tags.kt
object Tags {
const val TAG_AUTHENTICATE_BUTTON = "authenticate_button"
const val TAG_AUTHENTICATION_TOGGLE =
"authentication_mode_toggle"
}
With these tags de ned we can now use the tes Tag fun tion to a sign this tag to
//AuthenticationButton.kt
Button(
modifier = Modifier.testTag(TAG_AUTHENTICATE_BUTTON),
...
)
We’ll also do the same for the Toggl A thenti tio Mode Bu ton.
// ToggleAuthenticationMode.kt
TextButton(
modifier = Modifier.testTag(TAG_AUTHENTICATION_TOGGLE),
...
)
With these tags in place, it can now be used to lo ate a node wit in our co pos-
able hie archy u ing the o Nod Wit ag fun tion. On this node, we can then use
166
b
t
r
o
m
s
o
fi
s
fi
u
n
u
n
s
n
e
c
a
r
n
c
e
h
t
a
T
u
t
n
c
t
c
t
s
a
c
u
m
n
n
c
s
c
a
w
t
u
h
s
n
e
c
a
m
the a ser Te Equals fun tion to a sert that the text of this co po able is equal
@Test
fun Sign_Up_Button_Displayed_After_Toggle() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithTag(
TAG_AUTHENTICATION_TOGGLE
).performClick()
composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(R.string.action_sign_up)
)
}
With this test in place, we can now be ce tain that when the a the ti tion mode is
toggled, the co tent di played wit in the a the ti tion bu ton no longer re res-
Aside from the title, we’re g ing to want to check that our a the ti tion
toggle now di plays the co tent that r ects the change a the ti tion mode. For
this, we’re g ing to again i te act with the toggle bu ton to toggle the a the ti a-
tion mode, and then we’ll want to a sert that the text of that a the ti tion bu ton
Here we’ll set up a test that will co pose our A the ti tion co po able,
fo lowed by uti ising our TAG_A THENTI TION TOGGLE tag to again lo ate the
node that re re ents the toggle bu ton. We can then use this re e ence to pe form
167
c
l
p
s
s
n
t
o
p
s
s
x
l
t
n
s
x
r
c
c
c
s
n
n
n
c
r
o
U
h
m
t
s
s
e
fl
r
e
C
A
u
c
u
n
_
c
a
t
n
c
a
u
t
u
u
n
f
m
n
c
u
r
a
n
c
m
a
s
c
n
a
c
u
s
a
c
r
n
p
t
c
B cause we’re g ing to be pe for ing mu tiple i te a tions on this node, we’ll
use the ko lin a ply fun tion to chain mu tiple o e tions. We’ll start by u ing the
pe for Click fun tion to pe form a click a tion on the bu ton, when this is
clicked the a the ti tion mode will be ipped from sign in to sign up. When this
count r source. We’ll use the a ser Te Equals fun tion to a sert that this is
the case.
@Test
fun Already_Have_Account_Displayed_After_Toggle() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithTag(
TAG_AUTHENTICATION_TOGGLE
).apply {
performClick()
assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_already_have_account)
)
}
}
If this test passes, it means that the a the ti tion toggle bu ton is b ing su cess-
fully r co posed with the co re pon ing state for the sign-up mode that has been
switched to. Ot e wise, it means the co po able has not been co posed with the
e pe ted state.
The title and a the ti tion bu ton are d na ic co po ents in the sense that
any co po tion should take into a count the a the ti tion mode. Now we have
these tests, we’re able to a sert that the co tent of these co po ables co rectly re-
168
fl
x
c
e
r
c
e
m
m
e
m
t
s
r
i
u
t
u
h
p
o
n
r
u
n
c
c
a
c
a
n
c
c
s
a
s
r
r
t
s
r
s
m
c
d
u
t
m
n
fl
y
h
n
x
l
n
l
t
s
c
m
a
c
u
n
p
m
r
r
n
a
c
c
c
n
a
c
m
t
s
t
m
s
e
r
s
c
Tes ing the A the ti tion Bu ton
Aside from a ap ing to the a the ti tion mode, the A the ti tion bu ton is also
co posed based on ot er parts of our state. Based on the cu rent co tent that is
i put into the email and pas word text elds, the a the ti tion bu ton will be
co posed with an e abled state. This means that if the email or pas word in our
co tent in either of the email or pas word pro e ties of our state. Si i ar to the
pr v ous tests we’ve wri ten for our A the ti tion co po able, we’re g ing to
cr ate a new test that co poses our A the ti tion co po able, fo lowed by
u ing the test rule to lo ate a node u ing our pr v ously de ned TAG_A THENTIC-
@Test
fun Authentication_Button_Disabled_By_Default() {
composeTestRule.setContent {
Authentication()
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
}
If this node has been lo ated, then we’re g ing to need to pe form an a se tion to
check that the bu ton is di abled - this is b cause there the email and pas word
state pro e ties are cu rently empty. To pe form this a se tion we’re g ing to use
the a ser I No E abled fun tion. This S manti Matc er will check that the
s mantics for the lo tion node has the S manti Pro e ties.Di abled prop-
@Test
fun Authentication_Button_Disabled_By_Default() {
composeTestRule.setContent {
169
n
e
s
e
e
m
m
n
i
e
t
s
T
p
n
t
r
d
s
t
u
t
t
c
n
a
u
n
n
c
r
c
t
h
c
u
m
a
m
s
n
u
n
s
s
c
c
c
a
t
n
a
c
s
u
a
s
u
s
t
e
fi
n
o
e
r
e
n
c
t
e
a
c
p
c
i
a
s
s
r
c
s
u
s
s
u
p
m
r
r
n
fi
m
h
n
c
s
r
a
c
r
a
s
s
s
n
t
U
m
o
t
s
l
l
r
o
s
Authentication()
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.assertIsNotEnabled()
}
With this small test, we’ll now be able to a sert that by d fault, the a the ti tion
bu ton is di abled. On the ip side, when those i put elds do have co tent, we
want to a sert that the A the ti tion bu ton is e abled. We’ll start wri ing a new
@Test
fun Authentication_Button_Enabled_With_Valid_Content() {
composeTestRule.setContent {
Authentication()
}
}
While we could use the A the ti tion state to pr load va ues to be used for the
email and pas word elds, I wanted to si late user b h viour here - so we’re go-
ing to use the pe for Te I put fun tion to type some text into the sp ci ed
text eld. B fore we can i te act with our text elds in such a way, we’re g ing to
need to add tags for them so that the nodes can be lo ated from our UI.
We’ll then a sign these tags to each of the email and pas word i put elds u ing
// EmailInput.kt
TextField(
modifier = modifier.testTag(TAG_INPUT_EMAIL),
...
170
t
fi
t
s
e
s
s
s
s
d
r
i
fi
fi
m
n
u
n
u
x
i
fl
t
r
n
n
n
c
a
c
a
c
m
t
u
s
fi
n
n
e
c
e
fi
a
e
s
l
n
u
fi
t
n
n
o
e
c
a
s
fi
)
// PasswordInput.kt
TextField(
modifier = modifier.testTag(TAG_INPUT_PASSWORD),
...
)
With these tags in place, we can now uti ise the pe for Te I put fun tion to in-
composeTestRule.onNodeWithTag(
TAG_INPUT_EMAIL
).performTextInput("[email protected]")
composeTestRule.onNodeWithTag(
TAG_INPUT_PASSWORD
).performTextInput("password")
After our state has been co posed, we’ll use both the email and pas word i put
elds to pe form text i put - gi ing both of these elds va id co tent that would al-
low the user to a the ti ate against. Once these calls are in place, we can again
lo ate the A the ti tion Bu ton u ing its tag but this time a sert that it is e abled
u ing the a ser I E abled fun tion. We pr v ously used the a ser I No En-
abled fun tion, the key di fe ence here is that a ser I E abled is chec ing that
the S manti Pro e ties.Di abled s man ic pro erty is not present on the
sp ci c co po able.
@Test
fun Authentication_Button_Enabled_With_Valid_Content() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithTag(
TAG_INPUT_EMAIL
).performTextInput("[email protected]")
171
fi
s
c
e
fi
e
c
m
r
s
u
c
s
s
n
t
u
c
s
p
a
n
n
n
r
c
f
m
t
r
v
r
s
c
s
s
d
l
e
e
s
i
t
fi
r
t
p
m
s
l
n
x
s
t
n
n
s
s
t
c
s
k
n
t
n
composeTestRule.onNodeWithTag(
TAG_INPUT_PASSWORD
).performTextInput("password")
composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).assertIsEnabled()
}
B cause the email and pas word elds have va id co tent, the a the ti tion but-
ton, in this case, should be e abled - which our test should now be a ser ing for us.
Some fu ther tes ing here could i clude r mo ing text from the i put elds
and a ser ing that our a the ti tion bu ton is di abled from r co po tion. At this
point, the in tial test for the di abled state, fo lowed by the e able state serves as a
mi i al r quir ment for our tes ing - but feel free to e plore fu ther co e age
here!
Now that we’ve pe formed a se tions on the co tent that is used to a the ti ate
the user, we can look at the next stages in the a pli tion ow. While the user
might be su ces fully a the ti ated and move to the next screen, that isn’t a ways
g ing to be the case - to cover these sce ar os, we a ded a di log co po able to
To a sert that this di log is di played in the co rect sce ar os, we’re g ing to
go ahead and start by tes ing that the di log is not di played by d fault. This will
a low us to e sure that users are not g ing to be shown the e ror di log when an
@Test
fun Error_Alert_Not_Displayed_By_Default() {
172
l
r
o
e
n
m
s
t
s
s
r
t
e
i
c
n
u
e
s
t
r
a
n
u
u
r
c
t
s
a
n
n
n
c
s
s
s
c
c
a
r
t
fi
n
r
r
o
t
n
a
e
u
i
l
v
l
r
n
s
n
c
p
a
d
n
s
c
a
n
x
i
n
e
fl
a
r
u
m
r
n
e
s
a
n
s
m
i
o
u
c
t
a
fi
s
v
n
l
r
c
}
So that we can try to lo ate the node that re re ents our alert di log, we’re g ing
to de ne a ot er tag.
We’ll then a sign this tag to our Aler Di log co po able u ing the tes Tag mo i-
er.
AlertDialog(
modifier = Modifier.testTag(TAG_ERROR_ALERT),
...
)
With this tag in place, we can now a tempt to lo ate the node and then pe form as-
se tions against it. To do this we’ll use the o Nod Wit ag fun tion, fo lowed by
u ing a ser Doe N E ist to a sert that a node with this tag does not e ist -
mea ing that the e ror di log does not cu rently e ist wit in our UI.
@Test
fun Error_Alert_Not_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithTag(
TAG_ERROR_ALERT
).assertDoesNotExist()
}
Now that we know our alert di log is not sho ing when an e ror doesn’t e ist,
we’re g ing to want to test the ip side of this and a sert that the e ror di log is
di played when an e ror has o curred. We’ll start here by d ing a new test func-
@Test
173
fi
s
s
r
n
fi
o
s
p
n
s
s
t
h
s
r
r
o
t
c
x
a
c
a
fl
s
t
t
a
r
p
n
m
c
w
s
x
e
s
s
h
h
T
s
e
fi
n
c
r
a
r
t
l
r
a
x
o
x
d
fun Error_Alert_Displayed_After_Error() {
Next, we need to co pose our state so that the e ror di log is di played - we’ll do
this by co po ing our A the ti tio Co tent and provi ing an A the tic-
composeTestRule.setContent {
AuthenticationContent(
AuthenticationState(
error = "Some error"
)
) { }
}
B cause our state now has an e ror value, an alert di log will be co posed wit in
our UI. Ho ever, we’re g ing to want to na ise our test and a sert that this is the
case. We’ll wrap up this test by lo a ing the alert di log u ing the tag we pr v ously
a signed to the Aler Di log co po able, fo lowed by u ing the a ser I Dis-
played fun tion to ver fy that the alert di log has been co posed wit in our UI.
@Test
fun Error_Alert_Displayed_After_Error() {
composeTestRule.setContent {
AuthenticationContent(
AuthenticationState(
error = "Some error"
)
) { }
}
composeTestRule.onNodeWithTag(
TAG_ERROR_ALERT
).assertIsDisplayed()
}
174
a
s
e
n
m
w
c
s
f
r
m
t
i
u
o
a
n
r
c
m
c
a
t
r
s
n
a
fi
n
l
l
s
r
a
a
a
s
m
s
d
s
s
m
s
h
u
t
e
n
s
i
h
Tes ing the loa ing state
When the A the ti tion Bu ton is clicked, the a the ti tion pr cess is triggered -
in this sce ario we would likely be ma ing a ne work r quest, di pla ing a pro-
gress di log on-screen in the pr cess. Du ing these state changes, we show and
hide a large amount of the UI co po ents, so we want to be sure that these state
changes re ult in the e pe ted UI co d tions. We’ll write a couple more tests to en-
// Tags.kt
object Tags {
const val TAG_PROGRESS = "progress"
}
CircularProgressIndicator(
modifier = Modifier.testTag(TAG_PROGRESS)
)
We’ll then go ahead and add a simple rst test that a serts our pr gress i di a or is
not co posed of the d fault state of our UI. Here we use the o Nod Wit ag
fun tion to lo ate our node u ing the sp ci ed tag, fo lowed by a ser ing that the
node does not e ist u ing the a ser Doe N E ist fun tion.
@Test
fun Progress_Not_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithTag(
TAG_PROGRESS
).assertDoesNotExist()
175
c
t
m
t
a
n
s
u
c
s
n
x
d
r
i
fi
c
a
s
x
d
e
s
e
c
t
r
s
s
r
o
m
u
x
n
t
n
fi
i
r
c
k
e
s
r
o
fi
o
t
u
o
x
t
s
s
n
n
l
c
a
n
c
e
c
t
c
t
o
o
m
s
n
s
t
s
y
e
n
fi
c
h
t
s
T
}
Now we’ve a se ted that our pr gress i di a or is not co posed with the d fault
a the ti tion state, we can now write a test to e sure that the loa ing state is re-
e ted in our co posed UI. For this we’ll go ahead and co pose our A the tic-
tio Co tent, provi ing a re e ence to the A the ti tio State class with
With this in place, we can co ti ue to lo ate the node that re re ents our load-
ing i di a or, fo lowed by pe for ing the a se tion that it is di played wit in our
co posed UI.
@Test
fun Progress_Displayed_While_Loading() {
composeTestRule.setContent {
AuthenticationContent(
AuthenticationState(isLoading = true)
) { }
}
composeTestRule.onNodeWithTag(
TAG_PROGRESS
).assertIsDisplayed()
}
After our o e tion has ished loa ing, we’ve har coded our Vie Mo el to set
an e ror state. When this ha pens, our UI should hide the pr gress i di a or and
di play the a the ti tion form to the user. For us to a sert that this is the case,
we’ll need to tri ger the a the ti tion pr cess from our UI. To save us e te ing
text into the tex elds at runtime, we’ll co pose our test UI with some pre-loaded
Once that’s done, we next need to pe form a click i te a tion on our A the tic-
tion Bu ton - this will tri ger the a the ti tion pr cess and set the e ror state
from our Vie Mo el. When this ha pens, our pr gress i di a or should no longer
176
fl
a
a
u
s
m
c
r
n
s
n
n
c
c
t
n
a
t
d
p
w
s
u
r
a
t
l
fl
g
r
m
fi
d
n
d
c
a
d
fi
g
n
u
n
p
r
n
n
f
o
m
r
c
s
p
a
d
u
r
c
n
n
m
o
c
l
c
s
a
t
r
u
o
n
n
d
o
n
r
c
n
s
c
m
m
a
c
p
o
t
s
s
n
d
w
n
u
u
d
c
r
n
t
h
n
n
e
r
e ist in our UI. To e sure that this is the case, we’ll add an a se tion by u ing the
a ser Doe N E ist fun tion to check that the pr gress i di a or does not e ist
@Test
fun Progress_Not_Displayed_After_Loading() {
composeTestRule.setContent {
AuthenticationContent(
authenticationState = AuthenticationState(
email = "[email protected]",
password = "password"
)
) { }
}
composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).performClick()
composeTestRule.onNodeWithTag(
TAG_PROGRESS
).assertDoesNotExist()
}
Once we reach this state of our pr gress i di a or not b ing di played (b cause
the loa ing pr cess has been co pleted), we’re g ing to want to e sure that the
co tent of our UI has been co posed again - this is the a the ti tion form, a low-
ing users to a tempt re-a the ti tion. If this didn’t di play again wit in the UI,
things would be quite broken for the users - so we’ll write a test to a sert that this is
the case.
@Test
fun Content_Displayed_After_Loading()
B fore we can pe form a se tions against the co tent area of our UI, we’re g ing to
need to de ne a new tag and a sign it to the pa ent of our co tent area.
177
x
s
e
n
h
t
d
fi
s
o
o
t
t
r
x
n
s
u
c
r
m
n
s
c
m
a
o
n
c
r
n
t
o
o
s
u
e
n
n
s
n
c
c
r
s
a
t
s
n
h
s
o
e
l
x
// Tags.kt
object Tags {
const val TAG_CONTENT = "content"
}
We’ll then need to set this tag on the co re pon ing co po able wit in our Au-
// AuthenticationForm.kt
Column(
modifier = Modifier.testTag(TAG_CONTENT),
horizontalAlignment = Alignment.CenterHorizontally
)
While we could pe form a se tions against the i d vid al chi dren that already
have tags a signed to them, this a proach a lows us to refer to the co tent area as
a whole. Si i ar to the pr v ous test, we can pe form the a the ti tion ow, fol-
lowed by pe for ing the a se tion that the co tent area e ists wit in our UI.
B cause there has been an e ror state loaded at this point, there will be an
alert di log co posed wit in our UI. For this rea on, we use the e ists check in-
stead of a di played check, this is b cause the alert di log will be covered a
good chunk of the co tent UI so we ca not a ways gua a tee that the di played
@Test
fun Content_Displayed_After_Loading() {
composeTestRule.setContent {
Authentication()
}
composeTestRule.onNodeWithTag(
TAG_INPUT_EMAIL
).performTextInput("[email protected]")
composeTestRule.onNodeWithTag(
178
s
e
n
r
a
c
a
m
s
r
l
s
n
m
m
r
t
n
s
fi
e
h
s
s
i
r
r
r
p
n
e
r
s
l
n
l
r
s
d
n
i
r
m
u
x
n
a
u
s
l
n
h
x
c
a
n
h
s
fl
TAG_INPUT_PASSWORD
).performTextInput("password")
composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).performClick()
composeTestRule.onNodeWithTag(
TAG_CONTENT
).assertExists()
}
179
Tes ing the A the ti tion Title
Now that we have tests in place that pe forms a se tions against our A the ti a-
tion co po able, we’re g ing to f cus on wri ing some ne-grained tests for the
i d vid al co po able fun tions that re re ent our i d vid al se ting items. This al-
fun tion i self, without the co cern of our glo al state. We’ll start here by cr a ing a
class AuthenticationTitleTest {
@get:Rule
val composeTestRule = createComposeRule()
We’re g ing to start here by wri ing a test to a sert that the co po able co rectly
di plays the title co re pon ing title for the A the ti tio Mode that is
provided to it. The A the ti tio Title co tains the l gic that d picts which
string r source is used based on the A the ti tio Mode that is provided to it.
For this rea on, we’ll want to write these tests to e sure this l gic is wor ing as ex-
pe ted.
Here we’ll cr ate a new test fun tion, Sign_In Title_Di played, where we
will a sert that the Sign In title is co posed when the A the ti tio Mod-
e.SIGN_IN is provided for the a the ti tio Mode a g ment. We’ll start here
by co po ing an A the ti tio Title, passing the Sign In mode for the au-
180
n
s
c
c
i
t
n
s
m
u
t
e
c
o
m
a
t
s
s
o
e
m
s
n
u
s
u
u
n
r
r
n
s
c
n
c
o
a
m
u
n
c
d
c
a
a
c
n
t
u
s
n
o
t
n
n
r
n
u
m
e
r
p
c
c
s
a
n
b
a
_
s
s
t
n
c
n
n
a
n
fi
r
g
u
n
e
r
n
i
a
n
r
fi
u
s
o
u
u
o
c
a
m
t
m
n
n
s
o
u
c
e
e
a
k
m
e
n
n
r
t
s
c
@Test
fun Sign_In_Title_Displayed() {
composeTestRule.setContent {
AuthenticationTitle(
authenticationMode = AuthenticationMode.SIGN_IN
)
}
}
When the A the ti tio Title is co posed for the SIGN_IN A the ti a-
played. We’ll need to pe form an a se tion for this in our tests, so we’ll go ahead
and use the o Nod Wit Text fun tion on our test rule to lo ate a node that has
the text co tained wit in our l bel_sign_in_to_a count r source. We’ll then
use the a ser I Di played fun tion to pe form the a se tion that this co pos-
able is di played.
@Test
fun Sign_In_Title_Displayed() {
composeTestRule.setContent {
AuthenticationTitle(
authenticationMode = AuthenticationMode.SIGN_IN
)
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_in_to_account)
)
.assertIsDisplayed()
}
If this test fails, it means that the e pe ted title is not b ing co posed for the
SIGN_IN A the ti tio Mode. On the ip side, we’ll also want to a sert that the
e pe ted text co tent is co posed for the SIGN_UP A the ti tio Mode. This
test is g ing to look the same as the pr v ous, e cept this time we’ll pass in Au-
181
x
n
c
o
s
s
n
u
u
n
t
n
n
s
n
x
e
c
s
c
a
h
c
a
h
r
n
m
n
a
c
c
s
x
r
a
c
e
m
fl
i
r
x
c
u
s
e
r
n
c
c
e
c
a
m
u
s
n
n
m
c
the ti tio Mo e.SIGN_UP when co po ing the A the ti tio Title, as
well as u ing the l bel_sign_up for_a count r source when pe for ing our
a se tion.
@Test
fun Sign_Up_Title_Displayed() {
composeTestRule.setContent {
AuthenticationTitle(
authenticationMode = AuthenticationMode.SIGN_UP
)
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_up_for_account)
)
.assertIsDisplayed()
}
With these tests in place, we can now be sure that the A the ti tio Title is
u ing the e pe ted string r source du ing co po tion, based on the A the tic-
182
a
s
s
n
r
n
c
s
a
x
n
c
d
a
e
_
r
m
c
m
s
s
i
e
u
u
n
n
c
c
a
a
r
u
n
n
m
n
Tes ing the A the ti tion Bu ton
Alon side the A the ti tio Title b ing co posed based on the A then-
ti tio Mode that is provided to it, the A the ti tio Bu ton also b haves
in the same way - we’ll also want to write some tests to a sert this co po tion also.
We’ll start by cr a ing a new A the ti tio Bu to Test class, se ting up the
class AuthenticationButtonTest {
@get:Rule
val composeTestRule = createComposeRule()
Next, we’ll write the rst test wit in this test class, which will be used to a sert that
the Sign In a tion is di played wit in the bu ton when e pe ted. For this we’ll need
to co pose the A the ti tio Bu ton, passing the A the ti tio Mod-
@Test
fun Sign_In_Action_Displayed() {
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = true,
authenticationMode = AuthenticationMode.SIGN_IN,
onAuthenticate = { }
)
}
}
183
m
c
a
g
m
t
n
c
e
u
t
u
fi
n
s
o
n
c
u
a
u
c
a
u
n
h
n
h
n
c
n
a
n
t
c
a
e
n
c
t
u
a
n
n
m
r
t
u
c
a
n
x
s
c
n
u
t
t
n
m
c
t
a
s
i
s
u
e
n
Now that the A the ti tio Bu ton is g ing to be co posed in our test, we
can pe form the r quired a se tions. We already have the TAG_A THENTIC-
ATE_BU TON tag a signed to our co po able from some pr v ous tests that we
wrote, so we can use this to lo ate the r quired node. Once we’ve done that, the
a ser Te Equals can then be used to a sert that the e pe ted text of the re-
trieved node matches the value that we provide. Here we’ll provide the string value
for our a tion_sign_in r source, which re re ents the “Sign In” value that is ex-
the co po able.
@Test
fun Sign_In_Action_Displayed() {
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = true,
authenticationMode = AuthenticationMode.SIGN_IN,
onAuthenticate = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(R.string.action_sign_in)
)
}
We’ll also want to a sert that the a tion_sign_up string is di played when the
re pon ing test here which will mostly match the pr v ous test we wrote, e cept
we’ll pass A the ti tio Mo e.SIGN_UP for the a the ti tio Mode a gu-
ment, along with u ing the a tion_sign_up string r source when pe for ing the
@Test
184
s
u
s
s
c
m
t
n
t
d
r
T
c
x
c
x
s
u
t
a
t
u
s
n
n
s
s
e
s
n
c
d
a
s
c
a
r
e
n
c
s
c
n
d
r
t
u
c
m
n
e
s
c
s
p
o
a
s
n
e
e
u
m
d
i
m
s
n
x
e
c
c
s
i
a
n
r
U
m
x
r
fun Sign_Up_Action_Displayed() {
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = true,
authenticationMode = AuthenticationMode.SIGN_UP,
onAuthenticate = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_sign_up)
)
}
Alon side the A the ti tio Mode based a se tions that we’ve pe formed
above, the A the ti tio Bu ton also takes an onA thenti ate a g ment.
When this lambda is i voked by our A the ti tio Bu ton, the pa ent com-
po able will use this to tri ger the a the ti tion mode - if this broke, users would
not be able to pe form a the ti tion wit in our app. For this rea on, we’re g ing
to write a test to a sert that the lambda is i voked when e pe ted. Here we’re go-
ing to pass in a mock lambda fun tion for the onA thenti ate a g ment. This
means that we can use this mock to ver fy that i te a tions have taken place based
@Test
fun Authenticate_Triggered() {
val onAuthenticate: () -> Unit = mock()
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = false,
authenticationMode = AuthenticationMode.SIGN_UP,
onAuthenticate = onAuthenticate
)
}
185
s
g
m
s
u
u
r
s
n
c
n
n
a
u
g
c
a
n
n
c
t
a
n
c
u
i
u
n
h
n
c
a
n
n
c
s
a
r
u
c
r
n
u
x
t
c
c
s
c
r
u
r
r
r
u
o
}
With the A the ti tio Bu ton b ing co posed, we’ll now be able to r trieve
the node that re re ents this a the ti tion bu ton u ing the TAG_A THENTIC-
ATE_BU TON tag. We’ll then use the pe for Click fun tion to pe form a click ac-
tion on this node. When this click a tion is triggered, this is the point that we would
e pect the onA thenti ate to be i voked so that the pa ent co po able can
handle the a the ti tion event. We can ver fy this wit in our test by u ing mockito
and its ver fy fun tion to a sert that the lambda has been i voked. If this is the
case, the test will su ceed - ot e wise, the lambda not b ing triggered will mean
that our ver tion will not be sa i ed and the test will fail.
@Test
fun Authenticate_Triggered() {
val onAuthenticate: () -> Unit = mock()
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = false,
authenticationMode = AuthenticationMode.SIGN_UP,
onAuthenticate = onAuthenticate
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.performClick()
verify(onAuthenticate).invoke()
}
tio a g ment. It could also be b n cial to write some tests to a sert the com-
po tion based on the value of this a g ment - we already have some tests for the
A the ti tion co po able that i volved the e abled state, so we won’t cover
that here!
186
x
u
s
i
n
u
r
n
T
u
u
c
n
i
i
a
fi
u
c
a
c
n
u
a
n
p
c
c
c
c
s
a
a
m
n
c
t
s
n
s
t
h
u
t
r
s
m
e
c
fi
n
r
e
n
e
n
fi
u
r
c
s
a
i
m
m
t
n
h
s
c
e
r
n
n
r
e
s
m
u
s
U
s
e
c
187
Tes ing the A the ti tion Mode
Toggle
So far we’ve been wri ing tests for var ous co po ables that uti ise the A then-
ti tio Mode from our state o ject. The bu ton that is used to toggle this value
also takes an A the ti tio Mode re e ence, this is also so that it can be com-
posed to di play the co re pon ing co tent for the provided A the ti tion-
Mode. After se ting up a test class with a co re pon ing test rule, we’ll cr ate a test
fun tion that will be used to a sert the a tion_need_a count r source text is
class AuthenticationModeToggleTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun Need_Account_Action_Displayed() {
Wit in this test we’ll need to start by co po ing a Toggl A thenti tio Mode,
provi ing the a the ti tio Mode a g ment in the form of the A the ti a-
tio Mo e.SIGN_IN value. When this SIGN_IN value is provided, we e pect that
the co re pon ing co tent is di played i side the bu ton - this is in the form of the
a tion_need_a count r source. After lo a ing the node for this co po able us-
ing the o Nod Wit ag(TAG_A THENTI TION TOGGLE) fun tion call, we can
188
c
s
c
c
h
a
n
d
r
t
d
s
n
n
s
h
d
t
e
u
u
c
h
n
n
T
n
t
m
r
c
c
a
a
e
s
u
s
n
s
n
s
d
b
U
n
i
m
n
r
f
n
u
r
C
c
c
c
r
A
s
t
a
s
t
m
_
d
s
t
e
c
u
u
c
l
e
c
m
n
u
a
e
x
c
s
u
a
n
n
c
a sert that this e pe ted text is di played via the use of the a ser Te Equals
fun tion.
@Test
fun Need_Account_Action_Displayed() {
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_IN,
toggleAuthentication = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
)
}
We’ll next ip this around so that we can a sert that the e pe ted ac-
Mo e.SIGN_UP value is provided for the a the ti tio Mode a g ment. Our
test here is g ing to look the same as above, aside from the tweak to the a then-
@Test
fun Already_Have_Account_Action_Displayed() {
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_UP,
toggleAuthentication = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
189
s
d
x
c
c
t
a
n
fl
o
x
c
c
r
c
c
u
s
s
u
n
s
e
c
a
n
s
u
t
r
x
n
u
x
c
t
c
a
u
s
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_already_have_account)
)
}
With this test in place, we can now be ce tain that the passing tests mean the
provided a the ti tio Mode value is g ing to di play the e pe ted text i side
of our co po able. Whene er the user clicks the bu ton that is di pla ing this text,
the lambda fun tion that is provided to the co po able should be triggered - this
is the toggl A thenti tion lambda. If this for some rea on was not b ing
triggered, the user would not be able to switch to the sign-up mode - so if a user
does not cu rently have an a count, they wouldn’t be able to cr ate one. To e sure
this r mains fun tio al, let’s write a quick test to a sert that this event does o cur.
Wit in this next test, we’re g ing to pass in a mock lambda fun tion for the
toggl A thenti tion a g ment. This means that we can use this mock to
ver fy that i te a tions have taken place based off of co po able events.
@Test
fun Toggle_Authentication_Triggered() {
val toggleAuthentication: () -> Unit = mock()
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_UP,
toggleAuthentication = toggleAuthentication
)
}
}
With the Toggl A thenti tio Mode b ing co posed, we’ll now be able to
use the r trieve the node that re re ents this toggle bu ton u ing the TAG_AU-
THENTI TION TOGGLE tag. We’ll then use the pe for Click fun tion to per-
form a click a tion on this node. When this click a tion is triggered, this is the point
190
i
h
e
e
C
m
u
A
e
n
u
r
c
e
s
r
c
c
_
u
c
n
e
c
u
n
a
c
a
c
a
n
v
c
c
a
o
r
u
p
n
s
o
e
r
m
s
c
s
m
t
r
s
m
m
t
s
e
s
x
s
s
c
c
y
c
c
n
n
e
that we would e pect the toggl A thenti tion to be i voked so that the par-
ent co po able can handle the a the ti tion event. We can ver fy this wit in our
test by u ing mockito and its ver fy fun tion to a sert that the lambda has been
i voked. If this is the case, the test will su ceed - ot e wise, the lambda not b ing
triggered will mean that our ver tion will not be sa i ed and the test will fail.
@Test
fun Toggle_Authentication_Triggered() {
val toggleAuthentication: () -> Unit = mock()
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_UP,
toggleAuthentication = toggleAuthentication
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
.performClick()
verify(toggleAuthentication).invoke()
}
191
n
m
s
s
x
i
fi
e
c
u
i
a
u
n
c
c
c
a
c
a
s
h
t
s
r
fi
n
i
h
e
Tes ing the Email A dress I put
When it comes to the Emai I put co po able, an email a g ment is used to
provide the co tent that is to be di played i side of the text eld. This is a very im-
por ant part of the a the ti tion ow, so we’ll want to write a test to e sure that
this provided value is di played i side of our co po able. To do this we’ll need to
class EmailInputTest {
@get:Rule
val composeTestRule = createComposeRule()
We’ll start here by cr a ing a new test, co po ing the Emai I put co po able.
We’ll provide empty i pl men tions for the on mai Changed and o Focus-
Reque ted a g ments, but will need to provide a string value for the email a gu-
ment. We’ll cr ate a string var able re e ence here, provi ing this to our Emai In-
put co po able.
@Test
fun Email_Displayed() {
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = { },
onNextClicked = { }
)
}
192
t
s
t
m
t
s
r
e
n
u
u
e
m
t
s
n
e
c
l
a
i
n
t
a
n
fl
s
f
r
m
l
d
m
n
n
s
t
s
m
E
s
l
d
n
fi
l
r
n
u
m
n
n
s
l
r
}
Next, we’ll need to a sert that this email value is di played i side of our co pos-
able. In a pr v ous test, we de ned the TAG_I PUT_ MAIL tag, so we’ll use this
here to lo ate the node that re re ents our email text eld. Once this node has
been lo ated we can uti ise the a ser Te Equals fun tion to a sert that the text
s man ic value of the node matches our provided email var able.
@Test
fun Email_Displayed() {
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = { },
onNextClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.assertTextEquals(email)
}
With this test in place, we can now be ce tain that a passing test means the
the user enters co tent into the text eld to u date this email value that is co ing
from our state, the lambda fun tion that is provided to the co po able is triggered
- this is the on mai Changed lambda. If this for some rea on was not b ing
triggered, the user would be u able to enter their email a dress into the text eld.
To e sure this r mains fun tion, let’s write a quick test to a sert that this event does
o cur.
our a se tions against. We’ll need to provide a string value for the email a g ment
- we’ll cr ate a var able re e ence for this so that we can a sert the lambda is
193
e
c
n
e
s
t
c
r
e
c
e
i
e
E
n
i
s
l
l
c
o
f
m
r
c
n
fi
p
s
s
fi
t
s
x
t
p
r
l
N
n
n
s
E
c
fi
m
d
s
i
m
n
s
s
s
s
m
s
s
r
r
u
m
m
fi
e
triggered with the e pe ted value. We’re also g ing to pass in a mock lambda
fun tion for the on mai Changed a g ment. This means that we can use this mock
to ver fy that i te a tions have taken place based off of co po able events.
@Test
fun Email_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = onEmailChanged,
onNextClicked = { }
)
}
}
While we have provided this lambda to our co po able, we now need to tri ger it
so that we can ver fy the e pe ted b h viour. To tri ger this lambda we need to
enter some text into the i put eld, which we can do so u ing the pe for Te t-
I put on a sp ci ed node. We’re g ing to a pend some text onto the e is ing in-
put, which we’ll store in a var able re e ence, a pe de Text so that we can use
this du ing the a se tion. Here we’ll lo ate the i put eld node u ing our e is ing
tag, fo lowed by i pu ting this co tent u ing the pe for Te I put fun tion.
With this in place, we can now add the check to ver fy that the lambda fun tion is
called as e pe ted. When this is triggered, we would e pect that the email value
r turned here would re re ent the e is ing co tent with the a d tion of the ap-
pe de Text value. We can ver fy this wit in our test by u ing mockito and its
194
e
n
n
c
i
l
d
r
x
n
c
e
r
s
n
fi
E
c
i
r
x
t
l
p
c
n
x
s
i
fi
c
n
i
r
o
e
x
f
u
c
s
r
a
t
h
p
m
n
p
n
o
r
s
n
i
g
fi
x
d
m
m
s
x
t
s
s
d
n
s
i
r
c
x
m
c
t
g
x
x
t
ver fy fun tion to a sert that the lambda has been i voked with the e is ing value
in the i put eld (email) a pe ded with the value of a pe de Text. If this is the
case, the test will su ceed - ot e wise, the lambda not b ing triggered will mean
that our ver tion will not be sa i ed and the test will fail.
@Test
fun Email_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = onEmailChanged,
onNextClicked = { }
)
}
val appendedText = ".jetpack"
composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.performTextInput(appendedText)
verify(onEmailChanged).invoke(email + appendedText)
}
195
i
n
i
c
fi
fi
c
a
c
s
p
h
n
t
r
s
fi
n
p
e
n
d
x
t
Tes ing the Pas word I put
When it comes to the Pas wor I put co po able, a pas word a g ment is
used to provide the co tent that is to be di played i side of the text eld. This is a
very i por ant part of the a the ti tion ow, so we’ll want to write a test to e sure
that this provided value is di played i side of our co po able. To do this we’ll
class PasswordInputTest {
@get:Rule
val composeTestRule = createComposeRule()
We’ll start here by cr a ing a new test, co po ing the Pas wor I put co pos-
able. We’ll provide empty i pl men tions for the o Pas wor Changed and on-
Don Clicked a g ments, but will need to provide a string value for the pas word
a g ment. We’ll cr ate a string var able re e ence here, provi ing this to our Pass-
@Test
fun Password_Displayed() {
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
196
r
u
e
d
m
n
t
t
m
r
e
u
t
s
e
n
t
m
u
s
s
e
s
n
d
i
c
a
n
t
a
n
fl
m
f
s
r
m
s
s
n
s
n
d
n
n
m
s
s
t
s
d
s
d
d
n
fi
r
u
s
m
n
}
Next, we’ll need to a sert that this pas word value is in fact di played i side of our
co po able. In a pr v ous test, we de ned the TAG_I PUT PAS WORD tag, so
we’ll use this here to lo ate the node that re re ents our pas word text eld. Once
this node has been lo ated we can uti ise the a ser Te Equals fun tion to as-
sert that the text s man ic value of the node matches our provided pas word vari-
able.
@Test
fun Password_Displayed() {
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWORD)
.assertTextEquals(password)
}
With this test in place, we can now be ce tain that a passing test means the
When the user enters co tent into the text eld to u date this pas word value that
is co ing from our state, the lambda fun tion that is provided to the co po able
is triggered - this is the o Pas wor Changed lambda. If this for some rea on was
not b ing triggered, the user would be u able to enter their pas word into the text
eld. To e sure this r mains fun tion, let’s write a quick test to a sert that this event
does o cur.
As b fore, we’ll need to co pose the Pas wor I put co po able to per-
form our a se tions against. We’ll need to provide a string value for the pas word
197
fi
m
m
e
e
c
s
n
s
r
s
e
s
e
e
c
c
t
i
n
n
m
s
c
o
d
s
l
fi
n
c
fi
p
s
r
s
s
s
d
p
n
t
n
x
N
t
s
m
s
_
s
s
s
s
S
n
s
c
fi
m
m
s
s
s
s
a g ment - we’ll cr ate a var able re e ence for this so that we can a sert the
lambda is triggered with the e pe ted value. We’re also g ing to pass in a mock
lambda fun tion for the o Pas wor Changed a g ment. This means that we can
use this mock to ver fy that i te a tions have taken place based off of co po able
events.
@Test
fun Password_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
}
While we have provided this lambda to our co po able, we now need to tri ger it
so that we can ver fy the e pe ted b h viour. To tri ger this lambda we need to
enter some text into the i put eld, which we can do so u ing the pe for Te t-
I put on a sp ci ed node. We’re g ing to a pend some text onto the e is ing in-
put, which we’ll store in a var able re e ence, a pe de Text so that we can use
this du ing the a se tion. Here we’ll lo ate the i put eld node u ing our e is ing
tag, fo lowed by i pu ting this co tent u ing the pe for Te I put fun tion.
With this in place, we can now add the check to ver fy that the lambda fun tion is
called as e pe ted. When this is triggered, we would e pect that the pas word
value r turned here would re re ent the e is ing co tent with the a d tion of the
198
r
n
u
l
e
r
x
c
e
c
s
n
fi
i
e
r
i
t
n
n
x
n
p
i
i
fi
c
x
r
s
n
s
c
c
o
d
e
f
c
f
s
r
a
r
x
p
t
m
p
n
r
r
s
u
n
i
g
n
fi
d
m
x
s
o
x
t
n
s
d
r
i
m
c
x
s
m
c
t
g
s
x
s
x
t
a pe de Text value. We can ver fy this wit in our test by u ing mockito and its
ver fy fun tion to a sert that the lambda has been i voked with the e is ing value
in the i put eld (pas word) a pe ded with the value of a pe de Text. If this is
the case, the test will su ceed - ot e wise, the lambda not b ing triggered will
mean that our ver tion will not be sa i ed and the test will fail.
@Test
fun Password_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
val passwordText = "456"
composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWORD)
.performTextInput(passwordText)
verify(onEmailChanged).invoke(password + passwordText)
}
When it comes to the Pas wor I put co po able, we i pl me ted the abi ity to
toggle the vi i i ity of the pas word u ing a vi i i ity toggle bu ton. We’re g ing to
write a test to a sert that the state of this is r e ted co rectly, based on the i ter al
state of the fun tion that is b ing used to ma age the pas word vi i i ity.
When it comes to tes ing this, we’ll just write a single test to check that the vis-
i i ity toggle co po able r ects the e pe ted state. We’ll need to start here by
adding a new tag to our Tags o ject so that we can lo ate and i te act with the
vi i i ity co po able. We’ll end this tag with an u de score so that we can a pend
the cu rent boolean value of the toggle to the tag, mea ing that we can lo ate the
199
b
p
s
l
b
i
l
n
r
n
d
m
c
fi
s
b
c
s
s
m
l
i
fi
n
c
a
s
s
t
s
c
s
e
e
fl
s
p
d
b
n
i
n
h
s
r
t
x
s
fi
m
e
c
n
fl
h
s
s
c
b
n
l
n
r
r
n
c
m
s
p
e
s
t
n
e
n
s
n
b
d
l
r
x
t
c
o
n
p
l
n
// Tags.kt
object Tags {
...
const val TAG_PASSWORD_HIDDEN = "password_hidden_"
}
Next, we’ll need to a sign this tag our co po able u ing the tes Tag fun tion.
When d ing this we’ll also a pend the cu rent value of our i Pas wor Hi den
state, so that we can lo ate the node u ing this value. We do this as if the value is
not aligned as e pe ted, then the node won’t be found and the tests will fail.
// PasswordInput.kt
trailingIcon = {
Icon(
modifier = Modifier.testTag(TAG_PASSWORD_HIDDEN +
isPasswordHidden),
...
)
}
With this in place, we can now start wor ing on the test. Here we’ll b gin by com-
po ing the Pas wor I put with a string value for the pas word a g ment.
@Test
fun Password_Toggled_Reflects_state() {
composeTestRule.setContent {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
}
We’ll then want to lo ate the vi i i ity toggle co po able. By d fault the vi i i ity
ag will be true, si n f ing that the pas word is hi den. Here we’re g ing to lo ate
200
fl
s
o
s
x
g
c
d
i
c
s
y
n
c
p
s
b
l
s
s
k
m
r
s
m
d
s
s
s
s
e
r
t
s
u
o
e
d
s
c
d
b
c
l
the node with the value of true a pe ded to the tag, click on it and then a sert that
the tag with the value of false a pe ded to the tag is di played. Here we’re g ing
to start by lo a ing the node for the hi den state of our vi i i ity toggle co pos-
able - we’ll need this so that we can pe form a click i te a tion on the co po able.
Here we’ll use the TAG PAS WORD_HI DEN tag, a pen ing the value of true on
the end to match the e pe ted co d tion of the state for the pas word vi i i ity.
@Test
fun Password_Toggled_Reflects_state() {
composeTestRule.setContent {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDDEN + "true")
.performClick()
}
With this in place, we are not pe for ing a click i te a tion on the co po able,
this means that now the co po able state should have r co posed the vi i i ity
toggle co po able. This means that now, a node with the TAG PAS WORD_HID-
DEN tag for the vi ible state of the pas word should be vi ible. We can a sert this
here u ing the a ser I Di played fun tion on the lo ated node.
@Test
fun Password_Toggled_Reflects_state() {
composeTestRule.setContent {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
201
s
m
c
s
t
s
s
t
x
_
s
c
m
s
S
p
p
s
n
r
n
i
n
m
r
s
D
d
c
p
n
n
r
c
r
s
d
c
c
e
s
s
b
m
l
s
_
S
s
m
m
b
s
s
l
m
s
s
s
b
o
l
.onNodeWithTag(TAG_PASSWORD_HIDDEN + "true")
.performClick()
composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDDEN + "false")
.assertIsDisplayed()
}
While we could have a se a ate test here to a sert that the true co d tion is dis-
played, this test co ers both sce ar os. This is b cause the rst o Nod Wit ag
call will fail the test if the node is not found - this will mean that the tag for the hid-
den state of the vi i i ity toggle would not cu rently be b ing di played on the
screen. B cause this test r quires the hi den state of the vi i i ity toggle to per-
form the a ser I Di played a se tion, we cover both sce ar os in a single test.
202
e
s
t
s
s
v
b
s
l
p
e
r
s
n
r
i
d
s
r
e
e
n
fi
s
b
i
l
n
s
n
i
e
h
T
Tes ing the Pas word R quir ments
While we’re ver f ing the entry of a pas word from the tests above, our user is still
r quired to enter a pas word that meets the mi i um r quir ments that are
de ned wit in our Vie Mo el. These r quir ments are co m ni ated to the user
via the Pas wor R quir ments co po able, with the co po able co tai ing lo-
gic to d pict the me sage to be di played based on the provided r quir ment
statuses. So here, we’ll write some tests here to ver fy that this co po able is o er-
a ing as e pe ted.
// PasswordRequirementsTest.kt
class PasswordRequirementsTest {
@get:Rule
val composeTestRule = createComposeRule()
We’re rst g ing to write a test to a sert that each of the pas word r quir ments is
di played as e pe ted. To keep things simple here and avoid nee ing to write mul-
tiple test co d tions, we’re g ing to write a test that will a sign ra dom pas word
r quir ments as sa i ed. This way we can a sert that e pe ted r quir ments are
We’re g ing to start here by r trie ing the list of avai able r quir ments from
our Pas wor R quir ment type, along with ge ting a ra dom item from this list
203
e
e
t
s
s
fi
e
fi
t
o
s
e
x
s
h
n
o
c
d
x
i
i
e
d
y
c
e
t
t
t
s
s
fi
s
s
fi
w
e
fi
s
e
e
d
o
e
s
e
n
v
s
t
m
s
s
fi
e
s
s
e
s
e
t
i
n
m
l
x
m
n
m
s
e
c
s
e
e
u
s
m
d
c
e
e
e
n
e
s
e
e
n
e
n
e
s
p
val satisfiedRequirement = requirements[(0 until
requirements.count()).random()]
between tests, but we’ll use a single one here to keep things simple for e amples
sake. We’ll next co pose a Pas wor R quir ments, provi ing a list for the sat-
i fie R quir ments a g ment that co sists of the ra dom r quir ment that we
@Test
fun Password_Requirements_Displayed_As_Not_Satisfied() {
val requirements = PasswordRequirement.values().toList()
val satisfiedRequirement = requirements[(0 until
requirements.count()).random()]
composeTestRule.setContent {
PasswordRequirements(
satisfiedRequirements = listOf(satisfied)
)
}
}
When the Pas wor R quir ments is co posed, it should be the case that the
r quir ments are co posed based on the sa i ed r quir ments that are
provided. To test this we’re g ing to need to start by loo ing through each of the
PasswordRequirement.values().forEach {
Next, we’ll use each of their l bels, along with the provided sa i fie R quire-
ment to build the string that we’re g ing to a sert for. The Pas wor R quire-
ments co po able is forma ting two di fe ent string re re en tions based on the
word_r quir ment_sa i fied r source is used to build a string for that re-
204
e
e
s
t
s
l
fi
e
d
e
e
m
s
s
e
s
e
m
d
t
n
d
e
s
m
e
t
r
d
u
s
e
t
a
e
m
o
e
e
s
e
e
e
d
l
o
e
f
t
n
s
r
m
fi
e
s
e
t
s
fi
e
p
n
p
s
t
e
d
s
fi
t
a
t
e
e
s
s
e
d
d
e
e
x
m
quir ment, ot e wise the pas word_r quir ment_needed is used. Here for
each of the r quir ments in the loop, we’re g ing to r trieve the string for the l bel
of the r quir ment, along with buil ing a string based on whet er the r quir ment
in the loop matches the sa i fie R quir ment that we co gured earl er in the
test.
Now, this string is b ing built, we can use this to lo ate a node and pe form an as-
205
r
e
e
n
e
e
h
r
e
e
t
e
s
s
s
d
d
e
e
e
o
h
e
c
e
m
s
n
fi
h
r
e
i
e
a
R.string.password_requirement_needed,
requirement)
}
composeTestRule
.onNodeWithText(result)
.assertIsDisplayed()
}
With this loop, our test is now loo ing through each of the avai abl Pas wor Re-
quir ment va ues and a ser ing that the e pe ted r quir ment me sage is dis-
@Test
fun Password_Requirements_Displayed_With_State() {
composeTestRule.setContent {
PasswordRequirements(
satisfiedRequirements = listOf(satisfied)
)
}
PasswordRequirement.values().forEach {
val requirement =
InstrumentationRegistry.getInstrumentation()
.context.getString(it.label)
val result = if (it == satisfied) {
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_satisfied,
requirement)
} else {
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_needed,
requirement)
}
composeTestRule
206
e
h
l
m
s
s
t
p
x
c
e
e
l
e
s
s
d
.onNodeWithText(result)
.assertIsDisplayed()
}
}
207
Tes ing the E ror Di log
Wit in our co le tion of co po ables for the a the ti tion screen, we also have
the A the ti tio Er o Di log that is used to di play e rors to the user.
While this only fe tures two a g ments that are used to di play and di miss the er-
ror, these are key to the o e tion of the di log, so we’ll add some tests to a sert
that these o e ate as e pe ted. These tests will live i side of a new test class, Au-
class AuthenticationErrorDialogTest {
@get:Rule
val composeTestRule = createComposeRule()
We’ll start here by wri ing a test that will be used to a sert that the provided e ror
new test that will be used to house this test l gic, co po ing an A the ti a-
tio Er o Di log that will be co posed u ing the provided e ror re e ence.
With this co po tion in place, we can then use our text rule to a sert that there is a
node di played that has the e act text b ing provided via the e ror a g ment.
@Test
fun Error_Displayed() {
val error = "This is an error"
composeTestRule.setContent {
AuthenticationErrorDialog(
error = error,
dismissError = { }
208
s
h
n
n
u
t
s
c
r
a
r
n
m
p
l
s
n
a
r
c
a
c
s
a
i
r
r
n
t
x
a
r
h
p
c
r
m
o
r
r
g
r
x
a
u
s
a
m
a
e
a
a
x
s
o
u
c
n
s
n
c
m
a
s
s
s
r
s
r
r
u
r
s
u
f
n
r
fi
r
s
c
)
}
composeTestRule
.onNodeWithText(error)
.assertTextEquals(error)
}
When it comes to di mis ing the di log, the A the ti tio Er o Di log com-
po able that di missal has been r que ted and our state needs to be u dated. If
this was broken for some rea on, then the user wouldn’t be able to di miss the dia-
Si i ar to ot er tests that we’ve wri ten for these co po ables, we’re g ing to
by ver f ing that the lambda has been i voked when e pe ted. After co po ing
this A the ti tio Er o Di log and provi ing the r quired a g ments, we
can tri ger the di missal by clic ing the node the has the e ror_a tion string re-
source a signed to it. When this is clicked, it is e pe ted that the di missal lambda
will be triggered. So here we’ll use the ver fy fun tion from mockito to ver fy that
@Test
fun Dismiss_triggered_from_action() {
val dismissError: () -> Unit = mock()
composeTestRule.setContent {
AuthenticationErrorDialog(
error = "This is an error",
dismissError = dismissError
)
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(R.string.error_action)
)
.performClick()
209
m
s
s
l
u
g
i
s
y
s
s
n
s
h
n
r
c
s
a
s
s
s
n
s
r
s
s
r
r
r
s
k
u
a
m
e
r
a
t
n
u
e
c
s
a
n
i
t
a
u
d
x
c
n
x
m
c
c
c
a
x
s
r
e
s
c
o
n
i
s
s
r
c
s
r
r
r
s
u
o
a
r
p
m
l
i
s
verify(dismissError).invoke()
}
210
With all of these tests in place, we’ve covered a lot of di fe ent cases that help to
e sure our UI is wor ing as e pe ted. We’ve not only tested that co po ables are
b ing co posed based on the i for tion that they are provided with, but also
that they triggered the e pe ted cal backs and tri ger state m ni l tions wit in
our co po ables. While the tests here aren’t e ten ive, we’ve been able to learn
not only what o tions are avai able to us while tes ing co po ables, but also the
211
p
n
e
m
m
s
p
k
x
c
x
l
c
n
o
l
m
a
x
g
t
s
f
m
r
s
a
p
m
u
a
s
h