0% found this document useful (0 votes)
2K views211 pages

Practical Jetpack Compose

The document discusses building an authentication form for user accounts in apps. It explains that authentication is commonly done through email as this decouples users from third parties and offers a common authentication method. The document will build out an email authentication screen using React to demonstrate state management compared to traditional Android UI approaches.

Uploaded by

Brian Fox
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2K views211 pages

Practical Jetpack Compose

The document discusses building an authentication form for user accounts in apps. It explains that authentication is commonly done through email as this decouples users from third parties and offers a common authentication method. The document will build out an email authentication screen using React to demonstrate state management compared to traditional Android UI approaches.

Uploaded by

Brian Fox
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 211

Building an A the 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 ac-

cess an e is ing one. The abi ity to be able to achieve this will be provided through

an a the ti tion screen, provi ing a way for users to sign-up or sign-in u ing one

of the met ods that your a pli tion o fers. While many apps o fer a the ti tion

via third-party sites (such as s cial a counts), a co mon r quir ment is email au-

the ti tion - not only does this d couple users a counts from third-party sites, but

it also o fers a co mon a the ti tion met od for co nec ing with your a pli a-

tion.

With this in mind, we’re g ing to build out an email a the ti tion screen us-

ing je pack co pose. While an email form isn’t vis ally the most e qui ite e pe i-

ence to be buil ing, it o fers a co le tion of ways for us to think about state man-

ag ment and is a pe fect e ample of how di fe ent an i pl men tion may be

when co pared to the e is ing A droid UI toolkit.

1
















































































When it comes to this screen, we need to co ure and handle se e al di fe ent

things.

- Add the UI for user i te a tion to pe form a the ti tion

- Pe sist the state for the entered email and pas word

- Ma age the UI state based on the entered cr de tials, such as di abling

the bu ton when no cr de tials have been entered

- Handle the state change when the a the ti tion mode toggle bu ton is

pressed

- 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

2



















fi






fi













We can see here that when it comes to the a the ti tion screen, there is more to it

than a mi i al form that is used for cr de tial entry. The ma ag ment of state and

r co po tion of the UI based on those state changes gives us some good found-

tions to really start to e plore the concept of state wit in Je pack Co pose.

3



















D ing the A the ti tion
State
Wit in our a the ti tion form, we’re g ing to be dea ing with many di fe ent

pieces of state that d pict how the UI is g ing to be di played to the user. This

state will be used to co pose our UI, a lo ing us to co trol how our co po ables

look and b have to the user. Wit in our UI we’re g ing to need to show the fo low-

ing things:

- A title that will tell the user that they need to sign-in or sign-up

- An i put eld which will be used to enter an email a dress

- An i put eld which will be used to enter a pas word

- A pas word va i tion i di a or to n t fy the user whet er their pass-

word meets the mi i um r quir ments

- A bu ton that will a low the user to tri ger the a the ti tion ow. This

bu ton will also be di abled when there is no co tent i put into the

email and pas word elds

- A bu ton that will a low the user to toggle between sign-in and sign-up

- A pr gress i di a or which will di play when the sign-in or sign-up o er-

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.

// AuthenticationState.kt

4











fi



fi
fi













fi














































fl










