Flutter, JetPack Compose, Swift UI e Solidariedade. Parte II — Telas. JetPack Compose || Flutter, JetPack Compose, Swift UI and Solidarity. Flutter — Parte II — Screens. JetPack Compose.

Ricardo Ogliari
16 min readOct 13, 2022

--

Link Github: https://github.com/ricardoogliari/doted_jetpack_compose

Link da primeira parte — first part: https://ricardoogliari.medium.com/flutter-jetpack-compose-swift-ui-e-solidariedade-cb6085e2d207

Link da segunda parte — second part: https://ricardoogliari.medium.com/flutter-jetpack-compose-swift-ui-e-solidariedade-7e9a66726175

Depois da construção da primeira versão da aplicação no Flutter, agora vamos construir a mesma idéia inicial, porém, em uma aplicação nativa para Android, usando a linguagem de programação Kotlin e o Jetpack Compose. After building the first version of the application in Flutter, we are going to build the same initial idea, however, in a native application for Android, using the Kotlin programming language and Jetpack Compose.

O primeiro passo é usar o Android Studio para a criação do projeto. Aqui estou usando o Android Studio Chipmunk | 2021.2.1 Patch 1. Acredito ser uma das últimas versões disponíveis. Digo isso porque, somente nas últimas versões da IDE já temos a opção do “Empty Compose Acticity”. Isso é importante porque já cria um projeto com a estrutura pronta para uso do Jetpack Compose. The first step is to use Android Studio to create the project. I am using Android Studio Chipmunk | 2021.2.1 Patch 1. I believe it is one of the last versions available. I say this because, only in the latest versions of the IDE we already have the option “Empty Compose Acticity”. This is important because it already creates a project with the initial configuration for Jetpack Compose framework.

Depois de escolher o tipo do projeto vamos para suas configurações básicas. Usei basicamente as mesmas informações do projeto criado no Flutter. After choosing the project type, we go to basic settings. Use basically the same information as the project created in Flutter.

Antes de entrarmos realmente na codificação do nosso projeto, acredito ser importante dar uma olhada no código inicial criado pela própria IDE. Veja a listagem de código na sequência. Before we get into the definition of our project, it is important to study the initial code created by the IDE itself. See the code listing below.

No mundo Android, Activity representa uma tela. Logo, este projeto configuado para Jetpack Compose já possui uma classe filha de Activity chamada de ComponentActivity. Talvez, para um desenvolvedor iniciante o código não terá grandes surpresas. Porém, para um profissional Android pré-compose talvez esse trecho cause um certo espanto. In the Android world, Activity represents a screen. So, this project configured for Jetpack Compose already has a child class of Activity called ComponentActivity. Perhaps, for a novice developer the code will not have big surprises. However, for an Android professional before composer, this snippet might cause a bit of amazement.

No onCreate, que faz parte do ciclo de vida da Activity, temos uma chamada ao setContent. No lugar de chamar uma instância de uma classe que herde de View ou ViewGroup, temos chamadas a funções marcadas com a annotation @Composable. Mas a Surface não tem isso! O leitor poderá estar exclamando neste momento. Porém, se olharmos para o código do Surface veremos isso sim, lhe garanto. Já o Greeting podemos confirmar isso na própria sequência do código. In onCreate, which is part of the Activity lifecycle, we have a call to setContent. Instead of calling an instance of a class that inherits from View or ViewGroup, we have calls to functions marked with a @Composable annotation. But Surface doesn’t have that! However, if we look at the Surface code it does show that, I guarantee it. The greeting has the same annotation, we can confirm this in the code sequence itself.

Também, teremos uma última função com a anotação do @Composable, mas, também, a anotação do @Preview. Isso permite que na própria IDE, seja possível vermos uma representação visual do que está funcão apresentará. Also, we will have a last function with the @Composable annotation, but also the @Preview annotation. This allows that in the IDE itself to be able to see a visual representation of what our functions will present.

