Email Authentication Form in Jetpack Compose
Email Authentication Form in Jetpack Compose
ti tion 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 a 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 a the ti a-
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 tion.
1
u
c
a
l
o
c
n
n
a
u
c
s
l
a
c
d
t
c
c
e
r
s
f
m
x
x
f
m
t
u
o
p
c
n
f
e
s
d
n
h
c
a
c
e
a
x
m
m
r
x
e
i
c
t
u
f
u
r
c
n
n
p
x
k
c
a
t
c
m
a
u
d
u
e
n
n
f
h
c
t
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
2
a
v
r
r
n
f
r
s
t
n
r
c
t
r
u
n
fi
u
g
n
e
s
c
e
a
n
- Co rectly su port IME a tions, a lo ing eld f cus to
be changed and the form to be su mi ted via the
sof ware ke board
3
e
t
t
d
r
n
a
m
y
p
n
c
e
x
l
n
b
w
m
t
fi
o
e
u
m
n
c
s
a
i
h
D ing the A then-
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 be-
have to the user. Wit in our UI we’re g ing to need to show the
fo lo 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 au-
the ti tion ow. This bu ton will also be di abled
when there is no co tent i put into the email and
pas word elds
4
d
l
n
s
h
c
w
l
e
h
s
c
n
a
n
w
a
t
t
f
fi
s
fi
r
fi
fi
n
fl
u
n
s
l
d
c
h
a
a
n
n
l
l
t
n
n
c
t
e
n
o
m
m
o
o
g
s
u
i
e
s
s
m
l
o
- A pr gress i di a or which will di play when the
sign-in or sign-up o e tion is b ing pe formed
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. Let’s cr ate a
new class, A the ti tio State.
// [Link]
data class AuthenticationState(
5
d
o
v
p
r
u
o
l
d
s
n
n
c
p
c
t
n
p
r
a
a
s
n
s
f
e
r
o
s
m
e
e
s
r
s
e
n
h
e
e
o
Mo e ling the pas word re-
quir ments
Alon side the user b ing able to enter their pas word, we’re
also g ing to e force 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 pass-
word that is entered.
<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">
6
s
g
l
o
d
p
e
s
l
n
e
n
e
e
e
g
e
e
fi
m
e
u
s
o
c
n
s
e
s
At least 1 digit
</string>
We’ll then a just the Pas wor R quir ment to co tain a la-
bel in the form of a string r source i teger, se ting a r source
for each of the r quir ment enums va ues that we pr v ously
de ned.
CAPITAL_LETTER([Link].requirement_capi
tal),
NUMBER([Link].requirement_digit),
EIGHT_CHARACTERS([Link].requirement_ch
aracters)
}
Wit in the state, we’ll need the cu rent Pas wor R quire-
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 pas wor R quir ments
eld to our state that re re ents a list of r quir ments that have
been met.
7
fi
fi
h
d
s
e
t
s
fi
e
p
s
s
e
d
e
r
n
l
e
e
s
m
u
e
s
d
t
c
e
d
n
e
e
e
e
i
)
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 le ted mode changes.
8
l
p
d
e
o
e
s
m
l
s
o
v
p
g
u
e
c
i
a
c
o
p
n
h
c
a
c
e
a
e
l
fi
w
n
)
9
d
o
t
l
e
n
d
p
n
s
r
k
fl
o
m
p
r
a
x
n
d
c
m
t
p
n
p
r
a
s
n
fl
Mo e ling the e ror state
Once the loa ing of the r quest has been co pleted, we’ll
want to handle the r sponse that would come back from our
API. In the case of su cess, we would na i 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, the
co tent of that e ror.
10
u
n
l
p
d
d
s
l
l
d
p
r
n
c
a
e
r
e
c
b
r
e
c
n
r
fi
l
v
g
fi
r
m
l
n
m
fi
the form from b ing su mi ted if the pas word r quir ments
have not been sa i ed. To handle these cases we’ll add a new
fun tion to our state - this will r turn a boolean value that rep-
re ents whet er the form co tent is va id. We’re u ing a func-
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.
[Link]().toList()))
}
11
n
s
c
s
h
e
t
s
n
fi
h
b
r
t
n
e
h
l
l
s
c
e
s
e
o
}
With that in place, we now have a class that can be used to rep-
re ent the state of our UI. Over the next few se tions of this
chapter, we’ll uti ise this state when buil ing out our UI, mod fy-
ing its va ues as i te a tions with co po ables take place, trig-
ge ing r co po tions to r ect any state changes.
12
s
r
e
l
m
l
s
n
i
r
c
e
fl
m
d
s
c
i
Cr a ing the A then-
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 way to o che trate it
to the user i te face.
implementation
"[Link]:lifecycle-viewmodel-
compose:2.4.0"
// [Link]
13
i
e
e
m
d
n
c
a
t
e
a
l
i
w
n
n
w
d
e
t
r
e
d
o
l
r
k
w
p
d
w
d
x
w
r
w
d
o
c
u
w
u
d
d
d
u
r
n
e
k
n
c
s
n
c
a
class AuthenticationViewModel :
ViewModel() {
class AuthenticationViewModel :
ViewModel() {
val uiState =
MutableStateFlow(AuthenticationState())
}
14
c
d
a
a
e
e
l
c
f
r
w
n
e
p
n
d
f
r
n
u
r
n
e
t
o
e
t
d
u
e
d
n
o
c
f
a
r
e
b
d
a
e
p
s
v
u
e
l
y
f
f
t
fl
r
r
k
i
e
e
t
u
l
p
v
we’ll want to u date the state wit in our Vie Mo el so that the
o serving UI can r ect those changes.
// [Link]
sealed class AuthenticationEvent {
15
b
r
w
p
e
c
d
d
d
s
m
u
p
d
n
c
g
e
l
fl
t
f
u
r
p
w
s
d
n
c
a
n
h
p
w
o
o
u
e
g
c
m
f
u
g
a
d
r
s
w
f
c
r
d
c
d
u
a
n
fi
n
h
m
m
c
a
o
e
s
p
object ToggleAuthenticationMode:
AuthenticationEvent()
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 fun tion that
will be used to ‘ ip’ the cu rent a the ti tion mode - switc ing
it between sign-up and sign-in.
16
m
s
w
d
p
fl
r
u
n
e
c
t
a
h
o
c
n
h
d
With this code above, we take the cu rent a the ti tion mode
wit in our a the ti tion state and set it to the o po ite value.
We then use this to copy our e is ing a the ti tion state, set-
ting the a the ti tion mode to r ect the newly ca c lated
value. When this is done, the new value will be emi ted to the
o ser er of our live data - a lo ing our UI to be r co posed to
r ect this new state.
💡 The copy fun tion in Ko lin co ies the e is ing class ref-
e ence, r pl cing any va ues that have been provided as a gu-
ments to the fun tion.
Now that we have this fun tion avai able to handle the state
change of the a 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 will
take a re e ence to an A the ti tio Event.
fun handleEvent(authenticationEvent:
AuthenticationEvent) {
when (authenticationEvent) {
}
}
fun handleEvent(authenticationEvent:
AuthenticationEvent) {
17
e
r
b
fl
h
n
c
v
e
a
p
c
f
e
u
r
u
u
a
n
u
c
n
c
c
c
a
c
g
a
a
n
g
c
r
u
a
u
l
n
c
t
l
n
n
c
w
x
c
e
p
c
a
t
a
l
c
e
fl
r
t
n
u
n
t
o
x
u
n
t
w
c
a
n
e
p
c
f
d
a
m
t
r
s
g
w
l
u
r
d
when (authenticationEvent) {
is
[Link]
Mode -> {
toggleAuthenticationMode()
}
}
}
[Link](
[Link]
Mode
)
18
n
t
k
d
d
n
u
d
n
c
o
a
c
w
a
l
p
n
h
u
s
d
w
m
e
fi
g
h
p
m
s
s
e
fl
u
c
g
u
n
e
c
n
a
c
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.
object ToggleAuthenticationMode:
AuthenticationEvent()
19
d
v
d
v
c
e
fl
c
u
n
d
n
l
c
s
a
w
l
d
p
l
c
o
s
g
p
fi
f
m
r
e
}
if ([Link] > 7) {
20
n
p
w
d
p
h
e
e
o
t
s
fi
e
s
y
v
c
e
d
t
p
n
f
t
p
m
d
r
s
v
e
r
s
s
h
R
s
s
f
s
C
f
r
p
r
n
e
e
t
s
n
t
fi
e
e
e
l
s
e
e
d
e
[Link]([Link]
GHT_CHARACTERS)
}
if ([Link] { [Link]() }) {
[Link]([Link]
PITAL_LETTER)
}
if ([Link] { [Link]() }) {
[Link]([Link]
MBER)
}
21
i
s
d
e
e
e
n
s
e
s
h
s
t
d
.
n
n
e
P
T
t
s
e
fi
_
n
p
m
i
T
t
r
e
M
r
With this l gic now de ned, we can slot this into our u date-
Pas word fun tion and a sign the re ult to the pas wor Re-
quir ments wit in our A the ti tio State re e ence.
val requirements =
mutableListOf<PasswordRequirements>()
if ([Link] > 7) {
[Link]([Link]
GHT_CHARACTERS)
}
if ([Link]
{ [Link]() }) {
[Link]([Link]
PITAL_LETTER)
}
if ([Link] { [Link]() }) {
[Link]([Link]
MBER)
}
[Link] = [Link](
password = password,
passwordRequirements =
[Link]()
)
22
s
e
o
c
h
fi
u
s
n
c
a
s
n
f
s
r
p
d
}
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
event is triggered ou side of the Vie Mo el, the state can be
u dated based on the co re pon ing event.
fun handleEvent(authenticationEvent:
AuthenticationEvent) {
when (authenticationEvent) {
is
[Link]
Mode -> {
toggleAuthenticationMode()
}
is
[Link] -> {
updateEmail([Link]
ress)
}
is
[Link] -> {
updatePassword([Link]
ord)
}
}
23
p
c
fi
t
r
s
s
e
d
c
w
v
c
d
n
}
object ToggleAuthenticationMode:
AuthenticationEvent()
object Authenticate:
AuthenticationEvent()
24
h
h
l
d
u
c
n
d
c
a
e
u
g
u
n
n
u
c
n
a
s
t
n
r
u
c
c
a
a
n
c
a
e
e
fl
c
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 r spon ing to the a then-
ti tion r quest from the UI and r ec ing this via the loa ing
pro erty wit in the a the ti tion state.
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 a-
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
handled from ou side of our Vie Mo el.
fun handleEvent(authenticationEvent:
AuthenticationEvent) {
when (authenticationEvent) {
is
[Link]
Mode -> {
toggleAuthenticationMode()
}
is
[Link] -> {
25
c
m
a
t
c
u
p
e
e
l
h
w
c
g
t
t
u
n
e
u
n
c
a
n
x
c
w
k
e
fl
d
e
t
c
d
h
e
g
p
e
u
r
d
c
updateEmail([Link]
ress)
}
is
[Link] -> {
updatePassword([Link]
ord)
}
is
[Link] -> {
authenticate()
}
}
}
26
p
o
w
s
s
f
r
d
e
h
r
o
e
u
r
fi
n
p
n
p
r
g
c
w
r
r
a
d
u
u
u
p
n
n
n
l
c
c
c
a
a
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 state we will mod fy
our a the ti ate fun tion.
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 the a the ti tion
state.
[Link]([Link]) {
delay(2000L)
withContext([Link]) {
[Link] =
[Link](
isLoading = false,
error = “Something went
wrong!”
)
}
}
}
27
m
n
u
h
n
u
o
n
m
t
u
s
c
t
t
e
n
c
r
d
k
n
l
p
x
s
s
c
e
n
t
p
v
e
u
s
m
u
n
c
a
d
i
pe ted di patc ers. With this in place, the e ror will be emi ted
as part of the a the 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 tion. So that this can be
cleared from our state from our UI, we’re g ing to add a new
A the ti tio Event called E ro ismissed.
object ToggleAuthenticationMode:
AuthenticationEvent()
object Authenticate:
AuthenticationEvent()
object ErrorDismissed:
AuthenticationEvent()
}
28
u
a
c
n
c
s
a
s
e
n
h
u
a
n
a
y
c
a
r
n
m
r
p
D
r
s
i
e
e
r
o
m
e
r
m
fl
r
s
m
t
ment the state change for when this o curs. Here we’ll cr ate a
new fun tion that will be used to simply clear the e ror
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 tri ger our fun tion.
fun handleEvent(authenticationEvent:
AuthenticationEvent) {
when (authenticationEvent) {
is
[Link]
Mode -> {
toggleAuthenticationMode()
}
is
[Link] -> {
updateEmail([Link]
ress)
}
is
[Link] -> {
29
r
r
D
c
e
g
c
g
c
r
c
v
e
fi
updatePassword([Link]
ord)
}
is
[Link] -> {
authenticate()
}
is
[Link] -> {
dismissError()
}
}
}
30
u
s
n
c
a
m
y
e
d
n
a
p
u
e
d
n
e
c
n
r
Cr a ing the A then-
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 like the fol-
lo ing:
This UI will give our users a screen that can be used to log in to
an a pli tion - gi ing the o tion of pe for ing either a sign-in
31
w
c
p
e
a
c
u
a
o
w
n
t
c
a
d
v
p
m
e
n
e
e
t
r
fi
u
m
n
m
d
s
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 A the ti tion screen.
32
fi
e
e
t
fi
u
n
p
c
r
a
a
o
m
e
s
d
d
d
m
n
o
n
fi
x
.
o
o
r
i
// Provides an extended collection of
Material Iconography
implementation
"[Link]:material-
icons-extended:1.5.4"
// [Link]
@Composable
33
c
u
m
s
s
s
d
i
n
r
t
c
fi
c
a
e
d
e
m
r
u
u
n
a
u
n
n
d
c
o
c
a
n
h
o
a
l
c
m
a
t
m
d
n
e
s
r
s
t
s
c
d
w
e
a
fun Authentication() { }
@Composable
fun Authentication() {
MaterialTheme {
}
}
This now means that for any of the co po ables that are
co posed in the co tent block of our M te a Theme, these
34
c
e
e
n
o
t
a
m
m
p
s
h
r
r
n
a
p
n
m
n
u
p
h
s
o
c
c
a
n
m
m
o
n
t
o
m
s
t
n
m
u
s
r
l
z
a
m
r
c
v
v
r
g
m
m
i
s
u
u
i
a
t
l
c
o
s
s
r
n
n
i
l
c
c
u
a
m
v
a
y
m
m
n
l
s
h
u
c
s
s
a
u
will be themed a cor ing to the co ors and styles that are de-
clared wit in our theme.
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState:
AuthenticationState,
handleEvent: (event:
AuthenticationEvent) -> Unit
) {
35
u
y
m
s
n
s
o
t
h
c
s
m
s
a
u
n
s
o
fi
c
n
c
n
e
d
c
a
o
o
m
e
d
l
n
i
s
i
n
n
e
r
l
r
r
d
r
e
i
u
fi
o
u
n
p
s
n
e
f
e
m
x
i
r
m
s
c
m
u
m
s
d
m
fi
n
s
c
helps to keep your co po able fun tions re-u able across your
UI.
So, why can’t this just be the entry point to our a the ti a-
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 ser-
tions based on that, rather than nee ing to si late user ac-
tions and pe form moc ing to pr duce e pe ted states.
@Composable
fun Authentication() {
MaterialTheme {
AuthenticationContent(
modifier =
[Link](),
authenticationState = ...,
handleEvent = ...
)
}
36
e
p
m
n
s
l
u
s
m
c
a
n
s
m
r
n
c
a
s
n
e
m
k
fi
s
i
h
n
u
u
o
n
fi
c
n
d
c
b
a
m
c
r
x
a
u
c
n
s
n
m
m
u
u
e
r
m
n
n
c
s
s
d
}
37
u
m
c
d
n
w
e
s
n
d
r
e
n
r
e
u
c
e
m
w
l
m
d
s
r
s
fi
e
c
u
d
t
w
s
t
n
e
m
s
s
w
fi
e
k
d
w
m
f
t
e
s
e
a
w
r
d
i
d
e
c
d
u
n
s
d
h
f
c
r
l
n
c
n
e
o
n
c
a
w
f
t
u
s
n
i
m
n
m
r
e
n
w
s
s
c
@Composable
fun Authentication() {
val viewModel:
AuthenticationViewModel = viewModel()
MaterialTheme {
AuthenticationContent(
modifier =
[Link](),
authenticationState =
[Link]().value
,
handleEvent =
viewModel::handleEvent
)
}
}
38
c
n
u
n
e
i
m
m
s
m
s
k
p
m
c
s
e
t
s
a
e
o
u
n
c
c
a
u
d
m
n
c
• A Pa ent Box to hold the di fe ent co po ables that make up
our a the ti tion screen
With the above in mind, we can start buil ing out our Com-
po able UI to re re ent our a the ti tion screen. We’re g ing
39
s
fi
u
r
o
u
n
n
c
a
t
a
c
a
n
p
c
s
t
h
s
m
g
f
u
i
r
s
n
u
m
c
a
m
n
d
n
d
c
s
a
r
o
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.
40
o
m
m
e
u
e
fi
s
s
n
r
n
n
c
l
o
a
c
c
e
t
h
e
m
a
n
s
n
r
o
l
m
s
c
s
a
s
n
y
f
r
r
h
n
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 fe 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 com-
po ables - which makes it pe fect for what we need.
// [Link]
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier
) {
Box {
}
}
Box(
modifier = modifier,
contentAlignment = [Link]
) {
41
m
s
n
s
s
m
n
l
u
s
s
fi
l
n
p
m
c
a
r
l
s
p
x
r
p
n
l
d
d
i
fi
fi
f
n
r
w
n
r
n
p
n
i
l
fi
l
wit in its pa ent), along with u ing the co ten lig ment ar-
g ment to align all of its chi dren in the ce ter of the Box.
42
u
o
e
h
n
s
c
a
n
d
r
n
o
f
c
s
r
t
o
n
l
c
s
s
s
t
f
r
w
s
d
c
n
n
n
n
e
p
i
t
e
A
s
n
o
n
adding an if stat ment that checks the status of this loa ing
ag.
Box(
modifier = modifier,
contentAlignment = [Link]
) {
if ([Link]) {
} else {
}
}
if ([Link]) {
CircularProgressIndicator()
} else {
43
fl
u
c
c
d
t
t
r
o
n
d
l
s
e
n
n
c
fi
c
o
t
n
o
t
n
n
m
e
n
c
e
t
n
fi
n
e
m
h
s
r
n
u
e
r
m
m
o
s
o
n
d
s
l
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 co po able so that the in-
di a or can be co posed to re re ent that cu rent pr gress.
44
o
n
c
t
s
s
y
u
o
e
m
fi
n
c
a
o
e
p
o
u
s
o
n
m
n
c
o
o
a
c
s
t
r
n
fl
c
n
t
o
s
y
cr a ing a new co po able fun tion that will be used to house
our a the ti tion form.
// [Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
)
@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 co po ables to be p s tioned in the ce ter ho-
r zon ally. For this we’ll uti ise the h r zon a lig ment ar-
g ment, provi ing Cente H r zon ally as the value to be
used for alig ment.
45
n
i
u
e
r
m
m
m
k
t
c
t
u
f
s
s
s
r
n
c
n
a
d
m
m
p
m
m
u
s
s
o
s
n
n
r
l
c
a
o
i
c
o
o
t
i
i
n
u
o
t
e
n
l
fi
A
n
c
o
a
p
n
p
n
o
d
n
s
i
r
i
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment =
[Link]
) {
}
}
46
n
o
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
adding two new strings to our strin [Link] le:
<string name="label_sign_in_to_account">
Sign In to your account
</string>
47
s
fi
d
e
s
u
d
g
n
u
c
fi
r
a
n
c
a
e
<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 tion title, we’ll need to cre-
ate a new co po able fun tion, A the ti tio Title.
// [Link]
@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 to the co po able func-
tion.
@Composable
fun AuthenticationTitle(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
)
48
e
u
n
o
m
d
c
a
s
r
n
n
c
m
u
u
r
e
t
f
u
n
i
n
r
u
c
a
c
a
m
n
d
c
n
s
a
s
m
n
e
s
s
e
o
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 the r source to be
used for our Text co po able.
@Composable
fun AuthenticationTitle(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
[Link].label_sign_in_to_account
} else {
[Link].label_sign_up_for_account
}
)
)
}
Our title will now be di pla ing a string based on the A then-
ti tio Mode that is provided to the fun tion.
49
c
a
n
h
u
r
n
c
m
a
s
s
n
y
n
n
n
c
n
e
s
u
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 [Link] to the fon Size a g ment of
the co po able.
Text(
text = stringResource(
if
([Link]
==
AuthenticationMode.SIGN_IN)
{
50
p
o
e
m
c
a
m
s
s
m
o
s
c
p
fi
t
d
t
t
r
u
[Link].label_sign_in_to_account
} else {
[Link].label_sign_up_for_account
}
),
fontSize = [Link]
)
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
([Link]
==
AuthenticationMode.SIGN_IN)
{
[Link].label_sign_in_to_account
} else {
[Link].label_sign_up_for_account
}
),
fontSize = [Link],
51
d
t
t
.
s
m
s
p
fontWeight = [Link]
)
// [Link]
@Composable
fun AuthenticationForm(
52
fi
u
m
h
u
n
s
o
n
c
a
c
e
a
n
n
r
u
f
r
n
u
m
n
c
n
a
m
c
n
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
)
// [Link]
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState:
AuthenticationState,
handleEvent: (event:
AuthenticationEvent) -> Unit
) {
...
AuthenticationForm(
modifier =
[Link](),
authenticationMode =
[Link]
)
...
}
53
l
p
m
m
s
i
u
u
u
n
c
n
n
a
c
c
a
a
n
n
n
m
pass in the r quired A the ti tio 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
)
}
}
To cr ate a bit of vis al sp cing at the top of our UI, we’ll also
add a Spacer co 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(
54
e
c
s
e
m
u
s
d
u
i
fi
u
a
n
n
c
c
a
a
n
f
r
modifier = modifier
) {
Spacer(modifier =
[Link]([Link]))
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 tio Mode that is wit in our
state.
55
e
m
r
h
u
n
c
a
u
n
n
c
a
m
h
56
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 all g ing
to be co tained wit in a Card co po ent. The Card is a com-
po able used to hold child co po ents in a Card shaped con-
tai er, o ten e e ated from the su face that it is di played on
top of.
57
o
n
s
n
e
h
m
f
n
t
n
s
o
m
l
v
s
h
c
k
n
m
m
a
n
r
e
h
i
n
n
u
fi
m
n
s
n
n
n
c
t
o
u
co po tion from where we last a ded the A the ti tion-
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 then-
ti tio Title d cla tion - this is so that our Card does not
end up pressed up right against the bo tom of our title.
// [Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier =
[Link]([Link]))
AuthenticationTitle(
modifier =
[Link](),
authenticationMode =
authenticationMode
)
Spacer(modifier =
[Link]([Link]))
}
}
58
n
m
c
a
h
s
i
e
n
e
m
r
a
s
d
t
o
e
u
n
c
a
u
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(
modifier = modifier
) {
Spacer(modifier =
[Link]([Link]))
AuthenticationTitle(
modifier =
[Link](),
authenticationMode =
authenticationMode
)
Spacer(modifier =
[Link]([Link]))
Card {
Column(
modifier =
[Link]([Link]),
horizontalAlignment =
[Link]
59
m
s
e
r
a
) {
}
}
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = [Link])
) {
60
i
p
s
n
l
k
t
p
e
n
d
m
x
y
i
n
m
fi
d
d
r
n
r
i
fi
m
c
o
t
fi
s
l
r
l
c
o
m
m
i
n
n
m
m
s
s
t
s
e
s
n
d
w
l
A
s
e
n
m
l
n
d
i
o
fi
r
n
i
u
r
o
u
t
l
p
v
l
d
a
h
fi
Column(
modifier =
[Link]([Link]),
horizontalAlignment =
[Link]
) {
}
}
Cu rently, our card won’t look like much, but we’d be able to
see som thing like the fo lo ing cu rently.
61
r
e
l
w
r
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 back-
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-
tion a g ment of the Card.
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = [Link]),
elevation = [Link]
) {
We can see now that our Card has grea er pa ding a plied to
it.
62
e
e
r
u
o
n
l
v
a
s
t
d
r
p
r
l
d
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
) {
63
m
s
n
e
u
l
n
w
c
n
a
fi
n
Spacer(modifier =
[Link]([Link]))
AuthenticationTitle(
authenticationMode =
authenticationMode
)
Spacer(modifier =
[Link]([Link]))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal =
[Link]),
elevation = [Link]
) {
Column(
modifier =
[Link]([Link]),
horizontalAlignment =
[Link]
) {
}
}
}
}
64
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 start here by cr a ing a new
co po able, Emai I put.
// [Link]
@Composable
fun EmailInput(
modifier: Modifier = Modifier
65
n
n
m
s
fi
s
y
l
n
n
v
d
n
e
t
fi
)
For the email i put, we’re g ing to e plore the use of the
Tex Field co po able, which we can add di ectly to our
co po able fun tion.
@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 Tex Field r quires two a g ments for the func-
tion to be sa i ed. These are the value (that re re ents the cur-
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 co po able state). We’ll need to sa i fy these be-
fore we can co ti ue, so let’s start by f cu ing on the r quired
value a g ment.
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
66
p
n
e
m
h
p
t
t
r
s
o
u
m
n
d
t
u
s
s
m
fi
n
m
n
c
fi
t
n
n
p
s
c
s
a
s
s
o
s
e
o
r
n
o
x
r
t
fi
u
s
p
r
t
s
r
s
u
n
d
e
d
email: String?
) {
TextField(
modifier = modifier,
value = email ?: ""
)
}
@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 wit in the o Valu Change block.
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
67
t
d
m
m
l
s
d
e
c
e
r
c
fi
m
r
u
p
s
h
i
d
n
v
g
e
fl
e
o
r
n
c
t
fi
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 pl men ing that part of the pr ject.
68
m
n
s
l
c
m
m
n
l
d
s
e
w
n
t
c
fi
p
e
n
m
o
Adding a l bel to the email i put
eld
At the m ment out i put eld looks a little blank, and it isn’t too
clear what the i put eld is to be used for. To add some cla ity
here, we’re g ing to uti ise the l 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:
69
fi
e
m
s
o
o
n
o
a
n
fi
d
l
fi
a
a
r
u
o
n
r
<string name="label_email">Email
Address</string>
@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 =
[Link].label_email)
)
}
)
}
70
a
n
m
t
s
m
m
e
s
With this in place, our Tex Field is now di pla ing a l bel that
adds cla ity for what the eld is to be used for.
71
n
r
r
n
x
fi
t
x
p
n
l
fi
r
s
y
m
n
o
a
fi
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 gu-
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 r zon al
na i tion to move through an entered string when this is en-
abled.
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) ->
Unit
) {
TextField(
72
v
e
g
a
m
s
fi
l
w
m
l
x
s
c
n
e
l
a
e
o
i
r
t
modifier = modifier,
value = email ?: "",
onValueChange = { email ->
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id =
[Link].label_email)
)
},
singleLine = true
)
}
We can see now that with this b ing e forced, our Tex Field
b haves in a much more e pe ted way.
73
e
x
c
e
n
t
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 leadin-
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 any real pu pose ot er than b ing a vis al dec-
o tion.
@Composable
fun EmailInput(
74
g
I
r
o
a
s
n
r
u
l
u
l
m
r
c
s
n
r
a
o
h
n
s
fi
l
e
m
s
u
o
modifier: Modifier = Modifier,
email: String,
onEmailChanged: (email: String) ->
Unit
) {
TextField(
modifier = modifier,
value = email,
onValueChange = { email ->
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id =
[Link].label_email)
)
},
singleLine = true,
leadingIcon = {
Icon(
imageVector =
[Link],
contentDescription =
null
)
}
)
}
75
e
n
t
e
p
u
c
r
a
n
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 user.
With this in place, we can now see an email icon is di played at
the start of the i put eld.
76
fi
n
m
c
m
m
s
s
u
h
n
a
n
fi
n
c
e
a
e
e
b
n
m
o
E
l
m
r
n
n
f
n
fi
s
r
m
fi
our state, so we’ll need to rst add these as a g ments to our
A the ti tio Form co po able.
// [Link]
@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 re e ence from with-
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 handle-
Event lambda. When cal ing this, we’ll uti ise the Email-
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.
// [Link]
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState:
AuthenticationState,
handleEvent: (event:
AuthenticationEvent) -> Unit
77
u
g
c
n
fi
d
c
a
u
n
n
c
n
a
m
n
fi
l
i
n
t
s
E
d
e
f
l
r
u
s
l
f
m
l
n
r
r
u
c
a
d
n
) {
...
AuthenticationForm(
modifier =
[Link](),
authenticationMode =
[Link],
email =
[Link],
onEmailChanged = { email ->
handleEvent(
[Link](email))
}
)
...
}
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
email: String?,
78
e
c
p
s
u
m
E
n
c
l
a
u
n
l
c
n
a
f
r
n
m
authenticationMode:
AuthenticationMode,
onEmailChanged: (email: String) ->
Unit
) {
Column(
modifier = modifier,
horizontalAlignment =
[Link]
) {
Spacer(modifier =
[Link]([Link]))
AuthenticationTitle(
authenticationMode =
authenticationMode)
Spacer(modifier =
[Link]([Link]))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal =
[Link]),
elevation = [Link]
) {
Column(
modifier =
[Link]([Link]),
horizontalAlignment =
79
[Link]
) {
EmailInput(
modifier =
[Link](),
email = email ?: "",
onEmailChanged =
onEmailChanged
)
}
}
}
}
EmailInput(
modifier = [Link](),
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 cu rent email that is wit in our
80
o
m
fi
l
s
o
n
m
fi
l
e
d
m
r
m
l
u
p
l
n
h
h
r
fi
l
p
x
n
s
m
u
n
d
e
i
fi
n
t
d
h
c
i
fi
a
state, which in turn is then u dated via the use of our onE-
mai Changed lambda fun tion.
81
l
c
p
Di pla ing the Pas word In-
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 g ing to be the same as
our pr v ously cr ated co po able, so we’re g ing to start by
d pli a ing what we have so far, a ap ing it for pas word entry.
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
82
u
e
s
s
c
e
t
i
m
l
y
e
m
n
m
m
n
x
s
n
d
fi
t
s
o
o
o
s
password: String,
onPasswordChanged: (email: String)
-> Unit
) {
TextField(
modifier = modifier,
value = password,
onValueChange = {
onPasswordChanged(it)
},
singleLine = true,
label = {
Text(text =
stringResource(id =
[Link].label_password)
)
}
leadingIcon = {
Icon(
imageVector =
[Link],
contentDescription =
null
)
}
)
}
83
f
r
- We pass some slightly di fe ent a g ments into the
co po able. This time in the form of a pas word
and o Pas wor Changed cal back.
We are also u ing a slightly di fe ent l bel for our i put eld,
which will need to be a ded to our strin [Link] r source le
b fore we can co ti ue.
<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:
84
e
m
n
s
s
s
s
n
s
g
d
n
n
d
n
f
s
l
m
r
f
n
r
d
s
r
fi
u
a
g
s
l
e
v
n
fi
fi
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.
85
m
m
g
m
s
m
m
f
s
e
r
s
s
b
o
s
l
t
s
d
m
s
l
d
r
n
t
n
s
m
s
s
fi
e
d
e
b
n
n
t
s
fi
l
m
n
l
s
l
fi
s cu ity rea ons, fo lowed by wra ping this in r me ber so
that the value is pe sisted across r co po tions.
@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 =
[Link].label_password)
)
}
leadingIcon = {
Icon(
86
e
r
s
r
l
e
p
m
s
i
e
m
imageVector =
[Link],
contentDescription =
null
)
}
)
}
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 trailin-
con of the Tex Field co po able. We’ll start by co pos-
ing a new Icon, whose co tent will d pend on the cu rent
state of i Pas wor Hi den When the pas word is b ing hid-
den we want to show an icon that i di ates the vi i i ity is dis-
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 con-
ten D scri tion, as we’ll be i pl men ing that piece of lo-
gic shortly.
Icon(
imageVector = if (isPasswordHidden)
{
[Link]
} else [Link],
contentDescription = null
87
g
I
s
t
e
c
s
fi
p
s
s
r
t
d
p
h
s
d
s
b
l
n
o
m
m
s
n
e
c
l
e
e
t
s
fi
e
e
l
n
c
s
b
m
l
e
m
m
r
b
)
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 = [Link] {
isPasswordHidden = !
isPasswordHidden
},
imageVector = if (isPasswordHidden)
{
[Link]
} else [Link]
)
This means that now when our Icon is clicked, the state ag
will be ipped and our Icon will be r co posed to r ect this
state change.
88
n
fl
s
fl
s
n
d
fl
t
s
d
e
p
k
m
s
d
o
i
fi
e
fl
fl
With this in place, we have a fun tio ing icon that can be i ter-
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 scrip-
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 Hid-
den state. We’ll need to start here by adding two new string re-
sources to our strin [Link] r sources:
<string name="cd_show_password">Show
Password</string>
<string name="cd_hide_password">Hide
Password</string>
89
c
c
n
r
s
k
n
a
e
p
m
c
g
n
s
b
w
l
k
e
c
r
n
r
d
i
fi
s
s
l
e
d
n
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 func-
tion here to provide a string r source based on the cu rent
value of our state.
Icon(
modifier = [Link](
onClickLabel = if
(isPasswordHidden) {
stringResource(id =
[Link].cd_show_password)
} else stringResource(id =
[Link].cd_hide_password)
) {
isPasswordHidden = !
isPasswordHidden
},
imageVector = if (isPasswordHidden)
{
[Link]
} else [Link],
contentDescription = null
)
90
m
d
i
fi
s
t
e
p
l
p
t
n
e
g
e
p
a
m
m
n
g
I
s
r
@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 =
[Link],
contentDescription =
null
)
},
trailingIcon = {
Icon(
modifier =
[Link](
91
onClickLabel = if
(isPasswordHidden) {
stringResource(id =
[Link].cd_show_password)
} else
stringResource(id =
[Link].cd_hide_password)
) {
isPasswordHidden = !
isPasswordHidden
},
imageVector = if
(isPasswordHidden) {
[Link]
} else
[Link],
contentDescription =
null
)
},
label = {
Text(text =
stringResource(id =
[Link].label_password))
}
)
92
}
While we’ve i pl me ted the fun tio a ity to now toggle this
state ag, it’s not b ing used yet to a fect the vi i i ity of the
pas word eld co tent. For these sce ar os, the Tex Field con-
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 Pas wor Vis a Tran for a-
tion class, which can be provided for the vis a Tran form-
tion pro erty.
visualTransformation = if
(isPasswordHidden) {
PasswordVisualTransformation()
} else [Link]
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String)
-> Unit
) {
var isPasswordHidden by remember {
mutableStateOf(true)
93
a
l
n
s
fl
s
u
fi
p
l
u
m
m
o
m
l
a
e
s
n
e
s
n
m
a
m
a
t
s
c
s
s
p
n
n
f
d
s
h
l
i
r
m
m
u
a
u
s
l
s
l
s
s
b
d
m
t
l
s
a
s
n
m
}
TextField(
modifier = modifier,
value = password ?: "",
singleLine = true,
onValueChange = {
onPasswordChanged(it)
},
leadingIcon = {
Icon(
imageVector =
[Link],
contentDescription =
null
)
},
trailingIcon = {
Icon(
modifier =
[Link](
onClickLabel = if
(isPasswordHidden) {
stringResource(id =
[Link].cd_show_password)
} else
stringResource(id =
[Link].cd_hide_password)
94
) {
isPasswordHidden = !
isPasswordHidden
},
imageVector = if
(isPasswordHidden) {
[Link]
} else
[Link],
contentDescription =
null
)
},
label = {
Text(text =
stringResource(id =
[Link].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.
95
n
fl
g
p
s
s
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 com-
pose it i side of our A the ti tio Form, we need to en-
sure there is a cess to both a pas word and o Pas word-
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 pos-
able.
// [Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
96
m
n
r
f
u
r
c
s
h
u
n
n
u
m
c
e
a
s
n
s
m
n
c
a
n
n
fi
s
m
o
email: String,
password: String,
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 e 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 handle-
Event lambda. When cal ing this, we’ll uti ise the Pas word-
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.
// [Link]
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState:
AuthenticationState,
handleEvent: (event:
AuthenticationEvent) -> Unit
) {
...
AuthenticationForm(
modifier =
[Link](),
97
g
c
s
d
u
n
c
n
a
l
n
n
i
n
t
s
s
e
f
d
f
r
u
r
l
s
l
m
n
c
a
d
s
h
n
authenticationMode =
[Link],
email =
[Link],
password =
[Link],
onEmailChanged = {
handleEvent([Link]
nged(it))
},
onPasswordChanged = {
handleEvent(
[Link](it))
}
)
...
}
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
98
p
e
c
s
s
m
m
s
n
u
s
u
s
n
s
n
c
n
c
d
a
a
d
n
fi
n
f
r
m
u
authenticationMode:
AuthenticationMode,
email: String,
password: String,
onEmailChanged: (email: String) ->
Unit,
onPasswordChanged: (password:
String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment =
[Link]
) {
Spacer(modifier =
[Link]([Link]))
AuthenticationTitle(
authenticationMode =
authenticationMode)
Spacer(modifier =
[Link]([Link]))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal =
[Link]),
elevation = [Link]
) {
Column(
99
modifier =
[Link]([Link]),
horizontalAlignment =
[Link]
) {
EmailInput(
modifier =
[Link](),
email = email ?: "",
onEmailChanged =
onEmailChanged
)
Spacer(modifier =
[Link]([Link]))
PasswordInput(
password =
password ?: "",
onPasswordChanged =
onPasswordChanged
)
}
}
}
}
100
o
m
s
n
s
o
d
fi
n
l
d
fi
r
m
u
p
l
h
p
n
s
m
d
i
e
fi
t
the pa ent Column. For this, we’ll uti ise the fil Ma Width
mo er.
PasswordInput(
modifier = [Link](),
password = password ?: "",
onPasswordChanged =
onPasswordChanged
)
With this in place, the pas word i put eld for our a the ti a-
tion form is now b 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 of our
o Pas wor Changed lambda fun tion.
101
n
h
d
i
fi
s
r
d
m
e
m
s
n
c
h
p
l
fi
r
s
l
u
x
n
n
c
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 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 r ferred to as
IME a tions, som thing that we still have a cess to in Je pack
Co pose u der the name of Ke board O tions. In our a then-
ti tion screen we’re g ing to want to uti ise some of these op-
tions to a low:
102
x
c
n
a
m
c
s
c
n
m
d
l
n
r
e
n
a
x
s
t
l
p
r
e
i
n
t
o
y
l
y
l
w
y
n
l
e
fi
fi
l
p
d
c
c
e
y
v
u
g
t
- Na i tion from the email a dress eld to the pass-
word eld
These o tions t get er will a low the user to na i ate and sub-
mit the form, sea 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 cu 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 sp ci ally for email
i put (sho ing the ‘@‘ sy bol and cli board o tions).
TextField(
…,
keyboardOptions = KeyboardOptions(
keyboardType = [Link]
)
)
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 ke board o tions in the form of the Next a tion.
TextField(
…,
keyboardOptions = KeyboardOptions(
103
n
o
s
p
b
p
v
fi
y
g
p
r
a
s
o
w
n
d
s
p
o
y
m
y
h
e
m
y
fi
p
c
c
n
l
m
d
n
d
n
n
l
t
t
r
p
d
n
o
fi
s
fi
e
u
e
p
c
fi
v
fi
g
c
n
n
m
c
a
m
s
l
fi
c
y
m
n
fl
imeAction = [Link],
keyboardType = [Link]
)
)
We’ll be able to see now that the pr v ously shown ‘r turn’ icon
has now been r placed with a ‘next’ icon.
104
m
s
l
c
c
e
g
s
d
y
e
d
r
d
c
h
n
d
c
e
e
i
p
y
y
n
y
r
l
e
c
r
d
t
o
c
c
which in turn will be triggered when the co re pon ing ime-
A tion is i te a ted with. When the o Next a tion is i te ac-
ted with, we’re g ing to want to change the cu rent f cus on
the screen - ta ing this from the cu rently f cused email ad-
dress text eld, r ques ing f cus on the pas word text eld.
AuthenticationTitle(
modifier = [Link](),
authenticationMode =
authenticationMode
)
Spacer(modifier =
[Link]([Link]))
val passwordFocusRequester =
FocusRequester()
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = [Link]),
elevation = [Link]
) { ... }
105
o
c
f
s
m
r
o
e
s
fi
n
fi
r
k
v
d
o
d
fi
e
c
i
o
fi
i
fi
s
e
m
t
a
s
o
l
o
o
r
n
e
s
s
s
r
s
o
o
s
c
r
s
s
e
d
fi
e
fi
o
fi
l
n
o
n
r
re e ence to be used to r quest f cus for the co po able that
it is a signed to.
PasswordInput(
…,
modifier =
[Link](passwordFocusReq
uester)
)
TextField(
…,
keyboardActions = KeyboardActions(
onNext = {
}
)
)
106
e
f
d
r
g
s
n
y
y
t
a
r
o
d
r
d
s
o
c
p
y
e
r
n
fi
d
e
c
p
l
c
o
d
c
n
o
o
c
t
p
m
o
n
s
p
s
At this point though, we don’t have an thing that we can trig-
ger from wit in this o Next block. We could pass the f cus re-
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 lambda a g ment to our co po able func-
tion.
@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) ->
Unit,
onNextClicked: () -> Unit
)
TextField(
…,
keyboardActions = KeyboardActions(
onNext = {
onNextClicked()
}
)
)
107
h
r
o
u
m
f
h
r
n
o
e
n
g
n
r
e
n
r
u
m
t
l
t
n
s
n
y
y
t
r
m
d
m
c
s
m
s
r
s
r
o
n
u
m
Wit in this i pl men tion, we’re then g ing to use our Fo-
cu Requester to tri ger the r sues F cus() fun tion.
EmailInput(
modifier = [Link](),
email = email,
onEmailChanged = onEmailChanged
) {
[Link]()
}
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 man ally i te act with either of the co po ables.
108
o
s
h
d
p
s
t
c
s
s
m
c
t
u
e
g
t
h
n
d
t
a
c
t
r
n
e
o
b
v
g
b
m
s
t
l
l
o
o
w
o
e
s
m
d
m
fi
s
n
c
s
e
c
fi
e
s
s
tion Done a tion type to our Ke boa O tions - a sig ing
this to our Pas wor I put Tex Field.
TextField(
…,
keyboardOptions = KeyboardOptions(
imeAction = [Link]
)
)
We’ll be able to see now that the pr v ously shown ‘r turn’ icon
has now been r placed with a ‘done’ icon.
109
.
c
s
e
d
n
t
y
e
i
r
d
p
e
s
n
We then again need to provide a Ke boa A tions i pl ment-
tion to our tex eld, this time i pl men ing the o Done hand-
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 the user can move fo ward to a the ti ate their cr den-
tials.
TextField(
…,
keyboardActions = KeyboardActions(
onDone = {
}
)
)
@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String,
onPasswordChanged: (email: String)
-> Unit,
onDoneClicked: () -> Unit
)
110
a
u
d
h
t
fi
fi
n
r
u
n
s
r
m
fi
e
y
u
y
m
t
r
d
n
s
c
c
s
n
m
c
e
m
e
With that in mind, wit in this han ler, we’ll use our o Done-
Clicked() fun tion to tri ger the cal back.
TextField(
…,
keyboardActions = KeyboardActions(
onDone = {
onDoneClicked()
handleEvent([Link]
cate)
}
)
)
PasswordInput(
modifier = [Link]()
.focusRequester(passwordFocusReq
uester),
password = password,
onPasswordChanged =
onPasswordChanged,
onDoneClicked = {
111
m
e
o
c
e
h
g
n
d
e
l
r
r
u
m
n
s
}
)
// [Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
email: String?,
password: String?,
onEmailChanged: (email: String) ->
Unit,
onPasswordChanged: (password:
String) -> Unit,
onAuthenticate: () -> Unit
)
112
g
g
n
n
o
r
r
u
n
r
u
d
u
fi
o
n
m
c
a
u
s
l
m
fl
n
c
e
c
a
n
h
n
g
u
l
t
e
m
s
u
u
n
s
c
n
n
a
s
c
c
c
// [Link]
AuthenticationForm(
modifier = [Link](),
email = [Link],
password =
[Link],
authenticationMode =
[Link],
onEmailChanged = {
handleEvent([Link]
nged(it))
},
onPasswordChanged = {
handleEvent([Link]
Changed(it))
},
onAuthenticate = {
handleEvent([Link]
cate)
}
)
113
p
r
l
u
n
c
a
u
s
u
n
d
n
c
n
c
a
m
s
n
m
c
s
fi
is b cause the fun tion si n tures match - so there’s no need
for us to re-i pl ment the cal back.
PasswordInput(
modifier = [Link]()
.focusRequester(passwordFocusReq
uester),
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 provide a ke board type to our
Ke board O tions i stance. Si i ar to how we did for the Email
A dress text eld, this will a low the ke board to be cu to ised
based on the type of co tent that is b ing entered.
TextField(
…,
keyboardOptions = KeyboardOptions(
imeAction = [Link],
keyboardType = [Link]
)
)
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
114
u
d
y
e
l
n
d
c
m
a
m
p
fi
fl
e
s
c
y
n
fi
c
fi
n
g
l
n
a
f
l
m
l
e
s
y
o
fi
y
fi
s
fi
s
m
m
and le ters, r mo ing the use of some sp cial cha a ters and
cli board o tions.
115
p
t
p
e
v
e
r
c
Pas word R quir ments
At this point, our user can enter their cr de tials into our au-
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 Pass-
wor R quir ments, a lo ing us to e force ce tain r quire-
ments on the entered pas word. To r cap, these r quir ments
were:
116
n
h
d
c
e
a
s
e
fi
l
e
w
s
fi
fi
e
e
n
s
e
n
d
e
r
e
i
e
e
e
o
CAPITAL_LETTER, NUMBER,
EIGHT_CHARACTERS
}
117
e
e
m
e
t
s
e
e
s
fi
o
t
p
r
fi
o
s
s
e
e
r
c
k
k
m
e
e
n
s
e
e
m
e
s
p
t
s
t
u
For this co po able, we’re g ing to di play a simple UI com-
po ent that will di play a l bel for the r quir ment, along with
an icon that will i di ate if this r quir ment has been sa i ed.
We’ll start by cr a ing a new co po able fun tion that takes
our d fault mo er as an a g ment.
@Composable
fun Requirement(
modifier: Modifier = Modifier
)
118
n
e
m
d
s
i
e
fi
n
s
t
c
a
r
u
o
e
m
e
s
s
e
e
c
t
s
fi
We me tioned above about the two r quir ments for this com-
po able fun tion - the me sage to be di played, along with
whet er the r quir ment is cu rently sa i ed. We’ll add more
two a g ments - a string r source that will be used for the l bel
of our r quir ment co po able, along with a boolean ag to
su port the sa i ed status.
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
)
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
119
a
h
p
n
s
h
r
r
a
n
u
e
d
r
i
fi
c
s
e
r
r
e
t
c
u
s
r
c
fi
y
m
e
n
l
s
m
A
n
e
m
s
s
e
n
l
r
d
s
d
o
i
r
fi
e
i
u
t
c
s
fi
e
t
s
w
m
s
s
m
s
m
n
d
fl
i
s
a
i
) {
Row(
modifier =
[Link]([Link]),
verticalAlignment =
[Link]
) {
}
}
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier =
[Link]([Link]),
verticalAlignment =
[Link]
120
c
r
n
d
s
i
b
fi
l
e
fi
p
m
e
n
e
m
s
m
k
s
o
s
d
a
t
n
o
g
i
) {
Icon(
modifier =
[Link]([Link]),
imageVector =
[Link],
contentDescription = null
)
}
}
With our icon now in place, let’s add the l bel that will de-
scribe what the r quir ment is. For this, we’ll use the Text com-
po able and a sign the me sage from the co po able func-
tion a g ments.
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier =
[Link]([Link]),
121
h
c
s
r
r
e
t
m
u
t
s
e
e
e
e
e
p
m
s
e
r
p
d
c
m
a
m
s
e
m
s
p
s
m
verticalAlignment =
[Link]
) {
Icon(
modifier =
[Link]([Link]),
imageVector =
[Link],
contentDescription = null
)
Text(
text = message
)
}
}
Cu rently, we’ll have som thing that looks like this co posed:
122
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 with the icon.
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
123
m
r
e
d
i
fi
fi
t
p
e
r
n
u
m
m
s
s
k
s
modifier =
[Link]([Link]),
verticalAlignment =
[Link]
) {
Icon(
modifier =
[Link]([Link]),
imageVector =
[Link],
contentDescription = null
)
Spacer(modifier =
[Link]([Link]))
Text(
text = stringResource(id =
message),
fontSize = [Link]
)
}
}
💡 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.
124
p
i
y
e
t
s
e
y
m
o
d
c
t
r
t
l
Cu rently, we have this sa i fied ag wit in our co po able,
but we’re not d 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 ap-
pli tion to cr ate a co or re e ence.
125
c
r
a
e
e
u
s
e
o
p
t
s
l
s
l
fi
y
r
l
t
f
s
r
u
fl
t
s
n
fi
c
a
h
e
o
n
e
o
m
l
s
h
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 al-
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 a pl ing it to the l bel us-
ing the co or a g ment on the Text co po able.
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val tint = if (satisfied) {
[Link]
} else
[Link](alph
a = 0.4f)
Row(
modifier =
[Link]([Link]),
verticalAlignment =
[Link]
) {
Icon(
modifier =
[Link]([Link]),
imageVector =
[Link],
contentDescription = null,
126
s
s
l
n
l
r
r
r
u
f
u
s
r
l
l
p
p
m
p
y
s
e
i
y
e
a
tint = tint
)
Spacer(modifier =
[Link]([Link]))
Text(
text = stringResource(id =
message),
fontSize = [Link],
color = tint
)
}
}
127
e
e
t
s
fi
fl
m
m
s
p
s
s
r
I pro ing the r quir ment ac-
ces i i ity
While we have the above co po able fun tion in place to com-
pose a r quir ment, we can i prove things here when it comes
to the use of a ces i i ity se vices. Cu rently, the l bel of the re-
quir ment will be read - but we are r l ing on a co or re res-
en tion to si n fy whet er the r quir ment is sa i ed or not.
This isn’t 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 se vice is d scri ing
128
m
e
m
t
a
e
e
s
e
b
v
l
e
g
c
c
i
s
s
b
l
h
r
m
m
e
c
s
e
p
s
r
b
e
e
l
e
y
c
r
e
a
t
s
p
i
fi
fi
l
c
e
a
p
b
the el ment, it can be d scribed whet er or not the r quire-
ment is cu rently sa i ed.
<string
name="password_requirement_satisfied">
%s, satisfied
</string>
<string
name="password_requirement_needed">
%s, needed
</string>
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val requirementStatus = if
(satisfied) {
129
e
h
n
e
p
g
g
e
r
r
c
e
e
c
c
t
e
s
e
t
m
fi
s
s
s
fi
b
fi
e
d
l
s
m
a
r
e
s
c
h
e
e
e
r
c
e
e
e
stringResource(id =
[Link].password_requirement_satisfied,
message)
} else {
stringResource(id =
[Link].password_requirement_not_satisf
ied, message)
}
}
You’ll n tice here that we pass the me sage a g ment from our
co po able fun tion as an a g ment to the stri R source
fun tion. This is b cause we are u ing the %s plac hol er, so
any a g ments that are provided here will be used as the re-
plac ment. With this in place, we can now go ahead and a ply
the s mantics mo er to our Row co po able. When d ing
this we’ll want to set the merg De cen ants ag as true (as
we don’t need the child co po ables to be d scribed i d vidu-
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 ate String type, so we’ll i sta t ate an i stance by
provi ing our r quir men Status.
Row(
modifier = [Link]([Link])
.semantics(mergeDescendants =
true) {
text =
AnnotatedString(requirementStatus)
130
m
c
t
e
d
n
e
r
s
o
u
t
d
e
c
e
t
d
i
fi
e
n
m
t
r
r
u
s
e
s
s
e
s
n
m
d
n
s
i
e
r
u
fl
p
n
g
e
n
e
e
n
e
d
i
o
p
},
verticalAlignment =
[Link]
)
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 po able, u ing the
clea An Se S mantics mo er to do so. This will avoid any
d scri tions from b ing d pli ated with the s mantics tree.
Text(
modifier =
[Link] { },
text = message,
fontSize = [Link],
color = tint
)
@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val tint = if (satisfied) {
[Link]
131
e
e
r
r
p
d
e
t
m
e
n
e
t
c
u
s
c
b
d
i
n
l
fi
e
n
p
p
m
e
s
m
s
s
} else
[Link](alph
a = 0.4f)
val requirementStatus = if
(satisfied) {
stringResource(id =
[Link].password_requirement_satisfied,
message)
} else {
stringResource(id =
[Link].password_requirement_not_satisf
ied, message)
}
Row(
modifier =
[Link]([Link])
.semantics(mergeDescendants
= true) {
text =
AnnotatedString(requirementStatus)
},
verticalAlignment =
[Link]
) {
Icon(
modifier =
[Link]([Link]),
132
imageVector =
[Link],
contentDescription = null,
tint = tint
)
Spacer(modifier =
[Link]([Link]))
Text(
modifier =
[Link] { },
text = message,
fontSize = [Link],
color = tint
)
}
}
133
o
h
e
fi
n
d
h
c
w
m
d
m
r
s
e
c
t
e
s
fi
e
m
t
c
e
e
h
e
e
e
s
e
m
d
s
e
m
@Composable
fun PasswordRequirements(
modifier: Modifier = Modifier,
satisfiedRequirements:
List<PasswordRequirements>
) {
Column(modifier = modifier) {
}
}
@Composable
fun PasswordRequirements(
modifier: Modifier = Modifier,
satisfiedRequirements:
List<PasswordRequirements>
) {
Column(
modifier = modifier
134
fi
f
e
m
r
i
e
s
m
fi
v
s
e
a
l
e
e
h
s
h
s
e
a
p
s
d
e
e
m
s
p
e
s
s
r
s
e
u
n
m
d
d
e
e
e
r
r
o
e
e
t
) {
[Link]().forEach
{ requirement ->
Requirement(
message =
stringResource(
id =
[Link]),
satisfied =
[Link](
requirement
)
)
}
}
}
135
r
t
s
fi
m
l
c
e
e
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 e ence wit in our screen state, but
i stead, we’re g ing to uti ise the A i ate Vi i i ity com-
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 ib-
136
n
u
o
s
l
s
m
n
fl
o
s
c
a
n
s
s
m
o
n
d
e
l
l
f
d
m
e
r
a
s
e
n
h
fi
m
m
h
m
e
d
s
s
w
n
s
m
b
l
d
n
s
i ity co po able, se ting the vi ible ag u ing an a se tion
against the A the ti tio Mo e.SIGN_UP value, and
provide our Pas wor R quir ments co po able as the con-
tent.
// [Link]
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 Re-
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
not i pl me ted yet!).
//[Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
137
l
h
m
e
e
m
n
s
u
s
u
c
s
n
d
n
n
t
m
d
e
c
e
a
c
a
e
u
n
r
e
n
r
s
u
n
d
c
fl
u
a
m
e
m
m
n
s
n
s
n
m
s
t
c
a
c
s
s
f
h
n
r
c
d
r
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 Con-
[Link] le to mod fy the co po tion of our A the ti a-
tio Form co po able. We’ll need to pass a value for the com-
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 quire-
ments wit in it, so we’ll simply need to pass that here.
// [Link]
AuthenticationForm(
modifier = [Link](),
email = [Link],
password =
[Link],
completedPasswordRequirements =
[Link]
,
authenticationMode =
138
n
s
d
h
fi
s
m
d
s
h
e
i
r
e
u
m
s
i
u
n
f
r
r
c
u
a
n
e
n
c
[Link],
onEmailChanged = {
handleEvent([Link]
nged(it))
},
onPasswordChanged = {
handleEvent([Link]
Changed(it))
}
)
// [Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements:
List<PasswordRequirements>,
139
d
d
e
m
e
m
m
s
n
m
u
n
s
m
n
c
d
a
s
n
b
l
u
m
s
onEmailChanged: (email: String) ->
Unit,
onPasswordChanged: (password:
String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment =
[Link]
) {
EmailInput(
modifier =
[Link](),
email = email,
onEmailChanged =
onEmailChanged
) {
[Link]()
}
Spacer(modifier =
[Link]([Link]))
PasswordInput(
modifier =
[Link]()
.focusRequester(password
FocusRequester),
password = password,
onPasswordChanged =
onPasswordChanged,
140
onSubmitForm =
onAuthenticate
)
Spacer(modifier =
[Link]([Link]))
AnimatedVisibility(
visible = authenticationMode
==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRe
quirements)
}
}
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, en-
te ing a pas word will now mod fy the r quir ments UI to dis-
play the cu rently sa i ed r quir ments.
141
r
m
r
s
h
t
s
fi
u
e
n
c
a
e
i
e
s
e
d
e
n
e
142
Tri ge ing the A the ti tion
ow
In our UI the user can now enter their email a dress and pass-
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 a then-
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 ad-
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
143
fl
x
c
o
a
c
g
fl
o
r
s
c
t
s
e
g
l
t
n
s
u
u
g
n
c
n
a
r
s
d
c
fl
c
e
fl
a
t
u
l
co po able fun tion, A the ti tio Bu ton with a d fault
Mo f er a g ment.
// [Link]
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier
)
@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 an 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 a the ti a-
tio Mode from our a the ti tion state re e ence. We’ll need
144
m
m
n
d
p
i
g
r
i
s
s
r
r
u
t
y
c
s
p
t
u
t
s
u
e
n
c
n
m
a
d
n
c
s
a
p
n
s
f
l
t
r
e
r
t
d
m
u
e
n
s
w
c
to start by adding a couple of string r sources for the bu ton
text:
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
)
Next, we’ll use an if stat ment check to handle what the mes-
sage should be d pen ing on the state, then set this as the
co tent of a Text co po able wit in the body of our Bu ton.
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
) {
145
u
n
t
n
c
a
m
n
e
s
m
d
e
s
c
r
h
m
o
e
s
e
c
c
h
t
t
Button(
modifier = modifier
) {
Text(
text = stringResource(
if (authenticationMode
==
AuthenticationMode.SIGN_IN) {
[Link].action_sign_in
} else {
[Link].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.
146
h
p
s
r
u
n
c
a
t
At this point, we can now think about han ling the o Click
tri ger from our Bu 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 a 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 pos-
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 pos-
able.
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
147
g
r
c
m
h
s
u
t
n
u
c
n
r
c
c
u
a
o
r
fl
u
d
m
o
l
t
g
g
n
l
m
m
onAuthenticate: () -> Unit
) {
Button(
modifier = modifier,
onClick = {
onAuthenticate()
}
) {
Text(
text = stringResource(
if (authenticationMode
==
AuthenticationMode.SIGN_IN) {
[Link].action_sign_in
} else {
[Link].action_sign_up
}
)
)
}
}
148
n
r
p
l
h
o
t
e
u
n
w
n
c
a
d
n
u
fi
n
t
e
f
c
r
u
a
s
fl
c
a
l
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 en-
abled pro erty of the Bu ton, a sig ing thee abl Au-
thenti tion value to it.
@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
enableAuthentication: Boolean,
onAuthenticate: () -> Unit
) {
Button(
modifier = modifier,
onClick = {
onAuthenticate()
},
enabled = enableAuthentication
) {
Text(
text = stringResource(
if (authenticationMode
==
AuthenticationMode.SIGN_IN) {
[Link].action_sign_in
149
t
c
a
p
s
n
t
t
fl
l
e
s
n
e
n
r
n
c
e
} else {
[Link].action_sign_up
}
)
)
}
}
150
s
m
t
m
h
s
u
m
n
e
c
a
n
r
u
n
A the ti tio Form co po able, this will be e abl Au-
thenti tion in the form of a Boolean value.
@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
)
We’ll then also need to hop over to our A the ti tio Con-
[Link] le to mod fy the co po tion of our A the ti a-
tio Form co po able. We’ll need to pass a value for the en-
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 simply need to pass that here.
// [Link]
AuthenticationForm(
modifier = [Link](),
151
u
h
n
e
n
u
c
a
fi
h
c
a
m
c
r
n
a
u
s
i
m
m
s
s
i
f
r
u
r
n
c
u
n
a
n
n
n
e
s
c
email = [Link],
password =
[Link],
completedPasswordRequirements =
[Link]
,
authenticationMode =
[Link],
enableAuthentication =
[Link](),
onEmailChanged = {
handleEvent([Link]
nged(it))
},
onPasswordChanged = {
handleEvent([Link]
Changed(it))
},
onAuthenticate = {
handleEvent([Link]
cate)
}
)
152
d
m
u
u
n
c
n
a
c
a
n
n
t
m
s
we’ll also add a Spacer co po able so that there is some visu-
al space between UI co po ents.
// [Link]
@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 =
[Link]
) {
EmailInput(
modifier =
[Link](),
email = email,
onEmailChanged =
onEmailChanged
153
m
m
n
s
) {
[Link]()
}
Spacer(modifier =
[Link]([Link]))
PasswordInput(
modifier =
[Link]()
.focusRequester(password
FocusRequester),
password = password,
onPasswordChanged =
onPasswordChanged,
onSubmitForm =
onAuthenticate
)
Spacer(modifier =
[Link]([Link]))
AnimatedVisibility(
visible = authenticationMode
==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRe
quirements)
}
154
Spacer(modifier =
[Link]([Link]))
AuthenticationButton(
enableAuthentication =
enableAuthentication,
authenticationMode =
authenticationMode,
onAuthenticate =
onAuthenticate
)
}
}
155
m
n
g
u
u
u
n
n
c
n
a
c
c
a
a
fl
n
t
l
e
156
To gling the A the ti tion
mode
With all of the above in place, our user can pe form a the ti a-
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 not
cu rently have an a count, they won’t be able to cr ate one us-
ing our a the ti tion form. In this se tion we’re g ing to go
ahead and de ne a Bu ton that a lows the user to toggle
between sign up and sign in, r co po ing our a the ti tion
157
p
r
n
fi
g
s
u
n
fi
e
c
a
c
u
fi
t
n
c
u
e
a
w
m
l
c
s
n
r
c
a
u
e
o
u
n
r
r
c
n
a
c
form to r ect the cu rent mode. We’ll start here by cr a ing a
new co po able fun tion, Toggl A thenti tio Mode
with a d fault Mo f er a g 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 con-
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 i self use the su face co or from the a pli a-
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 pos-
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 e tion of the
se vice with the value of 8dp.
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
elevation = [Link]
) {
158
r
n
n
m
c
m
p
n
e
m
e
e
e
fl
fi
p
n
m
s
y
d
t
s
i
d
l
i
fi
i
x
r
s
c
l
m
r
r
c
c
d
u
s
r
r
n
r
u
e
e
l
u
p
l
t
r
s
l
v
a
c
m
m
a
m
m
s
e
s
o
p
n
m
t
t
s
s
c
}
}
Next, we’re g ing to add a bu ton to our Su face, and for this,
we will use the Tex Bu ton co po able. This is a co po able
offered by the co pose m te al 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 dis-
played wit in the co po able bu ton
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier
.padding(top = [Link]),
elevation = [Link]
) {
TextButton(
onClick = {
}
) {
}
}
}
159
m
h
fl
n
o
m
t
t
m
m
t
s
d
s
a
s
t
r
m
i
t
s
t
k
t
n
r
m
s
l
m
s
n
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. Be-
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>
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, d pen ing on
the cu rent a the ti tion mode re re e ted by our state. Be-
fore we can do this though, we’ll need to know the A then-
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 co po able
fun tion so that it can be passed in from the pa ent.
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
)
160
d
m
c
c
a
i
r
n
s
n
s
e
u
p
u
n
m
c
m
a
n
n
s
s
m
u
p
p
s
r
r
s
p
u
s
e
fi
n
r
t
m
m
e
t
s
m
t
s
u
d
m
t
s
With this in place, we can now uti ise this to co pose the but-
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 based on the provided
A the ti tio Mode.
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode
) {
Surface(
modifier = modifier
.padding(top = [Link]),
elevation = [Link]
) {
TextButton(
onClick = {
}
) {
Text(
text = stringResource(
if
(authenticationMode ==
AuthenticationMode.SIGN_IN) {
[Link].action_need_account
} else {
161
u
n
n
m
c
a
s
n
n
t
t
n
l
o
m
m
[Link].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 our co po able func-
tion.
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
toggleAuthentication: () -> Unit
)
162
i
g
m
t
s
t
c
u
a
n
n
c
h
a
n
t
u
r
l
u
n
w
c
e
a
d
w
n
c
d
m
l
fl
r
i
s
t
g
c
e
t
@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
toggleAuthentication: () -> Unit
) {
Surface(
modifier = modifier
.padding(top = [Link]),
elevation = [Link]
) {
TextButton(
modifier = Modifier
.background(MaterialThem
[Link])
.padding([Link]),
onClick = {
toggleAuthentication()
}
) {
Text(
text = stringResource(
if
(authenticationMode ==
AuthenticationMode.SIGN_IN) {
[Link].action_need_account
} else {
163
[Link].action_already_have_account
}
)
)
}
}
}
Wit in our UI, this toggle bu ton was pushed right to the bot-
tom of the screen - this isn’t som thing that can be co gured
wit in the pro e ties of the pa ent Column co tai er.
164
h
h
p
r
t
r
e
n
n
n
fi
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 g ment to our A then-
ti tio Form co po able, this will be o Toggl Mode in
the form of a lambda fun tion.
@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,
onToggleMode: () -> Unit
)
We’ll then also need to hop over to our A the ti tio Con-
[Link] le to mod fy the co po tion of our A the ti a-
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 fun 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
165
n
o
m
c
a
n
m
n
e
fi
m
s
h
t
s
m
i
g
s
c
m
u
u
e
m
n
n
m
n
e
s
c
i
c
e
a
a
r
u
u
n
n
n
n
r
t
u
c
u
a
e
m
u
n
n
s
c
c
do so in the form of the Toggl A thenti tio Mode type.
When this is triggered and handled by our Vie Mo el, the cur-
rent A the ti tio Mode will be toggled to the o po ite
value and emi ted to our UI.
// [Link]
AuthenticationForm(
modifier = [Link](),
email = [Link],
password =
[Link],
completedPasswordRequirements =
[Link]
,
authenticationMode =
[Link],
enableAuthentication =
[Link](),
onEmailChanged = {
handleEvent([Link]
nged(it))
},
onPasswordChanged = {
handleEvent([Link]
Changed(it))
},
166
u
n
t
c
a
n
e
u
c
a
w
n
d
p
s
onAuthenticate = {
handleEvent([Link]
cate)
},
onToggleMode = {
handleEvent(
[Link]
Mode)
}
)
// [Link]
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode:
AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements:
List<PasswordRequirements>,
167
x
t
d
u
n
n
m
e
c
a
n
u
u
n
n
f
c
r
c
c
a
a
n
n
t
m
s
s
onEmailChanged: (email: String) ->
Unit,
onPasswordChanged: (password:
String) -> Unit,
onToggleMode: () -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment =
[Link]
) {
EmailInput(
modifier =
[Link](),
email = email,
onEmailChanged =
onEmailChanged
) {
[Link]()
}
Spacer(modifier =
[Link]([Link]))
PasswordInput(
modifier =
[Link]()
.focusRequester(password
FocusRequester),
password = password,
168
onPasswordChanged =
onPasswordChanged,
onSubmitForm =
onAuthenticate
)
Spacer(modifier =
[Link]([Link]))
AnimatedVisibility(
visible = authenticationMode
==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRe
quirements)
}
Spacer(modifier =
[Link]([Link]))
AuthenticationButton(
enableAuthentication =
enableAuthentication,
authenticationMode =
authenticationMode,
onAuthenticate =
onAuthenticate
)
169
ToggleAuthenticationMode(
modifier =
[Link](),
authenticationMode =
authenticationMode,
toggleAuthentication = {
onToggleMode()
}
)
}
}
We’ll n tice here now that our Toggl A thenti tio Mode
co po able is pressed against the bo tom of our A the tic-
tio Bu ton co po able.
170
a
m
n
o
s
t
m
s
t
e
u
c
a
u
n
n
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 se tially want a large amount of
white space - this white spice needs to ll the avai 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 = [Link](1f))
171
p
m
m
t
y
s
s
t
l
m
t
t
s
t
d
e
i
p
s
t
e
n
y
h
n
t
e
f
fi
u
r
t
u
d
c
i
fi
a
n
l
o
m
c
n
s
h
to be di tri 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 po able, it takes the avai able space for i self.
// [Link]
@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 =
[Link]
) {
EmailInput(
172
s
m
e
s
s
f
r
b
s
t
s
l
l
n
m
t
s
e
t
s
b
modifier =
[Link](),
email = email,
onEmailChanged =
onEmailChanged
) {
[Link]()
}
Spacer(modifier =
[Link]([Link]))
PasswordInput(
modifier =
[Link]()
.focusRequester(password
FocusRequester),
password = password,
onPasswordChanged =
onPasswordChanged,
onSubmitForm =
onAuthenticate
)
Spacer(modifier =
[Link]([Link]))
AnimatedVisibility(
visible = authenticationMode
==
AuthenticationMode.SIGN_UP
173
) {
PasswordRequirements(completedPasswordRe
quirements)
}
Spacer(modifier =
[Link]([Link]))
AuthenticationButton(
enableAuthentication =
enableAuthentication,
authenticationMode =
authenticationMode,
onAuthenticate =
onAuthenticate
)
Spacer(modifier =
[Link](1f))
ToggleAuthenticationMode(
modifier =
[Link](),
authenticationMode =
authenticationMode,
toggleAuthentication = {
onToggleMode()
}
)
174
}
}
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.
175
c
n
t
m
m
m
s
s
r
f
r
s
d
m
s
u
n
t
c
a
s
p
c
Di pla ing A the ti tion Er-
rors
176
c
a
s
p
p
r
a
y
n
n
e
s
r
n
e
u
p
p
e
p
i
r
a
c
n
a
r
a
h
c
a
e
fl
c
e
c
u
l
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 g ing to cr ate a new co po able, Au-
the ti tio Er o Di log, which is g ing to take this er-
ror value and co pose it wit in our UI.
// [Link]
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
)
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
177
r
c
h
h
p
n
r
h
n
c
t
a
r
u
u
a
u
m
l
n
u
m
n
n
s
r
c
a
n
r
c
o
a
c
m
a
a
o
c
u
n
o
h
e
fl
c
e
t
o
a
r
o
m
r
r
s
m
r
c
m
s
l
l
},
confirmButton = {
},
title = {
},
text = {
}
)
}
178
a
g
n
t
s
n
d
m
fi
i
s
fi
m
n
s
n
p
s
t
m
s
p
n
s
fi
p
s
m
m
n
s
s
e
m
l
l
c
s
c
p
a
r
s
a
a
u
- 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 g ments. We’ll start with the title, which we’ll
need to start by adding a new string to our strin [Link] re-
source le.
<string name="error_title">Whoops</
string>
// [Link]
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
},
confirmButton = {
179
r
m
u
s
fi
a
e
e
a
m
r
u
m
s
t
n
g
s
e
k
d
d
m
m
r
s
s
m
n
g
c
s
s
s
},
title = {
Text(
text = stringResource(
id =
[Link].error_title),
fontSize = [Link]
)
},
text = {
}
)
}
At this point, we can see this text b ing used for the title of our
di log.
180
a
e
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.
// [Link]
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
) {
AlertDialog(
modifier = modifier,
181
c
r
u
n
h
r
r
u
t
a
m
s
m
m
s
s
m
s
onDismissRequest = {
},
confirmButton = {
},
title = {
Text(
text = stringResource(
id =
[Link].error_title),
fontSize = [Link]
)
},
text = {
Text(
text = error
)
}
)
}
With this in place, we can now see the title of our Aler Dia-
log b ing a co pa ied by our e ror me sage.
182
e
c
m
n
r
s
t
Next, we need to co pose the bu ton that will be used to dis-
miss the Aler Di log - this will be provided u ing the but-
ton a g ment of our co po able. We’ll rst start by co pos-
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 pos-
able, is that the Tex Bu ton has the e pe ted touch ta get siz-
ing of an i trac able co po ent, ma ing it a ces ible to all of
our users.
TextButton(
onClick = {
183
m
n
r
f
u
r
t
n
fl
t
m
t
t
e
l
t
a
s
t
m
t
m
m
r
u
n
s
u
m
t
n
k
x
p
t
c
fi
c
s
n
c
t
s
s
t
l
r
m
m
}
) {
We’ll start by co po ing the body of our bu ton, for which we’ll
need to add a new r source to our strin [Link] le.
<string name="error_action">OK</string>
TextButton(
onClick = {
}
) {
Text(text = stringResource(id =
[Link].error_action))
}
184
s
n
a
a
m
r
t
t
e
s
m
r
u
c
m
m
g
t
u
m
c
m
o
p
fi
s
n
s
c
@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 Tex Bu ton co po able.
// [Link]
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {
},
confirmButton = {
TextButton(
onClick = {
dismissError()
}
) {
185
n
t
t
m
s
g
h
Text(text =
stringResource(
id =
[Link].error_action))
}
},
title = {
Text(
text = stringResource(
id =
[Link].error_title),
fontSize = [Link]
)
},
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, me sage and a tions to the user.
186
s
s
u
c
m
r
a
The last thing to do here is to add some i pl men tion to the
onDi mi 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 tri ger this wit in
the onDi mi Request lambda.
// [Link]
@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
187
e
m
i
s
s
s
s
s
s
s
c
d
s
s
s
r
m
e
g
t
r
u
t
a
a
h
AlertDialog(
modifier = modifier,
onDismissRequest = {
dismissError()
},
buttons = {
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment =
[Link]
) {
TextButton(
onClick = {
dismissError()
}
) {
Text(text =
stringResource(
id =
[Link].error_action))
}
}
},
title = {
Text(
text = stringResource(
id =
[Link].error_title),
fontSize = [Link]
188
)
},
text = {
Text(
text = error
)
}
)
}
// [Link]
[Link]?.let { error
->
AuthenticationErrorDialog(
...
)
}
189
r
n
r
h
r
r
l
r
b
a
m
u
m
d
l
m
r
s
h
h
r
r
m
t
u
m
s
i
n
p
e
c
u
a
t
m
n
n
l
m
r
c
u
a
w
n
m
s
n
s
m
c
s
a
s
m
m
r
s
n
s
lambda. Wit in this lambda we’ll want to uti ise the handle-
Event lambda that is provided to our co po able fun tion, us-
ing the E 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).
// [Link]
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState:
AuthenticationState,
handleEvent: (event:
AuthenticationEvent) -> Unit
) {
Box(
modifier = modifier,
contentAlignment =
[Link]
) {
if
([Link]) {
CircularProgressIndicator()
} else {
AuthenticationForm(
modifier =
[Link](),
email =
[Link],
190
r
a
e
h
r
D
p
u
e
n
m
v
c
a
m
n
s
l
r
r
c
g
password =
[Link],
completedPasswordRequirements =
[Link]
,
authenticationMode =
[Link],
enableAuthentication =
[Link](),
onEmailChanged = {
handleEvent(
[Link](it))
},
onPasswordChanged = {
handleEvent(
AuthenticationEvent
.PasswordCha
nged(it))
},
onAuthenticate = {
handleEvent(
[Link])
},
191
onToggleMode = {
handleEvent(AuthenticationEvent
.ToggleAuthentic
ationMode)
}
)
[Link]?.let { error
->
AuthenticationErrorDialog(
error = error,
dismissError = {
handleEvent(
[Link])
}
)
}
}
}
}
192
l
w
h
m
u
v
c
t
a
u
p
n
c
s
a
r
r
193
Wra ping Up
Throug out this pr ject, we’ve now fully i pl me ted our au-
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 au-
the ti tion modes. With this to gling, we’ve been able to ex-
plore the co po tion of di fe ent states and co po ents with-
in our UI, on top of the ma ag ment of state when it comes to
our a the ti tion form.
194
n
n
u
p
c
c
a
a
h
p
c
n
a
c
m
a
a
s
i
o
l
w
n
f
l
r
e
g
m
e
m
n
n
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 go-
ing to e plore wri ing aut mated UI tests for these co pos-
ables.
195
e
x
c
n
t
h
o
n
m
m
n
Testing the A then-
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 str men tion tests u ing the
co pose ui-test-j nit pac age - a lo ing us to ver fy that our
co po ables are di played and fun tio ing as e pe ted.
B fore we can get sta ted with our tests, we’re g ing to
need to add a couple of test sp ci c d pen e cies to our pro-
ject:
androidTestImplementation
"[Link]:ui-test-
junit4:1.5.4"
debugImplementation
"[Link]:ui-test-
manifest:1.5.4"
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, a lo ing us to ea ily
ver fy they are triggered whene er e pe ted.
androidTestImplementation
"[Link]:mockito-
kotlin:4.0.0"
196
e
m
m
i
c
s
a
o
s
o
u
s
m
t
r
s
k
u
f
v
r
n
e
n
c
c
fi
l
a
c
x
u
w
u
e
n
c
t
l
a
d
w
n
x
n
o
m
c
i
s
o
s
l
s
androidTestImplementation
"[Link]:mockito-android:4.5.1"
class AuthenticationTest {
@get:Rule
val composeTestRule =
createComposeRule()
197
n
o
n
m
v
t
n
o
c
s
e
r
c
n
m
l
m
t
m
s
t
e
s
l
s
s
t
m
r
n
n
c
fi
h
f
r
m
u
l
e
n
w
s
n
f
e
r
c
a
o
m
n
n
s
v
Tes ing the A the ti tion
Co po 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 po able), is that this co pos-
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 A the ti tion co po able we can a sert not only that
the e pe ted UI co po ents are di played, but also that i ter-
a tions with them re ult in the e pe ted state changes.
198
c
e
n
d
x
u
e
m
t
l
t
f
c
m
h
s
x
r
n
c
s
c
i
s
a
s
n
u
u
m
m
a
s
f
r
r
o
r
n
n
n
u
n
m
o
c
a
u
c
m
m
a
e
s
fl
x
u
m
e
u
n
s
i
c
s
n
s
n
c
n
c
m
a
a
p
c
c
a
s
g
a
s
s
n
e
s
o
m
m
n
d
h
p
r
m
s
s
n
n
c
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.
@Test
fun Sign_In_Title_Displayed_By_Default()
{
@Test
fun Sign_In_Title_Displayed_By_Default()
{
[Link] {
Authentication()
}
}
While we aren’t yet pe for ing any a se tions, ru ning this test
will launch an acti ity that di plays the co tent of our A the tic-
tion co po able. With this now b ing di played, we can next
199
a
n
s
e
e
m
m
fi
fi
fi
s
s
m
t
h
m
s
fi
r
s
n
u
u
c
e
v
i
n
r
m
c
c
m
a
c
o
s
u
t
n
n
c
e
s
m
a
s
r
r
u
n
s
s
c
m
n
n
s
a
l
c
w
s
u
t
e
e
i
c
n
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 er-
ence.
@Test
fun Sign_In_Title_Displayed_By_Default()
{
[Link] {
Authentication()
}
[Link](
[Link]
on()
.[Link](
[Link].label_sign_in_to_account)
)
}
200
e
m
l
r
e
h
e
t
e
n
n
s
c
r
t
s
e
c
n
e
c
m
h
e
e
s
s
f
h
s
h
r
y
r
c
o
m
m
e
c
r
s
n
c
s
s
c
s
r
t
p
e
s
c
n
n
c
f
h
For this test we want to a sert that this node is b ing di played
wit in our co posed UI, so we’re g ing to go ahead and uti ise
the a ser I Di played fun 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()
{
[Link] {
Authentication()
}
[Link](
[Link]
on()
.[Link](
[Link].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 r quired string b ing com-
posed wit in the UI.
201
o
h
l
g
s
s
n
h
t
u
m
s
n
h
s
c
e
m
a
e
h
n
s
c
s
c
e
c
h
e
e
n
o
s
e
r
s
c
u
c
t
e
e
e
s
s
l
c
r
w
l
di fe ence here b ing that we need to a sert the co po tion
of the a tion_need_a count string as o posed to the sign in
title.
@Test
fun Need_Account_Displayed_By_Default()
{
[Link] {
Authentication()
}
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link](
[Link].action_need_account)
)
.assertIsDisplayed()
}
With these two tests in place, we are now able to a sert that the
e pe ted co po ables for the Sign In A the ti tion Mode are
b ing co posed. But what ha pens if the user toggles the Au-
the ti tion Mode? In this sce ario, we know that to gling to
the Sign Up mode will change the title and toggle me sages -
so we’re g ing to write some tests to a sert these r co po i-
tions are triggered when this toggle o curs.
202
x
e
f
n
r
c
c
a
c
m
o
m
s
e
c
p
n
c
u
s
s
p
n
c
a
s
e
m
g
s
m
s
i
s
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 start by se ting the
co tent to be co posed i side of our UI.
@Test
fun
Sign_Up_Title_Displayed_After_Toggled()
{
[Link] {
Authentication()
}
}
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 in-
te a tion we’re g ing to use the pe for Click fun tion, this is
a ge ture a tion that is avai able on the S manti Nod In-
te a tion class, a lo ing us to pe form a click ge ture on the
sp ci ed node.
@Test
fun
Sign_Up_Title_Displayed_After_Toggled()
{
[Link] {
Authentication()
}
203
r
e
n
r
c
s
c
fi
o
c
k
m
o
o
l
w
u
n
n
l
g
c
a
r
r
t
e
m
u
e
m
n
c
s
a
c
c
t
s
t
e
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link](
[Link].action_need_account)
).performClick()
}
Once this i te a tion has taken place, it is e pe ted that the au-
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
[Link].l bel_sign_up for_a count String r source.
@Test
fun
Sign_Up_Title_Displayed_After_Toggled()
{
[Link] {
Authentication()
}
composeTestRule
.onNodeWithText(
204
n
m
c
a
n
a
r
c
e
i
s
_
x
p
s
c
s
r
x
x
c
c
c
e
[Link]
on()
.[Link](
[Link].action_need_account)
).performClick()
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link](
[Link].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 cor ingly when the a the ti tion mode is
toggled. When this toggle o curs, we will also e pect the au-
the ti tion bu ton to be r co posed to r ect the Sign Up
a tion, as o posed to Sign In. For this, we’re g ing to start with
a test that looks very si i ar to the pr v ous - we’ll need to com-
pose our A the ti tion co po able, fo lowed by u ing
the pe for Click fun tion to i te act with the bu ton used to
toggle the a the ti tion mode.
@Test
205
e
c
n
m
c
r
a
p
m
u
u
c
t
n
n
c
d
a
c
m
a
c
l
e
c
m
n
m
r
e
s
i
u
s
e
n
fl
l
o
c
a
x
t
s
fun
Sign_Up_Button_Displayed_After_Toggle()
{
[Link] {
Authentication()
}
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link](
[Link].action_need_account)
).performClick()
}
//[Link]
object Tags {
206
u
c
a
n
n
c
a
c
a
t
fi
c
t
m
u
o
s
n
o
c
a
n
s
t
r
e
t
s
u
b
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 our Bu ton co po able.
//[Link]
Button(
modifier =
[Link](TAG_AUTHENTICATE_BUTTON
),
...
)
We’ll also do the same for the Toggl A thenti tio Mode
Bu ton.
// [Link]
TextButton(
modifier =
[Link](TAG_AUTHENTICATION_TOGG
LE),
...
)
207
t
s
fi
t
m
e
s
u
t
c
a
n
c
With these tags in place, it can now be used to lo ate a node
wit in our co po able hie archy u ing the o Nod Wit ag
fun tion. On this node, we can then use the a ser Te t-
Equals fun tion to a sert that the text of this co po able is
equal to the co tent of the a tion_sign_up r source.
@Test
fun
Sign_Up_Button_Displayed_After_Toggle()
{
[Link] {
Authentication()
}
[Link](
TAG_AUTHENTICATION_TOGGLE
).performClick()
[Link](
TAG_AUTHENTICATE_BUTTON
).assertTextEquals(
[Link]
on()
.[Link]([Link].
action_sign_up)
)
}
With this test in place, we can now be ce tain that when the au-
the ti tion mode is toggled, the co tent di played wit in the
208
h
c
n
c
a
c
m
n
s
s
r
c
s
n
r
s
e
n
s
m
c
e
t
s
h
h
T
x
a the ti tion bu ton no longer re re ents the sign in a tion,
but i stead the sign up a tion.
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 in-
te act with the toggle bu ton to toggle the a the ti tion
mode, and then we’ll want to a sert that the text of that a then-
ti tion bu ton re re ents the e pe ted value.
Here we’ll set up a test that will co pose our A the tic-
tion co po able, fo lowed by uti ising our TAG_AU-
THENTI TION TOGGLE tag to again lo ate the node that rep-
re ents the toggle bu ton. We can then use this re e ence to
pe form a tions and a se tions.
@Test
fun
Already_Have_Account_Displayed_After_Tog
gle() {
[Link] {
Authentication()
}
209
a
u
u
c
e
r
s
a
r
c
l
n
n
n
C
c
c
A
a
a
u
s
c
t
p
m
u
r
c
n
a
r
t
_
s
c
p
n
o
a
t
x
c
t
s
a
c
s
t
c
r
l
t
o
s
c
s
x
r
c
t
m
c
t
p
m
s
s
c
c
p
fl
l
n
s
t
l
o
u
s
e
c
r
u
n
n
f
e
fl
n
r
m
r
u
c
c
a
[Link](
TAG_AUTHENTICATION_TOGGLE
).apply {
performClick()
assertTextEquals(
[Link]
on()
.[Link](
[Link].action_already_have_account)
)
}
}
If this test passes, it means that the a the ti tion toggle but-
ton is b ing su ces fully r co posed with the co re pon ing
state for the sign-up mode that has been switched to. Ot er-
wise, it means the co po able has not been co posed with
the e pe ted state.
210
fl
s
x
u
e
c
n
r
c
a
u
c
u
n
s
n
m
c
n
a
c
a
e
s
m
m
t
s
i
u
m
h
y
n
s
c
m
a
m
r
m
r
s
c
d
h
Tes ing the A the ti tion But-
ton
Aside from a ap ing to the a the ti tion mode, the A the tic-
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 com-
posed with an e abled state. This means that if the email or
pas word in our state is empty, then the a the ti tion bu ton
will be di abled.
@Test
fun
Authentication_Button_Disabled_By_Defaul
t() {
[Link] {
Authentication()
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_
BUTTON)
}
211
a
e
p
s
i
e
r
t
l
s
t
s
d
u
fi
t
n
u
n
fi
s
n
n
c
m
r
a
m
c
u
a
m
U
l
u
u
n
m
n
n
t
n
C
c
u
a
c
s
a
c
e
a
c
n
i
u
n
T
c
h
t
a
n
c
a
o
s
u
s
s
m
n
t
If this node has been lo ated, then we’re g ing to need to per-
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 Se-
manti Pro e [Link] abled pro erty, mea ing that the
co po able is di abled.
@Test
fun
Authentication_Button_Disabled_By_Defaul
t() {
[Link] {
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
here to a sert this co d tion.
@Test
212
e
u
r
m
s
u
n
c
n
s
s
c
s
a
n
s
t
fi
c
s
r
p
a
r
s
t
t
n
e
n
t
r
i
c
s
n
n
c
s
s
s
r
c
p
t
a
s
o
e
fl
t
s
n
s
o
c
p
s
r
e
h
fun
Authentication_Button_Enabled_With_Valid
_Content() {
[Link] {
Authentication()
}
}
We’ll then a sign these tags to each of the email and pas word
i put elds u ing the tes Tag mo er.
// [Link]
TextField(
modifier =
[Link](TAG_INPUT_EMAIL),
...
)
213
fi
n
x
t
c
fi
n
e
o
e
s
s
a
c
n
r
t
u
n
s
c
a
d
i
fi
o
fi
fi
e
e
fi
r
s
l
m
// [Link]
TextField(
modifier =
[Link](TAG_INPUT_PASSWORD),
...
)
With these tags in place, we can now uti ise the pe for Te In-
put fun tion to i put a provided string into the co re pon ing
nodes.
[Link](
TAG_INPUT_EMAIL
).performTextInput("contact@[Link]
emy")
[Link](
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 a low the user to a then-
ti ate against. Once these calls are in place, we can again loc-
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 E abled fun tion, the key
di fe ence here is that a ser I E abled is chec ing that the
214
c
f
e
r
i
fi
c
s
u
n
n
l
n
c
n
a
s
n
fi
s
s
t
t
m
t
s
s
r
s
s
t
n
t
n
s
l
l
n
n
c
r
k
r
v
c
s
m
u
x
s
d
t
S manti Pro e [Link] 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() {
[Link] {
Authentication()
}
[Link](
TAG_INPUT_EMAIL
).performTextInput("contact@compose.
academy")
[Link](
TAG_INPUT_PASSWORD
).performTextInput("password")
[Link](
TAG_AUTHENTICATE_BUTTON
).assertIsEnabled()
}
B cause the email and pas word elds have va id co tent, the
a the ti tion bu 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
215
u
e
e
n
n
c
r
c
a
fi
s
p
t
e
t
r
fi
s
s
m
t
s
t
s
s
n
fi
e
u
e
t
n
v
c
l
n
a
p
n
t
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!
@Test
fun
Error_Alert_Not_Displayed_By_Default() {
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.
216
r
s
a
m
n
n
n
s
m
p
c
c
t
s
a
s
s
c
u
e
v
a
o
r
n
o
e
o
e
fl
c
u
m
a
r
s
l
e
c
s
fi
i
n
s
s
n
c
a
t
r
h
n
r
n
i
l
r
t
a
p
r
r
c
d
s
c
n
l
i
n
s
n
r
x
o
a
a
r
const val TAG_ERROR_ALERT =
"error_alert"
We’ll then a sign this tag to our Aler Di log co po able u ing
the tes Tag mo er.
AlertDialog(
modifier =
[Link](TAG_ERROR_ALERT),
...
)
With this tag in place, we can now a tempt to lo ate the node
and then pe form a 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 No-
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() {
[Link] {
Authentication()
}
[Link](
TAG_ERROR_ALERT
).assertDoesNotExist()
}
217
t
n
x
n
e
t
h
s
r
T
s
d
i
fi
s
r
c
r
a
l
t
t
a
s
r
m
s
c
s
t
x
s
x
s
h
Now that we know our alert di log is not sho ing when an er-
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 fun tion to rep-
re ent this test case.
@Test
fun Error_Alert_Displayed_After_Error()
{
[Link] {
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 nal-
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 as-
signed to the Aler Di log co po able, fo lowed by u ing
218
c
e
s
s
m
n
s
c
t
r
x
h
s
d
t
m
r
s
a
o
a
a
w
u
e
m
a
fi
r
n
m
s
n
s
s
s
c
a
o
u
n
w
l
fl
n
r
c
e
a
i
c
r
a
f
a
r
fi
s
the a ser I Di played fun tion to ver fy that the alert dia-
log has been co posed wit in our UI.
@Test
fun Error_Alert_Displayed_After_Error()
{
[Link] {
AuthenticationContent(
AuthenticationState(
error = "Some error"
)
) { }
}
[Link](
TAG_ERROR_ALERT
).assertIsDisplayed()
}
219
o
s
t
o
t
t
u
s
e
m
n
s
r
c
a
s
s
m
h
d
t
y
n
c
n
x
c
o
i
a
n
u
i
n
c
a
write a couple more tests to e sure that these changes b have
as e pe ted.
// [Link]
object Tags {
const val TAG_PROGRESS = "progress"
}
CircularProgressIndicator(
modifier =
[Link](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()
{
[Link] {
Authentication()
}
220
c
o
o
x
t
s
c
s
n
n
c
t
c
fi
x
t
r
e
s
fi
n
m
s
m
e
r
n
s
s
l
h
T
t
s
s
fi
s
c
o
e
t
s
x
o
t
t
s
r
c
u
e
c
d
i
fi
[Link](
TAG_PROGRESS
).assertDoesNotExist()
}
@Test
fun Progress_Displayed_While_Loading() {
[Link] {
AuthenticationContent(
AuthenticationState(isLoading = true)
) { }
}
[Link](
TAG_PROGRESS
).assertIsDisplayed()
}
221
a
s
p
c
a
r
n
s
n
n
n
s
r
e
d
s
u
n
d
d
c
s
n
n
t
h
c
n
a
o
d
f
l
r
fl
c
m
n
e
m
fl
c
c
t
r
u
m
u
n
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 a-
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 state for the email a dress
and pas word va ues.
@Test
fun
Progress_Not_Displayed_After_Loading() {
[Link] {
AuthenticationContent(
authenticationState =
AuthenticationState(
email =
"contact@[Link]",
password = "password"
)
) { }
}
[Link](
222
x
c
a
w
n
o
r
s
d
s
o
g
p
u
r
n
p
a
l
c
n
s
n
o
t
c
u
a
t
r
fi
n
n
n
o
c
a
s
t
t
c
fi
r
o
t
s
x
t
x
d
n
o
r
c
h
t
s
c
g
p
d
n
u
w
r
m
d
u
n
s
d
c
TAG_AUTHENTICATE_BUTTON
).performClick()
[Link](
TAG_PROGRESS
).assertDoesNotExist()
}
@Test
fun Content_Displayed_After_Loading()
// [Link]
object Tags {
const val TAG_CONTENT = "content"
}
We’ll then need to set this tag on the co re pon ing co pos-
able wit in our A the ti tio Form.
223
e
s
h
r
o
h
m
t
e
o
u
r
n
s
u
n
s
n
c
n
a
d
c
r
a
n
fi
o
u
o
n
r
c
a
n
n
s
c
d
t
n
s
m
s
l
m
w
e
// [Link]
Column(
modifier =
[Link](TAG_CONTENT),
horizontalAlignment =
[Link]
)
@Test
fun Content_Displayed_After_Loading() {
[Link] {
Authentication()
}
[Link](
TAG_INPUT_EMAIL
224
e
e
s
s
i
r
m
s
e
n
r
r
s
x
r
a
r
n
s
s
r
r
m
n
t
a
s
u
fi
n
l
n
n
c
a
h
r
n
fl
n
m
i
p
x
l
s
u
l
h
).performTextInput("contact@compose.
academy")
[Link](
TAG_INPUT_PASSWORD
).performTextInput("password")
[Link](
TAG_AUTHENTICATE_BUTTON
).performClick()
[Link](
TAG_CONTENT
).assertExists()
}
class AuthenticationTitleTest {
225
u
s
l
t
n
b
t
t
c
c
a
t
u
m
n
o
n
s
fi
t
c
a
e
p
u
c
s
r
m
m
t
n
n
fi
s
g
e
n
s
r
t
i
c
r
a
r
u
n
i
t
o
u
s
n
m
r
o
e
@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 then-
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 e pe ted.
@Test
fun Sign_In_Title_Displayed() {
[Link] {
AuthenticationTitle(
authenticationMode =
AuthenticationMode.SIGN_IN
)
}
}
226
e
n
m
m
c
a
u
s
o
n
n
s
m
o
c
e
r
a
s
u
n
n
u
k
n
s
u
s
u
n
c
a
n
x
c
n
o
a
u
t
n
c
c
c
s
a
a
n
n
c
n
n
r
c
r
e
a
u
s
d
n
s
d
u
When the A the ti tio Title is co posed for the
SIGN_IN A the ti tio Mode, it is e pe ted that the la-
bel_sign_in_to_a count will be di 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 la-
bel_sign_in_to_a count r source. We’ll then use the as-
ser I Di played fun tion to pe form the a se tion that this
co po able is di played.
@Test
fun Sign_In_Title_Displayed() {
[Link] {
AuthenticationTitle(
authenticationMode =
AuthenticationMode.SIGN_IN
)
}
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link](
[Link].label_sign_in_to_account)
)
.assertIsDisplayed()
}
227
m
r
t
s
s
n
s
u
s
u
e
r
n
s
n
h
c
c
c
a
c
c
a
n
n
c
e
n
r
s
x
c
m
s
h
r
c
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 A the ti tio Mo e.SIGN_UP when co po ing the
A the ti tio Title, as well as u ing the la-
bel_sign_up for_a count r source when pe for ing our
a se tion.
@Test
fun Sign_Up_Title_Displayed() {
[Link] {
AuthenticationTitle(
authenticationMode =
AuthenticationMode.SIGN_UP
)
}
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link](
[Link].label_sign_up_for_account)
)
.assertIsDisplayed()
}
228
fl
s
u
m
o
r
m
n
u
c
a
n
_
c
a
n
c
n
s
d
u
u
e
e
x
n
n
i
c
c
c
a
a
x
x
c
s
n
n
r
m
m
s
n
e
With these tests in place, we can now be sure that the A then-
ti tio Title is u ing the e pe ted string r source du ing
co po tion, based on the A the ti tio Mode that is
provided to it.
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 but-
ton when e pe ted. For this we’ll need to co pose the Au-
the ti tio Bu ton, passing the A the ti tio Mod-
e.SIGN_IN value for the a the ti tio Mode a g ment.
@Test
229
u
m
c
a
n
g
t
n
t
s
c
t
i
s
n
a
c
a
x
n
u
c
e
n
t
u
t
fi
n
t
s
m
n
c
a
u
u
c
a
c
n
x
n
h
u
e
u
n
s
c
c
a
n
n
n
s
u
c
c
n
o
c
a
a
a
e
n
m
m
e
n
n
c
r
a
h
s
t
u
i
m
u
n
n
r
fun Sign_In_Action_Displayed() {
[Link] {
AuthenticationButton(
enableAuthentication = true,
authenticationMode =
AuthenticationMode.SIGN_IN,
onAuthenticate = { }
)
}
}
@Test
fun Sign_In_Action_Displayed() {
[Link] {
AuthenticationButton(
enableAuthentication = true,
authenticationMode =
AuthenticationMode.SIGN_IN,
onAuthenticate = { }
230
d
c
s
m
x
t
s
c
x
t
u
c
e
e
n
U
s
c
e
a
r
e
n
C
i
p
t
m
s
e
s
T
u
o
s
s
n
s
r
c
a
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_
BUTTON)
.assertTextEquals(
[Link]
on()
.[Link]([Link]
ing.action_sign_in)
)
}
@Test
fun Sign_Up_Action_Displayed() {
[Link] {
AuthenticationButton(
enableAuthentication = true,
authenticationMode =
AuthenticationMode.SIGN_UP,
onAuthenticate = { }
)
231
e
u
n
r
m
u
c
a
s
n
r
s
u
d
m
e
n
i
c
s
a
s
c
r
n
t
s
d
c
x
t
d
u
x
n
s
c
a
r
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_
BUTTON)
.assertTextEquals(
[Link]
on()
.[Link](
[Link].action_sign_up)
)
}
@Test
fun Authenticate_Triggered() {
val onAuthenticate: () -> Unit =
mock()
[Link] {
232
c
g
r
u
u
u
n
n
c
n
s
g
c
c
c
a
a
r
u
n
u
n
x
u
o
t
r
c
n
n
m
c
c
a
u
a
s
u
n
n
s
r
i
c
c
r
t
a
o
m
n
s
r
AuthenticationButton(
enableAuthentication =
false,
authenticationMode =
AuthenticationMode.SIGN_UP,
onAuthenticate =
onAuthenticate
)
}
}
@Test
fun Authenticate_Triggered() {
val onAuthenticate: () -> Unit =
mock()
[Link] {
AuthenticationButton(
233
t
t
s
fi
e
i
s
u
r
e
r
c
n
m
x
i
c
m
a
s
U
s
n
c
c
h
t
u
c
C
p
e
s
r
c
T
h
i
fi
m
r
c
s
a
u
u
n
n
n
c
n
c
c
a
a
enableAuthentication =
false,
authenticationMode =
AuthenticationMode.SIGN_UP,
onAuthenticate =
onAuthenticate
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_
BUTTON)
.performClick()
verify(onAuthenticate).invoke()
}
234
l
c
c
t
e
a
a
u
u
t
r
n
u
n
u
m
c
c
a
a
n
s
f
r
s
c
n
n
t
a
r
t
u
u
n
n
m
m
n
s
i
s
i
c
n
a
m
s
e
b
e
u
u
fi
posed to di play the co re pon ing co tent for the provided
A the ti tio Mode. After se ting up a test class with a cor-
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 dis-
played wit in our co po able.
class AuthenticationModeToggleTest {
@get:Rule
val composeTestRule =
createComposeRule()
@Test
fun Need_Account_Action_Displayed()
{
235
u
u
s
r
h
t
s
n
d
c
x
a
t
s
d
h
c
U
a
s
n
x
n
c
n
c
c
C
m
A
c
r
s
m
d
s
_
s
e
s
u
s
d
t
c
c
n
n
u
s
c
m
n
a
c
e
n
s
e
c
n
c
a
n
t
x
d
n
e
e
h
c
@Test
fun Need_Account_Action_Displayed() {
[Link] {
ToggleAuthenticationMode(
authenticationMode =
AuthenticationMode.SIGN_IN,
toggleAuthentication = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATIO
N_TOGGLE)
.assertTextEquals(
[Link]
on()
.[Link](
[Link].action_need_account)
)
}
We’ll next ip this around so that we can a sert that the e pec-
ted a tion_already_have_a count value is di played
when the A the ti tio Mo e.SIGN_UP value is provided
for the a the ti tio Mode a g ment. Our test here is go-
ing to look the same as above, aside from the tweak to the au-
the ti tio Mode a g ment that we pass, along with the
a tion_already_have_a count value that is now b ing
provided to the a ser Te Equals fun tion call.
236
c
n
c
c
u
a
fl
u
n
n
n
s
c
a
c
a
t
r
n
u
x
n
c
t
d
r
c
u
c
s
s
x
e
@Test
fun
Already_Have_Account_Action_Displayed()
{
[Link] {
ToggleAuthenticationMode(
authenticationMode =
AuthenticationMode.SIGN_UP,
toggleAuthentication = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATIO
N_TOGGLE)
.assertTextEquals(
[Link]
on()
.[Link](
[Link].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
237
s
v
c
s
x
c
u
n
e
t
e
n
u
c
a
r
n
c
a
s
m
y
s
m
s
o
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.
@Test
fun Toggle_Authentication_Triggered() {
val toggleAuthentication: () -> Unit
= mock()
[Link] {
ToggleAuthenticationMode(
authenticationMode =
AuthenticationMode.SIGN_UP,
toggleAuthentication =
toggleAuthentication
)
}
}
238
s
c
h
u
c
r
n
n
c
n
a
t
e
e
u
s
c
e
m
u
r
c
o
c
x
e
a
c
s
m
r
n
c
n
U
a
i
i
m
c
c
e
s
C
n
A
e
h
r
r
u
u
c
_
m
p
r
s
e
c
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 suc-
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()
[Link] {
ToggleAuthenticationMode(
authenticationMode =
AuthenticationMode.SIGN_UP,
toggleAuthentication =
toggleAuthentication
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATIO
N_TOGGLE)
.performClick()
verify(toggleAuthentication).invoke()
}
239
s
h
i
fi
r
c
a
n
i
t
s
fi
e
c
s
Tes ing the Email A dress In-
put
When it comes to the Emai I put co po able, an email ar-
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 au-
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 start by se ting up a new test class, Emai In-
pu Test.
class EmailInputTest {
@get:Rule
val composeTestRule =
createComposeRule()
@Test
fun Email_Displayed() {
val email =
"contact@[Link]"
240
n
n
u
t
n
c
t
a
E
e
l
m
n
fl
l
s
fi
m
s
e
t
i
s
t
l
n
n
n
f
r
n
s
m
m
d
m
m
s
t
s
m
s
e
n
s
d
t
r
a
r
u
s
u
l
[Link] {
EmailInput(
email = email,
onEmailChanged = { },
onNextClicked = { }
)
}
}
Next, we’ll need to a sert that this email value is di played in-
side of our co po 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 =
"contact@[Link]"
[Link] {
EmailInput(
email = email,
onEmailChanged = { },
onNextClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.assertTextEquals(email)
241
s
N
c
p
E
m
s
i
s
s
e
l
t
e
s
i
fi
t
x
t
s
fi
c
c
}
With this test in place, we can now be ce tain that a passing test
means the provided email value is g ing to be di played in-
side of our co po able. When 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 un-
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.
@Test
fun Email_Changed_Triggered() {
val onEmailChanged: (email: String)
-> Unit = mock()
val email =
"contact@[Link]"
[Link] {
EmailInput(
email = email,
onEmailChanged =
onEmailChanged,
242
s
i
fi
e
e
s
f
r
c
m
p
r
c
s
c
x
c
c
e
d
E
m
s
r
m
l
E
s
o
r
r
u
l
s
i
l
fi
n
o
m
s
n
s
n
m
r
r
u
e
c
n
s
onNextClicked = { }
)
}
}
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 a pen-
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 (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
243
p
p
p
d
c
n
n
n
s
n
d
fi
x
n
g
c
c
x
t
e
t
g
x
fi
c
n
t
i
n
x
n
n
fi
t
c
h
i
p
x
r
s
n
c
o
s
s
d
s
h
i
d
e
i
r
r
n
p
r
i
x
x
m
m
fi
t
c
i
m
s
x
s
f
t
x
p
r
t
n
r
e
e
s
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 =
"contact@[Link]"
[Link] {
EmailInput(
email = email,
onEmailChanged =
onEmailChanged,
onNextClicked = { }
)
}
val appendedText = ".jetpack"
composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.performTextInput(appendedText)
verify(onEmailChanged).invoke(email
+ appendedText)
}
244
i
fi
c
a
t
s
fi
Tes ing the Pas word I put
When it comes to the Pas wor I put co po able, a pass-
word a g ment is used to provide the co tent that is to be dis-
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 need to start by se ting up a new test class,
Pas wor I pu Test.
class PasswordInputTest {
@get:Rule
val composeTestRule =
createComposeRule()
@Test
fun Password_Displayed() {
val password = "password123"
[Link] {
PasswordInput(
password = password,
245
r
u
s
d
u
n
t
r
n
u
d
n
n
c
a
n
t
m
s
e
fl
s
e
s
d
t
d
fi
n
s
s
s
d
m
i
n
t
n
s
n
n
f
m
m
r
m
e
n
s
s
m
t
m
e
s
s
n
r
t
onPasswordChanged = { },
onDoneClicked = { }
)
}
}
Next, we’ll need to a sert that this pas word value is in fact dis-
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 a sert that the text s man ic value of the node
matches our provided pas word var able.
@Test
fun Password_Displayed() {
val password = "password123"
[Link] {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWOR
D)
.assertTextEquals(password)
}
246
c
n
N
s
_
p
c
s
s
S
m
s
s
l
e
s
i
s
e
t
i
s
fi
t
x
t
fi
c
With this test in place, we can now be ce tain that a passing test
means the provided pas word value is g ing to be di played
i side of our co po able. 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 com-
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.
@Test
fun Password_Changed_Triggered() {
val onEmailChanged: (email: String)
-> Unit = mock()
val password = "password123"
[Link] {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
247
n
c
m
s
n
i
e
e
s
fi
f
c
r
c
m
s
p
r
x
c
s
c
s
s
m
c
s
e
s
n
r
s
n
s
m
r
s
r
o
u
d
s
s
s
d
fi
o
s
i
d
n
n
s
n
m
n
e
r
}
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 a pen-
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 (pass-
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() {
248
p
p
d
c
n
n
s
n
p
d
fi
x
n
g
x
c
n
c
t
e
t
g
x
fi
n
t
i
c
n
x
n
n
fi
t
c
i
s
x
i
h
fi
c
s
r
a
c
o
p
s
s
s
n
h
i
d
e
i
d
r
r
p
n
r
i
x
x
m
m
fi
t
c
i
m
t
s
s
x
s
fi
f
t
x
p
r
t
n
r
e
e
s
val onEmailChanged: (email: String)
-> Unit = mock()
val password = "password123"
[Link] {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
val passwordText = "456"
composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWOR
D)
.performTextInput(passwordText)
verify(onEmailChanged).invoke(password +
passwordText)
}
When it comes to tes ing this, we’ll just write a single test to
check that the vi i i ity toggle co po able r ects the e pec-
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
249
n
s
b
l
b
s
b
l
l
c
s
b
l
t
t
e
fl
s
c
c
e
d
m
n
o
s
r
b
l
s
n
m
r
e
fl
s
n
s
n
s
m
x
b
s
s
n
l
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 tag based on the e abled state
of the toggle.
// [Link]
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 cur-
rent value of our i Pas wor Hi den state, so that we can loc-
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.
// [Link]
trailingIcon = {
Icon(
modifier =
[Link](TAG_PASSWORD_HIDDEN +
isPasswordHidden),
...
)
}
250
m
t
n
p
s
x
c
s
c
s
r
s
c
s
o
d
d
n
m
r
s
p
n
s
With this in place, we can now start wor ing on the test. Here
we’ll b gin by co po ing the Pas wor I put with a string
value for the pas word a g ment.
@Test
fun Password_Toggled_Reflects_state() {
[Link] {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
}
@Test
fun Password_Toggled_Reflects_state() {
[Link] {
PasswordInput(
251
e
d
e
d
s
p
b
s
l
m
n
fl
o
c
r
s
s
b
r
l
s
u
_
o
s
S
b
s
l
s
n
b
p
l
r
c
c
m
g
n
k
d
c
t
i
n
y
D
s
x
m
c
s
p
m
s
n
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDD
EN + "true")
.performClick()
}
@Test
fun Password_Toggled_Reflects_state() {
[Link] {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDD
EN + "true")
252
s
c
m
s
s
e
s
m
s
s
s
t
r
b
s
l
m
_
s
S
m
m
s
n
c
s
s
r
D
c
.performClick()
composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDD
EN + "false")
.assertIsDisplayed()
}
While we could have a se a ate test here to a sert that the true
co d tion is di 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 hi den
state of the vi i i ity toggle would not cu rently be b ing dis-
played on the screen. B cause this test r quires the hi den
state of the vi i i ity toggle to pe form the a ser I Dis-
played a se tion, we cover both sce ar os in a single test.
// [Link]
253
e
n
d
i
n
t
x
n
s
m
e
s
c
n
r
d
fi
s
s
s
e
e
b
e
i
b
o
y
l
l
n
e
e
e
e
e
e
p
e
e
r
i
h
T
s
e
m
m
v
r
n
s
u
i
s
fi
s
c
r
m
e
s
s
s
s
n
h
i
t
e
s
s
m
p
d
d
r
class PasswordRequirementsTest {
@get:Rule
val composeTestRule =
createComposeRule()
We’re rst g ing to write a test to a sert that each of the pass-
word r quir ments is di played as e pe ted. To keep things
simple here and avoid nee ing to write mu tiple test co di-
tions, we’re g ing to write a test that will a sign ra dom pass-
word r quir ments as sa i ed. This way we can a sert that ex-
pe ted r quir ments are di played as both sa i ed and u sat-
i ed.
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 to be used as the sa is-
ed r quir ment.
val requirements =
[Link]().toList()
val satisfiedRequirement =
requirements[(0 until
[Link]()).random()]
254
fi
s
e
fi
c
e
e
fi
e
e
t
o
e
e
m
e
e
o
o
e
n
n
t
s
s
fi
x
s
d
m
s
e
d
v
s
e
x
t
s
c
fi
s
e
t
l
s
e
fi
s
n
e
l
m
n
n
t
Pas wor R quir ments, provi ing a list for the sa i fied-
R quir ments a g ment that co sists of the ra dom r quire-
ment that we r trieved above, sa i fie R quir ment.
@Test
fun
Password_Requirements_Displayed_As_Not_S
atisfied() {
val requirements =
[Link]().toList()
val satisfiedRequirement =
requirements[(0 until
[Link]()).random()]
[Link] {
PasswordRequirements(
satisfiedRequirements =
listOf(satisfied)
)
}
}
[Link]().forEach {
255
s
e
fi
s
s
e
e
d
d
e
e
e
e
s
r
e
e
e
d
u
e
e
p
l
e
d
t
n
s
m
d
m
e
n
e
t
s
e
l
o
}
Next, we’ll use each of their l bels, along with the provided
sa i fie R quir ment to build the string that we’re g ing
to a sert for. The Pas wor R quir ments co po able is
forma ting two di fe ent string re re en tions based on the
sa i ed state of each. If a r quir ment is marked as sa i ed,
then the pas word_r quir ment_sa i fied r source is
used to build a string for that r 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.
[Link]().forEach
{ requirement ->
val requirement =
[Link]
on()
.[Link]([Link])
[Link]
on()
.[Link](
[Link].password_requirement_satisfied,
256
t
t
t
s
s
fi
s
e
s
s
t
a
e
d
d
e
e
e
s
h
f
e
e
e
r
e
e
s
e
e
e
d
e
e
a
e
e
p
e
n
o
s
fi
e
e
t
t
a
s
e
i
d
m
h
e
r
s
t
s
o
fi
requirement)
} else {
[Link]
on()
.[Link](
[Link].password_requirement_needed,
requirement)
}
}
Now, this string is b ing built, we can use this to lo ate a node
and pe form an a se tion to e sure that it is b ing di played
wit in the co po able.
[Link]().forEach
{ requirement ->
val requirement =
[Link]
on()
.[Link]([Link])
val result = if (requirement ==
satisfiedRequirement) {
[Link]
on()
.[Link](
257
h
r
m
s
s
e
r
n
e
c
s
[Link].password_requirement_satisfied,
requirement)
} else {
[Link]
on()
.[Link](
[Link].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 R quir ment va ues and a ser ing that the
e pe ted r quir ment me sage is di played wit in the com-
po able.
@Test
fun
Password_Requirements_Displayed_With_Sta
te() {
val requirements =
[Link]().toList()
258
x
s
l
c
e
e
s
e
d
e
s
e
p
l
s
s
h
t
val satisfied = requirements[(0
until 3).random()]
[Link] {
PasswordRequirements(
satisfiedRequirements =
listOf(satisfied)
)
}
[Link]().forEach
{
val requirement =
[Link]
on()
.[Link]([Link]
bel)
val result = if (it ==
satisfied) {
[Link]
on()
.[Link](
[Link].password_requirement_satisfied,
requirement)
} else {
[Link]
on()
259
.[Link](
[Link].password_requirement_needed,
requirement)
}
composeTestRule
.onNodeWithText(result)
.assertIsDisplayed()
}
}
class AuthenticationErrorDialogTest {
@get:Rule
val composeTestRule =
createComposeRule()
260
r
u
h
s
t
s
l
p
c
r
r
a
p
r
u
r
u
m
s
n
x
n
s
a
c
a
c
c
a
a
n
s
n
r
r
r
r
u
a
a
r
a
o
n
g
c
a
}
We’ll start here by wri ing a test that will be used to a sert that
the provided e ror me sage is di played wit in our di log as
e pe ted. Here we’ll need to de ne a new test that will be used
to house this test l gic, co po ing an A the ti tio Err-
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 ex-
act text b ing provided via the e ror a g ment.
@Test
fun Error_Displayed() {
val error = "This is an error"
[Link] {
AuthenticationErrorDialog(
error = error,
dismissError = { }
)
}
composeTestRule
.onNodeWithText(error)
.assertTextEquals(error)
}
261
x
r
f
r
n
c
a
r
e
r
s
a
r
e
o
t
s
s
m
m
o
s
i
m
s
m
s
s
i
s
s
fi
r
s
r
s
a
r
s
u
u
m
s
h
n
s
s
s
c
u
a
r
s
n
p
a
n
r
r
c
able to di miss the di log and be u able to pe form a the tic-
tion.
@Test
fun Dismiss_triggered_from_action() {
val dismissError: () -> Unit =
mock()
[Link] {
AuthenticationErrorDialog(
error = "This is an error",
dismissError = dismissError
)
}
composeTestRule
.onNodeWithText(
[Link]
on()
.[Link]([Link]
ing.error_action)
262
a
m
l
t
a
u
r
s
u
n
h
x
o
i
c
a
c
r
s
n
a
n
s
s
r
c
r
x
r
g
s
c
a
s
n
s
t
l
x
r
c
s
e
i
r
s
i
d
s
y
c
m
m
u
k
m
n
s
)
.performClick()
verify(dismissError).invoke()
}
263
u
a
s
l
n
h
m
a
n
x
m
m
c
t
s
s
l
m
o
s
k
e
g
x
m
c
p
f
a
r