data class AuthenticationState(

Mo e ling the user cr de tials


We’ll start with the co tent that is g ing to be i put by the user - the email a dress

and pas word. B cause this is som thing that the user will enter and will be dis-

played wit in our UI, each of these is g ing to re re ent a piece of our state. For

this, we’ll go ahead and add two string re e ences to our state class.

data class AuthenticationState(


val email: String? = null,
val password: String? = null
)

Mo e ling the pas word r 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 pas word

that is entered.

enum class PasswordRequirements {


CAPITAL_LETTER, NUMBER, EIGHT_CHARACTERS
}

For each of the r quir ments we’re g ing to want to show a me sage, this will be

used to co m ni ate what the r quir ment is to the user. For this, we’ll need to

start by adding some new strings to our strin s.xml le.

5







































fi









<string name="password_requirement_characters">
At least 8 characters
</string>

<string name="password_requirement_capital">
At least 1 upper-case letter
</string>

<string name="password_requirement_digit">
At least 1 digit
</string>

We’ll then a just the Pas wor R quir ment to co tain a l bel in the form of a

string r source i teger, se ting a r source for each of the r quir ment enums val-

ues that we pr v ously de ned.

enum class PasswordRequirement(


@StringRes val label: Int
) {
CAPITAL_LETTER(R.string.requirement_capital),
NUMBER(R.string.requirement_digit),
EIGHT_CHARACTERS(R.string.requirement_characters)
}

Wit in the state, we’ll need the cu rent Pas wor R quir ments that are sa i ed

so that we can co m ni ate this to the user. For this rea on, we’ll add a new pass-

wor R quir ments eld to our state that re re ents a list of r quir ments that

have been met.

data class AuthenticationState(


val email: String? = null,
val password: String? = null,
val passwordRequirements: List<PasswordRequirements> =
emptyList()
)

6











fi

fi






















fi
Mo e ling the a the ti tion mode
The above gives us the key parts of the state that we need to a low the user to sign-

up or sign-in to our a pli tion. Ho ever, we’re g ing to be d ing a little more

than that! B cause a user can sign-up and sign-in to our a pli tion, we need to al-

low the form to be toggle-able between these two modes. To be able to re re ent

this in our state we’re g ing to start by d ing a new enum that will be used to

si n fy whet er the form is cu rently b ing used to sign up or sign in.

enum class AuthenticationMode {


SIGN_UP, SIGN_IN
}

Now we can go ahead and add this to our state - this will then a low us to ea ily

toggle between these two modes and have our UI r co pose whene er the s lec-

ted mode changes.

data class AuthenticationState(


val authenticationMode: AuthenticationMode =
AuthenticationMode.SIGN_IN,
val email: String? = null,
val password: String? = null,
val passwordRequirements: List<PasswordRequirements> =
emptyList()
)

Mo e ling the loa ing state


Now that our state re re ents both a sign-in and sign-up ow, we’re g ing to want

to be able to handle the state when a user reaches the point of pe for ing those

o e tions. While we won’t be hi ting an API in our UI e ample, in the real world

this would be an asy chro ous o e tion - mea ing that we would have to wait a

7





























fi










fl













second or two for it to co plete. In this sce ario, we would want to show a pro-

gress i di a or to the user so that they know a r quest is ta ing place. We’ll re res-

ent this in our state by adding a loa ing ag to the state class.

data class AuthenticationState(


val authenticationMode: AuthenticationMode =
AuthenticationMode.SIGN_IN,
val email: String? = null,
val password: String? = null,
val passwordRequirements: List<PasswordRequirements> =
emptyList(),
val isLoading: Boolean = false
)

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 re-

sponse that would come back from our API. In the case of su cess, we would na ig-

ate onto the next part of our a pli tion, but things might not a ways go as

planned. In these cases, we’ll want to show an e ror to the user, us ally in the form

of the e ror that has come back from the API. To a low for this, we’ll add a new eld

to our state class which will re re ent if an e ror has o curred and at the same time,

the co tent of that e ror.

data class AuthenticationState(


val authenticationMode: AuthenticationMode =
AuthenticationMode.SIGN_IN,
val email: String? = null,
val password: String? = null,
val passwordRequirements: List<PasswordRequirements> =
emptyList(),
val isLoading: Boolean = false,
val error: String? = null

8


















fl












fi

)

Mo e ling the va id state


As well as the co tent that can be entered into the i put elds, the user can also

d lete this co tent. These elds the selves will also start blank, so in both of these

states, we don’t want the user to be able to su mit the form. We also want to block

the form from b ing su mi ted if the pas word r quir ments have not been sa is-

ed. To handle these cases we’ll add a new fun tion to our state - this will r turn a

boolean value that re re ents whet er the form co tent is va id. We’re u ing a

fun tion for this so that the place that has the a cess to this state can ea ily check if

the cu rent state a lows the user to pr ceed, i stead of adding a ot er value wit in

our state.

data class AuthenticationState(


val authenticationMode: AuthenticationMode =
AuthenticationMode.SIGN_IN,
val email: String? = null,
val password: String? = null,
val passwordRequirements: List<PasswordRequirements> =
emptyList(),
val isLoading: Boolean = false,
val error: String? = null
) {
fun isFormValid(): Boolean {
return password?.isNotEmpty() == true &&
email?.isNotEmpty() == true &&
(authenticationMode == AuthenticationMode.SIGN_IN
|| passwordRequirements.containsAll(
PasswordRequirements.values().toList()))
}
}

9
fi












fi














fi








With that in place, we now have a class that can be used to re re ent the state of

our UI. Over the next few se tions of this chapter, we’ll uti ise this state when build-

ing out our UI, mod f ing its va ues as i te a tions with co po ables take place,

tri ge ing r co po tions to r ect any state changes.

10










fl









Cr a ing the A the ti tion
Vie Mo el
Now that we have the state mo elled for our A the ti tion screen, we can start

thin ing about the Vie Mo el that will be used to ma age that state and provide a

way to o che trate it to the user i te face.

11





















Se ting up the Vie Mo el
B fore we can get sta ted here, we’re g ing to add a new d pen ency to our pro-

ject that will give us a cess to the A droid L f cycle Vie Mo el class:

implementation "androidx.lifecycle:lifecycle-viewmodel-
compose:2.4.0"

💡 You aren’t r quired to use a Vie Mo el when wor ing with co pose. For the

sake of these e e cises, it helps us to keep things simple and fo low an a proach

that many d velopers are f mi ar with.

Next, we’ll cr ate a new Vie Mo el, called A the ti tio Vie Mo el.

// AuthenticationViewModel.kt
class AuthenticationViewModel : ViewModel() {

This Vie Mo el is g ing to need to hold a re e ence to the state of our screen. For

this we’re g ing to uti ise Stat Flow - this a lows us to cr ate a state-hol er ob-

ser able ow that will emit the d fault state we provide to it, along with any up-

dates that o cur du ing its lif time. Here we’ll cr ate a new Mu abl Stat Flow in-

stance, provi ing a re e ence to our A the ti tio State class as the d fault

value for our Stat Flow:

class AuthenticationViewModel : ViewModel() {


val uiState = MutableStateFlow(AuthenticationState())
}

12




fl































































With this in place, we now have a Stat Flow re e ence that is hol ing a re e ence to

our a the ti tion state, in tia ising it with a new i stance of the state class and r ly-

ing on the d faults that the co struc or provides.

13

















M ni la ing state u ing events
While this state is now in place, we need to start thin ing about the di fe ent ways

in which it can be m ni lated - whene er som thing is changed in our UI (text ed-

ited, bu ton pressed), we’ll want to u date the state wit in our Vie Mo el so that

the o serving UI can r ect those changes.

For when this is the case, we’re g ing to mo el some events that can be

triggered in our co po able UI and in turn these events will be used to m ni late

the state wit in the view mo el. This a lows us to have a single way of our co pos-

able UI co m ni a ing with the Vie Mo el, rather than nee ing to pass the e tire

Vie Mo el or many re e ences to se a ate fun tions which could be used to trig-

ger state changes. I stead, we can pass a single fun tion re e ence to our co pos-

able UI which can then be used to tri ger these events in the Vie Mo el. For these

events we’re g ing to need to de ne di fe ent types that can be triggered, so we’ll

go ahead and cr ate a new sealed class to re re ent these.

// AuthenticationEvent.kt
sealed class AuthenticationEvent {

14





















fl




fi



































Han ling a the ti tion mode tog-
gling
With this sealed class in place, we can start to think about u ing it to re re ent the

di fe ent events that can o cur. We’re g ing to start by han ling the sce ario where

the a the ti tion mode can be toggled between sign-up and sign-in. For this,

we’ll cr ate a new event called Toggl A thenti tio Mode.

sealed class AuthenticationEvent {

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 func-

tion that will be used to ‘ ip’ the cu rent a the ti tion mode - switc ing it

between sign-up and sign-in.

private fun toggleAuthenticationMode() {


val authenticationMode = uiState.value.authenticationMode
val newAuthenticationMode = if (
authenticationMode == AuthenticationMode.SIGN_IN
) {
AuthenticationMode.SIGN_UP
} else {
AuthenticationMode.SIGN_IN
}
uiState.value = uiState.value.copy(
authenticationMode = newAuthenticationMode
)
}

With this code above, we take the cu rent a the ti tion mode wit in our a then-

ti tion state and set it to the o po ite value. We then use this to copy our e is ing

15















fl







































a the ti tion state, se ting the a the ti tion mode to r ect the newly ca cu-

lated value. When this is done, the new value will be emi ted to the o ser er of our

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 re e ence, r pl cing

any va ues that have been provided as a g ments to the fun tion.

Now that we have this fun tion avai able to handle the state change of the au-

the ti tion mode, we need to go ahead and add su port for tri ge ing it from

ou side of our Vie Mo el. For this, we’ll add a new fun tion to our Vie Mo el that

will take a re e ence to an A the ti tio Event.

fun handleEvent(authenticationEvent: AuthenticationEvent) {


when (authenticationEvent) {

}
}

This fun tion takes an A the ti tio Event re e ence and then can use that to

tri ger the d sired ou come, based on the event which has been provided to it. So

in this case of the a the ti tion mode toggle event, we’re g ing to tri ger the

toggl A thenti tio Mode fun tion.

fun handleEvent(authenticationEvent: AuthenticationEvent) {


when (authenticationEvent) {
is AuthenticationEvent.ToggleAuthenticationMode -> {
toggleAuthenticationMode()
}
}
}

While we’re not g ing to i pl ment the call to handl Event u til the next se tion

of this chapter, our co po able UI will be able to make the fo lo ing call to tri ger

the a the ti tion mode toggle:

viewmodel.handleEvent(
AuthenticationEvent.ToggleAuthenticationMode

16

































































fl









fl

















)

Han ling i put eld changes


With the a the ti tion mode toggle in place, we can start thin ing about the oth-

er events that we want to tri ger from ou side of our Vie Mo el. If we take a look

at the a the ti tion state mo el then we can see the ot er pieces of the state that

we need to m ni late. Two key pieces of this state are the email a dress and

pas word, these will re re ent the data that has been i put by the user and will

need to be r e ted in our state whene er they change. For this, we’re g ing to

add two more events to our A the ti tion Event sealed class.

sealed class AuthenticationEvent {

object ToggleAuthenticationMode: AuthenticationEvent()

class EmailChanged(val emailAddress: String):


AuthenticationEvent()

class PasswordChanged(val password: String):


AuthenticationEvent()

Here, we’ve a ded an Emai Changed event, along with a Pas wor Changed

event. These will a low the UI to tri ger these events whene er the i put of the

email and pas word elds are changed. For that to be po sible, we’ll go ahead and

i pl ment some fun tions in our Vie Mo el that a low for this state change to be

r e ted. We’ll start with a fun tion, u dat mail, that will simply be used to up-

date our state re e ence with the provided email a dress.

private fun updateEmail(email: String) {

17


fl









fl













fi









fi

























uiState.value = uiState.value.copy(
email = email
)
}

When it comes to u da ing the pas word, we’ll need to cr ate a ot er new func-

tion, u dat Pas word, that will be used to u date the pas word re e ence wit in

our state.

private fun updatePassword(password: String) {


uiState.value = uiState.value.copy(
password = password
)
}

Ho ever, whene er the pas word is u dated we’ll a ways want to u date the valid-

ity of the pas word r quir ments. For this, we’re g ing to cr ate a new list that con-

sists of Pas wor R quir ments, for which we’ll add re e ences for the r quire-

ments that have been met. This list can then be set on our state so that the UI la er

can be aware of the r quir ments that have been sa i ed. When buil ing this list

of sa i ed r quir ments, we’ll need to base this on se e al di fe ent co straints.

- When the length of the pas word is grea er than 7, this means that the

mi i um pas word length has been met. This means that we can add

the Pas wor R quir men s.EIGHT_CHA A TERS to the list.

if (password.length > 7) {
requirements.add(PasswordRequirements.EIGHT_CHARACTERS)
}

- When the pas word co tains an u pe case cha a ter, this means a oth-

er one of our mi i um r quir ments has been met. In this case, we’ll

add the Pas wor R quir men s CA I AL LE TER to the list.

18






fi























































fi



















if (password.any { it.isUpperCase() }) {
requirements.add(PasswordRequirements.CAPITAL_LETTER)
}

- F nally, if the pas word co tains any d git, this means that this r quire-

ment has been sa i ed. In this case, we’ll add the Pas wor R quire-

men s.NU BER value to the list.

if (password.any { it.isDigit() }) {
requirements.add(PasswordRequirements.NUMBER)
}

With this l gic now de ned, we can slot this into our u dat Pas word fun tion

and a sign the re ult to the pas wor R quir ments wit in our A the ti a-

tio State re e ence.

private fun updatePassword(password: String) {

val requirements = mutableListOf<PasswordRequirements>()


if (password.length > 7) {
requirements.add(PasswordRequirements.EIGHT_CHARACTERS)
}
if (password.any { it.isUpperCase() }) {
requirements.add(PasswordRequirements.CAPITAL_LETTER)
}
if (password.any { it.isDigit() }) {
requirements.add(PasswordRequirements.NUMBER)
}
uiState.value = uiState.value.copy(
password = password,
passwordRequirements = requirements.toList()
)
}

With these email and pas word fun tions in place, these can now be triggered via

an event whene er the i put is changed for either of those elds. We can then go

ahead and add these fun tion calls to our handl Event fun tion - now when the

19













fi
fi














fi










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 AuthenticationEvent.ToggleAuthenticationMode -> {
toggleAuthenticationMode()
}
is AuthenticationEvent.EmailChanged -> {
updateEmail(authenticationEvent.emailAddress)
}
is AuthenticationEvent.PasswordChanged -> {
updatePassword(authenticationEvent.password)
}
}
}

Han ling a the ti tion tri gers


Now that users can toggle between a the ti tion modes and enter their a count

cr de tials, na u ally, the next step would be to a low the UI to tri ger a the ti a-

tion. For this event we don’t need to send any data from the UI - this is b cause the

entered email a dress and pas word are already r e ted wit in our A the ti a-

tion State. So here we can simply add a ot er event type to our A the ti tion

Event sealed class.

sealed class AuthenticationEvent {

object ToggleAuthenticationMode: AuthenticationEvent()

class EmailChanged(val emailAddress: String):


AuthenticationEvent()

class PasswordChanged(val password: String):


AuthenticationEvent()

20

























fl

















object Authenticate: AuthenticationEvent()

With this in place, we can next add a fun tion that this event will be used to tri ger.

For this e ample, we won’t be tri ge ing a ne work r quest, but i stead will be re-

spon ing to the a the ti tion r quest from the UI and r ec ing this via the load-

ing pro erty wit in the a the ti tion state.

private fun authenticate() {


uiState.value = uiState.value.copy(
isLoading = true
)
// trigger network request
}

With this fun tion in place, we now have som thing that will si late the ne work

r quest ta ing place wit in our a pli tion. We can now also add this event type to

our handl Event fun tion, a lo ing the A the ti ate event to be triggered and

handled from ou side of our Vie Mo el.

fun handleEvent(authenticationEvent: AuthenticationEvent) {


when (authenticationEvent) {
is AuthenticationEvent.ToggleAuthenticationMode -> {
toggleAuthenticationMode()
}
is AuthenticationEvent.EmailChanged -> {
updateEmail(authenticationEvent.emailAddress)
}
is AuthenticationEvent.PasswordChanged -> {
updatePassword(authenticationEvent.password)
}
is AuthenticationEvent.Authenticate -> {
authenticate()
}
}
}

21





































fl






Han ling a the ti tion e rors
With the above in place, we have a Vie Mo el that can handle the di fe ent re-

quired state pro e ties and events that a low the user to enter their cr de tials,

toggle between a the ti tion modes and then pr ceed with tri ge ing the au-

the ti tion pr cess.

Ho ever, we still have a nal pro erty from our A the ti tion State to handle -

and that is the e ror. This pro erty d faults to null wit in our state, so in that case,

our UI won’t need to di play any kind of e ror. But when this is not null, the UI will

re re ent that e ror in some way and then also provide a way for it to be di missed

from view. So that we can si late this sce ario and then ma age the e pe ted

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.

private fun authenticate() {


uiState.value = uiState.value.copy(
isLoading = true
)
viewModelScope.launch(Dispatchers.IO) {
delay(2000L)

withContext(Dispatchers.Main) {
uiState.value = uiState.value.copy(
isLoading = false,
error = “Something went wrong!”
)
}
}
}

You’ll n tice here that we’re ho ping between coroutine di patc ers - while we’re

not ma ing a real ne work r quest there, this is to si late a real sce ario and en-

22




























fi­




















































sure that the asy chro ous work and live data emi sions ha pen u ing the e pec-

ted di patc ers. With this in place, the e ror will be emi ted as part of the a then-

ti tion state for the UI to r ect. While we haven’t cr ated the UI yet, this e ror will

be co posed in the form of an alert di log - this means that we will also need to

be able to di miss this di log. To su port this, the e ror of our state will need to be

cleared so that the UI is r co posed the alert di log is not a part of the co po i-

tion. So that this can be cleared from our state from our UI, we’re g ing to add a

new A the ti tio Event called E ro ismissed.

sealed class AuthenticationEvent {

object ToggleAuthenticationMode: AuthenticationEvent()

class EmailChanged(val emailAddress: String):


AuthenticationEvent()

class PasswordChanged(val password: String):


AuthenticationEvent()

object Authenticate: AuthenticationEvent()

object ErrorDismissed: AuthenticationEvent()


}

With this in place, we are now able to r ceive e ror di missal events from our UI

la er, mea ing that we also need to i pl ment the state change for when this oc-

curs. Here we’ll cr ate a new fun tion that will be used to simply clear the e ror

private fun dismissError() {


uiState.value = uiState.value.copy(
error = null
)
}

23



















fl



























The last thing to do here is tri ger this fun tion whene er the E ro ismissed event

is triggered. For this, we’ll add a nal check to our handl Event when clause to trig-

ger our fun tion.

fun handleEvent(authenticationEvent: AuthenticationEvent) {


when (authenticationEvent) {
is AuthenticationEvent.ToggleAuthenticationMode -> {
toggleAuthenticationMode()
}
is AuthenticationEvent.EmailChanged -> {
updateEmail(authenticationEvent.emailAddress)
}
is AuthenticationEvent.PasswordChanged -> {
updatePassword(authenticationEvent.password)
}
is AuthenticationEvent.Authenticate -> {
authenticate()
}
is AuthenticationEvent.ErrorDismissed -> {
dismissError()
}
}
}

With this i pl me ted, we are now ma aging the state of our a the ti tion

screen and provi ing the r quired entry points for our UI la er to m ni late the

state based on user i te a tion. Our view mo el is now ready to be plugged into a

co po able UI, which we’ll cr ate in the next se tion of this chapter!

24













fi­

















Cr a ing the A the ti tion
UI
With the Vie Mo el and state ma ag ment all in place, we’re ready to move on

and start i pl men ing the co po able UI for our a the ti tion screen. When

we’re ished buil ing this UI, we’re g ing to end up with som thing that looks

like the fo lo ing:

25
fi

























This UI will give our users a screen that can be used to log in to an a pli tion - giv-

ing the o tion of pe for ing either a sign-in or sign-up o e tion. While buil ing

this UI we’ll dive into the sp ci cs of how the co po ables can be co gured,

along with adding some nice touches to i prove the User E pe ence of our Au-

the ti tion screen.

26







f­i













fi

Se ting up the entry point
B fore we can get start buil ing our pr ject, we’re g ing to need to add a couple

of d pen e cies that we’re g ing to need. We’ll start here by adding these to the

build gradle le for our new pr ject:

// provides access to the `ComponentActivity` class that can be


used to compose UI components
implementation 'androidx.activity:activity-compose:1.4.0'

// foundational classes from the Compose APIs


implementation
"androidx.compose.foundation:foundation:$compose_version"

// UI components from the Compose APIs


implementation "androidx.compose.ui:ui:$compose_version"

// Material Design components from the Compose APIs


implementation
"androidx.compose.material:material:$compose_version"

// Provides an extended collection of Material Iconography


implementation
"androidx.compose.material:material-icons-extended:
$compose_version"

// Tooling functionality for Composables, such as previews


implementation "androidx.compose.ui:ui-tooling-preview:
$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:
$compose_version"

The cu rent r lease of this book is buil ing against 1.1.0 of co pose - be sure to

check co pa ibly if u ing a ne er ve sion.

27










fi










With these a ded to our pr ject, we’re now ready to start buil ing out our UI.

We’re g ing to start here by buil ing the a cess point to our fe ture - this is how

the me saging fe ture will in tially be co posed wit in our user i te face.

Here we’ll b gin by buil ing a root co po able fun tion, A the ti tion

that will be used to house all of our Co po able UI for the A the ti tion screen.

For this, we’ll cr ate a new Ko lin le called A the ti tion.kt (to keep our

co po ables nicely o ga ised), fo lowed by cr a ing a new co po able fun tion,

A the ti tion:

// Authentication.kt
@Composable
fun Authentication() { }

This co po able is g ing to be the entry point to our A the ti tion screen - so

we don’t want this fun tion to have to take any a g ments. The point that is na i at-

ing to a the ti tion will be able to just co pose this fun tion, and this co pos-

able will handle everything else. While you won’t see an thing vis al just yet, you’ll

want to co pose this A the ti tion co po able wit in the se Co tent

block of the acti ity that was cr ated through the pr ject wi ard. Then as we build

out the pr ject, we’ll be able to vis a ise the A the ti tion when ru ning the

pr ject.

Wit in this root level co po able we’re g ing to want to force our a pli tion

theme on the co po ables that are co tained i side of it, so we’ll add a M te i-

a Theme co po able d cla tion here.

@Composable
fun Authentication() {
MaterialTheme {

}
}

28
















































fi
































































💡 Wra ping your co po able hie archy in a theme a lows all co po ents to be

co sis ently styled. In most cases, this can ha pen at the highest point of co po i-

tion - even wit in the se Co tent fun tion of the pa ent acti ity that is co po ing

the UI.

This now means that for any of the co po ables that are co posed in the con-

tent block of our M te a Theme, these will be themed a cor ing to the co ors

and styles that are d clared wit in our theme.

Sa ing that though, we don’t cu rently have any co po ables that are g ing to

make up the co tent of our a the ti tion form. We’ll go ahead and cr ate a new

co po able fun tion here, A the ti tio Co tent. The di fe ence here is that

this co po able is g ing to be r spon ible for co po ing our UI based on the

state of the screen, mea ing that this fun tion is g ing to take some form of a gu-

ment. B cause it’s co po ing UI based on our state, it’s also g ing to need to

propa ate any events so that the state can be u dated a cor ingly. For this rea on,

it’s also g ing to need to be able to handle the A the ti tio Event types that

we pr v ously de ned. We’ll need to de ne two a d tio al a g ments for our com-

po able to sa i fy these r quir ments.

@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {

💡 It’s good pra tice to a low a mo er to be passed into a co po able, this

means that the pa ent who is co po ing the child can co trol the co po tion to

some e tent. This also helps to keep your co po able fun tions re-u able across

your UI.

29




















fi



































fi­

fi






















































This co po able will take an A the ti tio State that re re ents the state

of our screen, along with an event han ler that a lows us to pass up A the ti a-

tio Event i stances when they are triggered.

So, why can’t this just be the entry point to our a the ti tion screen? One

clear thing here is that this d couples the co po able with b ing co cerned

about how the state is provided - passing in the state via an a g ment makes it sim-

pler, as in, it gets passed a state and co poses UI based on it. This also makes it

much eas er to write tests our co po able - b cause we can simply pass it a state

o ject and pe form a se tions based on that, rather than nee ing to si late user

a tions and pe form moc ing to pr duce e pe ted states.

With this co po able de ned, we can then hop up to the root A the ti a-

tion that we de ned and co pose the A the ti tio Co tent wit in our

theme block.

@Composable
fun Authentication() {

MaterialTheme {
AuthenticationContent(
modifier = Modifier.fillMaxWidth(),
authenticationState = ...,
handleEvent = ...
)
}
}

Things aren’t quite sa i ed here yet though, we need to provide both the state

and event han ler to our A the ti tio Co tent. We’ll be u ing our A then-

ti tio Vie Mo el that we cr ated in the pr v ous se tions to sa i fy these ar-

g ments, so we’ll rst need to r trieve an i stance of this. So that we can a cess

this i for tion, we’re g ing to need to o tain an i stance to this Vie Mo el in-

side of our co po able. Here we’ll use the vie Mo el fun tion from the l fe-

30





















fi

fi




fi



fi





































































cycle-vie mo el-co pose pac age. This will r trieve an i stance of the d sired

Vie Mo el, cr a ing one if it does not cu rently e ist.

val viewModel: AuthenticationViewModel = viewModel()

We can then use this Vie Mo el wit in our root co po able for our r quired a gu-

ments.

- We’ll use co le A State() to co lect the emi sions of our State-

Flow re e ence from our Vie Mo el as co po able state. We’ll then

pass the value of this emi sion as the state re e ence to our co po able.

- For event han ling, our Vie Mo el co tains a fun tion that matches the

r quir ments of our lambda fun tion a g ment. We can pass this func-

tion re e ence di ectly to our co po able u ing vie Mo el::handle-

Event.

@Composable
fun Authentication() {
val viewModel: AuthenticationViewModel = viewModel()

MaterialTheme {
AuthenticationContent(
modifier = Modifier.fillMaxWidth(),
authenticationState =
viewModel.uiState.collectAsState().value,
handleEvent = viewModel::handleEvent
)
}
}

With this in place, we have a co po able fun tion that will house the co po ables

ma ing up our a the ti tion UI. As you might r me ber from when we cr ated

the A the ti tio State, there’s a lot to take into a count when it comes to the UI.

While there is plenty of space for there to be more co ple ity and co d tions to

31














































































think about in r gards to the state, we have plenty to think about here when it

comes to buil ing the a t al UI. When it comes to this, we’re g ing to need to

break that down into a Co po able re re en tion.

• A Pa ent Box to hold the di fe ent co po ables that make up our a the ti tion

screen

• A Pr gress I di a or to si n fy the loa ing state of the screen

• An a the ti tion form co po able that will hold the i put elds, bu tons

and ot er form co po ents

• An Alert Di log used to di play a the ti tion e ror me sages

32








































fi






With the above in mind, we can start buil ing out our Co po able UI to rep-

re ent our a the ti tion screen. We’re g ing to build a co le tion of co po able

fun tions, all of which can be plugged t get er to cr ate the co plete screen.

33


















D ing the Pa ent Co tai er

We b gin at the start of the above i lu tr tion, the pa ent Box co po able. Be-

cause our UI can show three di fe ent child co po ables, we need a co tai er to

house those. Ot er than the a the ti tion form, we’re g ing to be di pla ing

either a pr gress i di a or (which needs to be di played in the ce ter of the par-

34


fi






























ent) or an alert di log (which will be shown over the top of the a the ti tion

form), we need these to be placed in a co tai er to a low su port for these di fer-

ent sce ar os. The Box co po able provides su port for the alig ment of child

co po ables, as well as the abi ity to show ove la ping co po ables - which

makes it pe fect for what we need.

// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier
) {
Box {

}
}

Here we’ve de ned an empty Box, which is enough to a low us to di play child

co po ables i side of it. Ho ever, we need to provide some pro e ties to have

the Box ll the e tire avai able space on-screen, while also provi ing alig ment for

any chi dren that it di plays.

Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {

Here we use the fil Ma Size mo er to have the Box ll all of the avai able

space on the screen (both the width and height wit in its pa ent), along with u ing

the co ten lig ment a g ment to align all of its chi dren in the ce ter of the

Box.

35







fi





fi















fi­









fi


















Di play a Pr gress State

B fore we go ahead and start sho ing co tent to users, we’re g ing to think about

the state that o curs b for hand - which is when a loa ing i di a or will be dis-

played to users. We’re g ing to start here by u ing the i Loa ing pro erty from

our A the ti tio State re e ence. U ing this we’re either g ing to want to show a

36



























pr gress i di a or or go on to di play co tent to the user. To handle these di fe ent

sce ar os, we’ll start by adding an if stat ment that checks the status of this loa ing

ag.

Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
if (authenticationState.isLoading) {

} else {

}
}

With this in place, we can now d cide whet er to co pose the loa ing state or the

co tent state. Here we’re just g ing to tackle the loa ing state of our UI, so we’ll go

ahead and uti ise one of the avai able pr gress i di a or co po ables - the Cir-

c la Pr gres I di a or.

if (authenticationState.isLoading) {
CircularProgressIndicator()
} else {

If we do not provide a pr gress value to the Ci c la Pr gres I di a or then the in-

di a or is co posed as an i d ter i ate i di a or, mea ing that it will spin i de n-

itely while it is di played on the screen. This is ne for our r quir ments, as we’re

just g ing to show it on screen u til the co tent is loaded. If you need to di play a

sp ci c pr gress value on the i di a or, you can provide this pr gress value to the

co po able so that the i di a or can be co posed to re re ent that cu rent pro-

gress.

37
fl









f­i









































fi



























f­i
Di pla ing the L gin Co tent

Even though now we have a pr gress i di a or in place for our l gin screen, this

isn’t som thing we’re g ing to be di pla ing u til the user has triggered the au-

the ti tion ow. So that we can have this be triggered, we’re now g ing to go

38





fl













ahead and build out the a the ti tion form UI. We’ll need to start here by cr a ing

a new co po able fun tion that will be used to house our a the ti tion form.

// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
)

I side of this, we’re g ing to start here by d ing a Column co po able - our au-

the ti tion form is g ing to re re ent a ve ti al stack of co po ents, so a Column

is most a pr pr ate for this. When co po ing this Column, we’ll pass the Mo fi-

er re e ence that was provided to our A the ti tio Form co po able to ap-

ply the provided co straints to this pa ent co po able.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {

}
}

Loo ing at the design of the screen, we’re also g ing to want all of the child com-

po ables to be p s tioned in the ce ter h r zon ally. For this we’ll uti ise the h ri-

zon a lig ment a g ment, provi ing Cente H r zon ally as the value to

be used for alig ment.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {

39











































fi





























}
}

With this in place, we now have a Column that will be used to house the co tents

of our l gin form.

40


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 s.xml le:

41


fi






fi








<string name="label_sign_in_to_account">
Sign In to your account
</string>
<string name="label_sign_up_for_account">
Sign Up for an account
</string>

We’re not g ing to use these just yet, but at least they’re now in place for when it

comes to slo ting them into our UI. So that we can start buil ing out our a the ti a-

tion title, we’ll need to cr ate a new co po able fun tion, A the ti tion-

Title.

// AuthenticationTitle.kt
@Composable
fun AuthenticationTitle(
modifier: Modifier = Modifier
)

And so that this knows what title needs to di play, it’s g ing to need to know the

cu rent A the ti tio Mode of the screen - which we’ll pass in as an a g ment

to the co po able fun tion.

@Composable
fun AuthenticationTitle(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)

Next, we’ll build out a mi i al Text co po able u ing a string r trieved from one

of the pr v ously a ded string r sources. The A the ti tio Mode re e ence we

have d picts whet er the user is cu rently sig ing-in or sig ing-up. Based on this

mode we want to set the title of the screen, so it is clear to the user whet er they

are sig ing-in or sig ing-up. U ing the provided A the ti tio Mode we’ll set

the r source to be used for our Text co po able.

@Composable

42





























































fun AuthenticationTitle(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Text(
text = stringResource(
if (authenticationMode == AuthenticationMode.SIGN_IN)
{
R.string.label_sign_in_to_account
} else {
R.string.label_sign_up_for_account
}
)
)
}

Our title will now be di pla ing a string based on the A the ti tio Mode that

is provided to the fun tion.

43








B cause this Text co po able is the title of the screen, we’re g ing to want to style

the text a cor ing to the theme of our a pli tion. Here we’re g ing to a ply a

fon Size to our Text co po able that feels a bit more ting for a title. We’ll do

this by passing the value of 24.sp to the fon Size a g ment of the co po able.

Text(
text = stringResource(
if (authenticationState.authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.label_sign_in_to_account
} else {
R.string.label_sign_up_for_account
}
),
fontSize = 24.sp

44














fi






)

We’ll also a just the fon Weight of our co po able - this will help to make it

stand out a bit more at the top of our UI. We don’t want this to be too bold when

di played, so we’ll a ply the weight as the Fon Weight Black value.

Text(
text = stringResource(
if (authenticationState.authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.label_sign_in_to_account
} else {
R.string.label_sign_up_for_account
}
),
fontSize = 24.sp,
fontWeight = FontWeight.Black
)

45








Now that our title is styled, we can go ahead and co pose it wit in our UI. B fore

we go ahead and co pose it i side of our A the ti tio Form, we need to en-

sure there is a cess to an A the ti tio Mode re e ence that our title can use.

We’re g ing to be passing this from our state, so we’ll need to rst add this as an

a g ment to our A the ti tio Form co po able.

// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)

46


























fi


Fo lowed by passing this into our A the ti tio Form fun tion at the point of

co po tion.

// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
...

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
authenticationMode =
authenticationState.authenticationMode
)

...
}

Ho ping back over to our A the ti tio Form co po able, we can then com-

pose our A the ti tio Title and pass in the r quired A the ti tion-

Mode re e ence that is now a ces ible from the a the ti tion form.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
AuthenticationTitle(
authenticationMode = authenticationMode
)
}
}

47




































To cr ate a bit of vis al sp cing at the top of our UI, we’ll also add a Spacer com-

po able, sized with a height of 32dp via the use of the height mo er.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode
)
}
}

With this in place, the title for our a the ti tion form is now b ing co posed

wit in our UI - this title is then co posed based on the cu rent A the ti tion-

Mode that is wit in our state.

48
















fi­