Talvez, neste ponto o leitor que acompanhou o artigo do Flutter já percebará algumas semelhanças. Não na linguagem de programação, mas na codificação do layout orientada aos dados. Uma função composable nada mais é do que uma resposta gráfica ao estado dos dados naquele dado momento. Também, percebam a idéia mais declarativa, também presente no Flutter. Maybe, at this point who followed the Flutter article might already look like similarities. Not programming language, but in data-oriented layout programming. A composable function is nothing more than a graphical response to the state of the data at that moment. Also, we introduce the declarative idea, also presents in Flutter.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DotedTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}

@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
DotedTheme {
Greeting("Android")
}
}

Desenvolvimento da aplicação — Application Development

A primeira mudança no projeto iniciado pela própria IDE é a criação de uma pasta model na raiz do projeto. Dentro da mesma, uma nova data class kotlin Story. Quer saber mais sobre este conceito? Indico esta leitura https://kotlinlang.org/docs/data-classes.html. The first change in the project started by the IDE itself is the creation of a model folder in the project project. Within it, a new kotlin data class, called Story. Do you want to know more about this concept? I recommend this reading https://kotlinlang.org/docs/data-classes.html.

O código da Story.kt ficará assim: The Story.kt code will look like this:

package com.trusted.donation.doted.model

data class Story(
val latitude: Double,
val longitude: Double,
val title: String,
val snippet: String,
val agree: Int,
val disagree: Int,
var distance: String = "...")

Agora, na MainActivity.kt, vamos criar uma val que armazenará a lista de histórias hipotéticas (por enquanto). Lembrando que ela fica fora da classe MainActivity. Now let’s create a val which it’s going to keep the hipotetic stories. Remembering that it is outside the MainActivity class.

class MainActivity : ComponentActivity() { ... }

private val stories = listOf(
Story(-28.263507236638343, -52.39932947157066, "Harosin", "Sollicitudin aliquam ipsum aptent id dictumst ligula curae libero senectus aliquet, cubilia scelerisque laoreet aliquet tempor quis fermentum ullamcorper interdum erat, massa placerat cubilia torquent arcu praesent tempor erat aptent.", 0, 0),
Story(-28.260889627977562, -52.400177049595555, "Dagar", "Viverra sodales vitae congue iaculis interdum class primis hac proin bibendum, diam erat ut aenean viverra gravida venenatis elit pulvinar conubia, primis est dui feugiat curae hac mauris egestas sodales. ", 0, 0),
Story(-28.261456624477784, -52.39208750743389, "Arve", "Habitant bibendum vel habitasse cursus quis sollicitudin dapibus tristique, congue suspendisse ut aptent ut tincidunt nam libero luctus, lorem ullamcorper quam ultricies congue curae pharetra. consectetur lacus faucibus sodales, imperdiet. ", 0, 0),
Story(-28.26597358890354, -52.402505206969835, "Husaol", "Pharetra pretium donec commodo torquent vestibulum class turpis, purus sed gravida dolor dictumst auctor adipiscing, mattis eros venenatis nostra augue rutrum. euismod malesuada etiam tellus cras fames, convallis donec sociosqu. ", 0, 0),
Story(-28.2705761904961, -52.39146318305821, "Tagalan", "Felis senectus habitasse facilisis torquent quis consectetur class, bibendum quam libero arcu pharetra proin iaculis nisl, praesent sed adipiscing nec nam iaculis. potenti imperdiet pellentesque facilisis nisl, quisque cursus purus. ", 0, 0),
Story(-28.242828, -52.381907, "Hiesaipen", "Mauris accumsan hendrerit consequat pharetra torquent elementum curabitur, etiam sed adipiscing cras vel tellus, a donec augue eu eget himenaeos. ", 0, 0),
Story(-28.241999, -52.438139, "Zokgaelu", "Nullam rutrum dictum mauris fermentum cursus quis fusce, litora augue pulvinar sem primis egestas, risus erat vestibulum curabitur lorem libero. ", 0, 0),
Story(-28.181835, -52.328675, "Curuas", "Tempor accumsan libero consequat phasellus tellus nullam mi, nibh placerat sagittis magna himenaeos tempus, rutrum proin lacus imperdiet ad nisl.", 0, 0),
Story(-28.124979, -52.296718, "Buigrak", "Vulputate elementum sem bibendum ad pretium pellentesque metus, nostra quisque in dolor lectus mollis, ante donec sapien netus laoreet congue. ", 0, 0),
Story(-28.107857, -52.145972, "Lomoa", "Dolor quisque tellus purus sagittis potenti ipsum nunc, porttitor magna sit aliquam erat lacinia, a phasellus curabitur diam tempus primis. ", 0, 0)
)

