Jetpack
Compose
uma nova forma de implementar UI no Android
Nelson Glauber
@nglauber
Jetpack
Compose
uma nova forma de implementar UI no Android
Nelson Glauber
@nglauber
Jetpack Compose é um moderno conjunto
de ferramentas para construir interfaces
gráficas para aplicações Android de forma
declarativa utilizando Kotlin.
app.sli.do/event/djw2yt2e
app.sli.do/event/djw2yt2e
UI Imperativa x UI Declarativa
• UI Imperativa é onde uma entidade de UI (com uma classe View, por
exemplo) é totalmente construída e posteriormente é modificada
usando métodos e/ou propriedades públicas.
• Na UI Declarativa, o desenvolvedor descreve o estado da UI e deixa
o transicionamento desse estado por conta de um framework.
app.sli.do/event/djw2yt2e
Por que?
• Todas as APIs do Android evoluíram, menos o UI toolkit.

Room, View Model, Paging, Work Manager, Camera, Navigation, …
• Não é simples criar uma custom view… Componente.kt,
componente.xml, attrs.xml, styles.xml…
app.sli.do/event/djw2yt2e
Jetpack Compose
• Segue uma abordagem declarativa similar ao SwiftUI, ReactNative, e
Flutter.
• É uma biblioteca separada do S.O.
• É uma nova forma se se pensar na elaboração da UI: componentes ao
invés de telas; Composição no lugar de Herança.
• Compatível com aplicações Android existentes, podendo ser adotado
progressivamente.
• EXPERIMENTAL! Ainda está em versão alpha desde… ontem 😅
app.sli.do/event/djw2yt2e
Benefícios
• No Compose, a UI é totalmente escrita em Kotlin e nós sabemos
organizar e refatorar código melhor que arquivos XML :)
• Qual o maior trabalho no front-end? 

Deixar UI e dados sincronizados e consistentes (Data Binding, Live
Data, …).
• O UI toolkit atual não está nem aí pros teus problemas! 

Ele tem o próprio estado e só avisa que o seu estado interno mudou.
Android Studio 4.2
(Preview)
Compose Preview
Interactive Mode
Launch
Composable
Inline
Documentation
Embedded
Emulator
@Preview
app.sli.do/event/djw2yt2e
@Composable
• É maneira de criar um “custom component”. 