49
Cr a ing the i put co tai er

With our title now in place, we can go ahead and start pu ting t get er the area

that will be used to hold the co po ents for the form i put area. As seen in the

end goal of our design, and the co po able stru ture in the di gram above, this is

50















all g ing to be co tained wit in a Card co po ent. The Card is a co po able

used to hold child co po ents in a Card shaped co tai er, o ten e e ated from

the su face that it is di played on top of.

We’re still g ing to be wor ing wit in the A the ti tio Form co po able

that we pr v ously de ned, co tin ing co po tion from where we last a ded the

A the ti tio Title. B fore we add our Card, we’re g ing to start by adding

a ot er Spacer co po able, this time b neath our A the ti tio Title de-

cla tion - this is so that our Card does not end up pressed up right against the

bo tom of our title.

// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode
)

Spacer(modifier = Modifier.height(40.dp))
}
}

With this in place, we can now add the d cla tion for our Card co po able.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(

51

















fi










































modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode
)

Spacer(modifier = Modifier.height(40.dp))

Card {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {

}
}
}
}

The Card co po able only has one r quired a g ment, which is the co tent of

the co po able. U der the hood, the Card uti ises a Su face which in turn uses a

Box to co tain the co tent that we provide it with. Ho ever, we are g ing to be

stac ing co tent ve ti ally - som thing that we can’t achieve from r l ing on the

Box here. We’ll co pose a Column wit in the co tent of our Card co po able,

which will a low us to then co pose our chi dren i side of the Column, achie ing

the re ult of ve ti ally stacked co po ables. When adding this we’ll also a ply

some sty ing so that the Column has some pa ding a plied to it, along with align-

ing its chi dren h r zon ally in the ce ter via the use of its h r zon a lig ment

a g ment.

Next, we’re g ing to add some co straints to our Card u ing mo ers. As

seen in the design, we want the Card to ll the ma i um width of our screen, but

with some sp cing around the ou side so that it isn’t pres ing against the edges of

52


































fi





















fi­









the screen. To ll the width of the area we’ll use the fil Ma Width mo er, along

with the pa ding mo er to a ply h r zon al pa ding to the co po able.

Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {

}
}

Cu rently, our card won’t look like much, but we’d be able to see som thing like

the fo lo ing cu rently.

53




fi



fi­












fi­
By d fault the Card uses an e e tion value of 1dp, which we can see doesn’t make

our card too vi ible on the su face bac ground. Here we’re g ing to i crease this

to 4dp by ove ri ing the d fault value by passing in our own value via the e e a-

tion a g ment of the Card.

Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {

We can see now that our Card has grea er pa ding a plied to it.

54



















With this in place, we now have an area that is co gured for our form i put area.

At this point, our A the ti tio Form co po able should look som thing like

the fo lo ing.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Column(
modifier = modifier
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode
)

55










fi


Spacer(modifier = Modifier.height(40.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {

}
}
}
}

56
Di pla ing the Email A dress I put
Field

Now that we have the Card in place to hold our i put form, we can now think

about mo ing ahead with adding the rst of our i put elds - the Email I put. We’ll

start here by cr a ing a new co po able, Emai I put.

57







fi





fi


// EmailInput.kt
@Composable
fun EmailInput(
modifier: Modifier = Modifier
)

For the email i put, we’re g ing to e plore the use of the Tex Field co po able,

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 Text-

Field r quires two a g ments for the fun tion to be sa i ed. These are the value

(that re re ents the cu rent value to be di played in the tex eld) and the value

change han ler (triggered when the i put changes and used to u date the com-

po able state). We’ll need to sa i fy these b fore we can co ti ue, so let’s start by

f cu ing on the r quired value a g ment.

Wit in our a the ti tion state, we have an email a dress pro erty, which rep-

re ents the cu rently entered email a dress from the user. We’re g ing to need to

use this piece of state for our email i put eld - so we’ll need to pass it into our

Emai I put co po able and a sign it to the value a g ment of the Tex field.

@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?
) {
TextField(
modifier = modifier,
value = email ?: ""
)

58






































fi









fi





fi






}

When present, this will be shown as the cu rent i put value in our email a dress

eld and if this state changes, then the Tex Field value will be u dated to r ect

that change. Ho ever, we are not cu rently provi ing a way for our state to be

changed - so this r co po tion is ne er g ing to o cur. To x this, let’s i pl ment

an a g ment in the form of a lambda fun tion that a lows us to hoist the latest

entered email up out of our co po able fun tion.

@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,
email: String?,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email ?: "",
onValueChange = { email ->
onEmailChanged(email)
}
)
}

This means that our email a dress will be passed up and out of our co po able

fun tion, a lo ing us to u date the state of our screen when it comes to i ple-

men ing that part of the pr ject.

59
fi
































fi










fl
At this point, we have an email i put eld that is b ing di played i side of our

co po able fun tion. It’s very mi i al, but it’s enough to a low user i put.

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 in-

put eld is to be used for. To add some cla ity here, we’re g ing to uti ise the la-

bel a g ment to provide a co po able that will act as a l bel. We’re g ing to want

this to simply read “Email A dress”, so we’ll need to add a new string r source to

our pr ject:

60

fi








fi






fi



fi









<string name="label_email">Email Address</string>

We can then go ahead and co pose a Text co po able for the l bel of our

Tex Field, passing this string r source as the co tent of that co po able.

@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email ?: "",
onValueChange = { email ->
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id = R.string.label_email)
)
}
)
}

With this in place, our Tex Field is now di pla ing a l bel that adds cla ity for

what the eld is to be used for.

61

fi













E fo cing the su po ted nu ber of lines

If we have a little play with the i put eld, we might n tice that if we provide an in-

put that e ceeds the length of the i put eld, the text will start to e pand onto mul-

tiple lines.

62







fi
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 g ment of the co po able, a lo ing us to

e force all entered text to r main on a single line - the co po able will a low h ri-

zon al na i tion to move through an entered string when this is e abled.

@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email ?: "",
onValueChange = { email ->

63
fi























onEmailChanged(email)
},
label = {
Text(text = stringResource(
id = R.string.label_email)
)
},
singleLine = true
)
}

We can see now that with this b ing e forced, our Tex Field b haves in a much

more e pe ted way.

64






Adding some ico graphy

Our Email I put is fee ing good at this point, but we’re g ing to add a small piece

of vis al de o tion by uti ising the leadi con. This a g ment a lows us to

provide a co po able that will be di played at the start of the i put eld. For this,

we’re g ing to use an Icon co po able to di play an email icon - this won’t serve

any real pu pose ot er than b ing a vis al de o tion.

@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String,
onEmailChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = email,
onValueChange = { email ->
onEmailChanged(email)
},
label = {
Text(text = stringResource(
id = R.string.label_email)
)
},
singleLine = true,
leadingIcon = {
Icon(
imageVector = Icons.Default.Email,
contentDescription = null
)
}
)
}

B cause this is just a vis al de o tion, we don’t need to provide a co ten De-

scri tion for our Icon. The email i put eld also has a l bel that d scribes the

pu pose of the eld, so we can rely on that for d scri ing the co po ent to the

65











fi
















fi















fi





user. With this in place, we can now see an email icon is di played at the start of the

i put eld.

Co po ing the Email I put eld

Now that our email i put is co plete, we can go ahead and co pose it wit in our

UI. B fore we go ahead and co pose it i side of our A the ti tio Form, we

need to e sure there is a cess to both an email and on mai Change re e ence

that the co po able can use. We’re g ing to be passing this from our state, so

we’ll need to rst add these as a g ments to our A the ti tio Form co pos-

able.

66



fi



fi










fi


















// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
onEmailChanged: (email: String) -> Unit
)

Along with then passing this into our A the ti tio Form fun tion at the point

where it is b ing co posed. For the email eld we can pass the email a dress ref-

e ence from wit in our screen state, while for the on mai Change we’ll need to

tri ger an A the ti tio Event by u ing our handl Event lambda. When

cal ing this, we’ll uti ise the Emai Changed event type - i sta t a ing a re e ence by

provi ing the email a dress that is passed through the cal back.

// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
...

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
authenticationMode =
authenticationState.authenticationMode,
email = authenticationState.email,
onEmailChanged = { email ->
handleEvent(
AuthenticationEvent.EmailChanged(email))
}
)

...
}

67

















fi
















Ho ping back over to our A the ti tio Form co po able, we can then com-

pose our Emai I put and pass in the r quired email and on mai Changed ref-

e ences that are now a ces ible from the a the ti tion form.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
email: String?,
authenticationMode: AuthenticationMode,
onEmailChanged: (email: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode)

Spacer(modifier = Modifier.height(40.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email ?: "",
onEmailChanged = onEmailChanged
)
}
}
}
}

68




















We’re g ing to make a small tweak here to cu to ise how the Emai I put is

co posed wit in our UI. When cr a ing the co po able, we a ded su port for an

o tio al mo er. We’re g ing to uti ise this a g ment so that we can force the

email i put eld to ll the avai able width, which is the width of the pa ent Column.

For this, we’ll uti ise the fil Ma Width mo er.

EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email ?: "",
onEmailChanged = onEmailChanged
)

With this in place, the email i put eld for our a the ti tion form is now b ing

co posed wit in our UI - the co tent of this is then co posed based on the cur-

rent email that is wit in our state, which in turn is then u dated via the use of our

on mai Changed lambda fun tion.

69








fi




fi­

fi









fi




fi­


















70
Di pla ing the Pas word I put Field

Now that we have our email i put eld, we’re g ing to want to cr ate a si i ar

co po ent, e cept this time for the entry of a pas word. A lot of this co po ent is

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.

71












fi
















@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String,
onPasswordChanged: (email: String) -> Unit
) {
TextField(
modifier = modifier,
value = password,
onValueChange = {
onPasswordChanged(it)
},
singleLine = true,
label = {
Text(text = stringResource(id =
R.string.label_password)
)
}
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
}
)
}

We can see a couple of di fe ences here:

- 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 now call this o Pas wor Changed whene er the pas word entry

has changed

- The leadi Ion for our co po able is uti ising the Lock icon

We are also u ing a slightly di fe ent l bel for our i put eld, which will need to be

a ded to our strin s.xml r source le b fore we can co ti ue.

72





















fi







fi






<string name="label_password">Password</string>

At this point we’ll have a simple i put eld that can be used for our pas word:

Now that we have the si i a i ies i pl me ted, we can go ahead and think about

the things that make our i put eld slightly di fe ent from the email i put.

To gling pas word vi i i ity

One co mon thing in pas word i put elds is the abi ity to toggle between vi i il-

i ies of the entered pas word. We’re g ing to i pl ment this for our Pas wor In-

put co po able, but to do so we’re g ing to need some form of state that a lows

73













fi





fi



fi














our co po able to know how the pas word eld should be co posed. We’ll need

to add a piece of mu able state to our co po able, i Pas wor Hi den. We’ll de-

fault this to false for s cu ity rea ons, fo lowed by wra ping this in r me ber so

that the value is pe sisted across r co po tions.

@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String) -> Unit
) {

var isPasswordHidden by remember {


mutableStateOf(true)
}

TextField(
modifier = modifier,
value = password ?: "",
onValueChange = {
onPasswordChanged(it)
},
singleLine = true,
label = {
Text(text = stringResource(id =
R.string.label_password)
)
}
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
}
)
}

74














fi









💡 Not all pieces of our state need to be d clared at a glo al level. This pas word

vi i i ity state is sp ci c to this co po able fun tion, so cou ling this here is some-

thing that makes sense for us to do.

With this state in place, we can now uti ise this to co pose our UI. The rst

thing we’re g ing to do is uti ise the traili con of the Tex Field co pos-

able. We’ll start by co po ing a new Icon, whose co tent will d pend on the cur-

rent state of i Pas wor Hi den When the pas word is b ing hi den we want to

show an icon that i di ates the vi i i ity is di abled, while on the ot er hand, we

want to i di ate that the pas word is cu rently vi ible. For now, we’ll use a null

co ten D scri tion, as we’ll be i pl men ing that piece of l gic shortly.

Icon(
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)

So that this icon is i trac able by the user, we’re g ing to want to e able click

events. U ing the clic able mo er we can toggle the i Pas wor Hi den

state so that when the icon is clicked, this state ag is ipped to the o po ite value.

Icon(
modifier = Modifier.clickable {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff
)

This means that now when our Icon is clicked, the state ag will be ipped and our

Icon will be r co posed to r ect this state change.

75


















f­i










fl






fi­









fl





fl



fl








fl






fi



With this in place, we have a fun tio ing icon that can be i te a ted with by the

user. Ho ever, at this point are icon isn’t very a ces ible - the click event is in place

has no form of d scri tion, mea ing that a ces i i ity se vices will not be aware of

the pu pose of this co po ent. What we’ll do here is uti ise the o Clic L bel of

the clic able mo er so that we can provide a d scri tion based on the cu rent

i Pas wor Hi den state. We’ll need to start here by adding two new string re-

sources to our strin s.xml r sources:

<string name="cd_show_password">Show Password</string>


<string name="cd_hide_password">Hide Password</string>

76











fi­
























With these in place, we can now a ply a l bel to our click mo er. We’ll uti ise

the stri R source co po able fun tion here to provide a string r source

based on the cu rent value of our state.

Icon(
modifier = Modifier.clickable(
onClickLabel = if (isPasswordHidden) {
stringResource(id =
R.string.cd_show_password)
} else stringResource(id =
R.string.cd_hide_password)
) {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)

With this d scri tion a plied, we now have a co pleted Icon co po able that

can be slo ted into the traili con block of our Tex Field.

@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String) -> Unit
) {
var isPasswordHidden by remember {
mutableStateOf(true)
}
TextField(
modifier = modifier,
value = password ?: "",
singleLine = true,
onValueChange = {
onPasswordChanged(it)
},
leadingIcon = {

77




















fi­




Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
},
trailingIcon = {
Icon(
modifier = Modifier.clickable(
onClickLabel = if (isPasswordHidden) {
stringResource(id =
R.string.cd_show_password)
} else stringResource(id =
R.string.cd_hide_password)
) {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)
},
label = {
Text(text = stringResource(id =
R.string.label_password))
}
)
}

While we’ve i pl me ted the fun tio a ity to now toggle this state ag, it’s not be-

ing used yet to a fect the vi i i ity of the pas word eld co tent. For these sce ari-

os, the Tex Field co tains a vis a Tran for tion pro erty that can be used to

provide a class that can provide a tran for tion to the i put co tent. Co pose

comes with a pas word tran for tion out of the box in the form of the Pass-

wor Vis a Tran for tion class, which can be provided for the vis al-

Tran for tion pro erty.

78



































fi




fl



Here we’re g ing to want to provide the Pas wor Vis a Tran for tion

when the pas word should be masked, and Vis a Tran for tion.None oth-

e wise.

visualTransformation = if (isPasswordHidden) {
PasswordVisualTransformation()
} else VisualTransformation.None

We can then slot this into our Tex Field co po able.

@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String?,
onPasswordChanged: (email: String) -> Unit
) {
var isPasswordHidden by remember {
mutableStateOf(true)
}
TextField(
modifier = modifier,
value = password ?: "",
singleLine = true,
onValueChange = {
onPasswordChanged(it)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null
)
},
trailingIcon = {
Icon(
modifier = Modifier.clickable(
onClickLabel = if (isPasswordHidden) {
stringResource(id =
R.string.cd_show_password)
} else stringResource(id =
R.string.cd_hide_password)

79


















) {
isPasswordHidden = !isPasswordHidden
},
imageVector = if (isPasswordHidden) {
Icons.Default.Visibility
} else Icons.Default.VisibilityOff,
contentDescription = null
)
},
label = {
Text(text = stringResource(id =
R.string.label_password))
}
)
}

When to gling this, we will now be able to see the pas word co tent i ping from

vi ible to masked.

80




fl

Now that our pas word i put is co plete, we can go ahead and co pose it wit in

our UI. B fore we go ahead and co pose it i side of our A the ti tio Form,

we need to e sure there is a cess to both a pas word and o Pas wor Changed

re e ence that the co po able can use. We’re g ing to be passing this from our

state, so we’ll need to rst add these as a g ments to our A the ti tio Form

co po able.

// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,

81








fi

























onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
)

Along with then passing this into our A the ti tio Form fun tion at the point

where it is b ing co posed. For the pas word we can pass the pas word re er-

ence from wit in our screen state, while for the o Pas wor Changed we’ll need to

tri ger an A the ti tio Event by u ing our handl Event lambda. When

cal ing this, we’ll uti ise the Pas wor Changed event type - i sta t a ing a re er-

ence by provi ing the email a dress that is passed through the cal back.

// AuthenticationContent.kt
@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
...

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
authenticationMode =
authenticationState.authenticationMode,
email = authenticationState.email,
password = authenticationState.password,
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(
AuthenticationEvent.PasswordChanged(it))
}
)

...
}

82



































Ho ping back over to our A the ti tio Form co po able, we can then com-

pose our Pas wor I put and pass in the r quired pas word and o Pas word-

Changed re e ences that are now a ces ible from the a the ti tion form. Here

we’ll also add a Spacer co po able so that there is some vis al space between

the email and pas word i put elds.

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
AuthenticationTitle(
authenticationMode = authenticationMode)

Spacer(modifier = Modifier.height(40.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email ?: "",
onEmailChanged = onEmailChanged

83










fi


















)
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
password = password ?: "",
onPasswordChanged = onPasswordChanged
)
}
}
}
}

We’re g ing to make a small tweak here to cu to ise how the Pas wor I put is

co posed wit in our UI. When cr a ing the co po able, we a ded su port for an

o tio al mo er. We’re g ing to uti ise this a g ment so that we can force the

pas word i put eld to ll the avai able width, which is the width of the pa ent

Column. For this, we’ll uti ise the fil Ma Width mo er.

PasswordInput(
modifier = Modifier.fillMaxWidth(),
password = password ?: "",
onPasswordChanged = onPasswordChanged
)

With this in place, the pas word i put eld for our a the ti tion form is now be-

ing co posed wit in our UI - the co tent of this is then co posed based on the

cu rent pas word that is wit in our state, which in turn is then u dated via the use

of our o Pas wor Changed lambda fun tion.

84














fi­
fi



fi










fi











fi­











85
Han ling Ke board A tions
When i te ac ing with mu tiple i put elds, you can provide a good user e pe i-

ence by a lo ing the elds to be na i ated through by u ing bu tons that are

provided on the ke board. In A droid d ve o ment these have a ways been re-

ferred to as IME a tions, som thing that we still have a cess to in Je pack Com-

pose u der the name of Ke board O tions. In our a the ti tion screen we’re go-

ing to want to uti ise some of these o tions to a low:

- Cu to is tion of the ke board d pen ing on the e pe ted i put con-

tent

- Na i tion from the email a dress eld to the pas word eld

- Su mi sion of the form when the pas word eld is in f cus

These o tions t get er will a low the user to na i ate and su mit the form, seam-

lessly co ple ing the a the ti tion ow without nee ing to sp ci ally i te act