Antes de avançarmos, é bom relembralos sobre a tela que desejamos construir. Na parte inferior da tela, teremos um BottomNavigationBar, um elemento comum no material design. Before we go any further, it is good to remind you about the screen we want to build. At the bottom of the screen we will have a BottomNavigationBar, a common element in material design.

Figura 1: Tela inicial da aplicação. Application’s initial screen

Seguindo a idéia do Jetpack Compose, vamos fazer funções que respondem graficamente a cada parte da tela, levando em conta os dados correntes. Outro detalhe indicado pela documentação é evitarmos ao máximo criar funções que podem conter algum efeito colateral. Com esses requisitos em mente, veja como ficou nossa primeira função, que está dentro do arquivo MainActivity.kt. Following the idea of ​​Jetpack Compose, let’s make functions that respond graphically to each part of the screen, using the current data. Another detail indicated by the documentation is to avoid as much as possible creating functions that may contain some unwaited effect. With these requirements in mind, here’s what our first function looks like, which is inside the MainActivity.kt file.

A annotation @Composable é um dos pontos principais aqui, e será algo corriqueiro no desenvolvimento com o Compose. Segundo a documentação é sempre bom deixar como parâmetro uma instância de Modifier, deixando em aberto a possibilidade de customização por parte do chamador deste componente. O segundo parâmetro da função recebe uma função que será acionada dentro do corpo da mesma, indicando ao chamador que a tab do BottomNavigation foi alterada. Perceba que isso evita o aparecimento de efeito colateral, principalmente quando entrarmos no quesito de tratamento de estados. The @Composable annotation is one of the main points here, and it will be a commonplace in development with Compose. According to the documentation, it is always good to leave a Modifier instance as a parameter, leaving open the possibility of customization by the caller of this component. The second parameter of the function receives a function that will be triggered within the body of the same, indicating to the caller that the BottomNavigation tab has been changed. Note that this avoids the appearance of unwanted effects, especially when we get into the states management.

Outro ponto importante, é se atentar ao uso da programação declarativa, bem distinta da forma usual de contrução de intefaces do Android com o uso do XML. Another important point is a focus in declarative programming, very different from the usual way of building Android interfaces using XML.

@Composable
private fun CustomBottomNavigation(
modifier: Modifier = Modifier,
currentIndex: Int = 0,
onSelected: (index: Int) -> Unit) {
BottomNavigation(
backgroundColor = MaterialTheme.colors.background,
modifier = modifier
) {
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Default.Map,
contentDescription = null
)
},
selected = currentIndex == 0,
onClick = {
onSelected(0)
}
)
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Default.List,
contentDescription = null
)
},
selected = currentIndex == 1,
onClick = {
onSelected(1)
}
)
}
}

Saindo das tabs na parte inferior, vamos para a parte central da tela, que poderá ser um mapa ou uma lista. Como anteriormente, teremos uma função marcada com @Composable que, responde graficamente ao estados dos dados naquele instante. Veja como o código ficou: Leaving the tabs at the bottom, we go to the central part of the screen, which can be a map or a list. As before, we will have a function marked with @Composable that graphically responds to the state of the data at that moment. Here’s the code:

