Thinking in Compose

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
LELAND RICHARDSON: Hi. My name is Leland Richardson, and I'm a software engineer on the Android Toolkit team. Today, I'm going to talk to you about Jetpack Compose. So, if you haven't heard about it, Jetpack Compose is our new next generation UI toolkit. It uses a declarative component-based paradigm for building your UIs easily and quickly. It's written entirely in Kotlin and embraces the style and ergonomics of the Kotlin language. It allows for interop with the existing view system, and it's built entirely in user space, unbundled from the underlying platform, which means you can take advantage of feature improvements and bug fixes on your own time. And I'm very excited to say that as of this morning, Compose is now officially in alpha, ready for you to try out yourself. So, Compose promotes a programming model that's quite a bit different from the existing model of building UI on Android. And while this has been met with a ton of excitement, it can also be a bit scary to know such a big shift is coming. So the goal of this talk is to help people get familiar with this new programming model. And to do that, we're going to build a chat application, JetChat, completely from scratch. The design we're using for this is actually from one of our official Compose sample apps we published this morning, which you can find here. So, our designer hands us this beautiful new mockup of this chat app. And the first thing I'm going to do is break down this mock into pieces I think of as modular components or chunks of the design. From a high level view, I think of the entire screen as one large component. And below that, the screen can be broken into the top app bar the bottom area where you compose a new message, and the center, where the center holds the entire conversation. Thinking a bit smaller, we might break these components into smaller pieces. So the conversation can be broken up in the messages, and each message might have a user photo component. And we can go further and further, even having different button types. In the case of Compose, all of these components are represented as composable functions that we refer to as Composables. So, for this talk, I'm going to take a somewhat bottom-up approach and start by building the chat message Composable. Here I'll talk about some useful ways of thinking about how to compose your app's UI out of Composable functions. And then I'll move on to creating the bottom area to compose new messages, where we'll get to understand a little bit about how Compose manages state. And then last, will look at tying all of these pieces together at the top level and talk a bit about app architecture. So let's start off talking about composition with this chat message Composable. The very first thing I'm going to do is just write an empty function annotated @Composable. We'll call it JetChat. With Android Studio, I can add a preview annotation to Composable functions in order to open up a live preview of just that Composable inside IDE. You can see that here on the right side. To start, I'll take some of the text from the design and pass them into this text Composable that Compose provides. By doing this, the strings get rendered to the preview on the right. Now, the execution of Composable functions ends up describing the actual hierarchy of the elements that get rendered on screen. So calling these three text Composables inside of JetChat results in a tree with three text nodes. This is kind of what we mean when we say that Compose is declarative. The body of the function can be thought of as a description of what the resulting UI looks like. But by default, there's no layoff policy here. So they're all stacked on top of one another. And we can add some structure by using the row and column Composables to define how these elements should layout with respect to one another. But everything here is still completely unstyled. So we can add various parameters to these Composables to style it like our mock. The mock also has the user's photo on the left side, and I decided to break this out into a separate Composable on its own, UserPhoto. Our API will give us user images in the form of a URL, so we create the initial API of this Composable as accepting the URL of an image. We then take that image URL and pass it into the CoilImage Composable, which is a Composable that uses the popular Kotlin image loading library Coil. And we've started by giving the image a size of 38 dp. The result is this 38-db square image on the right. But we wanted a round image. So to do this, we can use the clip modifier and pass in a circle shape. This tells Compose to clip the drawing along the circle shape. The design also had some spacing in between the image's edge and the border, so we can add some padding with the padding modifier and add a 2-dp border with a color and a shape. So you can see here that all of the styling that we just supplied was through this modifier parameter here. The modifier parameter is a parameter that all of UI-emitting Composables will have. You can think of modifiers as a chain of declarations or wrappings to apply to a given element. In this case, there's border, padding, clip, and size. And the order of these matter. So here, we applied the border and then the padding. And in the preview, you can see that the padding is inside the border. So for modifiers, the order that you read it in code is the same order that it ends up with on screen. In this example, we've started this modifier chain by using this capital-M Modifier object, which represents an empty modifier that we started this chain with. If we want to allow the consumers of the user photo Composable to add additional modifiers to decorate it or position it, we can allow that by adding a modifier parameter to this function with the default value of the empty modifier. You'll find that a lot of the Composables provided by Compose follow this convention. And if you want to make your Composables highly reusable, this is a good convention to follow. So now, we have a pretty reasonable user photo Composable. But our design actually had a different border color for each image. And so to handle this, we're going to pull out the border color into a local variable, ring color, and set it to a random color. In this case, we can imagine random color might be implemented something like this. So, this kind of works. We ended up getting a random color for every user photo, but there's a problem. Composable functions might be called again in order to update the state of the UI. Composable functions are just like normal functions, but with a few important differences. And this is one of them. The Compose runtime might re-execute the Composable function after they've been called the first time in order to update the current state of the UI. We call this process recomposition. So, if we go back to the user photo implementation, we see that this could be a problem. Every time the user photo Composable re-executed, a new random color will get generated. So, there is another aspect of the Composable function that can help us here-- that Composable functions have a memory. And what I mean by that is that a Composable function has access to what happened the last time it was called. And there's this primitive function, remember, which helps you tap into this. And so, going back to our user photo Composable, we're going to do the same thing that we did before. We call randomColor, except we're going to wrap it with a call to remember. And so in this case, nothing has changed here in terms of the type system. We're still initializing the ring color variable as a color, but by wrapping it in remember, we're saying to remember the first result of this function. And then instead of re-executing every time, just return us the remembered value. So, we can think of the remembered value as being part of the tree we're generating. A color is stored for every user photo Composable while on the tree. So in a sense, it's part of the tree, even though it's not part of the rendered UI. OK. So, stepping back to our chat message example, we're looking pretty good. Let's take a look at our design to see how we compare. Here, we realized that there's something we forgot. The design has messages with clickable links and code blocks, but we're only handling basic texts. So going back to our Composable, we can take the text Composable that we were using before and refactor this into our own custom text component, ChatText. So, starting out ChatText does nothing but call texts like it did before. But Compose actually provides two overloads of the text Composable, one that accepts a standard string and another that accepts an annotated string. This is a compose to find data type for handling more complex text functionality. So, we can build a function that takes in a string with some markup in it-- whether it's HTML or Markdown or something custom-- and we can build an annotated string from that using the annotated string DSL that Compose provides. So now, in our ChatText function, instead of passing in a string to text, we can convert the string into an annotated string and pass that into text. We see now that the links and code blocks are showing up on the right. But we have a problem again. The parseAnnotated call might be pretty expensive. So we want to make sure that it doesn't run more than it needs to. But ChatText is a Composable function and can get re-composed. And so again, we can use the remember function. In this case, we're going to use the first parameter remember. The remember function will remember the result of a calculation, as long as none of the provided input parameters have changed from the values passed in during the previous composition. So in a way, you can kind of think of these parameters as the keys to a local cache. Going back to our ChatText example, we can wrap the parseAnnotate call with remember and pass in text as a single input parameter. This means that we only pay the cost of parsing the string if the value of the text has changed, which is exactly what we wanted. Going back to our higher level chat message Composable, our public API looks something like this. At the moment, this is all of the data we need to show a message like this one on the right. However, looking at our design, we see that there is some more complicated variations of this component. For instance, there can sometimes be multiple messages, each in their own bubble. So instead of a string like text parameter, we might accept a list of strings. But then we see that the content of a message isn't always a string. Sometimes we have these stickers that a user can sent. So we then change it to be a list of either text or stickers. And then we can also have images, so we add a class for that. And then we have a feature where you can like an image, so we add the likeCounts. And we need to handle the user liking and unliking a message, so we add an onLikeClick callback. And we might want to handle the user clicking on the image itself, and also long clicking, and clicking on the user's photo. And so we might also want to add read receipts. So add a callback for when this message is on screen, and then we want to handle a way for a user to edit messages and share a message. And so now, we see that this relatively straightforward message component has actually become pretty complex, and it's going to have a lot of logic in its implementation. So at this point, we might say this Composable is trying to do too much. I've seen some people new to Compose a little bit stuck at this stage and not really know how to structure the code to be any more maintainable. And there's a very important concept here, and that's that Composable functions can be composed together. This can mean a few different things, but let's take a look at what we can do in this example. The chat message Composable, as it's defined now, needs to handle all of the different variations of a message. And as a first step, if we look at all the parameters that have to do with the content of the message, we can see that this is where a lot of the complexity comes from. And we can actually replace all of these parameters with a single Composable content lambda. This is really important. So, this allows us to make the ChatMessage Composable simpler and also more flexible at the same time. With this parameter, the consumer of ChatMessage can essentially put arbitrary content inside the body of a chat message. Similarly, we can make a ChatBubble component that just produces the styling of the chat bubbles without having any knowledge of what kind of things might go inside it. This allows us to organize our hierarchy in a more understandable tree of higher level components. And if you find yourself frustrated at the number of parameters you're needing to thread through a given Composable, remember that you can use a Composable lambda like this to let the consumers of your Composable pass an arbitrary content. OK. So, now we're going to move on to implementing the send message bar and talk a little bit about state in Compose. To implement this, we're going to need some sort of a text input. Compose provides a full implementation of the materials design components, so the first thing we might do is reach for one of Materials TextFields. Both the filled text field and the outline text field have quite different designs from what we actually need for this chat app. So, your first thought might be to try using one of these, but to tweak it until it looks like what we want. But I'm going to urge you to try not to do that. In addition to the material design components, Compose provides a lot of useful core primitives for building up user interfaces. All of the material design components are built on top of these primitives. And so in this case, Compose provides a raw text field Composable that we can use to get what we want. The TextField Composable has two required parameters-- value, which is a string, and onValueChange, which is a lambda that accepts a string. So to give it a try, we're going to pass a string literal hello for the value of the text field. So this renders what we want, but it doesn't really behave like we'd expect. When we go and type into this text field, the value of the text field won't actually change. And that's because the value isn't an initial value. It's actually the value of the TextField Composable. So, really what we're saying here is that we want to bind the value of the text field to a string constant. And another way to think about this is that in a declarative UI system, the code itself describes the UI. But we need to describe what the UI looks like at any point in time, not just the initial time. So, to have the text field represent a dynamic value we need to introduce a bit of state. And to do so, we would do something like this. Var text by remember mutableStateOf. This line has introduced a few new concepts at once, so let's try and break it down. mutableStateOf is a function that takes in a value of type T and returns a mutable state instance of that type, initialized to that value. MutableState to type with a writable value property of type T. And the key thing here is that when the value property is written to, it will schedule a recomposition of any Composable function that's subscribed to it. And a Composable function get subscribed to state instance any time the value property is read during its execution. And so, going back to our example, we can see that remember mutableStateOf will return a mutable state of string. But we see this "by" keyword here to the left if it. And this might not be familiar to a lot of you. This "by" keyword is part of the Kotlin language feature called property delegates. When you have a local variable in a function, there are really only two things you can do with it. You can get the value, or if it's a var like text is here, you can assign it or set the value. With property delegates, Kotlin allows you to give different meaning to the getting and setting of a local variable by defining these get value and set value operator functions. So now, if we want to de-sugar this example, we could take away the "by" keyword. And when we do this, the type of the text variable goes from string to mutable state of string. So, anywhere we get the value of text before, we now need to replace that with a call to get value. And similarly, we would replace all assignments of the text variable with set value calls. And with state, the implementation of get value and set value calls are just the getter and setter of the value property. So this is equivalent to just using the value property. And so, we see that using state with the "by" keyword is equivalent to just treating the state object's value as the value itself. So now, going back to our example, we have a working TextField, which will respond to the keyboard input and update the value of the text based on it. It's still completely unstyled though, and it's missing a few features. So here we've added some style to the text field, and also added this clickable expand icon on the right side of the text field. Additionally the design-- we have wanted this text field have some placeholder text when the value is empty. But the lower level TextField API doesn't have any notion of a placeholder. So we ended up implementing one ourselves without too much effort. We do this by actually conditionally calling a text Composable only if the value of text is empty. So, this is a good demonstration of how you can build dynamic UIs by writing natural logic that depends on state. OK. So, now we're going to move on and talk about how we can tie in some of the things that we've built together into something resembling an actual application. And along the way, we'll talk about some architecture considerations we might have when doing so. So, here's a high-level hierarchy of the components that make up our chat screen. What we want to do is find the minimal representation of UI state that we can implement our screen with. First up is our text field. The user needs to be able to type up a new message, so we hold that in the text state. Next, we have the input mode. The user can have different modes of creating message, like text, emoji, or an image. And the current mode is reflected in the UI below the text field. Now, we need to think about where the state needs to live. To do this, we first need to think about where it's actually going to be used. The current text value of the text field obviously needs to be used by the text field, but we also want to enable or disable the Send button based on whether or not the value is empty or not. Additionally, when the user clicks the Send button, we'll want to reset the text value and input mode. So as a result, we need to move or lift this state up the tree a little bit. As a rule of thumb, we want to put state in the lowest part of the tree where it's still accessible to all of the things that need it, but no lower than that. And this is related to the fact that we want to have a single source of truth for all states. We never want to have to synchronize two states that are meant to represent the same thing. So the result is that we end up with both the text and mode state in the SendMessageBar Composable. And this allows for this state-- or some projection of it-- to be passed to the things below it. So our SendMessageBar Composable might end up looking something like this. We've got our text state, which we use in the ChatTextField Composable. And then the Send button is enabled based on whether or not the text is empty or not. And it gets cleared whenever the Send button is clicked. We also have the mode state, which gets passed into the input options Composable and gets reset if the user types into the text field. And then, we're also passing in this ChatViewModel into the Composable. And whenever the user clicks on the Send button, we end up calling the send method. And so, this is a good time to talk about separation of concerns. At this point, you can see that some Composable functions can end up with a lot of logic in them. And it's reasonable to ask whether or not this is adhering to the spirit of separation of concerns. Separation of concerns is sometimes defined instead in terms of coupling and cohesion. And I find this a little bit easier to understand and identify. So, abstractly, when we write code, we think of our application in terms of modules. And we might think of our module as a set of units. And we can think of dependencies between these modules as coupling. One way to think of it is if I make a change somewhere, how many other changes am I going to have to make as a result? And in general, we want to reduce or minimize the amount of coupling that we have in our application. Sometimes coupling can actually be implicit. There's some aspect of a dependency somewhere that we're relying on, but indirectly. And this means that when we change something, something somewhere else might break as a result. And sometimes this can be even worse if we don't have the tooling to identify when this would be the case. On the other hand, we have cohesion. Cohesion is how units inside of a given module belong to one, or how they're related to one another. Cohesion is generally seen as a good thing, so we want to maximize cohesion. Separation of concerns can be thought of as grouping as much related code together as possible and drawing the lines of separation so that coupling is minimized. So, framing this in terms of something more familiar to you, let's imagine we have a layout XML file and a corresponding activity file. In this case, the activity is inflating the layout, handling some user interaction, and things like that. And so it turns out there's a fair amount of coupling between the two. The activity needs to know some things about the layout. This can manifest in a lot of ways, but one prime example of this is something like Find view by id. And then sometimes it's quite a bit more subtle than that. And we might be relying on some of the exact structure of the layout implicitly. And as your application grows, these dependencies grow, as well. And following best practices, we'd want to pull as much UI logic out of the activity as possible. And we might instead handle this type of data binding inside of a view model instead. This doesn't really get rid of any of this coupling. It just shifts it over to the view model. At the end of the day, the responsibilities of a view model are inherently related to the layout, and so it must have some knowledge of it. In other words, they're tightly coupled. One of the fundamental issues here is that UI-related code is written in one language, and the layouts themselves are defined in another language. And this creates a forced line of separation. And because they're forced to be in different languages, this leads to implicit coupling. So, the general idea here is if we decided to define the structure of our UI in the same language-- Kotlin-- then these dependencies at least can become much more explicit. So if we do this, we now have the full power of Kotlin to draw lines of separation where they might make the most sense to us. This means that some of the highly-coupled logic can be refactored and moved, and the result is our code can has less coupling and more cohesion. So, let's take a look at how this might work in real code. Here is a bind function. You might see something like this in typical Android code today. Here, we're subscribing to a LiveData of messages, and we have a call to observe. So, for observe, you need to pass in a lifecycle owner and a lambda that runs wherever the data changes. This lambda is where you'd end up executing some code to update the state of your UI. The situation in Compose is similar, but in a lot of ways simpler. Here, we're leveraging this observeAsState Composable call. And this is an extension method on LiveData. This method essentially converts the live data of some type into a state object of the same type. So, this function is for live data, but Compose provides equivalent functions for libraries like Flow and Rx, and rolling your own for your own data solution would be easy to do. Going back to the example, we see that we're leveraging property delegates again with the "by" keyword. And the type of messages here is list of messages. This line is equivalent to LiveData's observe method. But because Composable functions have a lifecycle and the ability to re-compose, the Composable function itself can act as both the lifecycle owner and the function to execute whenever the data changes. So we don't need to specify either of them. This means we're free to just use the messages value naturally inside of the chat screen Composable. In this case, the live data is passed in to the Composable as a parameter. Another way to do this would be to have the view model that holds the messages data itself and pass that in. Even better, since the Composable functions imply a scope, you can get the view model instance using the viewModel function provided by Compose, instead of passing it in as a parameter. This leverages a viewModel provider under the hood and will provide you a properly scoped viewModel for these higher-level Composables without needing to pass it in as a parameter. So now, we have a viewModel and our data in our top-level screen Composable. Let's try and actually build the screen from this data. Since messages is just a list of messages, we can start with something super simple and just compose chat messages in a loop. This works, but for really long conversation, it might be too expensive to compose all of the messages when only a few of them need to be visible. So today, we would likely use a recycler view for this. In Compose, we actually have a similar concept, LazyColumn. Just like it was with a call for each, this allows us to define a Composable lambda for the content of each item. It really is just this simple. But our chat screen is more than just a list of messages. We have a top bar and a bottom bar and more around the Chrome of the conversation. Compose also provides a Scaffold Composable, which is a useful way to set up the common structure of a top-level screen. Here, I just pass in a Composable lambda to define the content for my top app bar, my bottom app bar, and my content. And that's it. It's really exciting for me to see UI development Android working like this. So much boilerplate that just vanishes. So, when building apps with Compose, I want to encourage people to view Composable functions themselves not as part of the Compose toolkit, but as an addition to the Kotlin language. Composable functions are like normal Kotlin functions, but with some additional capabilities that really transform how you build abstractions on top of them. Composable functions can be re-invoked and have a corresponding lifecycle. This also means that they have a memory. They represent something that has been called before and can be called again. And you have the tools to peek into that history. So in this talk, we went into depth about how a chat app like JetChat might come together. JetChat is an official Compose sample app that was released this morning. And there are four more sample apps for you to look through. Looking through samples can be a great way to understand this new programming model better, so you should definitely check them out. In addition to the samples, there are several new code labs for you to check out, and the API reference docs are always a good way to learn more. Also, we encourage you to give us feedback. You can follow this feedback link to file bugs or feature requests in our official tracker. And you can also join the Compose channel on the Kotlin Links Slack community and provide feedback there. I really can't emphasize enough how excited I am about Compose. I'm incredibly lucky to be a part of this project, and now that it's an alpha, I can't wait to see how y'all use it. I really believe it has the potential to revolutionize UI development on Android. Thanks for watching.
Info
Channel: Android Developers
Views: 55,306
Rating: 4.9159455 out of 5
Keywords: Thinking in compose, thinking in jetpack compose, Jetpack compose, intro to jetpack compose, jetpack compose tutorial, kotlin, 11 weeks of android, ui, android ui, android user interface, user interface, android 11, android 11 beta launch, android news, android updates, android latest, android featured, google developers, android developers, android developers news, android developers updates, android developers latest, android, google, Leland Richardson
Id: SMOhl9RK0BA
Channel Id: undefined
Length: 25min 27sec (1527 seconds)
Published: Wed Aug 26 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.