with the UI co po ents via touch. To add these fun tio a i ies we’ll start by cus-

to ising the o tions provided by the email a dress Text Field. For this, we’ll use

the Ke board o tions class to sp cify the ke board type that is to be used for the

email i put eld. This means that if su po ted, our ke board will be laid out spe-

ci ally for email i put (sho ing the ‘@‘ sy bol and cli board o tions).

TextField(
…,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email
)
)

86
fi



















fi













fi


















fi
fl
fi











fi
















fi











fi







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(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Email
)
)

We’ll be able to see now that the pr v ously shown ‘r turn’ icon has now been re-

placed with a ‘next’ icon.

87








fi











With this now di played wit in the ke board, we’re g ing to want to add some

form of han ling so that the i te a tion with the IME a tion tri gers an event for

the user. For this, we’re g ing to uti ise the ke boa A tions pro erty of our Text-

Field co po able, provi ing an i stance of the Ke boa A tions class to handle

the r quired IME a tion. This class a lows us to provide fun tion han lers for any

r quired ke board a tions, which in turn will be triggered when the co re pon ing

im A tion is i te a ted with. When the o Next a tion is i te a ted with, we’re

g ing to want to change the cu rent f cus on the screen - ta ing this from the cur-

rently f cused email a dress text eld, r ques ing f cus on the pas word text eld.

To i pl ment this b h viour we’re g ing to need to uti ise the F cus-

Requester class. This can be a signed to a co poser via a mo er, and then

88



























fi



































fi­




fi

used els where to r quest the f cus on the gi en co po able. We’ll start here by

d ing a re e ence to a F cu Requester for our pas word text eld.

AuthenticationTitle(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode
)
Spacer(modifier = Modifier.height(40.dp))
val passwordFocusRequester = FocusRequester()
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
elevation = 4.dp
) { ... }

With this de ned, we’re now g ing to need to a sign it to our pas word text eld.

This can be done u ing the f cu Requester mo er, which a lows for the Fo-

cu Requester re e ence to be used to r quest f cus for the co po able that it is

a signed to.

TextField(
…,
modifier = modifier.focusRequester(passwordFocusRequester)
)

With this F cu Requester now in place we’re g ing to need to tri ger the f cus

r quest, this is g ing to ha pen when the user i te acts with the next IME a tion

that we’ve e abled via the ke board o tions. While we’ve provided this o tion, we

haven’t provided any form of han ler for it - this is done u ing the ke boa Ac-

tions pro erty of the Tex Field. This pro erty takes a Ke boa A tions in-

stance, used to provide han lers for each of the avai able IME a tions. In our case

for the email a dress text eld, we’re just g ing to provide an i pl men tion for

the o Next a tion.

TextField(

89




fi






fi











fi






















fi­










fi














fi

…,
keyboardActions = KeyboardActions(
onNext = {

}
)
)

At this point though, we don’t have an thing that we can tri ger from wit in this

o Next block. We could pass the f cus r quester re e ence into the Emai I put

co po able, but it’ll make for a clea er (and more tes able) co po able if we pass

this event up to the pa ent co po able. For this, we’ll add a new a g ment-less

lambda a g ment to our co po able fun tion.

@Composable
fun EmailInput(
modifier: Modifier = Modifier,
email: String?,
onEmailChanged: (email: String) -> Unit,
onNextClicked: () -> Unit
)

Wit in the o Next block of our Ke boa A tions i stance we’re g ing to trig-

ger the o Nex Clicked lambda.

TextField(
…,
keyboardActions = KeyboardActions(
onNext = {
onNextClicked()
}
)
)

We’re now g ing to need to hop over to the pa ent co po able and i pl ment

this r quired o Nex Clicked a g ment. Wit in this i pl men tion, we’re then

g ing to use our F cu Requester to tri ger the r sues F cus() fun tion.

90




























































EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}

When this fun tion call is triggered, the f cus will be r que ted for our pas word

Tex Field, which is the co po able that our F cu Requester is a tached to. If

the r quest is su ces ful, this Tex Field will come into f cus, a lo ing the user to

move between the Email A dress and Pas word text elds without nee ing to

man ally i te act with either of the co po ables.

Su por ing form su mi sions via IME a tions

As well as u ing IME a tions to na i ate between i put elds, there is a range of

ot er events which can be handled. For e ample, we can also use them to su mit

forms that the user has entered data into - this is done in the form of the Done IME

A tion. This can be handled in a very si i ar way to the Next a tion that we just

handled, i stead of provi ing the Im A tion Done a tion type to our Ke boar-

O tions - a sig ing this to our Pas wor I put Tex Field.

TextField(
…,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
)
)

We’ll be able to see now that the pr v ously shown ‘r turn’ icon has now been re-

placed with a ‘done’ icon.

91


















































fi

fi










We then again need to provide a Ke boa A tions i pl men tion to our tex eld,

this time i pl men ing the o Done han ler. In most cases the pas word eld will

be vi ited after the email a dress eld, mea ing that the form will be co plete and

the user can move fo ward to a the ti ate their cr de tials.

TextField(
…,
keyboardActions = KeyboardActions(
onDone = {

}
)
)

92








fi
















fi

fi
At this point though, we don’t have an thing that we can tri ger from wit in this

o Done block. For this, we’ll add a new a g ment-less lambda a g ment to our

co po able fun tion.

@Composable
fun PasswordInput(
modifier: Modifier = Modifier,
password: String,
onPasswordChanged: (email: String) -> Unit,
onDoneClicked: () -> Unit
)

With that in mind, wit in this han ler, we’ll use our o Don Clicked() fun tion to

tri ger the cal back.

TextField(
…,
keyboardActions = KeyboardActions(
onDone = {
onDoneClicked()
handleEvent(AuthenticationEvent.Authenticate)
}
)
)

We’re now g ing to need to hop up to the pa ent co po able and i pl ment this

r quired o Don Clicked a g ment.

PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onDoneClicked = {

93





























)

Even though we’ve now i pl me ted this cal back, we want to tri ger the a then-

ti tion ow but we don’t have any way of tri ge ing that yet. To be able to achieve

this, we’ll need to add an a g ment to our A the ti tio Form co po able -

this is g ing to work in the same way as the email + pas word change han lers.

// AuthenticationForm.kt
@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String?,
password: String?,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onAuthenticate: () -> Unit
)

We’ll then also need to hop up again into the A the ti tio Co tent co pos-

able so that we can sa i fy this new r quired a g ment. At this point wit in the Au-

the ti tio Co tent.kt le, we have a cess to the handl Event fun tion -

we’re now g ing to uti ise this to tri ger the A the ti ate event.

// AuthenticationContent.kt

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
authenticationMode = authenticationState.authenticationMode,
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(AuthenticationEvent.Authenticate)

94





fl










fi


































}
)

Ho ping back down into the A the ti tio Form.kt le, we can now uti ise

the onA thenti ate fun tion that is passed into our A the ti tio Form

co po able. Here we can di ectly pass this to our Pas wor I put co po able,

this is b cause the fun tion si n tures match - so there’s no need for us to re-im-

pl ment the cal back.

PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onDoneClicked = onAuthenticate
)

Now, when the IME A tion for the pas word eld is pressed, the a the ti tion

ow will be triggered. One nal thing we can do here for the pas word eld is

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 = ImeAction.Done,
keyboardType = KeyboardType.Password
)
)

When co pared the email i put type that we de ned for our email a dress text

eld, we can see here now that the keys avai able on the ke board di fer slightly,

f cu ing on nu bers and le ters, r mo ing the use of some sp cial cha a ters and

cli board o tions.

95
fl
fi





















fi

fi­















fi

fi



fi



















fi






96
Pas word R quir ments

At this point, our user can enter their cr de tials into our a the ti tion form.

When we de ned some of the bus ness l gic for our state, we de ned an enum,

Pas wor R quir ments. Wit in our state class, we also de ned a list of these

97




fi








fi

fi



Pas wor R quir ments, a lo ing us to e force ce tain r quir ments on the

entered pas word. To r cap, these r quir ments were:

enum class PasswordRequirements {


CAPITAL_LETTER, NUMBER, EIGHT_CHARACTERS
}

This means that the pas word must co tain a ca i al le ter, a nu ber and be at

least 8 cha a ters long. While we have these r quir ments de ned, we need to let

the user know about them - so we’re g ing to cr ate a co po able that di plays

the r quir ments du ing sign-up, mar ing the r quir ments vis ally as sa i ed

when they have been met.

Cr a ing the R quir ment Co po able

We’re g ing to start here by loo ing at the co po able that will be used to re res-

ent each of the r quir ments that we’ve stated in our enum.

98











































fi








fi
For this co po able, we’re g ing to di play a simple UI co po ent that will dis-

play a l bel for the r quir ment, along with an icon that will i di ate if this r quire-

ment has been sa i ed. We’ll start by cr a ing a new co po able fun tion that

takes our d fault mo er as an a g ment.

@Composable
fun Requirement(
modifier: Modifier = Modifier
)

99








fi

fi­















We me tioned above about the two r quir ments for this co po able fun tion -

the me sage to be di played, along with whet er the r quir ment is cu rently sa is-

ed. We’ll add more two a g ments - a string r source that will be used for the la-

bel of our r quir ment co po able, along with a boolean ag to su port the sa is-

ed status.

@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
)

With these a g ments in place, we can start to build out the co tent of our com-

po able. Here we’ll add a Row co po able so that we can la out the chi dren h ri-

zon ally next to one a ot er, along with adding some mo ers to cu to ise the

b h viour + di play of co tent. We’ll chain from the mo f er that is passed into

the co po able fun tion, u ing the pa ding mo er to add some pa ding to our

co po able. We’ll also use the ve tica lig ment a g ment to align the chil-

dren in the ve ti al ce ter - b cause we’re sho ing an icon with a text l bel to the

side of it, we want these to be aligned on the Y axis which can be achieved via the

use of Alig ment Cente Ve ti ally.

@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier = modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {

100
fi
fi
















































fi­




fl






fi­














}

We’ll now go ahead and add an Icon co po able which will be used to show a

Check icon u ing the Ico s.D fault.Check icon from the co pose pac age.

We’ll use the size mo er to x this size to 12.dp, along with se ting null as the

co tent d scri tion. While we will be u ing this to si n fy the cu rent status of the

r quir ment, we’re g ing to f cus on the a ces i i ity of this co po ent after the

foun tions are in place.

@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null
)
}
}

💡 It’s i por ant to r me ber that null should only be used in cases where a de-

scri tion is not a pli able for the co po able. This could be either b cause the

co po able is purely de o a ive, or b cause you are provi ing a d scri tion some

ot er way - such as through the pa ent co po able when me ging s mantics.

With our icon now in place, let’s add the l bel that will d scribe what the re-

quir ment is. For this, we’ll use the Text co po able and a sign the me sage

from the co po able fun tion a g ments.

@Composable

101























fi­







fi




































fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null
)
Text(
text = message
)
}
}

Cu rently, we’ll have som thing that looks like this co posed:

102



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(

103
fi











fi­






modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = message),
fontSize = 12.sp
)
}
}

💡 When mod f ing the fon Size, it’s i por ant to not make the font too small - it

still needs to r main rea able for users. It can be hel ful in these cases to use the

t p graphy va ues from the theme to help pr vent these i sues from o cu ring.

We can see now, that things look a little bit be ter here.

104
















Cu rently, we have this sa i fied ag wit in our co po able, but we’re not do-

ing an thing with it. We’re g ing to uti ise this here to vis ally re re ent the status

of the r quir ment - which we’ll do by u ing a co or for the icon and text based on

whet er the r quir ment is sa i ed. Here we’ll use the theme of our a pli tion to

cr ate a co or re e ence.

val tint = if (satisfied) {


MaterialTheme.colors.primary
} else MaterialTheme.colors.onSurface.copy(alpha = 0.4f)

We use the primary co or in our sa i ed sce ario, as this will stand out to the user

du ing the a the ti tion pr cess. For the case where the pas word does not meet

the r quir ments, we copy the o Su face co or from our theme, mod f ing the

105

























fi


fl

fi


















a pha value so that the co or a pears slightly faded out in our UI. U ing this co or

re e ence, we can then a ply this to the Icon u ing the tint a g ment, along with

a pl ing it to the l bel u 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) {
MaterialTheme.colors.onSurface
} else MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
Row(
modifier = Modifier.padding(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null,
tint = tint
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = message),
fontSize = 12.sp,
color = tint
)
}
}

With this in place, we now have a co po able that will di play a r quir ment,

along with the ing it to re re ent the cu rent state of the sa i ed ag.

106























fi



fl





I pro ing the r quir ment a ces i i ity

While we have the above co po able fun tion in place to co pose a r quire-

ment, we can i prove things here when it comes to the use of a ces i i ity ser-

vices. Cu rently, the l bel of the r quir ment will be read - but we are r l ing on a

co or re re 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 ser-

vice is d scri ing the el ment, it can be d scribed whet er or not the r quir ment

is cu rently sa i ed.

107
















fi


















fi













fi














We’ll start here by adding two new string r sources to our strin s.xml re-

source le.

<string name="password_requirement_satisfied">
%s, satisfied
</string>
<string name="password_requirement_needed">
%s, needed
</string>

We use %s as a plac hol er for a string that we will be r pl cing it with, this will be

the l bel for the r quir ment. This means that the a ces i i ity se vices will de-

scribe this as “At least 8 cha a ters, sa i ed” or “At least 8 cha a ters, needed”.

Wit in our R quir ment co po able we can now build our d scri tion by ac-

ces ing this r source via the use of the stri R source co po able fun tion.

@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val requirementStatus = if (satisfied) {
stringResource(id =
R.string.password_requirement_satisfied, message)
} else {
stringResource(id =
R.string.password_requirement_not_satisfied, message)
}
}

You’ll n tice here that we pass the me sage a g ment from our co po able func-

tion as an a g ment to the stri R source fun tion. This is b cause we are us-

ing the %s plac hol er, so any a g ments that are provided here will be used as

the r plac ment. With this in place, we can now go ahead and a ply the se-

mantics mo er to our Row co po able. When d ing this we’ll want to set the

108




fi








fi­





















fi



























merg De cen ants ag as true (as we don’t need the child co po ables to be

d scribed i d vid ally), along with se ting the text s mantics as the r quire-

men Status that we ge e ated above. This pro erty r quires the A no ated-

String type, so we’ll i sta t ate an i stance by provi ing our r quir ment-

Status.

Row(
modifier = Modifier.padding(6.dp)
.semantics(mergeDescendants = true) {
text = AnnotatedString(requirementStatus)
},
verticalAlignment = Alignment.CenterVertically
)

B cause we’re now se ting the s mantics for our Row and me ging the co tent of

the co tai er, we can go ahead and clear the s mantics on the child Text co pos-

able, u ing the clea An Se S mantics mo er to do so. This will avoid any de-

scri tions from b ing d pli ated with the s mantics tree.

Text(
modifier = Modifier.clearAndSetSemantics { },
text = message,
fontSize = 12.sp,
color = tint
)

With this in place, we now have an u dated co po able fun tion that has im-

proved a ces i i ity su port.

@Composable
fun Requirement(
modifier: Modifier = Modifier,
message: String,
satisfied: Boolean
) {
val tint = if (satisfied) {
MaterialTheme.colors.primary

109



















fl




















fi­

















} else MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
val requirementStatus = if (satisfied) {
stringResource(id =
R.string.password_requirement_satisfied, message)
} else {
stringResource(id =
R.string.password_requirement_not_satisfied, message)
}
Row(
modifier = Modifier.padding(6.dp)
.semantics(mergeDescendants = true) {
text = AnnotatedString(requirementStatus)
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.Check,
contentDescription = null,
tint = tint
)
Spacer(modifier = Modifier.width(8.dp))
Text(
modifier = Modifier.clearAndSetSemantics { },
text = message,
fontSize = 12.sp,
color = tint
)
}
}

Buil ing the R quir ment items

At this point, we now have a R quir ment co po able fun tion, but we’re not

buil ing these wit in our UI. We’ll start here by d ing a new co po able func-

tion, Pas wor R quir ments, that takes a list of sa i ed r quir ments - we have

110













fi




fi





this value wit in our state, so this will simply be provided from there when the time

comes to i pl men ing that.

Wit in this fun tion, we’ll also co pose a Column, as we’re g ing to be show-

ing a ve ti al list of the r quir ment co po ables.

@Composable
fun PasswordRequirements(
modifier: Modifier = Modifier,
satisfiedRequirements: List<PasswordRequirements>
) {
Column(modifier = modifier) {

}
}

Co po ing the r quir ments is a very mi i al pr cess. We pr v ously de ned the

Pas wor R quir ments enum, with each value ha ing a l bel pro erty in the

form of a string r source. That means that wit in this co po able we can simply

loop through the va ues of the Pas wor R quir ments enum, co po ing a Re-

quir ment u ing the cu rent item in the loop. Here we’ll use the l bel pro erty

to r trieve a string that can be passed for the me sage a g ment of the R quire-

ment, along with u ing the Pas wor R quir ments re e ence to check whet er

the r quir ment is cu rently sa i ed.

@Composable
fun PasswordRequirements(
modifier: Modifier = Modifier,
satisfiedRequirements: List<PasswordRequirements>
) {
Column(
modifier = modifier
) {
PasswordRequirements.values().forEach { requirement ->
Requirement(
message = stringResource(
id = requirement.label),

111































fi































fi


satisfied = satisfiedRequirements.contains(
requirement
)
)
}
}
}

With this, we can now co pose a co le tion of r quir ments and their cu rent sa is-

ed status.

112
fi







Co po ing the R quir ment items

With the Pas wor R quir ments co po able now in place, we’re g ing to want

to slot this into our UI. Ho ever, we’re only g ing to want to show this to the user

when they are sig ing up - a user who has signed in will know their pas word and

have a va id pas word, so this va i tion wit in the UI does not make too much

sense. We could simply co pose this based on our A the ti tio Mode re er-

ence wit in our screen state, but i stead, we’re g ing to uti ise the A i ate Vis-

i i ity co po able to a i ate our co po able in and out, based on its vi ible

ag. This means we could de ne, anA i ate Vi i i ity co po able, se ting

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 co tent.

// AuthenticationForm.kt

AnimatedVisibility(
visible = authenticationMode == AuthenticationMode.SIGN_UP
) {
PasswordRequirements(...)
}

This would mean that when the cu rent A the ti tio Mode wit in our state is

not equal to SIGN_UP, the Pas wor R quir ments would a i ated out of view -

a i a ing into view when that state is toggled by the user (som thing that we have

not i pl me ted yet!).

While the above would work, we need to co pose it wit in our UI. For the Au-

the ti tio Form co po able fun tion, we already have a cess to an A then-

ti tio Mode re e ence - we’ll just need to add a new a g ment to the fun tion

in the form of List<Pas wor R quir ments>.

//AuthenticationForm.kt

@Composable

113
fl

















fl

























fi







































































fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
)

We’ll then also need to hop over to our A the ti tio Co tent.kt le to

