[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]