Simplesmente criando uma função!
• @Composable não usa processamento de anotação. É uma
instrução para o plugin do compilador do Kotlin.
• Só pode ser chamada dentro de um contexto de uma @Composable
function.
app.sli.do/event/djw2yt2e
setContent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
app.sli.do/event/djw2yt2e
setContent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
app.sli.do/event/djw2yt2e
setContent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
app.sli.do/event/djw2yt2e
setContent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
app.sli.do/event/djw2yt2e
setContent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(stringResource(R.string.hello, name))
}
app.sli.do/event/djw2yt2e
Material Theme
• Define o tema da aplicação por meio de cores, estilos, fontes e
decoração de componentes.
• Normalmente é o elemento raiz da tela.
setContent {
AppTheme {
Greeting("Android")
}
}
app.sli.do/event/djw2yt2e
Material Theme
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes,
content = content
)
}
private val DarkColorPalette =
darkColors(
primary = purple200,
primaryVariant = purple700,
secondary = teal200
)
private val LightColorPalette =
lightColors(
primary = purple500,
primaryVariant = purple700,
secondary = teal200
)
app.sli.do/event/djw2yt2e
TextStyle
Text(
text = "Hello!",
style = MaterialTheme.typography.h4
)
app.sli.do/event/djw2yt2e
Modifiers
• Decoram um elemento
• Provê parâmetros de layout
• Comportamentos adicionais
• São encadeados e a ordem importa!
val shape = CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp)
Text(
text = "Text 1",
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.border(2.dp, MaterialTheme.colors.secondary, shape)
.padding(1.dp)
.background(MaterialTheme.colors.primary, shape)
.clickable(onClick = {
// Click event
})
.padding(16.dp)
)
val shape = CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp)
Text(
text = "Text 1",
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.border(2.dp, MaterialTheme.colors.secondary, shape)
.padding(1.dp)
.background(MaterialTheme.colors.primary, shape)
.clickable(onClick = {
// Click event
})
.padding(16.dp)
)
val shape = CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp)
Text(
text = "Text 1",
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.border(2.dp, MaterialTheme.colors.secondary, shape)
.padding(1.dp)
.background(MaterialTheme.colors.primary, shape)
.clickable(onClick = {
// Click event
})
.padding(16.dp)
)
val shape = CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp)
Text(
text = "Text 1",
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.border(2.dp, MaterialTheme.colors.secondary, shape)
.padding(1.dp)
.background(MaterialTheme.colors.primary, shape)
.clickable(onClick = {
// Click event
})
.padding(16.dp)
)
val shape = RoundedCornerShape(8.dp)
Text(
text = "Text 1",
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.border(2.dp, MaterialTheme.colors.secondary, shape)
.padding(1.dp)
.background(MaterialTheme.colors.primary, shape)
.clickable(onClick = {
// Click event
})
.padding(16.dp)
)
val shape = CircleShape
Text(
text = "Text 1",
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.border(2.dp, MaterialTheme.colors.secondary, shape)
.padding(1.dp)
.background(MaterialTheme.colors.primary, shape)
.clickable(onClick = {
// Click event
})
.padding(16.dp)
)
app.sli.do/event/djw2yt2e
Layouts
• Column
• Row
• Stack
• ConstraintLayout
Stack(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text("Column Text 1")
Text("Column Text 2")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text(text = "Row Text 1")
Text(text = "Row Text 2")
}
}
Text(
"Stack Text",
modifier = Modifier
.gravity(Alignment.TopEnd)
.padding(end = 16.dp, top = 16.dp)
)
}
Stack(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text("Column Text 1")
Text("Column Text 2")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text(text = "Row Text 1")
Text(text = "Row Text 2")
}
}
Text(
"Stack Text",
modifier = Modifier
.gravity(Alignment.TopEnd)
.padding(end = 16.dp, top = 16.dp)
)
}
Stack(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text("Column Text 1")
Text("Column Text 2")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text(text = "Row Text 1")
Text(text = "Row Text 2")
}
}
Text(
"Stack Text",
modifier = Modifier
.gravity(Alignment.TopEnd)
.padding(end = 16.dp, top = 16.dp)
)
}
Stack(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text("Column Text 1")
Text("Column Text 2")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text(text = "Row Text 1")
Text(text = "Row Text 2")
}
}
Text(
"Stack Text",
modifier = Modifier
.gravity(Alignment.TopEnd)
.padding(end = 16.dp, top = 16.dp)
)
}
app.sli.do/event/djw2yt2e
ConstraintLayout
ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)) {
val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs()
Text("Nome", modifier = Modifier.constrainAs(text1Ref) {
top.linkTo(parent.top)
centerHorizontallyTo(parent)
})
TextField(modifier = Modifier.padding(top = 8.dp)
.constrainAs(edit1Ref) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(text1Ref.bottom)
})
Button(onClick = {}, modifier = Modifier.padding(top = 8.dp)
.constrainAs(btn1Ref) {
end.linkTo(edit1Ref.end)
top.linkTo(edit1Ref.bottom)
}
)
TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp)
.constrainAs(btn2Ref) {
end.linkTo(btn1Ref.start)
baseline.linkTo(btn1Ref.baseline)
}
)
}
ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)) {
val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs()
Text("Nome", modifier = Modifier.constrainAs(text1Ref) {
top.linkTo(parent.top)
centerHorizontallyTo(parent)
})
TextField(modifier = Modifier.padding(top = 8.dp)
.constrainAs(edit1Ref) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(text1Ref.bottom)
})
Button(onClick = {}, modifier = Modifier.padding(top = 8.dp)
.constrainAs(btn1Ref) {
end.linkTo(edit1Ref.end)
top.linkTo(edit1Ref.bottom)
}
)
TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp)
.constrainAs(btn2Ref) {
end.linkTo(btn1Ref.start)
baseline.linkTo(btn1Ref.baseline)
}
)
}
ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)) {
val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs()
Text("Nome", modifier = Modifier.constrainAs(text1Ref) {
top.linkTo(parent.top)
centerHorizontallyTo(parent)
})
TextField(modifier = Modifier.padding(top = 8.dp)
.constrainAs(edit1Ref) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(text1Ref.bottom)
})
Button(onClick = {}, modifier = Modifier.padding(top = 8.dp)
.constrainAs(btn1Ref) {
end.linkTo(edit1Ref.end)
top.linkTo(edit1Ref.bottom)
}
)
TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp)
.constrainAs(btn2Ref) {
end.linkTo(btn1Ref.start)
baseline.linkTo(btn1Ref.baseline)
}
)
}
ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)) {
val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs()
Text("Nome", modifier = Modifier.constrainAs(text1Ref) {
top.linkTo(parent.top)
centerHorizontallyTo(parent)
})
TextField(modifier = Modifier.padding(top = 8.dp)
.constrainAs(edit1Ref) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(text1Ref.bottom)
})
Button(onClick = {}, modifier = Modifier.padding(top = 8.dp)
.constrainAs(btn1Ref) {
end.linkTo(edit1Ref.end)
top.linkTo(edit1Ref.bottom)
}
)
TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp)
.constrainAs(btn2Ref) {
end.linkTo(btn1Ref.start)
baseline.linkTo(btn1Ref.baseline)
}
)
}
ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)) {
val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs()
Text("Nome", modifier = Modifier.constrainAs(text1Ref) {
top.linkTo(parent.top)
centerHorizontallyTo(parent)
})
TextField(modifier = Modifier.padding(top = 8.dp)
.constrainAs(edit1Ref) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(text1Ref.bottom)
})
Button(onClick = {}, modifier = Modifier.padding(top = 8.dp)
.constrainAs(btn1Ref) {
end.linkTo(edit1Ref.end)
top.linkTo(edit1Ref.bottom)
}
)
TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp)
.constrainAs(btn2Ref) {
end.linkTo(btn1Ref.start)
baseline.linkTo(btn1Ref.baseline)
}
)
}
ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)) {
val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs()
Text("Nome", modifier = Modifier.constrainAs(text1Ref) {
top.linkTo(parent.top)
centerHorizontallyTo(parent)
})
TextField(modifier = Modifier.padding(top = 8.dp)
.constrainAs(edit1Ref) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(text1Ref.bottom)
})
Button(onClick = {}, modifier = Modifier.padding(top = 8.dp)
.constrainAs(btn1Ref) {
end.linkTo(edit1Ref.end)
top.linkTo(edit1Ref.bottom)
}
)
TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp)
.constrainAs(btn2Ref) {
end.linkTo(btn1Ref.start)
baseline.linkTo(btn1Ref.baseline)
}
)
}
app.sli.do/event/djw2yt2e
Button
Button(
content = { Text("Button") },
onClick = {}
)
OutlinedButton(
content = { Text("OutlinedButton") },
onClick = {}
)
TextButton(
content = { Text("TextButton") },
onClick = {}
)
app.sli.do/event/djw2yt2e
Card + Ripple + Clickable
@Composable
fun UserItem(user: User) {
Card(
elevation = 4.dp,
shape = RoundedCornerShape(4.dp),
modifier = Modifier.fillMaxWidth().padding(8.dp)
.clickable(
onClick = {
// Click event
}
)
) {
// Card Content
}
}
app.sli.do/event/djw2yt2e
Image
Image(
asset = imageResource(R.drawable.recife),
contentScale = ContentScale.FillHeight
)
Image(
asset = vectorResource(id = R.drawable.ic_android),
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(Color.Cyan)
)
CoilImage(
modifier = Modifier
.size(96.dp)
.clip(CircleShape),
data = photoUrl
)
Scaffold
Scaffold(
topBar = {...},
drawerContent = {...},
bodyContent = {...},
floatingActionButton = {...},
bottomBar = {...}
)
TopAppBar(
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.Yellow,
title = { Text(text = "Compose") },
actions = {
IconButton(
onClick = {},
icon = { Icon(Icons.Default.Search) }
)
DropdownMenu(…)
},
navigationIcon = {
IconButton(
icon = { Icon(Icons.Default.Menu) },
onClick = {
scaffoldState.drawerState.open()
}
)
}
)
FloatingActionButton(
onClick = {},
icon = { Icon(Icons.Filled.Add) },
backgroundColor = Color.Red,
contentColor = Color.White,
shape = CutCornerShape(
topLeft = 20.dp, bottomRight = 20.dp
)
)
BottomAppBar(
backgroundColor = MaterialTheme.colors.primary,
content = {
BottomNavigationItem(
icon = { Icon(Icons.Filled.Home) },
selected = selectedTab == 0,
onSelect = { selectedTab = 0 },
selectedContentColor = Color.White,
unselectedContentColor = Color.DarkGray,
label = { Text(text = "Home") }
)
BottomNavigationItem(…)
}
)
BottomAppBar(
backgroundColor = MaterialTheme.colors.primary,
content = {
BottomNavigationItem(
icon = { Icon(Icons.Filled.Home) },
selected = selectedTab == 0,
onSelect = { selectedTab = 0 },
selectedContentColor = Color.White,
unselectedContentColor = Color.DarkGray,
label = { Text(text = "Home") }
)
BottomNavigationItem(…)
}
)
app.sli.do/event/djw2yt2e
State
• Single Source of Truth
• O Componente é atualizado quando o seu estado muda
var nameState by remember { mutableStateOf("") }
TextField(
value = nameState,
label = { Text("Digite seu nome") },
onValueChange = { s: String ->
nameState = s
}
)
app.sli.do/event/djw2yt2e
Switch & Checkbox
var isChecked by remember { mutableStateOf(true) }
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it }
)
var isOn by remember { mutableStateOf(true) }
Switch(
checked = isOn,
onCheckedChange = { isOn = it }
)
app.sli.do/event/djw2yt2e
RadioButton
val options = listOf("Opção 1", "Opção 2", "Opção 3")
var currentOption by remember { mutableStateOf("Opção 1") }
options.forEach { text ->
Row(
Modifier.fillMaxWidth()
) {
RadioButton(
selected = (text == currentOption),
onClick = { currentOption = text }
)
Text(text = text)
}
}
app.sli.do/event/djw2yt2e
State
data class Score(
var team: String,
var score: Int
)
app.sli.do/event/djw2yt2e
State
class Score(
team: String,
score: Int
) {
var team by mutableStateOf(team)
var score by mutableStateOf(score)
}
app.sli.do/event/djw2yt2e
class Score(
team: String,
score: Int
) {
var team by mutableStateOf(team)
var score by mutableStateOf(score)
}
@Composable
fun TeamScore(score: Score) {
Column(horizontalGravity = Alignment.CenterHorizontally) {
Text(text = score.team, style = MaterialTheme.typography.h6)
Button(
content = { Text("+") },
onClick = { score.score += 1 }
)
Text(text = score.score.toString(), style = MaterialTheme.typography.h5)
Button(
content = { Text("-") },
onClick = { score.score = max(score.score - 1, 0) }
)
}
}
@Composable
fun ScoreScreen(homeScore: Score, visitorScore: Score) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalGravity = Alignment.CenterHorizontally
) {
Row {
TeamScore(score = homeScore)
Text(text = "x",
modifier = Modifier.padding(horizontal = 8.dp),
style = MaterialTheme.typography.h6)
TeamScore(score = visitorScore)
}
OutlinedButton(
modifier = Modifier.padding(top = 16.dp),
content = { Text("Reset") },
onClick = {
homeScore.score = 0
visitorScore.score = 0
}
)
}
}
https://link.medium.com/Uax00dnd31
app.sli.do/event/djw2yt2e
Testes
dependencies {
...
testImplementation 'junit:junit:4.13'
androidTestImplementation "androidx.ui:ui-test:$compose_version"
}
<activity
android:name="androidx.activity.ComponentActivity" />
@Composable
fun TeamScore(score: Score) {
Column(horizontalGravity = Alignment.CenterHorizontally) {
Text(text = score.team, style = MaterialTheme.typography.h6)
Button(
content = { Text("+") },
onClick = { score.score += 1 }
)
Text(text = score.score.toString(), style = MaterialTheme.typography.h5)
Button(
content = { Text("-") },
onClick = { score.score = max(score.score - 1, 0) }
)
}
}
@Composable
fun TeamScore(score: Score) {
Column(horizontalGravity = Alignment.CenterHorizontally) {
Text(text = score.team, style = MaterialTheme.typography.h6)
Button(
content = { Text("+") },
modifier = Modifier.testTag("TeamScore_Inc"),
onClick = { score.score += 1 }
)
Text(text = score.score.toString(), style = MaterialTheme.typography.h5)
Button(
content = { Text("-") },
modifier = Modifier.testTag("TeamScore_Dec"),
onClick = { score.score = max(score.score - 1, 0) }
)
}
}
@RunWith(JUnit4::class)
class TeamScoreTest {
@get:Rule
val composeTestRule = createComposeRule()
fun setContent(score: Score) {
composeTestRule.setContent {
MaterialTheme {
TeamScore(score)
}
}
}
@Test
fun teamNameVisible() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithText("Corinthians").assertIsDisplayed()
}
@RunWith(JUnit4::class)
class TeamScoreTest {
@get:Rule
val composeTestRule = createComposeRule()
fun setContent(score: Score) {
composeTestRule.setContent {
MaterialTheme {
TeamScore(score)
}
}
}
@Test
fun teamNameVisible() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithText("Corinthians").assertIsDisplayed()
}
@RunWith(JUnit4::class)
class TeamScoreTest {
@get:Rule
val composeTestRule = createComposeRule()
fun setContent(score: Score) {
composeTestRule.setContent {
MaterialTheme {
TeamScore(score)
}
}
}
@Test
fun teamNameVisible() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithText("Corinthians").assertIsDisplayed()
}
@Test
fun testIncrementDecrement() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithTag("TeamScore_Inc")
.performClick()
.performClick()
onNodeWithText("2").assertIsDisplayed()
runOnIdle {
assertEquals(2, score.score)
}
onNodeWithTag("TeamScore_Dec")
.performClick()
onNodeWithText("1").assertIsDisplayed()
runOnIdle {
assertEquals(1, score.score)
}
}
@Test
fun testIncrementDecrement() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithTag("TeamScore_Inc")
.performClick()
.performClick()
onNodeWithText("2").assertIsDisplayed()
runOnIdle {
assertEquals(2, score.score)
}
onNodeWithTag("TeamScore_Dec")
.performClick()
onNodeWithText("1").assertIsDisplayed()
runOnIdle {
assertEquals(1, score.score)
}
}
@Test
fun testIncrementDecrement() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithTag("TeamScore_Inc")
.performClick()
.performClick()
onNodeWithText("2").assertIsDisplayed()
runOnIdle {
assertEquals(2, score.score)
}
onNodeWithTag("TeamScore_Dec")
.performClick()
onNodeWithText("1").assertIsDisplayed()
runOnIdle {
assertEquals(1, score.score)
}
}
@Test
fun testIncrementDecrement() {
val score = Score(team = "Corinthians", score = 0)
setContent(score)
onNodeWithTag("TeamScore_Inc")
.performClick()
.performClick()
onNodeWithText("2").assertIsDisplayed()
runOnIdle {
assertEquals(2, score.score)
}
onNodeWithTag("TeamScore_Dec")
.performClick()
onNodeWithText("1").assertIsDisplayed()
runOnIdle {
assertEquals(1, score.score)
}
}
app.sli.do/event/djw2yt2e
ScrollableColumn
ScrollableColumn {
for (i in 0..200) {
Text(
"Item: $i",
modifier = Modifier.padding(8.dp).fillMaxWidth()
)
}
}
app.sli.do/event/djw2yt2e
Lists
@Composable
fun UserList(users: List<User>) {
LazyColumnFor(
items = users,
modifier = Modifier.fillMaxSize()) {
UserItem(user = it)
}
}
app.sli.do/event/djw2yt2e
Lists
@Composable
fun UserList(users: List<User>) {
LazyColumnForIndexed(
items = users,
itemContent = { index, item ->
UserItem(user = item, index = index)
}
)
}
Interoperabilidade
app.sli.do/event/djw2yt2e
Em fragments…
class MyFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
AppTheme {
SeuComposable()
}
}
}
}
}
app.sli.do/event/djw2yt2e
Em fragments…
class MyFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
AppTheme {
SeuComposable()
}
}
}
}
}
app.sli.do/event/djw2yt2e
Em arquivos de layout
<androidx.compose.ui.platform.ComposeView
android:id="@+id/my_composable"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
findViewById<ComposeView>(R.id.my_composable).setContent {
MaterialTheme {
Surface {
Text(text = "Hello!")
}
}
}
app.sli.do/event/djw2yt2e
@Composable
fun MyCalendar(onDateUpdate: (Date) -> Unit) {
AndroidView(
viewBlock = { context: Context ->
val view =
LayoutInflater.from(context).inflate(R.layout.my_layout, null, false)
val textView = view.findViewById<TextView>(R.id.txtDate)
val calendarView = view.findViewById<CalendarView>(R.id.calendarView)
calendarView?.setOnDateChangeListener { cv, year, month, day ->
val date = Calendar.getInstance().apply {
set(year, month, day)
}.time
textView?.text = date.toString()
onDateUpdate(date)
}
view
},
update = { view ->
// Update view
}
)
}
app.sli.do/event/djw2yt2e
@Composable
fun MyCalendar(onDateUpdate: (Date) -> Unit) {
AndroidView(
viewBlock = { context: Context ->
val view =
LayoutInflater.from(context).inflate(R.layout.my_layout, null, false)
val textView = view.findViewById<TextView>(R.id.txtDate)
val calendarView = view.findViewById<CalendarView>(R.id.calendarView)
calendarView?.setOnDateChangeListener { cv, year, month, day ->
val date = Calendar.getInstance().apply {
set(year, month, day)
}.time
textView?.text = date.toString()
onDateUpdate(date)
}
view
},
update = { view ->
// Update view
}
)
}
app.sli.do/event/djw2yt2e
@Composable
fun MyCalendar(onDateUpdate: (Date) -> Unit) {
AndroidView(
viewBlock = { context: Context ->
val view =
LayoutInflater.from(context).inflate(R.layout.my_layout, null, false)
val textView = view.findViewById<TextView>(R.id.txtDate)
val calendarView = view.findViewById<CalendarView>(R.id.calendarView)
calendarView?.setOnDateChangeListener { cv, year, month, day ->
val date = Calendar.getInstance().apply {
set(year, month, day)
}.time
textView?.text = date.toString()
onDateUpdate(date)
}
view
},
update = { view ->
// Update view
}
)
}
app.sli.do/event/djw2yt2e
Resources
• stringResources(R.string.your_string)
• dimensionResource(R.dimen.padding_small)
• colorResource(R.color.blue)
• …
Mudança de
estado
app.sli.do/event/djw2yt2e
Observando Estado
• LiveData.observeAsState
• Flow.collectAsState
• Observable.subscribeAsState
app.sli.do/event/djw2yt2e
LiveData
@Composable
fun UserScreen(
usersLiveData: LiveData<List<UserBinding>>,
onSaveUser: (UserBinding) -> Unit,
onDeleteUser: (UserBinding) -> Unit
) {
val users by usersLiveData.observeAsState()
Column(modifier = Modifier.fillMaxSize()) {
InputPanel(currentUser, onInsertUser = { user ->
onSaveUser(user)
})
UserList(
users = users ?: emptyList(),
onDeleteUser = onDeleteUser
)
}
}
app.sli.do/event/djw2yt2e
LiveData
@Composable
fun UserScreen(
usersLiveData: LiveData<List<UserBinding>>,
onSaveUser: (UserBinding) -> Unit,
onDeleteUser: (UserBinding) -> Unit
) {
val users by usersLiveData.observeAsState()
Column(modifier = Modifier.fillMaxSize()) {
InputPanel(currentUser, onInsertUser = { user ->
onSaveUser(user)
})
UserList(
users = users ?: emptyList(),
onDeleteUser = onDeleteUser
)
}
}
app.sli.do/event/djw2yt2e
LiveData
@Composable
fun UserScreen(
usersLiveData: LiveData<List<UserBinding>>,
onSaveUser: (UserBinding) -> Unit,
onDeleteUser: (UserBinding) -> Unit
) {
val users by usersLiveData.observeAsState()
Column(modifier = Modifier.fillMaxSize()) {
InputPanel(currentUser, onInsertUser = { user ->
onSaveUser(user)
})
UserList(
users = users ?: emptyList(),
onDeleteUser = onDeleteUser
)
}
}
app.sli.do/event/djw2yt2e
LiveData
@Composable
fun UserScreen(
usersLiveData: LiveData<List<UserBinding>>,
onSaveUser: (UserBinding) -> Unit,
onDeleteUser: (UserBinding) -> Unit
) {
val users by usersLiveData.observeAsState()
Column(modifier = Modifier.fillMaxSize()) {
InputPanel(currentUser, onInsertUser = { user ->
onSaveUser(user)
})
UserList(
users = users ?: emptyList(),
onDeleteUser = onDeleteUser
)
}
}
app.sli.do/event/djw2yt2e
LiveData
@Composable
fun UserScreen(
usersLiveData: LiveData<List<UserBinding>>,
onSaveUser: (UserBinding) -> Unit,
onDeleteUser: (UserBinding) -> Unit
) {
val users by usersLiveData.observeAsState()
Column(modifier = Modifier.fillMaxSize()) {
InputPanel(currentUser, onInsertUser = { user ->
onSaveUser(user)
})
UserList(
users = users ?: emptyList(),
onDeleteUser = onDeleteUser
)
}
}
app.sli.do/event/djw2yt2e
LiveData
UserScreen(
usersLiveData = viewModel.allUsers,
onSaveUser = { user ->
viewModel.saveUser(user)
},
onDeleteUser = { user ->
viewModel.deleteUser(user)
}
)
@Composable
fun UserScreen(
usersLiveData: LiveData<List<UserBinding>>,
onSaveUser: (UserBinding) -> Unit,
onDeleteUser: (UserBinding) -> Unit
) { … }
Coroutines
app.sli.do/event/djw2yt2e
Coroutines
@Composable
fun MyComposable() {
val welcomeMsg = remember { mutableStateOf("") }
launchInComposition {
val s = withContext(Dispatchers.IO) {
delay(5_000)
"Hello Compose!"
}
welcomeMsg.value = s
}
Text(text = welcomeMsg.value)
}
launchInComposition lança uma
coroutine quando a composição é
adicionada e a cancela automaticamente
quando a execução deixa a composição.
app.sli.do/event/djw2yt2e
Coroutines
@Composable
fun MyComposable() {
val scope = rememberCoroutineScope()
val count = remember { mutableStateOf(0) }
Text(text = "Current count: ${count.value}")
Button(onClick = {
scope.launch {
for (i in 1..10) {
withContext(Dispatchers.IO) {
delay(1_000)
}
count.value = i
}
}
}, content = { Text("Start!") })
}
rememberCoroutineScope retorna
o CoroutineScope associado ao ponto
específico da composição. Deve ser usado
para lançar coroutines em resposta à
eventos de callback
app.sli.do/event/djw2yt2e
Roadmap
app.sli.do/event/djw2yt2e
Conclusão
• A ideia do Compose parece ser muito interessante, pois segue o
paradigma moderno de desenvolvimento de UI.
• Podemos pensar em UI em outras plataformas?
• Está em versão alpha, logo NÃO USE EM PRODUÇÃO!
• Quando será que teremos todos os recursos existentes no Toolkit atual?
E quando teremos a mesma quantidade de componentes third party?
• Nós, como desenvolvedores devemos estar preparados. 