mod fy the co po tion of our A the ti tio Form co po able. We’ll need to

pass a value for the co plete Pas wor R quir ments - the state re e ence that

we are u ing for the ot er a g ments here has the cu rent r quir ments wit in it,

so we’ll simply need to pass that here.

// AuthenticationContent.kt

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
}
)

Hea ing back over to our A the ti tio Form co po able, we can now com-

pose our A i ate Vi i i ity and Pas wor R quir ments co po ables.

Here we’ll also add a Spacer co po able so that there is some vis al space

between UI co po ents.

114

























































fi

// AuthenticationForm.kt

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))

AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}
}

115
With this in place, we now have our Pas wor R quir ments co posed wit in

our a the ti tion UI. When sig ing up, e te ing a pas word will now mod fy the

r quir ments UI to di play the cu rently sa i ed r quir ments.

116













fi










Tri ge ing the A the ti tion ow

In our UI the user can now enter their email a dress and pas word, but they can’t

yet tri ger the a the ti tion ow to a low them to pr ceed in our app. To make

this po sible, we’re now g ing to add a bu ton that a lows them to pe form this au-

117









fl










fl

the ti tion ow u ing the cr de tials that they have entered into the form. While

this is po sible u ing the IME a tion that we a ded in the last se tion, a bu ton to

tri ger this ow would be e pe ted by a lot of users. We’ll start here by cr a ing a

new co po able fun tion, A the ti tio Bu ton with a d fault Mo f er ar-

g ment.

// AuthenticationButton.kt

@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier
)

Next, we’ll add a Bu ton co po able, this r quires two of its pro e ties to be

provided - an o Click cal back han ler and a co po able that re re ents the

body of the Bu ton.

@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier
) {
Button(
modifier = modifier
) {

}
}

As it is, this Bu ton isn’t too much use to us as it’s not sho ing or tri ge ing any-

thing. To change this we’ll add a co po able for the body of the bu ton, this will

re re ent either a “Sign in” or “Sign up” me sage, d pen ing on the cu rent au-

the ti tio Mode from our a the ti tion state re e ence. We’ll need to start by

adding a couple of string r sources for the bu ton text:

<string name="action_sign_up">Sign Up</string>

118













fl

fl
























































<string name="action_sign_in">Sign In</string>

So that we know what one of these strings to show wit in our bu ton, the co pos-

able fun tion is g ing to need to know what A the ti tio Mode is cu rently

s le ted. For this, we’re g ing to need to pass this into our co po able fun tion.

@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)

Next, we’ll use an if stat ment check to handle what the me sage should be de-

pen ing on the state, then set this as the co tent of a Text co po able wit in the

body of our Bu ton.

@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Button(
modifier = modifier
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_sign_in
} else {
R.string.action_sign_up
}
)
)
}
}

With this Text in place, we’ll now be able to see a bu ton on-screen whose body

re re ents the cu rent a the ti tion mode that is set wit in our state.

119


































At this point, we can now think about han ling the o Click tri ger from our But-

ton. What we want to do here is tri ger an event that will start the a the ti tion

ow - si i ar to how else we have handled events in this pr ject, we’re g ing to al-

low the pa ent co po able to handle this event for us. With this in mind, we’ll add

a lambda fun tion a g ment to our co po able fun tion, onA thenti ate. We’ll

then want to tri ger this lambda wit in the o Click a g ment of our Bu ton

co po able.

@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
onAuthenticate: () -> Unit
) {

120
fl































Button(
modifier = modifier,
onClick = {
onAuthenticate()
}
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_sign_in
} else {
R.string.action_sign_up
}
)
)
}
}

Now when our Bu ton is pressed, the a the ti tion ow will be triggered and the

cr de tials from our Tex Fields will be used du ing this pr cess. Ho ever, these

elds might not a ways co tain va id data - we have an e abl A thenti tion

pro erty wit in our a the ti tion state re e ence. While this only checks if both

the email a dress and pas word are not empty, this helps to avoid the ow b ing

triggered when data might not have been entered yet. We can handle this via the

Bu ton by di abling the bu ton from b ing i te a ted with when the form co tent is

not va id. For this we’ll use the e abled pro erty of the Bu ton, a sig ing theen-

abl A thenti tion value to it.

@Composable
fun AuthenticationButton(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
enableAuthentication: Boolean,
onAuthenticate: () -> Unit
) {
Button(
modifier = modifier,

121
fi




































fl








fl




onClick = {
onAuthenticate()
},
enabled = enableAuthentication
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_sign_in
} else {
R.string.action_sign_up
}
)
)
}
}

122
Now that our bu ton co po able is i pl me ted, we can go ahead and co pose

it wit in our A the ti tio Form co po able. We’ll need to start by adding a

new a g ment to our A the ti tio Form co po able, this will be e abl Au-

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

123




























)

We’ll then also need to hop over to our A the ti tio Co tent.kt le to

mod fy the co po tion of our A the ti tio Form co po able. We’ll need to

pass a value for the e abl A thenti tion - our state re e ence that we are

u ing for the ot er a g ments here has the cu rent e abled state wit in it, so we’ll

simply need to pass that here.

// AuthenticationContent.kt

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
enableAuthentication = authenticationState.isFormValid(),
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(AuthenticationEvent.Authenticate)
}
)

Hea ing back over to our A the ti tio Form co po able, we can now com-

pose our A the ti tio Bu ton. Here we’ll also add a Spacer co po able so

that there is some vis al space between UI co po ents.

// AuthenticationForm.kt

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,

124


















































fi
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))

AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}

Spacer(modifier = Modifier.height(12.dp))

AuthenticationButton(
enableAuthentication = enableAuthentication,
authenticationMode = authenticationMode,
onAuthenticate = onAuthenticate
)

125
}
}

With this in place, our A the ti tio Bu ton is now b ing co posed i side of

our a the ti tion form - this now a lows the user to tri ger the a the ti tion ow.

126



















fl
To gling the A the ti tion mode

With all of the above in place, our user can pe form a the ti tion u ing the

provided elds. Ho ever, the UI only cu rently su ports the d fault a the ti tion

type - which is cu rently co gured to be sign in. This means that if the user does

127

fi



fi

















not cu rently have an a count, they won’t be able to cr ate one u ing our a then-

ti tion form. In this se tion we’re g ing to go ahead and de ne a Bu ton that al-

lows the user to toggle between sign up and sign in, r co po ing our a the ti a-

tion form to r ect the cu rent mode. We’ll start here by cr a ing a new co pos-

able fun tion, Toggl A thenti tio Mode with a d fault Mo f er a gu-

ment.

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
)

With this co po able fun tion in place, we can build out the co tent r quired to

di play our toggle bu ton. We’re g ing to start by d ing the use of a Su face,

which act as the co tai er for our toggle co po able. The Su face co po able

will co pose the provided body i side of a Box co po able. It will also theme it-

self use the su face co or from the a pli tion theme, which is what we want to

be a plied to our se tings item in terms of sty ing. This saves us from u ing a Box

co po able and a pl ing a co le tion of sty ing ourselves when this co po ent

already e ists to do it for us. When co po ing this, we’ll a ply the mo er from

the a g ment of our co po able fun tion, along with ove ri ing the d fault e ev-

tion of the se vice with the value of 8dp.

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
elevation = 8.dp
) {

}
}

128















fl






































fi










fi

















fi­









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 di played wit in the co po able

bu ton

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier
.padding(top = 16.dp),
elevation = 8.dp
) {
TextButton(
onClick = {

}
) {

}
}
}

Now that we have the co po able de ned, we need to go ahead and po late

these pro e ties of the co po able. B fore we add this co po able, we’ll go

ahead and add some a d tio al r sources to be used for the body of the co pos-

ables.

<string name="action_need_account">
Need an account?
</string>
<string name="action_already_have_account">
Already have an account?
</string>

129



























fi
fl






















Next, we’ll use the co tent pro erty of the Tex Bu ton so that we can see some

form of vis al re ult on our screen. Now we can add a Text co po able to the

body of our Tex Bu ton, this will di play the co tent of a string r source, de-

pen ing on the cu rent a the ti tion mode re re e ted by our state. B fore we

can do this though, we’ll need to know the A the ti tio Mode which should

be used when co po ing this co po able. We’ll add this as an a g ment for our

co po able fun tion so that it can be passed in from the pa ent.

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
)

With this in place, we can now uti ise this to co pose the bu ton co tent. I side of

our bu ton, we’re g ing to co pose a Text co po able, se ting the co tent

based on the provided A the ti tio Mode.

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode
) {
Surface(
modifier = modifier
.padding(top = 16.dp),
elevation = 8.dp
) {
TextButton(
onClick = {

}
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {

130























































R.string.action_need_account
} else {
R.string.action_already_have_account
}
)
)
}
}
}

F nally, we use need to handle the o Click cal back for our Tex Bu ton. When

the bu ton is clicked we want to tri ger an event that will change the a the ti tion

mode for the state of our screen. - this will be an event in the form of Toggl Au-

thenti tio Mode, which we cr ated in an earl er se tion. Again we’ll want to

pass this event up to the pa ent, so we’ll need to add a new lambda a g ment to

our co po able fun tion.

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
toggleAuthentication: () -> Unit
)

When this event is triggered, our Vie Mo el will ip the value of our a the ti a-

tion mode, a lo ing us to switch between the sign in and sign up state. So this will

o cur, we’ll go ahead and tri ger this wit in the o Click han ler for our Text-

Bu ton co po able.

@Composable
fun ToggleAuthenticationMode(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
toggleAuthentication: () -> Unit
) {
Surface(
modifier = modifier
.padding(top = 16.dp),

131























fl
















elevation = 8.dp
) {
TextButton(
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(8.dp),
onClick = {
toggleAuthentication()
}
) {
Text(
text = stringResource(
if (authenticationMode ==
AuthenticationMode.SIGN_IN) {
R.string.action_need_account
} else {
R.string.action_already_have_account
}
)
)
}
}
}

Wit in our UI, this toggle bu ton was pushed right to the bo tom of the screen -

this isn’t som thing that can be co gured wit in the pro e ties of the pa ent

Column co tai er.

132






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 gu-

ment to our A the 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,

133



























completedPasswordRequirements: List<PasswordRequirements>,
enableAuthentication: Boolean,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onToggleMode: () -> Unit
)

We’ll then also need to hop over to our A the ti tio Co tent.kt le to

mod fy the co po tion of our A the ti tio Form co po able. We’ll need to

pass an a g ment for the o Toggl Mode - for this, we’ll i pl ment a lambda func-

tion that will be used to tri ger handl Event. For this call, we’re g ing to need to

pass an A the ti tio Event, which we’ll do so in the form of the Toggl Au-

thenti tio Mode type. When this is triggered and handled by our Vie Mo el,

the cu rent A the ti tio Mode will be toggled to the o po ite value and emit-

ted to our UI.

// AuthenticationContent.kt

AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =
authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
enableAuthentication = authenticationState.isFormValid(),
onEmailChanged = {
handleEvent(AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(AuthenticationEvent.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(AuthenticationEvent.Authenticate)
},
onToggleMode = {
handleEvent(

134











































fi


AuthenticationEvent.ToggleAuthenticationMode)
}
)

Hea ing back over to our A the ti tio Form co po able, we can now com-

pose our A the ti tio Bu ton u ing the e is ing a the ti tio Mode ref-

e ence, along with the now provided o Toggl Mode lambda fun tion.

// AuthenticationForm.kt

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onToggleMode: () -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))

135


























AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}

Spacer(modifier = Modifier.height(12.dp))

AuthenticationButton(
enableAuthentication = enableAuthentication,
authenticationMode = authenticationMode,
onAuthenticate = onAuthenticate
)

ToggleAuthenticationMode(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode,
toggleAuthentication = {
onToggleMode()
}
)
}
}

We’ll n tice here now that our Toggl A thenti tio Mode co po able is

pressed against the bo tom of our A the ti tio Bu ton co po able.

136

















As per the design, we want the Toggl A thenti tio Mode to be pushed

against the bo tom of the pa ent co po able. Between these bu tons we e sen-

tially want a large amount of white space - this white spice needs to ll the avail-

able space between these two bu tons, which in e fect will push our toggle bu ton

to the bo tom of our UI. For this, we’re still g ing to use the Spacer co po able for

cr a ing space between these two co po ables, with the a d tion of the weight

mo er.

Spacer(modifier = Modifier.weight(1f))

A pl ing a weight of 1f to this Spacer will cause it to take up all of the avai able

height to it wit in the Column - which in this case will be all of the space between

the A the ti ate and toggle bu tons. We are not a pl ing any weigh ing to any

137






fi­


























fi





ot er co po ables so that the r mai ing weight here does not need to be di trib-

uted between mu tiple co po ables. If we were to a sign some weigh ing to one

of those bu tons also, we would see a di fe ent re ult as the weight would b come

di tri uted. But b cause we are only a sig ing weight here to the Spacer co pos-

able, it takes the avai able space for i self.

// AuthenticationForm.kt

@Composable
fun AuthenticationForm(
modifier: Modifier = Modifier,
authenticationMode: AuthenticationMode,
email: String,
password: String,
completedPasswordRequirements: List<PasswordRequirements>,
onEmailChanged: (email: String) -> Unit,
onPasswordChanged: (password: String) -> Unit,
onToggleMode: () -> Unit
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
modifier = Modifier.fillMaxWidth(),
email = email,
onEmailChanged = onEmailChanged
) {
passwordFocusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(16.dp))
PasswordInput(
modifier = Modifier.fillMaxWidth()
.focusRequester(passwordFocusRequester),
password = password,
onPasswordChanged = onPasswordChanged,
onSubmitForm = onAuthenticate
)
Spacer(modifier = Modifier.height(12.dp))

138
























AnimatedVisibility(
visible = authenticationMode ==
AuthenticationMode.SIGN_UP
) {
PasswordRequirements(completedPasswordRequirements)
}

Spacer(modifier = Modifier.height(12.dp))

AuthenticationButton(
enableAuthentication = enableAuthentication,
authenticationMode = authenticationMode,
onAuthenticate = onAuthenticate
)

Spacer(modifier = Modifier.weight(1f))

ToggleAuthenticationMode(
modifier = Modifier.fillMaxWidth(),
authenticationMode = authenticationMode,
toggleAuthentication = {
onToggleMode()
}
)
}
}

With this change, we can now see the toggle bu ton is pushed to the bo tom of the

pa ent co po able, via the u age of the Spacer co po able with its a signed

weight.

139









With these co po ables now a ded, our a the ti tion form is fun tio ally com-

plete - o fe ing a way for users to both sign up and sign in to our a pli a-

tion.

140













Di pla ing A the ti tion E rors

With everything we’ve done pr v ously in this se tion, the user can enter some cre-

de tials and pe form either a sign-in or sign-up o e tion. While this o e tion can

su ceed and a low the user to co ti ue into our a pli tion, som times that re-

141


























quest may fail. In these cases, we’ll want to r ect this state to the user so they

know som thing has gone wrong. Our a the ti tion state has an e ror pro erty

wit in it, so we’ll be able to use this pro erty to di play an alert di log to the user.

Wit in our A the ti tio State class there is an e ror pro erty, which is

used to d pict that an e ror has o curred du ing the a the ti tion pr cess. While

this e ror might be set du ing the a the ti tion ow, we’re not cu rently uti ising

this wit in our UI to co m ni ate this to the user. To handle this sce ario, we’re

g ing to cr ate a new co po able, A the ti tio Er o Di log, which is go-

ing to take this e ror value and co pose it wit in our UI.

// AuthenticationErrorDialog.kt

@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
)

Wit in this co po able fun tion, we’re then g ing to co pose an Aler Di log.

There are two Aler Di log co po able fun tions avai able, we’re g ing to use

the one with the fo lo ing set of a g ments.

@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {

},
confirmButton = {

},

142


















































fl




fl






















title = {

},
text = {

}
)
}

As we can see here, there are a co le tion of a g ments that we need to provide to

the co po able fun tion:

- mo er: this is o tio al, but a lows us to provide co straints to be ap-

plied to the co po able

- onDi mi Request: triggered when the di log is r que ted to be dis-

missed, which we’ll need to use to tri ger an u date to our state so that

the di log is not co posed

- co r Bu ton: a co po able that re re ents the bu ton used to con-

rm the r quest of the di log, with the i te tion to di miss it

- title: the co po able to be used as the title of the di log

- text: the co po able to be used for the co tent body of the di log

With that in mind, let’s start buil ing out the co po ables for each of these a gu-

ments. We’ll start with the title, which we’ll need to start by adding a new string to

our strin s.xml r source le.

<string name="error_title">Whoops</string>

Then u ing the stri R source co po able fun tion, we’ll co pose a Text

co po able provi ing this string for the text a g ment. B cause we’re wor ing

with a title here, we’ll ove ride the d fault fon Size of the co po able and as-

sign a value of 18sp - this is so that is styled la ger than the me sage of the di log.

// AuthenticationErrorDialog.kt

143
fi





fi




fi­

























fi








































@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {

},
confirmButton = {

},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {

}
)
}

At this point, we can see this text b ing used for the title of our di log.

144


Next, we’ll use a ot er Text co po able, but this time for the text a g ment of

the Aler Di log co po able. Here we will simply provide the e ror that is

passed to our co po able fun tion for the text a g ment of the co po able.

// AuthenticationErrorDialog.kt

@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {

},

145


















confirmButton = {

},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
Text(
text = error
)
}
)
}

With this in place, we can now see the title of our Aler Di log b ing a co pan-

ied by our e ror me sage.

146







Next, we need to co pose the bu ton that will be used to di miss the Aler Dia-

log - this will be provided u ing the bu ton a g ment of our co po able. We’ll

rst start by co po ing a Tex Bu ton to be used for this a tion. This a lows us to

co pose a at bu ton that is vis ally re re e ted as some text. The di fe ence with

this, when co pared to a Text co po able, is that the Tex Bu ton has the ex-

pe ted touch ta get si ing of an i trac able co po ent, ma ing it a ces ible to all

of our users.

There are two r quired a g ments for the Tex Bu ton - the o Click cal back

and the co tent, which is used to provide the co po able body of our bu ton.

TextButton(
onClick = {

147
fi



fl















































}
) {

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 s.xml le.

<string name="error_action">OK</string>

We can then use this to co pose a Text co po able for the co tent of our but-

ton.

TextButton(
onClick = {

}
) {
Text(text = stringResource(id = R.string.error_action))
}

Next up we’ll need to add some a tion to our o Click cal back. When our bu ton

is clicked we’re simply g ing to want to di miss the di log, so we’ll need to com-

m ni ate this a tion back up to the pa ent so that our state can be u dated and in

turn, the di log will no longer be co posed. For this, we’ll need to add a new

lambda a g ment to our co po able fun tion.

@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.

// AuthenticationErrorDialog.kt

148

















fi


















@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {

},
confirmButton = {
TextButton(
onClick = {
dismissError()
}
) {
Text(text = stringResource(
id = R.string.error_action))
}
},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
Text(
text = error
)
}
)
}

