[MUSIC PLAYING]
FLORINA MUNTENESCU:
Hello, everyone.
My name is Florina
Muntenescu, and I'm
a developer advocate at Google.
So in the last few months,
together with three
of my colleagues, we've been
working on bringing Plaid back
in fashion.
So more precisely,
Plaid is an application
that was initially developed
by my colleague, Nick Butcher,
as a way of showcasing
material design.
And what you see
here is actually
the state of the app in 2016
when it was pretty much,
let's say, Plaid's glory days.
So Nick used here
a lot of the APIs
from the animations,
transitions, any method vector
drawables.
So all of these really
made the app shine.
They really improved
the user experience.
So Plaid [INAUDIBLE] data
from three different sources,
and well, these sources
from 2016 until now,
well, some of them
were deprecated.
So that meant that,
out of three sources,
we were left with
one and a little bit.
So from all of these nice,
fancy UI features that we had,
well, we were left
with almost nothing.
We were left with a
pretty boring application.
So we decided we didn't
want to leave it like this,
and we decided we wanted to
fix this broken functionality.
But, apart from
this, we also knew
that we wanted to
go towards something
that's modular and extensible
from the architecture
point of view.
But the thing is that Plaid was
developed as a UI sample, not
as an architecture sample.
So you won't be surprised
to see all the tie
dependencies in
the code and, well,
code that was actually a
bit behind, because Nick
started building this in 2014.
At that time, we didn't have
the guide to app architecture
that we released last year.
And also, although
we had Kotlin,
we didn't really use it.
So we knew that we
wanted to rebuild Plaid,
but we wanted to rebuild
it in the right way,
to have it in a good state
for any future changes
that we wanted to build.
So in this talk, I want to
tell you what we've learned
and also how we managed to
leverage Kotlin in our app.
So we released this opinionated
guide to the app architecture.
But if you read it,
you'll see that it's also
still quite vague.
So we have these main
classes, some idea of how
we should architect our app.
But we decided we want to
create some clear guidelines.
So we defined some main
types of classes in our app,
and we also defined
a set of rules
for each of these classes.
So let's see which
ones these were.
So we defined a
RemoteDataSource, whose role is
actually to just construct
the request data,
fetch the data from the
API service, and that's it.
It would only request
information and return
the response received.
Next, we would have a
LocalDataSource, whose role
is just to store data on disk.
So it would either do
this in shared preferences
or in the database.
Next, it would have the
repository, whose role
is to fetch and store data.
And, optionally, it could
also do in-memory cache.
So the repository
will be the class
that mediates between the local
and the remote data source.
Because the business
logic was quite complex,
we decided to add another layer.
We decided to add use cases.
So the role of the use cases
is just to process data
based on business logic.
This will be small,
lightweight classes
that could be also reused.
So the use cases would depend
on repositories and/or other use
cases.
Next, we would
have a view model.
So the ViewModel's
role is to expose
data to be displayed by the
UI and also to trigger actions
based on the user's actions.
And the ViewModel would
depend on use cases.
As an input, the ViewModel
would get maybe IDs.
So it would get IDs
in the case where
it's a ViewModel for a
details screen, for example.
And, of course, it would get
the user's actions as an input.
And as an output, it
would return a live data.
Next, in the UI, we would
work with activities and XML.
So the role of these is
to just display the data
and to forward actions
to the ViewModel.
As an input, they would
get the optional ID
and the user's actions.
So we looked at our application,
or at our architecture,
and we divided it
into three layers.
Data, domain, and the UI layer.
We decided to go one step
further and be a bit more
opinionated in the way
we're using [? data ?]
or in the libraries
that we're using.
So we knew that
the LiveData really
shines when it's used together
with an activity or a fragment.
So we decided to really
keep the LiveData only
between the ViewModel and
the UI, and that's it.
And even more, because
of the nice integration
between LiveData
and DataBinding,
we decided to also use
DataBinding in our XMLs.
But, again, still with
all of these constraints
and all of these
guidelines that we've set,
there are so many ways in
which we can actually implement
this kind of architecture.
And we knew that Kotlin and
the Kotlin language features
will help us improve
this even more.
And more precisely, what
we'd particularly like
are the functional constructs
that Kotlin supports.
So actually, one of the first
decisions that we had to make
was how do we handle
asynchronous operations?
And we decided to
work with coroutines
as the, pretty much,
backbone of our app
because with coroutines,
it's easy to just launch
a coroutines and
handle the response.
And more precisely,
what we liked
is the fact that
coroutines have a scope.
So, for example, let's say that
you're opening the activity.
You're triggering
a network request.
You want to make
sure that when you're
pressing back and
exiting that activity,
you're also canceling
that network request.
So this scoping
of the coroutines
was something that we liked.
So
This meant that we decided
that in the ViewModel
would be the place
where we're launching
and we're canceling
coroutines, and we're also
making that transition between
coroutines and LiveData.
But then, for all the other
layers above the ViewModel,
we would just use
suspension functions.
But these suspension functions
would return a result class.
So, more precisely, this
result will have two types--
success or error.
And this is because
we wanted to make sure
that we're not throwing
exceptions here and there,
but rather, that these
exceptions, those errors,
represent a state.
So what's interesting
in Kotlin is
that if you want to be
able to extend the class,
you have to mark it as open.
So this means that classes
are final by default,
and you have to be intentional
when using inheritance.
So this means that Kotlin really
supports this idea, this best
practices of favoring
composition versus inheritance.
But we can do better
than using open classes.
We can use a sealed class,
because with a sealed class,
we can restrict the
class [INAUDIBLE] case.
It means that we can't extend
the class outside this file.
So a lot of times when we
would use this result class,
we will typically
use it inside a when.
So, first of all, when
support smart casts.
So this meant that it was easy
to do when result is success,
do something.
When result is error,
do something else.
But because every time
we were using it we
wanted to make sure that we're
always handling every case,
we wanted to make sure that
if, I know, by mistake,
we're not handling something,
we wanted the compiler
to tell us that, hey,
you forgot something.
You forgot to handle
the error case.
So this meant that the when
needs to be exhaustive.
But when is exhaustive only
when used as an expression.
So we created the
exhaustive property.
So, more precisely, we created
an extension property on T
where we're just
returning the object.
Here's another
problem that we had.
So we had a comment class,
and a comment with replies.
So the difference between
these two is in the fact
that the comment also holds the
information about the user that
posted the comment.
So it will have the display
name and the portrait URL,
whereas the comment with replies
is pretty much a tree structure
that holds the
replies of the comment
and the replies of
the replies and so on.
But what we had to do was
to build a comment out
of the comment with
replies and a user object.
So you say, OK, that's easy.
We will just create a
new constructor that
gets us parameters, the user
and the comment for replies,
and that's it.
But the thing is
that we didn't really
like this because the classes
were in two different layers.
And why should the comments know
about the comment with replies?
Why should it know
necessarily about the user?
Maybe the data comes
from somewhere else.
Why should we need
to change this API?
So what we ended up using
is an extension function.
So, more precisely, we
built an extension function
to the comment with replies
that, based on the user object,
it would create a comment.
So this, when you're building
an extension function,
you only have access
to the public fields.
So this means that
we're not, by mistake,
accessing or changing any
private implementation data.
And it allows us to keep
our classes focused,
focused on what they do
without extending them.
So it meant that we didn't
have to change the public API,
and we would avoid accessing
private implementation details.
So what I like about
data classes is the fact
that there are value
objects, and this actually
shines when used in tests.
So, for example, we had an
upvote flag in the comments.
So when we built a test to check
whether a comment is upvoted,
it would create our comment
with the upvoted flag to false,
whatever the
comment, and then we
would check whether
the expected result is
similar to the comment,
the initial comment,
but with that
upvoted flag to true.
But the thing is
that, especially
in our case, because the
comment has so many fields,
it was easy to make mistakes.
And it was easy to
miss what's actually
important here, the fact that
the upvoted flag has changed.
With Kotlin, you can
use the copy method,
and there we would just create
a copy of the object that
is called on.
And we're setting the flag,
the flag that we're actually
changing, and that's it.
The code ends up being more
concise and more readable,
more comprehensible.
So let's take another example.
In our app, we were working
with a remote data source
to post a comment.
And here, we would expose
a suspension function that
would return a result.
And inside this method,
we would create a
new comment request.
We'd trigger that
request at the back end.
We would wait for the response.
And then we would handle
the response, building
either a result success
or result of error,
depending on what's needed.
But, if you look at this code,
this is actually not enough,
because in the case when
your device is offline,
this code will crash.
So what we actually had to
do is to wrap every request
inside a try-catch.
And we have a lot of requests.
So we saw that we keep on
adding and adding this try-catch
everywhere, and then
our methods were loaded.
So we couldn't
really focus on what
really mattered on
building the response,
triggering the request.
So what we did is create
a top-level function.
So this would be a
suspension function
that would get us a parameter of
suspending lambda and the error
message.
So here it would just
trigger the call,
wrap it inside a try-catch,
and then we would build,
in case of an
exception, a result
of error based on the error
message with [INAUDIBLE]..
So that this means that
in our mode data source,
we could just create
a safe API call,
and then put the call that
actually matters for us
inside another function.
Like this the code became
more readable, more easy
to understand.
So this safe API
call, I was saying
that it has the call
as the first parameter
and then the error
message as the second one.
But in Kotlin, if the
last parameter of a method
is a lambda, it
means that you can
use this as a trailing lambda.
So that meant that when
you're calling this,
instead of passing
these two parameters,
we can just pass
the error message
as a parameter of
the function and then
use the trailing lambda
syntax to call the method.
So like this, the code
becomes more concise,
but is it really more readable?
So when we looked
at this, it felt
what matters here the most
is the error message, which
is not really the case.
What matters for us a lot
is that the method that gets
called is this post comment.
So we decided that, although
the code is more concise,
it doesn't mean more readable.
So brevity isn't
necessarily a good thing.
So even if Kotlin offers
all of these kind of options
and features, be mindful and
think whether you actually
need all of these, or use
them in the right places.
Here's another example.
So as soon as we were
switching to Kotlin, especially
in our activities, the
first thing that we did
is make all of our
views [INAUDIBLE]
because we didn't want to
handle all of this nullability.
But then we looked
again at our code,
and we saw that we
shouldn't do this,
that some views, for
example, our no result views,
were only inflated when
specific conditions are met.
So, actually,
nullability was good.
Nullability can be meaningful.
Nullability was telling us
that something is missing,
and we should really handle it.
So, overall, we saw how all
of these features from Kotlin,
like coroutines, immutability,
and functions as first class
citizens, can help
us shape our app.
And together, with the
guide to app architecture,
helped us build this
maintainable, this safer
and faster-to-develop
architecture
that we wanted to have.
Thank you.
[APPLAUSE]
[MUSIC PLAYING]