@Composable
fun MapTab(modifier: Modifier = Modifier, currentPosition: LatLng){
val cameraPositionState = CameraPositionState(
position = CameraPosition.fromLatLngZoom(currentPosition, 15f)
)

GoogleMap(
modifier = modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
) {
Marker(
state = MarkerState(position = currentPosition)
)
}
}

Cabe ressaltar que foram necessárias algumas mudanças no build.gradle do módulo, para fornecer suporte a biblioteca de mapas específica para uso no Jetpack Compose, além da funcionalidade de geolocalização e por fim, alguns ícones que foram usados nas telas. It should be noted that some changes were necessary in the module’s build.gradle, to support the specific map library for use in Jetpack Compose, library for geolocation functionality and finally, library for some icons that were used on the screens.

implementation 'com.google.android.gms:play-services-location:20.0.0'
implementation 'com.google.maps.android:maps-compose:2.5.3'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation "androidx.compose.material:material-icons-extended:$compose_version"

A outra tab do aplicativo mostrará uma listagem dos mesmos pontos, porém, mudando somente a forma de representação dos mesmos. Lembrando que os dados do mapa e da lista ainda são todos simulados, então, não farão sentidos neste momento. Pelo menos no que diz respeito as histórias apresentadas. The other tab of the application will show a list of the same points, however, only changing the way they are represented. Remembering that the map and list data are all still simulated, so they won’t make sense at this point. At least the stories presented at this moment.

A fun recebe um modificador, levando em conta os mesmos preceitos discorridos anteriormente. Além disso, seu último parâmetro recebe a posição corrente em uma instância da classe LatLng. No seu corpo usamos um LazyColumn, que, como o nome indica, é uma listagem vertical de itens que vai se populando conforme o evento de scroll do usuário. The fun receives a modifier, using the same conecpts discussed above. Furthermore, its last parameter receives the current position in an instance of the LatLng class. In its body we use a LazyColumn, which, as the name implies, is a vertical listing of items that is populated according to the user’s scroll event.

O items vai iterar sobre as histórias, chamando uma função interna anônima em cada um deles. Em cada caso, vamos chamar a função itemListTab (será estudado na sequência) e também criar uma instâncoa do composable Divider. The items will iterate over the stories, calling an anonymous built-in function on each of them. In each case, we will call the itemListTab function (which will be discussed next) and also create an instance of the composable Divider.

@Composable
fun ListTab(modifier: Modifier = Modifier, currentPosition: LatLng){
LazyColumn(
contentPadding = PaddingValues(all = 16.dp),
modifier = modifier,
) {
items(stories) { item ->
itemListTab(modifier, item, currentPosition)
Divider()
}
}
}

Antes de continuar, relembre que a aplicação tem somente uma tela, porém, uma aba inicial de mapa e uma de lista, que estamos trabalhando na sua codificação neste momento. Before continuing, remember that the application has only one screen, however, an initial map tab and a list tab, which we are currently working on coding.

Figura 2: aba de listagem de histórias.

Perceba que cada item é visualmente igual, alterando apenas o conteúdo de cada elemento. Sendo assim, criamos a função que será responsável por criar o componente (Composable) de cada um destes itens. Realize that each item is visually the same, changing only the content of each element. Therefore, we create the function that will be responsible for creating the component (Composable) of each of these items.

Esta função traz alguns detalhes extras interessantes. Já havíamos usado o Column anteriormente (LazyColumn) e agora usamos o Row. Ambos serão usados de forma muito intensiva na construção de layouts complexos com o Jetpack Compose. Além disso, temos tratamento de distância entre dois pontos (classe Location e método estático distanceBetween) e uso do modifier com o fillMaxWidth, padding e weight. This function brings some interesting extra details. We had already used Column before (LazyColumn) and now we use Row. Both will be used very intensively in building complex layouts with Jetpack Compose. In addition, we have calculate of distance between two points (Location class and distanceBetween static method) and use of the modifier with fillMaxWidth, padding and weight.