At this point, we’ll now have a vis ally co plete e ror di log that di plays the title,

me sage and a tions to the user.

149







The last thing to do here is to add some i pl men tion to the onDi mi s-

Request lambda body of the Aler Di log. In the pr v ous step, we a ded the

di mi E ror a g ment to our co po able fun tion, so we’ll now want to trig-

ger this wit in the onDi mi Request lambda.

// AuthenticationErrorDialog.kt

@Composable
fun AuthenticationErrorDialog(
modifier: Modifier = Modifier,
error: String,
dismissError: () -> Unit
) {
AlertDialog(
modifier = modifier,
onDismissRequest = {

150
























dismissError()
},
buttons = {
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
TextButton(
onClick = {
dismissError()
}
) {
Text(text = stringResource(
id = R.string.error_action))
}
}
},
title = {
Text(
text = stringResource(
id = R.string.error_title),
fontSize = 18.sp
)
},
text = {
Text(
text = error
)
}
)
}

With the co po able in place, we’ll now want to co pose this wit in our UI. One

thing to note here is that we want to co d tio ally co pose this, so it will only be

co posed when there is an e ror present wit in our state. When pe for ing com-

po tion, we can wrap the co po tion of the A the ti tio Er o Di log

wit in a ko lin let block, a lo ing us to a sert the nu la i ity of the e ror pro erty.

151




































This way the block will only be run if e ror is not null, so our co po able will only

ever be co posed if it should be.

// AuthenticationContent.kt

authenticationState.error?.let { error ->


AuthenticationErrorDialog(
...
)
}

We can slot this into our A the ti tio Co tent co po able, provi ing the

e ror from our a the ti tion state for the e ror a g ment, along with i ple-

men ing the di mi E ror lambda. Wit in this lambda we’ll want to uti ise the

handl Event lambda that is provided to our co po able fun tion, u ing the Er-

ro ismissed A the ti tio Er or to tri ger our state b ing u dated and

r mo ing the e ror (and in turn, the alert di log will no longer be co posed).

// AuthenticationContent.kt

@Composable
fun AuthenticationContent(
modifier: Modifier = Modifier,
authenticationState: AuthenticationState,
handleEvent: (event: AuthenticationEvent) -> Unit
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
if (authenticationState.isLoading) {
CircularProgressIndicator()
} else {
AuthenticationForm(
modifier = Modifier.fillMaxSize(),
email = authenticationState.email,
password = authenticationState.password,
completedPasswordRequirements =

152


















































authenticationState.passwordRequirements,
authenticationMode =
authenticationState.authenticationMode,
enableAuthentication =
authenticationState.isFormValid(),
onEmailChanged = {
handleEvent(
AuthenticationEvent.EmailChanged(it))
},
onPasswordChanged = {
handleEvent(
AuthenticationEvent
.PasswordChanged(it))
},
onAuthenticate = {
handleEvent(
AuthenticationEvent.Authenticate)
},
onToggleMode = {
handleEvent(AuthenticationEvent
.ToggleAuthenticationMode)
}
)
authenticationState.error?.let { error ->
AuthenticationErrorDialog(
error = error,
dismissError = {
handleEvent(
AuthenticationEvent.ErrorDismissed)
}
)
}
}
}
}

At this point we now have an Aler Di log that will be co posed wit in our UI

whene er the state re re ents an e ror state, a lo ing us to co m ni ate any au-

the ti tion e rors to the user.

153

















154
Wra ping Up

Throug out this pr ject, we’ve now fully i pl me ted our a the ti tion fe ture -

a lo ing our users to sign-up or sign-in to our a pli tion, with the abi ity to toggle

between the two a the ti tion modes. With this to gling, we’ve been able to ex-

155






















plore the co po tion of di fe ent states and co po ents wit in our UI, on top of

the ma ag ment of state when it comes to our a the ti tion form.

With all of this in place, we’ll want to e sure these co po ents r main fun tio al

wit in our app. In the next chapter, we’re g ing to e plore wri ing aut mated UI

tests for these co po ables.

156



























Testing the A the ti tion UI
Now that we’ve built our A the ti tion screen, we’re g ing to take a look at how

we can write tests for our co po ables. We’re g ing to be wri ing some i stru-

men tion tests u ing the co pose ui-test-j nit pac age - a lo ing us to ver fy that

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 pr ject:

androidTestImplementation(
"androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation(
"androidx.compose.ui:ui-test-manifest:$compose_version")

We’re also g ing to need to add mocks to our test - this a lows us to ea ily provide

mock re e ences to any liste ers that are provided to our co po able fun tions, al-

lo ing us to ea ily ver fy they are triggered whene er e pe ted.

androidTestImplementation(
"org.mockito.kotlin:mockito-kotlin:3.2.0")
androidTestImplementation("org.mockito:mockito-android:3.12.4")

With these in place, we now have a cess to the r quired rules and fun tio a ity that

a low us to test our co po able UI. Ho ever, alon side these d pen e cies, we’re

also g ing to need to add some rules to our build gradle le that will x some

of the co pi tion e rors that we’d cu rently see when tr ing to run our tests. Here

we’ll add some pac agi O tions that will e clude ce tain pac ages from the

a ded d pen e cies. We won’t dive too much into this concept and it’s us ally de-

pen ant on the ve sions of d pen e cies that are b ing used, so this may be re-

dun ant if you come to u da ing ve sions.

157
















f­i


































































fi











fi



android {
packagingOptions {
exclude "**/attach_hotspot_windows.dll"
exclude "META-INF/AL2.0"
exclude "META-INF/LGPL2.1"
exclude "META-INF/licenses/ASM"
}
}

158
Se ting up the test class
We’ll next start by cr a ing a new class, A the ti tio Test - this class will be used

to co tain the di fe ent tests that we’re g ing to write.

class AuthenticationTest {

I side of this class, we now need to de ne a re e ence to the Co p s Content-

Tes Rule class - this is what we’re g ing to use to set the co po able co tent on

screen, a lo ing us to pe form i te a tions and a se tions from wit in our tests.

@get:Rule
val composeTestRule = createComposeRule()

When u ing this rule, we don’t need to sp cify any form of acti ity for our co pos-

ables to be launched in, the test rule will handle that for us. So u ing this rule we

will set the co po able co tent to be co posed, the test will then launch a host

acti ity which will be used to co pose our provided co tent i side of.

159






















fi























Tes ing the A the ti tion Co pos-
able
At the root of our fe ture is the A the ti tion co po able. What makes this

di fe ent from our lower level co po ables (such as each UI co po ent co pos-

able), is that this co po able a lows us to i te act with el ments to tri ger state

u dates and r co po tion of our UI. This means that with the tests for the Au-

the ti tion co po able we can a sert not only that the e pe ted UI co pon-

ents are di played, but also that i te a tions with them re ult in the e pe ted state

changes.

Tes ing for A the ti tion Mode changes

With that in mind, we’re g ing to start with some tests to e sure that the e pe ted

state is co posed when the UI is i te a ted with. Wit in the A the ti tion

co po able the user can toggle between the sign-in + sign-up mode, so we’ll start

with some tests that a sert the co po tions that are d pen ant on these di fe ent

modes.

To start with, the title of the a the ti tion screen is d pen ing on the a then-

ti tion mode re re e ted wit in our state. So here, we’re g ing to write some

tests to a sert that the co tent of the title co rectly r ects the state of our screen.

By d fault our screen is in the sign in state, so we’re g ing to write a test to a sert

that this is the case.

To write this rst test we’ll use the @Test a not tion and cr ate a new fun tion

to test that the Sign In title is di played by d fault wit in our UI.

160
















fi






















































fl


































@Test
fun Sign_In_Title_Displayed_By_Default() {

I side of this test, we’re g ing to need to start by se ting the co po able co tent

that is to be di played on screen for us to a sert against. Here we’ll use the test rule

that we pr v ously de ned, along with its se Co tent fun tion. This fun tion takes

a co po able fun tion as an a g ment, a lo ing us to de ne what is to be com-

posed on screen for our tests. B cause we’re wan ing to test the A the ti tion

Co po able that we de ned in the pr v ous se tions of this chapter, we’ll go

ahead and pass the A the ti tion co po able fun tion for this co po able

a g ment.

@Test
fun Sign_In_Title_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}
}

While we aren’t yet pe for ing any a se tions, ru ning this test will launch an activ-

ity that di plays the co tent of our A the ti tion co po able. With this now be-

ing di played, we can next pe form the r quired a se tions to e sure that the Sign

In title is b ing di played wit in our co po able UI. We’ll do this by uti ising the

o Nod Wit Text fun tion from our test rule re e ence.

The o Nod Wit Text fun tion can be used to lo ate a co po able that is dis-

pla ing the text that we have provided to the fun tion. Co po ables will be loc-

ated in the form of a s man ic node - b cause our co po ables are re re e ted

via s mantics, in our tests we are g ing to be lo a ing nodes wit in our s man ic

tree. In this case, this is done u ing the o Nod Wit Text fun tion, which will re-

161























fi





fi






















































fi























turn us with a S manti Nod I te a tion re e ence to pe form a se tions

against.

@Test
fun Sign_In_Title_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_in_to_account)
)
}

For this test we want to a sert that this node is b ing di played wit in our com-

posed UI, so we’re g ing to go ahead and uti ise the a ser I Di played func-

tion. This is one of the a se tions avai able on the S manti Nod I te a tion

class, a lo ing us to a sert whet er this node is b ing di played on the screen.

@Test
fun Sign_In_Title_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_in_to_account)
).assertIsDisplayed()
}

If you run this test wit in your IDE, you’ll not only see the UI spin up i side of the

co ne ted device / em la or, but the tests should also be passing due to the re-

quired string b ing co posed wit in the UI.

162















































Alon side the Sign In title b ing di played by the d fault Sign In a the ti a-

tion mode, the Need A count a count should also be co posed wit in our UI. For

these tests, we’re g ing to be able to r use a lot of the same code, with the key dif-

fe ence here b ing that we need to a sert the co po tion of the ac-

tion_need_a count string as o posed to the sign in title.

@Test
fun Need_Account_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}

composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
)
.assertIsDisplayed()
}

With these two tests in place, we are now able to a sert that the e pe ted co pos-

ables for the Sign In A the ti tion Mode are b ing co posed. But what ha pens

if the user toggles the A the ti tion Mode? In this sce ario, we know that tog-

gling to the Sign Up mode will change the title and toggle me sages - so we’re go-

ing to write some tests to a sert these r co po tions are triggered when this

toggle o curs.

For this, we’re g ing to start by tes ing that the title is changed to the Sign Up

title when the A the ti tion Mode toggle is clicked. Just like the last test, we’ll

start by se ting the co tent to be co posed i side of our UI.

@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
composeTestRule.setContent {
Authentication()

163






















































}
}

Now that we have some form of state b ing co posed into our UI, we’re g ing to

want to tri ger the title change - this will be done by clic ing the A the ti tion

Mode toggle bu ton, which switches our screen from the sign-in to sign-up state.

For this i te a tion we’re g ing to use the pe for Click fun tion, this is a ges-

ture a tion that is avai able on the S manti Nod I te a tion class, a lo ing

us to pe form a click ge ture on the sp ci ed node.

@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
composeTestRule.setContent {
Authentication()
}

composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
).performClick()
}

Once this i te a tion has taken place, it is e pe ted that the a the ti tion mode

will be toggled. When this o curs, the title of the screen should switch to re re ent

the Sign Up mode. So with this test, we want to a sert that the e pe ted Sign Up

title is co posed. Here we’ll match the a se tion that we used for the title in the

pr v ous test, e cept this time we’ll check for our R.string.l bel_sign_up_-

for_a count String r source.

@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
composeTestRule.setContent {
Authentication()
}

164























fi
































composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
).performClick()

composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_up_for_account)
).assertIsDisplayed()
}

With this test in place, we’re now able to a sert that our title is r co posed a cord-

ingly when the a the ti tion mode is toggled. When this toggle o curs, we will

also e pect the a the ti tion bu ton to be r co posed to r ect the Sign Up ac-

tion, as o posed to Sign In. For this, we’re g ing to start with a test that looks very

si i ar to the pr v ous - we’ll need to co pose our A the ti tion co pos-

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
fun Sign_Up_Button_Displayed_After_Toggle() {
composeTestRule.setContent {
Authentication()
}

composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
).performClick()
}

165

































fl









Now, this is clicked, our a the ti tion bu ton should be sho ing the Sign Up ac-

tion. We’re g ing to want to a sert this to e sure this is the case, so we’ll need to

start by adding a tag to the a the ti tion bu ton co po able. We’ll cr ate a new

o ject, Tags, and de ne a new tag that can be a signed to our a the ti tion but-

ton. We’re also g ing to be i te ac ing with the a the ti tion toggle across a

couple of tests, so we’ll add a tag for that too.

//Tags.kt

object Tags {
const val TAG_AUTHENTICATE_BUTTON = "authenticate_button"
const val TAG_AUTHENTICATION_TOGGLE =
"authentication_mode_toggle"
}

With these tags de ned we can now use the tes Tag fun tion to a sign this tag to

our Bu ton co po able.

//AuthenticationButton.kt

Button(
modifier = Modifier.testTag(TAG_AUTHENTICATE_BUTTON),
...
)

We’ll also do the same for the Toggl A thenti tio Mode Bu ton.

// ToggleAuthenticationMode.kt

TextButton(
modifier = Modifier.testTag(TAG_AUTHENTICATION_TOGGLE),
...
)

With these tags in place, it can now be used to lo ate a node wit in our co pos-

able hie archy u ing the o Nod Wit ag fun tion. On this node, we can then use

166







fi

fi













































the a ser Te 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() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithTag(
TAG_AUTHENTICATION_TOGGLE
).performClick()

composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(R.string.action_sign_up)
)
}

With this test in place, we can now be ce tain that when the a the ti tion mode is

toggled, the co tent di played wit in the a the ti tion bu ton no longer re res-

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 i te act with the toggle bu ton to toggle the a the ti a-

tion mode, and then we’ll want to a sert that the text of that a the ti tion bu ton

re re ents the e pe ted value.

Here we’ll set up a test that will co pose our A the ti tion co po able,

fo lowed by uti ising our TAG_A THENTI TION TOGGLE tag to again lo ate the

node that re re ents the toggle bu ton. We can then use this re e ence to pe form

a tions and a se tions.

167



































fl













































B cause we’re g ing to be pe for ing mu tiple i te a tions on this node, we’ll

use the ko lin a ply fun tion to chain mu tiple o e tions. We’ll start by u ing the

pe for Click fun tion to pe form a click a tion on the bu ton, when this is

clicked the a the ti tion mode will be ipped from sign in to sign up. When this

o curs, the bu ton should di play the co tent of our a tion_already_have_ac-

count r source. We’ll use the a ser Te Equals fun tion to a sert that this is

the case.

@Test
fun Already_Have_Account_Displayed_After_Toggle() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithTag(
TAG_AUTHENTICATION_TOGGLE
).apply {
performClick()
assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_already_have_account)
)
}
}

If this test passes, it means that the a the ti tion toggle bu ton is b ing su cess-

fully r co posed with the co re pon ing state for the sign-up mode that has been

switched to. Ot e wise, it means the co po able has not been co posed with the

e pe ted state.

The title and a the ti tion bu ton are d na ic co po ents in the sense that

any co po tion should take into a count the a the ti tion mode. Now we have

these tests, we’re able to a sert that the co tent of these co po ables co rectly re-

ects the cu rent a the ti tion mode wit in our state.

168
fl
















































fl





































Tes ing the A the ti tion Bu ton

Aside from a ap ing to the a the ti tion mode, the A the ti tion bu ton is also

co posed based on ot er parts of our state. Based on the cu rent co tent that is

i put into the email and pas word text elds, the a the ti tion bu ton will be

co posed with an e abled state. This means that if the email or pas word in our

state is empty, then the a the ti tion bu ton will be di abled.

By d fault, our a the ti tion bu ton should be di abled, as there will be no

co tent in either of the email or pas word pro e ties of our state. Si i ar to the

pr v ous tests we’ve wri ten for our A the ti tion co po able, we’re g ing to

cr ate a new test that co poses our A the ti tion co po able, fo lowed by

u ing the test rule to lo ate a node u ing our pr v ously de ned TAG_A THENTIC-

ATE_BU TON tag.

@Test
fun Authentication_Button_Disabled_By_Default() {
composeTestRule.setContent {
Authentication()
}

composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
}

If this node has been lo ated, then we’re g ing to need to pe form an a se tion to

check that the bu ton is di abled - this is b cause there the email and pas word

state pro e ties are cu rently empty. To pe form this a se tion we’re g ing to use

the a ser I No E abled fun tion. This S manti Matc er will check that the

s mantics for the lo tion node has the S manti Pro e ties.Di abled prop-

erty, mea ing that the co po able is di abled.

@Test
fun Authentication_Button_Disabled_By_Default() {
composeTestRule.setContent {

169





























































fi





























fi

























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
fun Authentication_Button_Enabled_With_Valid_Content() {
composeTestRule.setContent {
Authentication()
}
}

While we could use the A the ti tion state to pr load va ues to be used for the

email and pas word elds, I wanted to si late user b h viour here - so we’re go-

ing to use the pe for Te I put fun tion to type some text into the sp ci ed

text eld. B fore we can i te act with our text elds in such a way, we’re g ing to

need to add tags for them so that the nodes can be lo ated from our UI.

const val TAG_INPUT_EMAIL = "input_email"


const val TAG_INPUT_PASSWORD = "input_password"

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.

// EmailInput.kt

TextField(
modifier = modifier.testTag(TAG_INPUT_EMAIL),
...

170

fi










fi­
fi







fl














fi





fi






fi








fi
)

// PasswordInput.kt

TextField(
modifier = modifier.testTag(TAG_INPUT_PASSWORD),
...
)

With these tags in place, we can now uti ise the pe for Te I put fun tion to in-

put a provided string into the co re pon ing nodes.

composeTestRule.onNodeWithTag(
TAG_INPUT_EMAIL
).performTextInput("[email protected]")

composeTestRule.onNodeWithTag(
TAG_INPUT_PASSWORD
).performTextInput("password")

After our state has been co posed, we’ll use both the email and pas word i put

elds to pe form text i put - gi ing both of these elds va id co tent that would al-

low the user to a the ti ate against. Once these calls are in place, we can again

lo ate the A the ti tion Bu ton u ing its tag but this time a sert that it is e abled

u ing the a ser I E abled fun tion. We pr v ously used the a ser I No En-

abled fun tion, the key di fe ence here is that a ser I E abled is chec ing that

the S manti Pro e ties.Di abled s man ic pro erty is not present on the

sp ci c co po able.

@Test
fun Authentication_Button_Enabled_With_Valid_Content() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithTag(
TAG_INPUT_EMAIL
).performTextInput("[email protected]")

171
fi



f­i






































fi





















composeTestRule.onNodeWithTag(
TAG_INPUT_PASSWORD
).performTextInput("password")

composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).assertIsEnabled()
}