Pois desaprender é mais difícil que aprender 😉
app.sli.do/event/djw2yt2e
Referências
• Página oficial do Jetpack Compose

https://developer.android.com/jetpack/compose
• Codelab Jetpack Compose

https://codelabs.developers.google.com/codelabs/jetpack-compose-
basics/#0
• Jetpack Compose Samples

https://github.com/android/compose-samples
• Romain Guy Sample

https://github.com/romainguy/sample-materials-shop
app.sli.do/event/djw2yt2e
Referências
• Lista de classes do Compose

https://developer.android.com/reference/kotlin/androidx/ui/classes
• Compose Academy

https://compose.academy/
• Classic Android to Jetpack (by Vinay Gaba)

https://jetpackcompose.app/
• Canal #Compose no Slack do Kotlin

slack.kotlinlang.org (#compose)
app.sli.do/event/djw2yt2e
Referências
• Understanding Compose (Android Dev Summit 2019)

https://www.youtube.com/watch?v=Q9MtlmmN4Q0
• What’s new in Jetpack Compose (Android Dev Summit 2019)

https://www.youtube.com/watch?v=dtm2h-_sNDQ
• Jetpack Compose (#Android11 - 2020)

https://www.youtube.com/watch?v=U5BwfqBpiWU
• Jetpack Compose - Next Gen Kotlin UI Toolkit for Android (Right?)

https://www.youtube.com/watch?v=I5zRmCheVVg
app.sli.do/event/djw2yt2e
Referências
• Thinking in Compose

https://www.youtube.com/watch?v=SMOhl9RK0BA
• Repositório do Jetpack Compose

https://android.googlesource.com/platform/frameworks/support/+/refs/
heads/androidx-master-dev/compose/
• Request Features & Bug Tracker

https://issuetracker.google.com/issues/new?component=612128
Obrigado!
Nelson Glauber
@nglauber

Jetpack Compose a nova forma de implementar UI no Android

  • 1.
    Jetpack Compose uma nova formade implementar UI no Android Nelson Glauber @nglauber
  • 2.
    Jetpack Compose uma nova formade implementar UI no Android Nelson Glauber @nglauber
  • 3.
    Jetpack Compose éum moderno conjunto de ferramentas para construir interfaces gráficas para aplicações Android de forma declarativa utilizando Kotlin. app.sli.do/event/djw2yt2e
  • 4.
    app.sli.do/event/djw2yt2e UI Imperativa xUI Declarativa • UI Imperativa é onde uma entidade de UI (com uma classe View, por exemplo) é totalmente construída e posteriormente é modificada usando métodos e/ou propriedades públicas. • Na UI Declarativa, o desenvolvedor descreve o estado da UI e deixa o transicionamento desse estado por conta de um framework.
  • 5.
    app.sli.do/event/djw2yt2e Por que? • Todasas APIs do Android evoluíram, menos o UI toolkit.
 Room, View Model, Paging, Work Manager, Camera, Navigation, … • Não é simples criar uma custom view… Componente.kt, componente.xml, attrs.xml, styles.xml…
  • 6.
    app.sli.do/event/djw2yt2e Jetpack Compose • Segueuma abordagem declarativa similar ao SwiftUI, ReactNative, e Flutter. • É uma biblioteca separada do S.O. • É uma nova forma se se pensar na elaboração da UI: componentes ao invés de telas; Composição no lugar de Herança. • Compatível com aplicações Android existentes, podendo ser adotado progressivamente. • EXPERIMENTAL! Ainda está em versão alpha desde… ontem 😅
  • 7.
    app.sli.do/event/djw2yt2e Benefícios • No Compose,a UI é totalmente escrita em Kotlin e nós sabemos organizar e refatorar código melhor que arquivos XML :) • Qual o maior trabalho no front-end? 
 Deixar UI e dados sincronizados e consistentes (Data Binding, Live Data, …). • O UI toolkit atual não está nem aí pros teus problemas! 
 Ele tem o próprio estado e só avisa que o seu estado interno mudou.
  • 8.
  • 9.
  • 10.
    app.sli.do/event/djw2yt2e @Composable • É maneirade criar um “custom component”. 
 Simplesmente criando uma função! • @Composable não usa processamento de anotação. É uma instrução para o plugin do compilador do Kotlin. • Só pode ser chamada dentro de um contexto de uma @Composable function.
  • 11.
    app.sli.do/event/djw2yt2e setContent class MainActivity :AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Greeting("Android") } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") }
  • 12.
    app.sli.do/event/djw2yt2e setContent class MainActivity :AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Greeting("Android") } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") }
  • 13.
    app.sli.do/event/djw2yt2e setContent class MainActivity :AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Greeting("Android") } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") }
  • 14.
    app.sli.do/event/djw2yt2e setContent class MainActivity :AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Greeting("Android") } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") }
  • 15.
    app.sli.do/event/djw2yt2e setContent class MainActivity :AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Greeting("Android") } } } } @Composable fun Greeting(name: String) { Text(stringResource(R.string.hello, name)) }
  • 16.
    app.sli.do/event/djw2yt2e Material Theme • Defineo tema da aplicação por meio de cores, estilos, fontes e decoração de componentes. • Normalmente é o elemento raiz da tela. setContent { AppTheme { Greeting("Android") } }
  • 17.
    app.sli.do/event/djw2yt2e Material Theme @Composable fun AppTheme( darkTheme:Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colors = if (darkTheme) { DarkColorPalette } else { LightColorPalette } MaterialTheme( colors = colors, typography = typography, shapes = shapes, content = content ) } private val DarkColorPalette = darkColors( primary = purple200, primaryVariant = purple700, secondary = teal200 ) private val LightColorPalette = lightColors( primary = purple500, primaryVariant = purple700, secondary = teal200 )
  • 18.
  • 19.
    app.sli.do/event/djw2yt2e Modifiers • Decoram umelemento • Provê parâmetros de layout • Comportamentos adicionais • São encadeados e a ordem importa!
  • 20.
    val shape =CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp) Text( text = "Text 1", style = TextStyle( color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth() .padding(16.dp) .border(2.dp, MaterialTheme.colors.secondary, shape) .padding(1.dp) .background(MaterialTheme.colors.primary, shape) .clickable(onClick = { // Click event }) .padding(16.dp) )
  • 21.
    val shape =CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp) Text( text = "Text 1", style = TextStyle( color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth() .padding(16.dp) .border(2.dp, MaterialTheme.colors.secondary, shape) .padding(1.dp) .background(MaterialTheme.colors.primary, shape) .clickable(onClick = { // Click event }) .padding(16.dp) )
  • 22.
    val shape =CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp) Text( text = "Text 1", style = TextStyle( color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth() .padding(16.dp) .border(2.dp, MaterialTheme.colors.secondary, shape) .padding(1.dp) .background(MaterialTheme.colors.primary, shape) .clickable(onClick = { // Click event }) .padding(16.dp) )
  • 23.
    val shape =CutCornerShape(topLeft = 16.dp, bottomRight = 16.dp) Text( text = "Text 1", style = TextStyle( color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth() .padding(16.dp) .border(2.dp, MaterialTheme.colors.secondary, shape) .padding(1.dp) .background(MaterialTheme.colors.primary, shape) .clickable(onClick = { // Click event }) .padding(16.dp) )
  • 24.
    val shape =RoundedCornerShape(8.dp) Text( text = "Text 1", style = TextStyle( color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth() .padding(16.dp) .border(2.dp, MaterialTheme.colors.secondary, shape) .padding(1.dp) .background(MaterialTheme.colors.primary, shape) .clickable(onClick = { // Click event }) .padding(16.dp) )
  • 25.
    val shape =CircleShape Text( text = "Text 1", style = TextStyle( color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth() .padding(16.dp) .border(2.dp, MaterialTheme.colors.secondary, shape) .padding(1.dp) .background(MaterialTheme.colors.primary, shape) .clickable(onClick = { // Click event }) .padding(16.dp) )
  • 26.
  • 27.
    Stack(modifier = Modifier.fillMaxWidth()){ Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() ) { Text("Column Text 1") Text("Column Text 2") Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { Text(text = "Row Text 1") Text(text = "Row Text 2") } } Text( "Stack Text", modifier = Modifier .gravity(Alignment.TopEnd) .padding(end = 16.dp, top = 16.dp) ) }
  • 28.
    Stack(modifier = Modifier.fillMaxWidth()){ Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() ) { Text("Column Text 1") Text("Column Text 2") Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { Text(text = "Row Text 1") Text(text = "Row Text 2") } } Text( "Stack Text", modifier = Modifier .gravity(Alignment.TopEnd) .padding(end = 16.dp, top = 16.dp) ) }
  • 29.
    Stack(modifier = Modifier.fillMaxWidth()){ Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() ) { Text("Column Text 1") Text("Column Text 2") Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { Text(text = "Row Text 1") Text(text = "Row Text 2") } } Text( "Stack Text", modifier = Modifier .gravity(Alignment.TopEnd) .padding(end = 16.dp, top = 16.dp) ) }
  • 30.
    Stack(modifier = Modifier.fillMaxWidth()){ Column( modifier = Modifier .padding(16.dp) .fillMaxWidth() ) { Text("Column Text 1") Text("Column Text 2") Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { Text(text = "Row Text 1") Text(text = "Row Text 2") } } Text( "Stack Text", modifier = Modifier .gravity(Alignment.TopEnd) .padding(end = 16.dp, top = 16.dp) ) }
  • 31.
  • 32.
    ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)){ val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs() Text("Nome", modifier = Modifier.constrainAs(text1Ref) { top.linkTo(parent.top) centerHorizontallyTo(parent) }) TextField(modifier = Modifier.padding(top = 8.dp) .constrainAs(edit1Ref) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(text1Ref.bottom) }) Button(onClick = {}, modifier = Modifier.padding(top = 8.dp) .constrainAs(btn1Ref) { end.linkTo(edit1Ref.end) top.linkTo(edit1Ref.bottom) } ) TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp) .constrainAs(btn2Ref) { end.linkTo(btn1Ref.start) baseline.linkTo(btn1Ref.baseline) } ) }
  • 33.
    ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)){ val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs() Text("Nome", modifier = Modifier.constrainAs(text1Ref) { top.linkTo(parent.top) centerHorizontallyTo(parent) }) TextField(modifier = Modifier.padding(top = 8.dp) .constrainAs(edit1Ref) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(text1Ref.bottom) }) Button(onClick = {}, modifier = Modifier.padding(top = 8.dp) .constrainAs(btn1Ref) { end.linkTo(edit1Ref.end) top.linkTo(edit1Ref.bottom) } ) TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp) .constrainAs(btn2Ref) { end.linkTo(btn1Ref.start) baseline.linkTo(btn1Ref.baseline) } ) }
  • 34.
    ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)){ val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs() Text("Nome", modifier = Modifier.constrainAs(text1Ref) { top.linkTo(parent.top) centerHorizontallyTo(parent) }) TextField(modifier = Modifier.padding(top = 8.dp) .constrainAs(edit1Ref) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(text1Ref.bottom) }) Button(onClick = {}, modifier = Modifier.padding(top = 8.dp) .constrainAs(btn1Ref) { end.linkTo(edit1Ref.end) top.linkTo(edit1Ref.bottom) } ) TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp) .constrainAs(btn2Ref) { end.linkTo(btn1Ref.start) baseline.linkTo(btn1Ref.baseline) } ) }
  • 35.
    ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)){ val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs() Text("Nome", modifier = Modifier.constrainAs(text1Ref) { top.linkTo(parent.top) centerHorizontallyTo(parent) }) TextField(modifier = Modifier.padding(top = 8.dp) .constrainAs(edit1Ref) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(text1Ref.bottom) }) Button(onClick = {}, modifier = Modifier.padding(top = 8.dp) .constrainAs(btn1Ref) { end.linkTo(edit1Ref.end) top.linkTo(edit1Ref.bottom) } ) TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp) .constrainAs(btn2Ref) { end.linkTo(btn1Ref.start) baseline.linkTo(btn1Ref.baseline) } ) }
  • 36.
    ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)){ val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs() Text("Nome", modifier = Modifier.constrainAs(text1Ref) { top.linkTo(parent.top) centerHorizontallyTo(parent) }) TextField(modifier = Modifier.padding(top = 8.dp) .constrainAs(edit1Ref) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(text1Ref.bottom) }) Button(onClick = {}, modifier = Modifier.padding(top = 8.dp) .constrainAs(btn1Ref) { end.linkTo(edit1Ref.end) top.linkTo(edit1Ref.bottom) } ) TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp) .constrainAs(btn2Ref) { end.linkTo(btn1Ref.start) baseline.linkTo(btn1Ref.baseline) } ) }
  • 37.
    ConstraintLayout(modifier = Modifier.fillMaxSize().padding(16.dp)){ val (text1Ref, edit1Ref, btn1Ref, btn2Ref) = createRefs() Text("Nome", modifier = Modifier.constrainAs(text1Ref) { top.linkTo(parent.top) centerHorizontallyTo(parent) }) TextField(modifier = Modifier.padding(top = 8.dp) .constrainAs(edit1Ref) { start.linkTo(parent.start) end.linkTo(parent.end) top.linkTo(text1Ref.bottom) }) Button(onClick = {}, modifier = Modifier.padding(top = 8.dp) .constrainAs(btn1Ref) { end.linkTo(edit1Ref.end) top.linkTo(edit1Ref.bottom) } ) TextButton(onClick = {}, modifier = Modifier.padding(end = 8.dp) .constrainAs(btn2Ref) { end.linkTo(btn1Ref.start) baseline.linkTo(btn1Ref.baseline) } ) }
  • 38.
    app.sli.do/event/djw2yt2e Button Button( content = {Text("Button") }, onClick = {} ) OutlinedButton( content = { Text("OutlinedButton") }, onClick = {} ) TextButton( content = { Text("TextButton") }, onClick = {} )
  • 39.
    app.sli.do/event/djw2yt2e Card + Ripple+ Clickable @Composable fun UserItem(user: User) { Card( elevation = 4.dp, shape = RoundedCornerShape(4.dp), modifier = Modifier.fillMaxWidth().padding(8.dp) .clickable( onClick = { // Click event } ) ) { // Card Content } }
  • 40.
    app.sli.do/event/djw2yt2e Image Image( asset = imageResource(R.drawable.recife), contentScale= ContentScale.FillHeight ) Image( asset = vectorResource(id = R.drawable.ic_android), contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(Color.Cyan) ) CoilImage( modifier = Modifier .size(96.dp) .clip(CircleShape), data = photoUrl )
  • 41.
    Scaffold Scaffold( topBar = {...}, drawerContent= {...}, bodyContent = {...}, floatingActionButton = {...}, bottomBar = {...} )
  • 42.
    TopAppBar( backgroundColor = MaterialTheme.colors.primary, contentColor= Color.Yellow, title = { Text(text = "Compose") }, actions = { IconButton( onClick = {}, icon = { Icon(Icons.Default.Search) } ) DropdownMenu(…) }, navigationIcon = { IconButton( icon = { Icon(Icons.Default.Menu) }, onClick = { scaffoldState.drawerState.open() } ) } )
  • 43.
    FloatingActionButton( onClick = {}, icon= { Icon(Icons.Filled.Add) }, backgroundColor = Color.Red, contentColor = Color.White, shape = CutCornerShape( topLeft = 20.dp, bottomRight = 20.dp ) )
  • 44.
    BottomAppBar( backgroundColor = MaterialTheme.colors.primary, content= { BottomNavigationItem( icon = { Icon(Icons.Filled.Home) }, selected = selectedTab == 0, onSelect = { selectedTab = 0 }, selectedContentColor = Color.White, unselectedContentColor = Color.DarkGray, label = { Text(text = "Home") } ) BottomNavigationItem(…) } )
  • 45.
    BottomAppBar( backgroundColor = MaterialTheme.colors.primary, content= { BottomNavigationItem( icon = { Icon(Icons.Filled.Home) }, selected = selectedTab == 0, onSelect = { selectedTab = 0 }, selectedContentColor = Color.White, unselectedContentColor = Color.DarkGray, label = { Text(text = "Home") } ) BottomNavigationItem(…) } )
  • 46.
    app.sli.do/event/djw2yt2e State • Single Sourceof Truth • O Componente é atualizado quando o seu estado muda var nameState by remember { mutableStateOf("") } TextField( value = nameState, label = { Text("Digite seu nome") }, onValueChange = { s: String -> nameState = s } )
  • 47.
    app.sli.do/event/djw2yt2e Switch & Checkbox varisChecked by remember { mutableStateOf(true) } Checkbox( checked = isChecked, onCheckedChange = { isChecked = it } ) var isOn by remember { mutableStateOf(true) } Switch( checked = isOn, onCheckedChange = { isOn = it } )
  • 48.
    app.sli.do/event/djw2yt2e RadioButton val options =listOf("Opção 1", "Opção 2", "Opção 3") var currentOption by remember { mutableStateOf("Opção 1") } options.forEach { text -> Row( Modifier.fillMaxWidth() ) { RadioButton( selected = (text == currentOption), onClick = { currentOption = text } ) Text(text = text) } }
  • 49.
  • 50.
    app.sli.do/event/djw2yt2e State class Score( team: String, score:Int ) { var team by mutableStateOf(team) var score by mutableStateOf(score) }
  • 51.
    app.sli.do/event/djw2yt2e class Score( team: String, score:Int ) { var team by mutableStateOf(team) var score by mutableStateOf(score) }
  • 52.
    @Composable fun TeamScore(score: Score){ Column(horizontalGravity = Alignment.CenterHorizontally) { Text(text = score.team, style = MaterialTheme.typography.h6) Button( content = { Text("+") }, onClick = { score.score += 1 } ) Text(text = score.score.toString(), style = MaterialTheme.typography.h5) Button( content = { Text("-") }, onClick = { score.score = max(score.score - 1, 0) } ) } }
  • 53.
    @Composable fun ScoreScreen(homeScore: Score,visitorScore: Score) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalGravity = Alignment.CenterHorizontally ) { Row { TeamScore(score = homeScore) Text(text = "x", modifier = Modifier.padding(horizontal = 8.dp), style = MaterialTheme.typography.h6) TeamScore(score = visitorScore) } OutlinedButton( modifier = Modifier.padding(top = 16.dp), content = { Text("Reset") }, onClick = { homeScore.score = 0 visitorScore.score = 0 } ) } }
  • 54.
  • 55.
    app.sli.do/event/djw2yt2e Testes dependencies { ... testImplementation 'junit:junit:4.13' androidTestImplementation"androidx.ui:ui-test:$compose_version" } <activity android:name="androidx.activity.ComponentActivity" />
  • 56.
    @Composable fun TeamScore(score: Score){ Column(horizontalGravity = Alignment.CenterHorizontally) { Text(text = score.team, style = MaterialTheme.typography.h6) Button( content = { Text("+") }, onClick = { score.score += 1 } ) Text(text = score.score.toString(), style = MaterialTheme.typography.h5) Button( content = { Text("-") }, onClick = { score.score = max(score.score - 1, 0) } ) } }
  • 57.
    @Composable fun TeamScore(score: Score){ Column(horizontalGravity = Alignment.CenterHorizontally) { Text(text = score.team, style = MaterialTheme.typography.h6) Button( content = { Text("+") }, modifier = Modifier.testTag("TeamScore_Inc"), onClick = { score.score += 1 } ) Text(text = score.score.toString(), style = MaterialTheme.typography.h5) Button( content = { Text("-") }, modifier = Modifier.testTag("TeamScore_Dec"), onClick = { score.score = max(score.score - 1, 0) } ) } }
  • 58.
    @RunWith(JUnit4::class) class TeamScoreTest { @get:Rule valcomposeTestRule = createComposeRule() fun setContent(score: Score) { composeTestRule.setContent { MaterialTheme { TeamScore(score) } } } @Test fun teamNameVisible() { val score = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithText("Corinthians").assertIsDisplayed() }
  • 59.
    @RunWith(JUnit4::class) class TeamScoreTest { @get:Rule valcomposeTestRule = createComposeRule() fun setContent(score: Score) { composeTestRule.setContent { MaterialTheme { TeamScore(score) } } } @Test fun teamNameVisible() { val score = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithText("Corinthians").assertIsDisplayed() }
  • 60.
    @RunWith(JUnit4::class) class TeamScoreTest { @get:Rule valcomposeTestRule = createComposeRule() fun setContent(score: Score) { composeTestRule.setContent { MaterialTheme { TeamScore(score) } } } @Test fun teamNameVisible() { val score = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithText("Corinthians").assertIsDisplayed() }
  • 61.
    @Test fun testIncrementDecrement() { valscore = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithTag("TeamScore_Inc") .performClick() .performClick() onNodeWithText("2").assertIsDisplayed() runOnIdle { assertEquals(2, score.score) } onNodeWithTag("TeamScore_Dec") .performClick() onNodeWithText("1").assertIsDisplayed() runOnIdle { assertEquals(1, score.score) } }
  • 62.
    @Test fun testIncrementDecrement() { valscore = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithTag("TeamScore_Inc") .performClick() .performClick() onNodeWithText("2").assertIsDisplayed() runOnIdle { assertEquals(2, score.score) } onNodeWithTag("TeamScore_Dec") .performClick() onNodeWithText("1").assertIsDisplayed() runOnIdle { assertEquals(1, score.score) } }
  • 63.
    @Test fun testIncrementDecrement() { valscore = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithTag("TeamScore_Inc") .performClick() .performClick() onNodeWithText("2").assertIsDisplayed() runOnIdle { assertEquals(2, score.score) } onNodeWithTag("TeamScore_Dec") .performClick() onNodeWithText("1").assertIsDisplayed() runOnIdle { assertEquals(1, score.score) } }
  • 64.
    @Test fun testIncrementDecrement() { valscore = Score(team = "Corinthians", score = 0) setContent(score) onNodeWithTag("TeamScore_Inc") .performClick() .performClick() onNodeWithText("2").assertIsDisplayed() runOnIdle { assertEquals(2, score.score) } onNodeWithTag("TeamScore_Dec") .performClick() onNodeWithText("1").assertIsDisplayed() runOnIdle { assertEquals(1, score.score) } }
  • 65.
    app.sli.do/event/djw2yt2e ScrollableColumn ScrollableColumn { for (iin 0..200) { Text( "Item: $i", modifier = Modifier.padding(8.dp).fillMaxWidth() ) } }
  • 66.
    app.sli.do/event/djw2yt2e Lists @Composable fun UserList(users: List<User>){ LazyColumnFor( items = users, modifier = Modifier.fillMaxSize()) { UserItem(user = it) } }
  • 67.
    app.sli.do/event/djw2yt2e Lists @Composable fun UserList(users: List<User>){ LazyColumnForIndexed( items = users, itemContent = { index, item -> UserItem(user = item, index = index) } ) }
  • 68.
  • 69.
    app.sli.do/event/djw2yt2e Em fragments… class MyFragment:Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return ComposeView(requireContext()).apply { setContent { AppTheme { SeuComposable() } } } } }
  • 70.
    app.sli.do/event/djw2yt2e Em fragments… class MyFragment:Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return ComposeView(requireContext()).apply { setContent { AppTheme { SeuComposable() } } } } }
  • 71.
    app.sli.do/event/djw2yt2e Em arquivos delayout <androidx.compose.ui.platform.ComposeView android:id="@+id/my_composable" android:layout_width="wrap_content" android:layout_height="wrap_content" /> findViewById<ComposeView>(R.id.my_composable).setContent { MaterialTheme { Surface { Text(text = "Hello!") } } }
  • 72.
    app.sli.do/event/djw2yt2e @Composable fun MyCalendar(onDateUpdate: (Date)-> Unit) { AndroidView( viewBlock = { context: Context -> val view = LayoutInflater.from(context).inflate(R.layout.my_layout, null, false) val textView = view.findViewById<TextView>(R.id.txtDate) val calendarView = view.findViewById<CalendarView>(R.id.calendarView) calendarView?.setOnDateChangeListener { cv, year, month, day -> val date = Calendar.getInstance().apply { set(year, month, day) }.time textView?.text = date.toString() onDateUpdate(date) } view }, update = { view -> // Update view } ) }
  • 73.
    app.sli.do/event/djw2yt2e @Composable fun MyCalendar(onDateUpdate: (Date)-> Unit) { AndroidView( viewBlock = { context: Context -> val view = LayoutInflater.from(context).inflate(R.layout.my_layout, null, false) val textView = view.findViewById<TextView>(R.id.txtDate) val calendarView = view.findViewById<CalendarView>(R.id.calendarView) calendarView?.setOnDateChangeListener { cv, year, month, day -> val date = Calendar.getInstance().apply { set(year, month, day) }.time textView?.text = date.toString() onDateUpdate(date) } view }, update = { view -> // Update view } ) }
  • 74.
    app.sli.do/event/djw2yt2e @Composable fun MyCalendar(onDateUpdate: (Date)-> Unit) { AndroidView( viewBlock = { context: Context -> val view = LayoutInflater.from(context).inflate(R.layout.my_layout, null, false) val textView = view.findViewById<TextView>(R.id.txtDate) val calendarView = view.findViewById<CalendarView>(R.id.calendarView) calendarView?.setOnDateChangeListener { cv, year, month, day -> val date = Calendar.getInstance().apply { set(year, month, day) }.time textView?.text = date.toString() onDateUpdate(date) } view }, update = { view -> // Update view } ) }
  • 75.
  • 76.
  • 77.
    app.sli.do/event/djw2yt2e Observando Estado • LiveData.observeAsState •Flow.collectAsState • Observable.subscribeAsState
  • 78.
    app.sli.do/event/djw2yt2e LiveData @Composable fun UserScreen( usersLiveData: LiveData<List<UserBinding>>, onSaveUser:(UserBinding) -> Unit, onDeleteUser: (UserBinding) -> Unit ) { val users by usersLiveData.observeAsState() Column(modifier = Modifier.fillMaxSize()) { InputPanel(currentUser, onInsertUser = { user -> onSaveUser(user) }) UserList( users = users ?: emptyList(), onDeleteUser = onDeleteUser ) } }
  • 79.
    app.sli.do/event/djw2yt2e LiveData @Composable fun UserScreen( usersLiveData: LiveData<List<UserBinding>>, onSaveUser:(UserBinding) -> Unit, onDeleteUser: (UserBinding) -> Unit ) { val users by usersLiveData.observeAsState() Column(modifier = Modifier.fillMaxSize()) { InputPanel(currentUser, onInsertUser = { user -> onSaveUser(user) }) UserList( users = users ?: emptyList(), onDeleteUser = onDeleteUser ) } }
  • 80.
    app.sli.do/event/djw2yt2e LiveData @Composable fun UserScreen( usersLiveData: LiveData<List<UserBinding>>, onSaveUser:(UserBinding) -> Unit, onDeleteUser: (UserBinding) -> Unit ) { val users by usersLiveData.observeAsState() Column(modifier = Modifier.fillMaxSize()) { InputPanel(currentUser, onInsertUser = { user -> onSaveUser(user) }) UserList( users = users ?: emptyList(), onDeleteUser = onDeleteUser ) } }
  • 81.
    app.sli.do/event/djw2yt2e LiveData @Composable fun UserScreen( usersLiveData: LiveData<List<UserBinding>>, onSaveUser:(UserBinding) -> Unit, onDeleteUser: (UserBinding) -> Unit ) { val users by usersLiveData.observeAsState() Column(modifier = Modifier.fillMaxSize()) { InputPanel(currentUser, onInsertUser = { user -> onSaveUser(user) }) UserList( users = users ?: emptyList(), onDeleteUser = onDeleteUser ) } }
  • 82.
    app.sli.do/event/djw2yt2e LiveData @Composable fun UserScreen( usersLiveData: LiveData<List<UserBinding>>, onSaveUser:(UserBinding) -> Unit, onDeleteUser: (UserBinding) -> Unit ) { val users by usersLiveData.observeAsState() Column(modifier = Modifier.fillMaxSize()) { InputPanel(currentUser, onInsertUser = { user -> onSaveUser(user) }) UserList( users = users ?: emptyList(), onDeleteUser = onDeleteUser ) } }
  • 83.
    app.sli.do/event/djw2yt2e LiveData UserScreen( usersLiveData = viewModel.allUsers, onSaveUser= { user -> viewModel.saveUser(user) }, onDeleteUser = { user -> viewModel.deleteUser(user) } ) @Composable fun UserScreen( usersLiveData: LiveData<List<UserBinding>>, onSaveUser: (UserBinding) -> Unit, onDeleteUser: (UserBinding) -> Unit ) { … }
  • 84.
  • 85.
    app.sli.do/event/djw2yt2e Coroutines @Composable fun MyComposable() { valwelcomeMsg = remember { mutableStateOf("") } launchInComposition { val s = withContext(Dispatchers.IO) { delay(5_000) "Hello Compose!" } welcomeMsg.value = s } Text(text = welcomeMsg.value) } launchInComposition lança uma coroutine quando a composição é adicionada e a cancela automaticamente quando a execução deixa a composição.
  • 86.
    app.sli.do/event/djw2yt2e Coroutines @Composable fun MyComposable() { valscope = rememberCoroutineScope() val count = remember { mutableStateOf(0) } Text(text = "Current count: ${count.value}") Button(onClick = { scope.launch { for (i in 1..10) { withContext(Dispatchers.IO) { delay(1_000) } count.value = i } } }, content = { Text("Start!") }) } rememberCoroutineScope retorna o CoroutineScope associado ao ponto específico da composição. Deve ser usado para lançar coroutines em resposta à eventos de callback
  • 87.
  • 88.
    app.sli.do/event/djw2yt2e Conclusão • A ideiado Compose parece ser muito interessante, pois segue o paradigma moderno de desenvolvimento de UI. • Podemos pensar em UI em outras plataformas? • Está em versão alpha, logo NÃO USE EM PRODUÇÃO! • Quando será que teremos todos os recursos existentes no Toolkit atual? E quando teremos a mesma quantidade de componentes third party? • Nós, como desenvolvedores devemos estar preparados. 
 Pois desaprender é mais difícil que aprender 😉
  • 89.
    app.sli.do/event/djw2yt2e Referências • Página oficialdo Jetpack Compose
 https://developer.android.com/jetpack/compose • Codelab Jetpack Compose
 https://codelabs.developers.google.com/codelabs/jetpack-compose- basics/#0 • Jetpack Compose Samples
 https://github.com/android/compose-samples • Romain Guy Sample
 https://github.com/romainguy/sample-materials-shop
  • 90.
    app.sli.do/event/djw2yt2e Referências • Lista declasses do Compose
 https://developer.android.com/reference/kotlin/androidx/ui/classes • Compose Academy
 https://compose.academy/ • Classic Android to Jetpack (by Vinay Gaba)
 https://jetpackcompose.app/ • Canal #Compose no Slack do Kotlin
 slack.kotlinlang.org (#compose)
  • 91.
    app.sli.do/event/djw2yt2e Referências • Understanding Compose(Android Dev Summit 2019)
 https://www.youtube.com/watch?v=Q9MtlmmN4Q0 • What’s new in Jetpack Compose (Android Dev Summit 2019)
 https://www.youtube.com/watch?v=dtm2h-_sNDQ • Jetpack Compose (#Android11 - 2020)
 https://www.youtube.com/watch?v=U5BwfqBpiWU • Jetpack Compose - Next Gen Kotlin UI Toolkit for Android (Right?)
 https://www.youtube.com/watch?v=I5zRmCheVVg
  • 92.
    app.sli.do/event/djw2yt2e Referências • Thinking inCompose
 https://www.youtube.com/watch?v=SMOhl9RK0BA • Repositório do Jetpack Compose
 https://android.googlesource.com/platform/frameworks/support/+/refs/ heads/androidx-master-dev/compose/ • Request Features & Bug Tracker
 https://issuetracker.google.com/issues/new?component=612128
  • 93.