@Composable
fun itemListTab(modifier: Modifier = Modifier, story: Story, currentPosition: LatLng){
var floatResults = FloatArray(1)
val df = DecimalFormat("#.##")
Location.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
story.latitude,
story.longitude,
floatResults
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
Column(
modifier = Modifier.weight(1f).padding(horizontal = 16.dp)
) {
Text(text = story.title)
Text(text = story.snippet)
}
Text(text = "${df.format(floatResults[0] / 1000)} km")
}
}

A próxima função composable é a mais importante desta explicação, justamente porque vamos falar sobre o conceito de estado, statefull e stateless. Calma leitor, se você é desenvolvedor Flutter não se engane, ainda estamos falando de Jetpack Compose :). The next composable function is the most important in this explanation, precisely because we are going to talk about the concept of state, statefull and stateless. Calm down reader, if you are a Flutter developer don’t get involved, we are still talking about Jetpack Compose :).

Primeiramente, ignore as duas primeiras linhas da função, pois são conceitos fundamentais que precisamos aprender com mais calma. O primeiro teste lógico é apenas para saber se as permissões para geolocalização foram fornecidas pelo usuário. Claro, este projeto de aplicativo ainda está na fase inicial e no futuro pensarei em um tratamento melhor para exigir o aceite destas permissões. Ignore the first two lines of the function, they are fundamental concepts that we will learn more calmly. The first logical test is just to see if users provided permissions for geolocation feature. Of course, this app project is still at an early stage and is not thinking about a better handling for the future to require the accept these permissions.

No caso do else, estamos com as permissões necessárias e podemos utilizar a instância de FusedLocationProviderCliente para consultar a última localização conhecida. Perceba que no listener de sucesso estamos re-populando a variável position. Logo, logo falaremos mais sobre ela. In the case of the else, we have the necessary permissions and we can use the FusedLocationProviderCliente instance to query the last known location. Notice that in the success listener we are repopulating the position variable. Soon, we will talk more about it.

Depois do if-else temos algo bem pontual em relação aos projetos criados no Android Studio com o Jetpack Compose. Um tema já é criado por padrão, com o nome do projeto acrescido de Theme. Perceba que na estrutura de pastas já teremos uma delas com o nome ui.theme. Dentro dela um arquivo Theme.kt que, por sua vez, possui uma função @Composable com o nome do projeto acrescido de Theme. Como mostra a Figura depois da listagem de código. After the if-else we have something very specific about projects created in Android Studio with Jetpack Compose. A theme is already created by default, with the project name appended to Theme. Notice that in the folder structure we will already have one folder with the name ui.theme. Inside it is a Theme.kt file which has a @Composable function with the name of the project plus Theme. As shown in Figure after code listing.

Olha o Scaffold do Flutter aí!!! Ops, ainda não. Estamos no Kotlin e falando sobre Jetpack Compose ainda. :). Porém, para os desenvolvedores Flutter podem pensar nessa classe com a mesma finalidade mesmo. O biblioteca do Compose já nos traz essa possibilidade para criarmos uma aplicação seguindos os preceitos do Material Design. Pra finalizar essa parte, o teste lógico com o index, serve para sabermos se precisamos mostrar a visão de mapa ou lista. Look at the Flutter’s Scaffold here!!! Oops, not yet. We’re in Kotlin and talking about Jetpack Compose yet. :). However, for Flutter developers they can think of this class with the same purpose. The Compose library already gives us this possibility to create an application following the concepts of Material Design. To finish this part, the logical test with the index serves to know if we need to show the map or list view.

@Composable
fun DotedApp(context: Context) {
var index by rememberSaveable { mutableStateOf(0) }
var position by rememberSaveable { mutableStateOf(LatLng(0.0, 0.0)) }
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
// TODO: Consider calling ActivityCompat#requestPermissions
} else {
fusedLocationClient.lastLocation
.addOnSuccessListener { location: Location? ->
location?.let {
position = LatLng(it.latitude, it.longitude)
}
}
}