B cause the email and pas word elds have va id co tent, the a the ti tion but-

ton, in this case, should be e abled - which our test should now be a ser ing for us.

Some fu ther tes ing here could i clude r mo ing text from the i put elds

and a ser ing that our a the ti tion bu ton is di abled from r co po tion. At this

point, the in tial test for the di abled state, fo lowed by the e able state serves as a

mi i al r quir ment for our tes ing - but feel free to e plore fu ther co e age

here!

Tes ing a the ti tion e rors

Now that we’ve pe formed a se tions on the co tent that is used to a the ti ate

the user, we can look at the next stages in the a pli tion ow. While the user

might be su ces fully a the ti ated and move to the next screen, that isn’t a ways

g ing to be the case - to cover these sce ar os, we a ded a di log co po able to

be di played when an e ror o curs du ing a the ti tion.

To a sert that this di log is di played in the co rect sce ar os, we’re g ing to

go ahead and start by tes ing that the di log is not di played by d fault. This will

a low us to e sure that users are not g ing to be shown the e ror di log when an

e ror hasn’t happened.

@Test
fun Error_Alert_Not_Displayed_By_Default() {

172










































fi






























fl


















fi






}

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.

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 i-

er.

AlertDialog(
modifier = Modifier.testTag(TAG_ERROR_ALERT),
...
)

With this tag in place, we can now a tempt to lo ate the node and then pe form as-

se tions against it. To do this we’ll use the o Nod Wit ag fun tion, fo lowed by

u ing a ser Doe N E ist to a sert that a node with this tag does not e ist -

mea ing that the e ror di log does not cu rently e ist wit in our UI.

@Test
fun Error_Alert_Not_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithTag(
TAG_ERROR_ALERT
).assertDoesNotExist()
}

Now that we know our alert di log is not sho ing when an e ror doesn’t e ist,

we’re g ing to want to test the ip side of this and a sert that the e ror di log is

di played when an e ror has o curred. We’ll start here by d ing a new test func-

tion to re re ent this test case.

@Test

173
fi­




fi


















fl




















fi













fun Error_Alert_Displayed_After_Error() {

Next, we need to co pose our state so that the e ror di log is di played - we’ll do

this by co po ing our A the ti tio Co tent and provi ing an A the tic-

tio State re e ence that has an e ror value a signed to it.

composeTestRule.setContent {
AuthenticationContent(
AuthenticationState(
error = "Some error"
)
) { }
}

B cause our state now has an e ror value, an alert di log will be co posed wit in

our UI. Ho ever, we’re g ing to want to na ise our test and a sert that this is the

case. We’ll wrap up this test by lo a ing the alert di log u ing the tag we pr v ously

a signed to the Aler Di log co po able, fo lowed by u ing the a ser I Dis-

played fun tion to ver fy that the alert di log has been co posed wit in our UI.

@Test
fun Error_Alert_Displayed_After_Error() {
composeTestRule.setContent {
AuthenticationContent(
AuthenticationState(
error = "Some error"
)
) { }
}

composeTestRule.onNodeWithTag(
TAG_ERROR_ALERT
).assertIsDisplayed()
}

174



























fi­
























Tes ing the loa ing state

When the A the ti tion Bu ton is clicked, the a the ti tion pr cess is triggered -

in this sce ario we would likely be ma ing a ne work r quest, di pla ing a pro-

gress di log on-screen in the pr cess. Du ing these state changes, we show and

hide a large amount of the UI co po ents, so we want to be sure that these state

changes re ult in the e pe ted UI co d tions. We’ll write a couple more tests to en-

sure that these changes b have as e pe ted.

So that we can pe form a se tions against the pr gress i di a or, we’ll de ne a

new tag and a sign it to our Ci c la Pr gres I di a or co po able u ing

the tes Tag mo er.

// Tags.kt

object Tags {
const val TAG_PROGRESS = "progress"
}

CircularProgressIndicator(
modifier = Modifier.testTag(TAG_PROGRESS)
)

We’ll then go ahead and add a simple rst test that a serts our pr gress i di a or is

not co posed of the d fault state of our UI. Here we use the o Nod Wit ag

fun tion to lo ate our node u ing the sp ci ed tag, fo lowed by a ser ing that the

node does not e ist u ing the a ser Doe N E ist fun tion.

@Test
fun Progress_Not_Displayed_By_Default() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithTag(
TAG_PROGRESS
).assertDoesNotExist()

175















fi­





















fi








fi































fi





}

Now we’ve a se ted that our pr gress i di a or is not co posed with the d fault

a the ti tion state, we can now write a test to e sure that the loa ing state is re-

e ted in our co posed UI. For this we’ll go ahead and co pose our A the tic-

tio Co tent, provi ing a re e ence to the A the ti tio State class with

the i Loa ing ag marked as true.

With this in place, we can co ti ue to lo ate the node that re re ents our load-

ing i di a or, fo lowed by pe for ing the a se tion that it is di played wit in our

co posed UI.

@Test
fun Progress_Displayed_While_Loading() {
composeTestRule.setContent {
AuthenticationContent(
AuthenticationState(isLoading = true)
) { }
}

composeTestRule.onNodeWithTag(
TAG_PROGRESS
).assertIsDisplayed()
}

After our o e tion has ished loa ing, we’ve har coded our Vie Mo el to set

an e ror state. When this ha pens, our UI should hide the pr gress i di a or and

di play the a the ti tion form to the user. For us to a sert that this is the case,

we’ll need to tri ger the a the ti tion pr cess from our UI. To save us e te ing

text into the tex elds at runtime, we’ll co pose our test UI with some pre-loaded

state for the email a dress and pas word va ues.

Once that’s done, we next need to pe form a click i te a tion on our A the tic-

tion Bu ton - this will tri ger the a the ti tion pr cess and set the e ror state

from our Vie Mo el. When this ha pens, our pr gress i di a or should no longer

176
fl


























fl



fi






fi




































































e ist in our UI. To e sure that this is the case, we’ll add an a se tion by u ing the

a ser Doe N E ist fun tion to check that the pr gress i di a or does not e ist

wit in our UI.

@Test
fun Progress_Not_Displayed_After_Loading() {
composeTestRule.setContent {
AuthenticationContent(
authenticationState = AuthenticationState(
email = "[email protected]",
password = "password"
)
) { }
}

composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).performClick()

composeTestRule.onNodeWithTag(
TAG_PROGRESS
).assertDoesNotExist()
}

Once we reach this state of our pr gress i di a or not b ing di played (b cause

the loa ing pr cess has been co pleted), we’re g ing to want to e sure that the

co tent of our UI has been co posed again - this is the a the ti tion form, a low-

ing users to a tempt re-a the ti tion. If this didn’t di play again wit in the UI,

things would be quite broken for the users - so we’ll write a test to a sert that this is

the case.

@Test
fun Content_Displayed_After_Loading()

B fore we can pe form a se tions against the co tent area of our UI, we’re g ing to

need to de ne a new tag and a sign it to the pa ent of our co tent area.

177







fi















































// Tags.kt

object Tags {
const val TAG_CONTENT = "content"
}

We’ll then need to set this tag on the co re pon ing co po able wit in our Au-

the ti tio Form.

// AuthenticationForm.kt

Column(
modifier = Modifier.testTag(TAG_CONTENT),
horizontalAlignment = Alignment.CenterHorizontally
)

While we could pe form a se tions against the i d vid al chi dren that already

have tags a signed to them, this a proach a lows us to refer to the co tent area as

a whole. Si i ar to the pr v ous test, we can pe form the a the ti tion ow, fol-

lowed by pe for ing the a se tion that the co tent area e ists wit in our UI.

B cause there has been an e ror state loaded at this point, there will be an

alert di log co posed wit in our UI. For this rea on, we use the e ists check in-

stead of a di played check, this is b cause the alert di log will be covered a

good chunk of the co tent UI so we ca not a ways gua a tee that the di played

a se tion would be sa i ed.

@Test
fun Content_Displayed_After_Loading() {
composeTestRule.setContent {
Authentication()
}

composeTestRule.onNodeWithTag(
TAG_INPUT_EMAIL
).performTextInput("[email protected]")

composeTestRule.onNodeWithTag(

178



















fi






































fl
TAG_INPUT_PASSWORD
).performTextInput("password")

composeTestRule.onNodeWithTag(
TAG_AUTHENTICATE_BUTTON
).performClick()

composeTestRule.onNodeWithTag(
TAG_CONTENT
).assertExists()
}

179
Tes ing the A the ti tion Title
Now that we have tests in place that pe forms a se tions against our A the ti a-

tion co po able, we’re g ing to f cus on wri ing some ne-grained tests for the

i d vid al co po able fun tions that re re ent our i d vid al se ting items. This al-

lows us to f cus on pe for ing a se tions on the b h viour of the co po able

fun tion i self, without the co cern of our glo al state. We’ll start here by cr a ing a

new test class, A the ti tio Ti l Test, co u ing our Co p s Content-

Tes Rule ready for use.

class AuthenticationTitleTest {

@get:Rule
val composeTestRule = createComposeRule()

We’re g ing to start here by wri ing a test to a sert that the co po able co rectly

di plays the title co re pon ing title for the A the ti tio Mode that is

provided to it. The A the ti tio Title co tains the l gic that d picts which

string r source is used based on the A the ti tio Mode that is provided to it.

For this rea on, we’ll want to write these tests to e sure this l gic is wor ing as ex-

pe ted.

Here we’ll cr ate a new test fun tion, Sign_In Title_Di played, where we

will a sert that the Sign In title is co posed when the A the ti tio Mod-

e.SIGN_IN is provided for the a the ti tio Mode a g ment. We’ll start here

by co po ing an A the ti tio Title, passing the Sign In mode for the au-

the ti tio Mode.

180
















































































fi











fi





























@Test
fun Sign_In_Title_Displayed() {
composeTestRule.setContent {
AuthenticationTitle(
authenticationMode = AuthenticationMode.SIGN_IN
)
}
}

When the A the ti tio Title is co posed for the SIGN_IN A the ti a-

tio Mode, it is e pe ted that the l bel_sign_in_to_a count will be dis-

played. We’ll need to pe form an a se tion for this in our tests, so we’ll go ahead

and use the o Nod Wit Text fun tion on our test rule to lo ate a node that has

the text co tained wit in our l bel_sign_in_to_a count r source. We’ll then

use the a ser I Di played fun tion to pe form the a se tion that this co pos-

able is di played.

@Test
fun Sign_In_Title_Displayed() {
composeTestRule.setContent {
AuthenticationTitle(
authenticationMode = AuthenticationMode.SIGN_IN
)
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_in_to_account)
)
.assertIsDisplayed()
}

If this test fails, it means that the e pe ted title is not b ing co posed for the

SIGN_IN A the ti tio Mode. On the ip side, we’ll also want to a sert that the

e pe ted text co tent is co posed for the SIGN_UP A the ti tio Mode. This

test is g ing to look the same as the pr v ous, e cept this time we’ll pass in Au-

181







































fl





















the ti tio Mo e.SIGN_UP when co po ing the A the ti tio Title, as

well as u ing the l bel_sign_up for_a count r source when pe for ing our

a se tion.

@Test
fun Sign_Up_Title_Displayed() {
composeTestRule.setContent {
AuthenticationTitle(
authenticationMode = AuthenticationMode.SIGN_UP
)
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.label_sign_up_for_account)
)
.assertIsDisplayed()
}

With these tests in place, we can now be sure that the A the ti tio Title is

u ing the e pe ted string r source du ing co po tion, based on the A the tic-

tio Mode that is provided to it.

182






































Tes ing the A the ti tion Bu ton
Alon side the A the ti tio Title b ing co posed based on the A then-

ti tio Mode that is provided to it, the A the ti tio Bu ton also b haves

in the same way - we’ll also want to write some tests to a sert this co po tion also.

We’ll start by cr a ing a new A the ti tio Bu to Test class, se ting up the

co pose rule in the pr cess.

class AuthenticationButtonTest {

@get:Rule
val composeTestRule = createComposeRule()

Next, we’ll write the rst test wit in this test class, which will be used to a sert that

the Sign In a tion is di played wit in the bu ton when e pe ted. For this we’ll need

to co pose the A the ti tio Bu ton, passing the A the ti tio Mod-

e.SIGN_IN value for the a the ti tio Mode a g ment.

@Test
fun Sign_In_Action_Displayed() {
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = true,
authenticationMode = AuthenticationMode.SIGN_IN,
onAuthenticate = { }
)
}
}

183












fi
























































Now that the A the ti tio Bu ton is g ing to be co posed in our test, we

can pe form the r quired a se tions. We already have the TAG_A THENTIC-

ATE_BU TON tag a signed to our co po able from some pr v ous tests that we

wrote, so we can use this to lo ate the r quired node. Once we’ve done that, the

a ser Te Equals can then be used to a sert that the e pe ted text of the re-

trieved node matches the value that we provide. Here we’ll provide the string value

for our a tion_sign_in r source, which re re ents the “Sign In” value that is ex-

pe ted to be di played when the A the ti tio Mo e.SIGN_IN is provided to

the co po able.

@Test
fun Sign_In_Action_Displayed() {
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = true,
authenticationMode = AuthenticationMode.SIGN_IN,
onAuthenticate = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(R.string.action_sign_in)
)
}

We’ll also want to a sert that the a tion_sign_up string is di played when the

A the ti tio Mo e.SIGN_UP is passed to the co po able. We’ll write a cor-

re pon ing test here which will mostly match the pr v ous test we wrote, e cept

we’ll pass A the ti tio Mo e.SIGN_UP for the a the ti tio Mode a gu-

ment, along with u ing the a tion_sign_up string r source when pe for ing the

a ser Te Equals a se tion.

@Test

184

















































































fun Sign_Up_Action_Displayed() {
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = true,
authenticationMode = AuthenticationMode.SIGN_UP,
onAuthenticate = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_sign_up)
)
}

Alon side the A the ti tio Mode based a se tions that we’ve pe formed

above, the A the ti tio Bu ton also takes an onA thenti ate a g ment.

When this lambda is i voked by our A the ti tio Bu ton, the pa ent com-

po able will use this to tri ger the a the ti tion mode - if this broke, users would

not be able to pe form a the ti tion wit in our app. For this rea on, we’re g ing

to write a test to a sert that the lambda is i voked when e pe ted. Here we’re go-

ing to pass in a mock lambda fun tion for the onA thenti ate a g ment. This

means that we can use this mock to ver fy that i te a tions have taken place based

off of co po able events.

@Test
fun Authenticate_Triggered() {
val onAuthenticate: () -> Unit = mock()
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = false,
authenticationMode = AuthenticationMode.SIGN_UP,
onAuthenticate = onAuthenticate
)
}

185
























































}

With the A the ti tio Bu ton b ing co posed, we’ll now be able to r trieve

the node that re re ents this a the ti tion bu ton u ing the TAG_A THENTIC-

ATE_BU TON tag. We’ll then use the pe for Click fun tion to pe form a click ac-

tion on this node. When this click a tion is triggered, this is the point that we would

e pect the onA thenti ate to be i voked so that the pa ent co po able can

handle the a the ti tion event. We can ver fy this wit in our test by u ing mockito

and its ver fy fun tion to a sert that the lambda has been i voked. If this is the

case, the test will su ceed - ot e wise, the lambda not b ing triggered will mean

that our ver tion will not be sa i ed and the test will fail.

@Test
fun Authenticate_Triggered() {
val onAuthenticate: () -> Unit = mock()
composeTestRule.setContent {
AuthenticationButton(
enableAuthentication = false,
authenticationMode = AuthenticationMode.SIGN_UP,
onAuthenticate = onAuthenticate
)
}

composeTestRule
.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
.performClick()

verify(onAuthenticate).invoke()
}

The A the ti tio Bu ton co po able also takes an e abl A thenti a-

tio a g ment. It could also be b n cial to write some tests to a sert the com-

po tion based on the value of this a g ment - we already have some tests for the

A the ti tion co po able that i volved the e abled state, so we won’t cover

that here!

186
















fi
































fi






fi­



























187
Tes ing the A the ti tion Mode
Toggle
So far we’ve been wri ing tests for var ous co po ables that uti ise the A then-

ti tio Mode from our state o ject. The bu ton that is used to toggle this value

also takes an A the ti tio Mode re e ence, this is also so that it can be com-

posed to di play the co re pon ing co tent for the provided A the ti tion-

Mode. After se ting up a test class with a co re pon ing test rule, we’ll cr ate a test

fun tion that will be used to a sert the a tion_need_a count r source text is

di played wit in our co po able.

class AuthenticationModeToggleTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun Need_Account_Action_Displayed() {

Wit in this test we’ll need to start by co po ing a Toggl A thenti tio Mode,

provi ing the a the ti tio Mode a g ment in the form of the A the ti a-

tio Mo e.SIGN_IN value. When this SIGN_IN value is provided, we e pect that

the co re pon ing co tent is di played i side the bu ton - this is in the form of the

a tion_need_a count r source. After lo a ing the node for this co po able us-

ing the o Nod Wit ag(TAG_A THENTI TION TOGGLE) fun tion call, we can

188



























































































a sert that this e pe ted text is di played via the use of the a ser Te Equals

fun tion.

@Test
fun Need_Account_Action_Displayed() {
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_IN,
toggleAuthentication = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_need_account)
)
}

We’ll next ip this around so that we can a sert that the e pe ted ac-

tion_already_have_a count value is di played when the A the ti tion-

Mo e.SIGN_UP value is provided for the a the ti tio Mode a g ment. Our

test here is g ing to look the same as above, aside from the tweak to the a then-

ti tio Mode a g ment that we pass, along with the ac-

tion_already_have_a count value that is now b ing provided to the a sert-

Te Equals fun tion call.

@Test
fun Already_Have_Account_Action_Displayed() {
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_UP,
toggleAuthentication = { }
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)

189








fl































.assertTextEquals(
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.action_already_have_account)
)
}

With this test in place, we can now be ce tain that the passing tests mean the

provided a the ti tio Mode value is g ing to di play the e pe ted text i side

of our co po able. Whene er the user clicks the bu ton that is di pla ing this text,

the lambda fun tion that is provided to the co po able should be triggered - this

is the toggl A thenti tion lambda. If this for some rea on was not b ing

triggered, the user would not be able to switch to the sign-up mode - so if a user

does not cu rently have an a count, they wouldn’t be able to cr ate one. To e sure

this r mains fun tio al, let’s write a quick test to a sert that this event does o cur.

Wit in this next test, we’re g ing to pass in a mock lambda fun tion for the

toggl A thenti tion a g ment. This means that we can use this mock to

ver fy that i te a tions have taken place based off of co po able events.

@Test
fun Toggle_Authentication_Triggered() {
val toggleAuthentication: () -> Unit = mock()
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_UP,
toggleAuthentication = toggleAuthentication
)
}
}

With the Toggl A thenti tio Mode b ing co posed, we’ll now be able to

use the r trieve the node that re re ents this toggle bu ton u ing the TAG_AU-

THENTI TION TOGGLE tag. We’ll then use the pe for Click fun tion to per-

form a click a tion on this node. When this click a tion is triggered, this is the point

190






































































that we would e pect the toggl A thenti tion to be i voked so that the par-

ent co po able can handle the a the ti tion event. We can ver fy this wit in our

test by u ing mockito and its ver fy fun tion to a sert that the lambda has been

i voked. If this is the case, the test will su ceed - ot e wise, the lambda not b ing

triggered will mean that our ver tion will not be sa i ed and the test will fail.

@Test
fun Toggle_Authentication_Triggered() {
val toggleAuthentication: () -> Unit = mock()
composeTestRule.setContent {
ToggleAuthenticationMode(
authenticationMode = AuthenticationMode.SIGN_UP,
toggleAuthentication = toggleAuthentication
)
}
composeTestRule
.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
.performClick()

verify(toggleAuthentication).invoke()
}

191






fi


















fi




Tes ing the Email A dress I put
When it comes to the Emai I put co po able, an email a g ment is used to

provide the co tent that is to be di played i side of the text eld. This is a very im-

por ant part of the a the ti tion ow, so we’ll want to write a test to e sure that

this provided value is di played i side of our co po able. To do this we’ll need to

start by se ting up a new test class, Emai I pu Test.

class EmailInputTest {

@get:Rule
val composeTestRule = createComposeRule()

We’ll start here by cr a ing a new test, co po ing the Emai I put co po able.

We’ll provide empty i pl men tions for the on mai Changed and o Focus-

Reque ted a g ments, but will need to provide a string value for the email a gu-

ment. We’ll cr ate a string var able re e ence here, provi ing this to our Emai In-

put co po able.

@Test
fun Email_Displayed() {
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = { },
onNextClicked = { }
)
}

192

























fl


















fi










}

Next, we’ll need to a sert that this email value is di played i side of our co pos-

able. In a pr v ous test, we de ned the TAG_I PUT_ MAIL tag, so we’ll use this

here to lo ate the node that re re ents our email text eld. Once this node has

been lo ated we can uti ise the a ser Te Equals fun tion to a sert that the text

s man ic value of the node matches our provided email var able.

@Test
fun Email_Displayed() {
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = { },
onNextClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.assertTextEquals(email)
}

With this test in place, we can now be ce tain that a passing test means the

provided email 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 email value that is co ing

from our state, the lambda fun tion that is provided to the co po able is triggered

- this is the on mai Changed lambda. If this for some rea on was not b ing

triggered, the user would be u able to enter their email a dress into the text eld.

To e sure this r mains fun tion, let’s write a quick test to a sert that this event does

o cur.

As b fore, we’ll need to co pose the Emai I put co po able to pe form

our a se tions against. We’ll need to provide a string value for the email a g ment

- we’ll cr ate a var able re e ence for this so that we can a sert the lambda is

193


























fi



fi













fi


















fi

triggered with the e pe ted value. We’re also g ing to pass in a mock lambda

fun tion for the on mai Changed a g ment. This means that we can use this mock

to ver fy that i te a tions have taken place based off of co po able events.

@Test
fun Email_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = onEmailChanged,
onNextClicked = { }
)
}
}

While we have provided this lambda to our co po able, we now need to tri ger it

so that we can ver fy the e pe ted b h viour. To tri ger this lambda we need to

enter some text into the i put eld, which we can do so u ing the pe for Te t-

I put on a sp ci ed node. We’re g ing to a pend some text onto the e is ing in-

put, which we’ll store in a var able re e ence, a pe de Text so that we can use

this du ing the a se tion. Here we’ll lo ate the i put eld node u ing our e is ing

tag, fo lowed by i pu ting this co tent u ing the pe for Te I put fun tion.

val appendedText = ".jetpack"


composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.performTextInput(appendedText)

With this in place, we can now add the check to ver fy that the lambda fun tion is

called as e pe ted. When this is triggered, we would e pect that the email value

r turned here would re re ent the e is ing co tent with the a d tion of the ap-

pe de Text value. We can ver fy this wit in our test by u ing mockito and its

194















fi













fi


























fi























ver fy fun tion to a sert that the lambda has been i voked with the e is ing value

in the i put eld (email) a pe ded with the value of a pe de Text. If this is the

case, the test will su ceed - ot e wise, the lambda not b ing triggered will mean

that our ver tion will not be sa i ed and the test will fail.

@Test
fun Email_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val email = "[email protected]"
composeTestRule.setContent {
EmailInput(
email = email,
onEmailChanged = onEmailChanged,
onNextClicked = { }
)
}
val appendedText = ".jetpack"
composeTestRule
.onNodeWithTag(TAG_INPUT_EMAIL)
.performTextInput(appendedText)

verify(onEmailChanged).invoke(email + appendedText)
}

195




fi
fi










fi







Tes ing the Pas word I put
When it comes to the Pas wor I put co po able, a pas word a g ment is

used to provide the co tent that is to be di played i side of the text eld. This is a

very i por ant part of the a the ti tion ow, so we’ll want to write a test to e sure

that this provided value is di played i side of our co po able. To do this we’ll

need to start by se ting up a new test class, Pas wor I pu Test.

class PasswordInputTest {

@get:Rule
val composeTestRule = createComposeRule()

We’ll start here by cr a ing a new test, co po ing the Pas wor I put co pos-

able. We’ll provide empty i pl men tions for the o Pas wor Changed and on-

Don Clicked a g ments, but will need to provide a string value for the pas word

a g ment. We’ll cr ate a string var able re e ence here, provi ing this to our Pass-

wor I put co po able.

@Test
fun Password_Displayed() {
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}

196
































fl























fi





}

Next, we’ll need to a sert that this pas word value is in fact di played i side of our

co po able. In a pr v ous test, we de ned the TAG_I PUT PAS WORD tag, so

we’ll use this here to lo ate the node that re re ents our pas word text eld. Once

this node has been lo ated we can uti ise the a ser Te Equals fun tion to as-

sert that the text s man ic value of the node matches our provided pas word vari-

able.

@Test
fun Password_Displayed() {
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWORD)
.assertTextEquals(password)
}

With this test in place, we can now be ce tain that a passing test means the

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 co po able

is triggered - this is the o Pas wor Changed lambda. If this for some rea on was

not b ing triggered, the user would be u able to enter their pas word into the text

eld. To e sure this r mains fun tion, let’s write a quick test to a sert that this event

does o cur.

As b fore, we’ll need to co pose the Pas wor I put co po able to per-

form our a se tions against. We’ll need to provide a string value for the pas word

197
fi



























fi


fi


























fi






a g ment - we’ll cr ate a var able re e ence for this so that we can a sert the

lambda is triggered with the e pe ted value. We’re also g ing to pass in a mock

lambda fun tion for the o Pas wor Changed a g ment. This means that we can

use this mock to ver fy that i te a tions have taken place based off of co po able

events.

@Test
fun Password_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
}

While we have provided this lambda to our co po able, we now need to tri ger it

so that we can ver fy the e pe ted b h viour. To tri ger this lambda we need to

enter some text into the i put eld, which we can do so u ing the pe for Te t-

I put on a sp ci ed node. We’re g ing to a pend some text onto the e is ing in-

put, which we’ll store in a var able re e ence, a pe de Text so that we can use

this du ing the a se tion. Here we’ll lo ate the i put eld node u ing our e is ing

tag, fo lowed by i pu ting this co tent u ing the pe for Te I put fun tion.

val passwordText = "456"


composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWORD)
.performTextInput(passwordText)

With this in place, we can now add the check to ver fy that the lambda fun tion is

called as e pe ted. When this is triggered, we would e pect that the pas word

value r turned here would re re ent the e is ing co tent with the a d tion of the

198












fi












fi
































fi

























a pe de Text value. We can ver fy this wit in our test by u ing mockito and its

ver fy fun tion to a sert that the lambda has been i voked with the e is ing value

in the i put eld (pas word) a pe ded with the value of a pe de Text. If this is

the case, the test will su ceed - ot e wise, the lambda not b ing triggered will

mean that our ver tion will not be sa i ed and the test will fail.

@Test
fun Password_Changed_Triggered() {
val onEmailChanged: (email: String) -> Unit = mock()
val password = "password123"
composeTestRule.setContent {
PasswordInput(
password = password,
onPasswordChanged = { },
onDoneClicked = { }
)
}
val passwordText = "456"
composeTestRule
.onNodeWithTag(TAG_INPUT_PASSWORD)
.performTextInput(passwordText)

verify(onEmailChanged).invoke(password + passwordText)
}

When it comes to the Pas wor I put co po able, we i pl me ted the abi ity to

toggle the vi i i ity of the pas word u ing a vi i i ity toggle bu ton. We’re g ing to

write a test to a sert that the state of this is r e ted co rectly, based on the i ter al

state of the fun tion that is b ing used to ma age the pas word vi i i ity.

When it comes to tes ing this, we’ll just write a single test to check that the vis-

i i ity toggle co po able r ects the e pe ted state. We’ll need to start here by

adding a new tag to our Tags o ject so that we can lo ate and i te act with the

vi i i ity co po able. We’ll end this tag with an u de score so that we can a pend

the cu rent boolean value of the toggle to the tag, mea ing that we can lo ate the

tag based on the e abled state of the toggle.

199













fi








fi











fl













fi




fl



































// Tags.kt

object Tags {
...
const val TAG_PASSWORD_HIDDEN = "password_hidden_"
}

Next, we’ll need to a sign this tag our co po able u ing the tes Tag fun tion.

When d ing this we’ll also a pend the cu rent value of our i Pas wor Hi den

state, so that we can lo ate the node u ing this value. We do this as if the value is

not aligned as e pe ted, then the node won’t be found and the tests will fail.

// PasswordInput.kt

trailingIcon = {
Icon(
modifier = Modifier.testTag(TAG_PASSWORD_HIDDEN +
isPasswordHidden),
...
)
}

With this in place, we can now start wor ing on the test. Here we’ll b gin by com-

po ing the Pas wor I put with a string value for the pas word a g ment.

@Test
fun Password_Toggled_Reflects_state() {
composeTestRule.setContent {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
}

We’ll then want to lo ate the vi i i ity toggle co po able. By d fault the vi i i ity

ag will be true, si n f ing that the pas word is hi den. Here we’re g ing to lo ate

200
fl











































the node with the value of true a pe ded to the tag, click on it and then a sert that

the tag with the value of false a pe ded to the tag is di played. Here we’re g ing

to start by lo a ing the node for the hi den state of our vi i i ity toggle co pos-

able - we’ll need this so that we can pe form a click i te a tion on the co po able.

Here we’ll use the TAG PAS WORD_HI DEN tag, a pen ing the value of true on

the end to match the e pe ted co d tion of the state for the pas word vi i i ity.

@Test
fun Password_Toggled_Reflects_state() {
composeTestRule.setContent {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDDEN + "true")
.performClick()
}

With this in place, we are not pe for ing a click i te a tion on the co po able,

this means that now the co po able state should have r co posed the vi i i ity

toggle co po able. This means that now, a node with the TAG PAS WORD_HID-

DEN tag for the vi ible state of the pas word should be vi ible. We can a sert this

here u ing the a ser I Di played fun tion on the lo ated node.

@Test
fun Password_Toggled_Reflects_state() {
composeTestRule.setContent {
PasswordInput(
password = "password123",
onPasswordChanged = { },
onDoneClicked = { }
)
}
composeTestRule

201






























































.onNodeWithTag(TAG_PASSWORD_HIDDEN + "true")
.performClick()

composeTestRule
.onNodeWithTag(TAG_PASSWORD_HIDDEN + "false")
.assertIsDisplayed()
}

While we could have a se a ate test here to a sert that the true co d tion is dis-

played, this test co ers both sce ar os. This is b cause the rst o Nod Wit ag

call will fail the test if the node is not found - this will mean that the tag for the hid-

den state of the vi i i ity toggle would not cu rently be b ing di played on the

screen. B cause this test r quires the hi den state of the vi i i ity toggle to per-

form the a ser I Di played a se tion, we cover both sce ar os in a single test.

202






















fi











Tes ing the Pas word R quir ments
While we’re ver f ing the entry of a pas word from the tests above, our user is still

r quired to enter a pas word that meets the mi i um r quir ments that are

de ned wit in our Vie Mo el. These r quir ments are co m ni ated to the user

via the Pas wor R quir ments co po able, with the co po able co tai ing lo-

gic to d pict the me sage to be di played based on the provided r quir ment

statuses. So here, we’ll write some tests here to ver fy that this co po able is o er-

a ing as e pe ted.

// PasswordRequirementsTest.kt

class PasswordRequirementsTest {

@get:Rule
val composeTestRule = createComposeRule()

We’re rst g ing to write a test to a sert that each of the pas word r quir ments is

di played as e pe ted. To keep things simple here and avoid nee ing to write mul-

tiple test co d tions, we’re g ing to write a test that will a sign ra dom pas word

r quir ments as sa i ed. This way we can a sert that e pe ted r quir ments are

di played as both sa i ed and u sa 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 i ed r quir ment.

val requirements = PasswordRequirement.values().toList()

203





fi

fi
























fi


fi


fi















fi








































val satisfiedRequirement = requirements[(0 until
requirements.count()).random()]

We could use a ra dom nu ber of sa i ed r quir ments to vary the nu ber

between tests, but we’ll use a single one here to keep things simple for e amples

sake. We’ll next co pose a Pas wor R quir ments, provi ing a list for the sat-

i fie R quir ments a g ment that co sists of the ra dom r quir ment that we

r trieved above, sa i fie R quir ment.

@Test
fun Password_Requirements_Displayed_As_Not_Satisfied() {
val requirements = PasswordRequirement.values().toList()
val satisfiedRequirement = requirements[(0 until
requirements.count()).random()]

composeTestRule.setContent {
PasswordRequirements(
satisfiedRequirements = listOf(satisfied)
)
}
}

When the Pas wor R quir ments is co posed, it should be the case that the

r quir ments are co posed based on the sa i ed r quir ments that are

provided. To test this we’re g ing to need to start by loo ing through each of the

avai able Pas wor R quir ment va ues:

PasswordRequirement.values().forEach {

Next, we’ll use each of their l bels, along with the provided sa i fie R quire-

ment to build the string that we’re g ing to a sert for. The Pas wor R quire-

ments co po able is forma ting two di fe ent string re re en tions based on the

sa i ed state of each. If a r quir ment is marked as sa i ed, then the pass-

word_r quir ment_sa i fied r source is used to build a string for that re-

204






fi














































fi





fi









fi














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.

PasswordRequirement.values().forEach { requirement ->


val requirement =
InstrumentationRegistry.getInstrumentation()
.context.getString(it.label)

val result = if (requirement == satisfiedRequirement) {


InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_satisfied,
requirement)
} else {
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_needed,
requirement)
}
}

Now, this string is b ing built, we can use this to lo ate a node and pe form an as-

se tion to e sure that it is b ing di played wit in the co po able.

PasswordRequirement.values().forEach { requirement ->


val requirement =
InstrumentationRegistry.getInstrumentation()
.context.getString(it.label)
val result = if (requirement == satisfiedRequirement) {
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_satisfied,
requirement)
} else {
InstrumentationRegistry.getInstrumentation()
.context.getString(

205




























fi






R.string.password_requirement_needed,
requirement)
}

composeTestRule
.onNodeWithText(result)
.assertIsDisplayed()
}

With this loop, our test is now loo ing through each of the avai abl Pas wor Re-

quir ment va ues and a ser ing that the e pe ted r quir ment me sage is dis-

played wit in the co po able.

@Test
fun Password_Requirements_Displayed_With_State() {

val requirements = PasswordRequirement.values().toList()


val satisfied = requirements[(0 until 3).random()]

composeTestRule.setContent {
PasswordRequirements(
satisfiedRequirements = listOf(satisfied)
)
}
PasswordRequirement.values().forEach {
val requirement =
InstrumentationRegistry.getInstrumentation()
.context.getString(it.label)
val result = if (it == satisfied) {
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_satisfied,
requirement)
} else {
InstrumentationRegistry.getInstrumentation()
.context.getString(
R.string.password_requirement_needed,
requirement)
}

composeTestRule

206

















.onNodeWithText(result)
.assertIsDisplayed()
}
}

207
Tes ing the E ror Di log
Wit in our co le tion of co po ables for the a the ti tion screen, we also have

the A the ti tio Er o Di log that is used to di play e rors to the user.

While this only fe tures two a g ments that are used to di play and di miss the er-

ror, these are key to the o e tion of the di log, so we’ll add some tests to a sert

that these o e ate as e pe ted. These tests will live i side of a new test class, Au-

the ti tio Er o Di l Test.

class AuthenticationErrorDialogTest {

@get:Rule
val composeTestRule = createComposeRule()

We’ll start here by wri ing a test that will be used to a sert that the provided e ror

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 a-

tio Er o Di log that will be co posed u ing the provided e ror re e ence.

With this co po tion in place, we can then use our text rule to a sert that there is a

node di played that has the e act text b ing provided via the e ror a g ment.

@Test
fun Error_Displayed() {
val error = "This is an error"
composeTestRule.setContent {
AuthenticationErrorDialog(
error = error,
dismissError = { }

208













































































fi



)
}
composeTestRule
.onNodeWithText(error)
.assertTextEquals(error)
}

When it comes to di mis ing the di log, the A the ti tio Er o Di log com-

po able takes a di mi E ror a g ment that is used to n t fy the pa ent com-

po able that di missal has been r que ted and our state needs to be u dated. If

this was broken for some rea on, then the user wouldn’t be able to di miss the dia-

log and be u able to pe form a the ti tion.

Si i ar to ot er tests that we’ve wri ten for these co po ables, we’re g ing to

use mockito to provide a mock i pl men tion of our di mi E ror, fo lowed

by ver f ing that the lambda has been i voked when e pe ted. After co po ing

this A the ti tio Er o Di log and provi ing the r quired a g ments, we

can tri ger the di missal by clic ing the node the has the e ror_a tion string re-

source a signed to it. When this is clicked, it is e pe ted that the di missal lambda

will be triggered. So here we’ll use the ver fy fun tion from mockito to ver fy that

the di mi E ror lambda has been triggered as e pe ted.

@Test
fun Dismiss_triggered_from_action() {
val dismissError: () -> Unit = mock()
composeTestRule.setContent {
AuthenticationErrorDialog(
error = "This is an error",
dismissError = dismissError
)
}
composeTestRule
.onNodeWithText(
InstrumentationRegistry.getInstrumentation()
.context.getString(R.string.error_action)
)
.performClick()

209























































































verify(dismissError).invoke()
}

210
With all of these tests in place, we’ve covered a lot of di fe ent cases that help to

e sure our UI is wor ing as e pe ted. We’ve not only tested that co po ables are

b ing co posed based on the i for tion that they are provided with, but also

that they triggered the e pe ted cal backs and tri ger state m ni l tions wit in

our co po ables. While the tests here aren’t e ten ive, we’ve been able to learn

not only what o tions are avai able to us while tes ing co po ables, but also the

a proaches that we can take when d ing so.

211
































You might also like