DotedTheme {
Scaffold(
topBar = {
Text(
modifier = Modifier.padding(all = 16.dp),
text = "We Need Help"
)
},
bottomBar = {
CustomBottomNavigation(currentIndex = index) {
index = it
}
}
) { padding ->
if (index == 0)
MapTab(Modifier.padding(padding), position)
else
ListTab(Modifier.padding(padding), position)
}
}
}

Vamos voltar para as duas primeiras linhas da função, que são de fundamental importância para a aplicação e para os conceitos que ser aprendidos no Jetpack Compose. Let’s go back to the first two lines of the function, which are fundamental for the application and for the concepts that will be learned in Jetpack Compose.

As duas variáveis são estados que controlam as chamadas para as funções ou, ao processo de recomposição. Como esta função tem estes estados, dizemos que a mesma é uma stateful. As funções que codificamos anteriormente eram chamadas de stateless, porque não haviam estados controlando as mesmas, pelo menos diretamente. The two variables are states that control the calls to the functions or the recomposition process. As this function has these states, we say that it is stateful. The functions we coded earlier were called stateless, because there were no states controlling them, at least directly.

Ah, convém falar da diferença entre o remember e o rememberSaveable. No último caso, o estado é salvo quando ocorrem mudanças na configuração do aplicativo. Por exemplo, quando acontece uma rotação na tela. Se usarmos somente o remember e irmos para a tab de lista, ao rotacionar a tela, o aplicativo volta para a tab de mapa, ou seja, ocasionando um problema. It’s worth talking about the difference between remember and rememberSaveable. In the latter case, the state is saved when the application configuration changes. For example, when the screen rotates. If we only use remember and go to the list tab, when rotating the screen, the application goes back to the map tab, that is, causing a problem.

Essa forma como programamos o controle das tabs segue o conceito chamado de state hoisting. Onde um stateful controla outros stateless. Que é justamente o que fizemos com a função DotedApp controlando a MapTab e ListTab, em relação a passagem dos dados de geoposicionamento (position). This way we program the tab control follows the concept called state hoisting. Where one stateful controls other stateless. Which is exactly what we did with the DotedApp function controlling the MapTab and ListTab, regarding the passage of geopositioning data (position).

var index by rememberSaveable { mutableStateOf(0) }
var position by rememberSaveable { mutableStateOf(LatLng(0.0, 0.0)) }

Para finalizar, temos a classe que herda de MainActivity e chamada a sequência de funções @Composable. Para os programadores Android das antigas, além da herança de ComponentActivity, também temos uma mudança no setContent. Não passamos uma instância de View ou ViewGroup, mas sim, uma função com a annotation Composable. Finally, we have the class that inherits from MainActivity and call the sequence of functions @Composable. For old-school Android programmers, in addition to the ComponentActivity inheritance, we also have a change to setContent. We don’t pass a View or ViewGroup instance, but a function with the Composable annotation.

private lateinit var fusedLocationClient: FusedLocationProviderClient

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
setContent {
DotedApp(this)
}
}
}

Com isso, chegamos ao fim da primeira versão do aplicativo Doted para Android nativo, com Jetpack Compose. Acredito que o leitor tenha percebido as diversas semelhanças entre Flutter, State, Stateless, Stateful e Compose. Obrigado pela leitura. Comentários são sempre bem vindos. With that, we’ve reached the end of the first version of the Doted app for native Android, with Jetpack Compose. I believe the reader has noticed the many similarities between Flutter, State, Stateless, Stateful, and Compose. Thanks for reading. Comments are always welcome.

Observação: deixo aqui um artigo sensacional sobre estados no Compose — https://developer.android.com/jetpack/compose/state

Note: I leave here a sensational article about states in Compose — https://developer.android.com/jetpack/compose/state

--

--

No responses yet