Neste tutorial, você vai apreender a criar um aplicativo de bloco de notas, utilizando componente de IU simples com funções declarativas. Você não vai editar nenhum layout XML nem usar o Layout Editor. Em vez disso, você chamará funções de composição para definir quais elementos quer usar e o compilador do Compose fará o restante. O App será desenvolvido na Arquitetura MVVM, base de dados Room para salvar, lista, alterar e excluir os dados localmente e o Hilt & Dagger para injeção de dependência.
Pré-requisitos
É necessário conhecer o Kotlin, os conceitos de projeto orientados a objetos e os princípios básicos de desenvolvimento do Android, principalmente.
- Jetpack Compose
- Banco de dados SQLite e a linguagem de consulta SQLite. O que você aprenderá
- Corrotinas básicas.
- Android Studio 4.0 ou mais recente e conhecimento sobre como usá-lo.
O que você aprenderá
Você aprenderá a projetar e construir um app usando a IDE Android Studio
- Implementar a arquitetura MVVM
- Trabalhar com um banco de dados, implementar CRUD completo.
- Criar UI Jepack Compose
Jetpack Compose
é um kit de ferramentas moderno recomendado pelo Android para criar IUs nativas. Ele simplifica e acelera o desenvolvimento da IU no Android.
Composable Preview: por meio de uma anotação de @Preview em uma função @Composable, na aba Split e/ou design do Android Studio, é possível visualizar o componente criado;
Modo Iterativo: nessa ferramenta, é possível interagir com a visualização no próprio Android Studio e testar diferentes funcionalidades, como alterar o estado de um radiobutton, sem a necessidade de um emulador;
Deploy Preview: nesta funcionalidade é possível rodar um @Preview — clicando no ícone do emulador na opção ‘Run’ — diretamente no emulador o que permite, além de uma simulação mais verossímil, utilizar as permissões e o contexto geral da aplicação.
funções de composição:Essas funções permitem que você defina a IU do app de maneira programática, descrevendo as dependências de dados e de formas dela, em vez de se concentrar no processo de construção da IU (inicializando um elemento, anexando-o a um pai etc.). Para criar uma função que pode ser composta, basta adicionar a anotação @Composable ao nome da função.
Componentes Arquitetura MVVM
Veja um diagrama simples que apresenta os Componentes da arquitetura e como eles funcionam juntos.
Entidade: classe com anotação que descreve uma tabela de banco de dados ao trabalhar com o Room.
Banco de dados do Room: simplifica o trabalho com o banco de dados e serve como ponto de acesso para o banco de dados SQLite (oculta SQLiteOpenHelper). O banco de dados do Room usa o DAO para realizar consultas ao banco de dados SQLite.
Banco de dados SQLite: armazenamento no dispositivo. A biblioteca de persistência Room cria e mantém esse banco de dados para você.
Repositório: uma classe que você cria e que é usada principalmente para gerenciar várias fontes de dados.
DAO: objeto de acesso a dados. Um mapeamento de consultas SQL para funções. Ao usar um DAO, você chama os métodos e o Room faz o resto.
LiveData: uma classe armazenadora de dados. Ela sempre mantém/armazena em cache a versão mais recente dos dados e notifica os observadores quando os dados mudam.
ViewModel: atua como um centro de comunicação entre o repositório (dados) e a IU. A IU não precisa mais se preocupar com a origem dos dados. As instâncias do ViewModel sobrevivem à recriação de atividade/fragmento.
Criação dos pacotes para organização das camadas da aplicação
Configuração do Ambiente
Antes de começar, é importante configurar o ambiente de desenvolvimento. Certifique-se de que o Android Studio esteja instalado e configurado corretamente para desenvolvimento Android. Você também precisará das seguintes dependências no seu projeto:
Kotlin: A linguagem de programação oficial para o desenvolvimento Android.
Jetpack Compose: Uma biblioteca moderna para a criação da UI.
Room: Uma biblioteca de persistência para trabalhar com bancos de dados locais.
Abra build.gradle (Module: app).
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'//Adicione
id 'dagger.hilt.android.plugin'//adicione
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.6.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.3.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
// Jetpack Compose Navigation
implementation("androidx.navigation:navigation-compose:$nav_version")
// Room components
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// Dagger - Hilt
implementation('com.google.dagger:hilt-android:2.45')
kapt('com.google.dagger:hilt-android-compiler:2.45')
kapt "androidx.hilt:hilt-compiler:1.0.0"
//icons
implementation "androidx.compose.material:material-icons-extended:1.3.1"
//swipe
implementation 'me.saket.swipe:swipe:1.1.1'
}
No arquivo build.gradle (Project: JetNotes), adicione os números de versão ao final do arquivo, conforme abaixo:
buildscript {
ext {
compose_ui_version = '1.3.3'
nav_version = "2.5.3"
room_version = '2.5.0'
}
}
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id("com.google.dagger.hilt.android") version "2.44" apply false
}
Criar Entidade (modelo)
O modelo é responsável pela lógica de negócios e pelos dados do aplicativo. Neste exemplo, usaremos o Room para criar um banco de dados local e definir a entidade NoteModel:
Crie um novo arquivo de classe do Kotlin com o nome NoteModel contendo a classe de dados. Esta classe descreve a entidade (que representa a tabela SQLite). Cada propriedade na classe representa uma coluna na tabela. A Room usará estas propriedades para criar a tabela e instanciar objetos de linhas no banco de dados.
Veja o Código Abaixo:
@Entity(//representa a tabela no banco
//nome da tabela no banco de dados
tableName = "tb_note",
indices = [
Index("title", unique = true)//Regra no banco para não cadastrar titulos iquais
]
)
data class NoteModel(
@PrimaryKey(autoGenerate = true)//chave primaria
@ColumnInfo(name = "id")//Anotação especifica o nome para a tabela no banco sqlite, caso queira mudar
val id: Int = 0,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "description")
val description: String
)
Vejamos o que essas anotações fazem:
• @Entity(tableName = "tb_note") Cada classe @Entity representa uma tabela SQLite.
• @PrimaryKey Toda entidade precisa de uma chave primária. Para simplificar, cada palavra funciona como a própria chave primária.
• @ColumnInfo(name = "id") Especifica o nome da coluna na tabela se você quiser que seja diferente do nome da variável de membro. Dessa forma, a coluna terá o nome "id”, “title”,”description”
CRIAR DAO (Data Access Object) do App
É uma parte fundamental de qualquer aplicativo que envolve a persistência de dados. Ele age como uma camada de abstração entre a lógica de negócios do aplicativo e o banco de dados, facilitando a criação, leitura, atualização e exclusão (CRUD) dos dados.
Para operações comuns do banco de dados, a biblioteca Room fornece anotações de conveniência, como @Insert, @Delete e @Update. A anotação @Query é usado para todo o restante. É possível programar qualquer consulta com suporte do SQLite.
OBS: O DAO precisa ser uma interface ou uma classe abstrata.
@Dao
interface NoteDao {
//Listar Anotações
@Query("SELECT * FROM tb_notes ORDER BY id ASC")
fun getAllNotes(): Flow<List<NoteModel>>
//Selecionar Anotação
@Query("SELECT * FROM tb_notes WHERE id=:noteID")
fun selectNoteID(noteID: Int): Flow<NoteModel?>
//Inserir Anotações
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(noteModel: NoteModel)
//Atualizar Anotação
@Update
suspend fun updateNote(noteModel: NoteModel)
//Deletar Anotação
@Delete
suspend fun deleteNote(noteModel: NoteModel)
}
obs: Usar o Flow ou o LiveData como tipo de retorno vai garantir que uma notificação seja enviada sempre que os dados no banco de dados mudarem.
IMPLEMENTAR BANCO DE DADOS
A classe do banco de dados da Room precisa ser abstrata e estender RoomDatabase. Normalmente, você só precisa de uma instância de um banco de dados da Room para todo o app.
A anotação @Database requer vários argumentos para que o Room possa criar o banco de dados.
• Especifique o NoteModel como a única classe com a lista de entities.
• Defina a version como 1. Sempre que o esquema da tabela do banco de dados mudar, será necessário aumentar o número da versão.
• Defina o exportSchema como false, para não manter os backups do histórico de versões do esquema
Veja abaixo:
//A anotação @Database requer rgumentos para que o Room possa criar o banco de dados. Após
//liste as entidades do banco de dados e configure o número da versão.
@Database(
//listar entidades do App
entities = [
NoteModel::class
],
//versão do banco de dados
version = 1,
//exportar DB declarar com false p/ não manter historico do DB
exportSchema = false
)
abstract class NoteDatabase: RoomDatabase() {
//função para o retorna NoteDao
abstract fun noteDao(): NoteDao
}
Criar instância para a Banco de dados
Vamos um objeto NoteModule, defina um método getNoteDb() com um parâmetro Context, necessário para o builder do banco de dados.
Vamos criar um metodo para que retorne um tipo getNoteDao.
@Module//fornecer instâncias de determinados tipos
@InstallIn(SingletonComponent::class)//informar em qual classe do Android cada módulo vai ser usado ou instalado.
@Module
@InstallIn(SingletonComponent::class)
object NoteModule {
@Provides
@Singleton
fun getNoteDb(
@ApplicationContext
context: Context)= Room.databaseBuilder(
context = context,
NoteDB::class.java,
"note.db"
).build()
@Provides
@Singleton
fun getNoteDao(db: NoteDB) = db.noteDao()
}
Classe do Aplicativo Hilt
Todos os apps que usam o Hilt precisam conter uma classe Application anotada com @HiltAndroidApp.
O @HiltAndroidApp aciona a geração de código do Hilt, incluindo uma classe base para seu aplicativo que serve como contêiner de dependências no nível do app.
@HiltAndroidApp
class NoteApplication: Application()
Esse componente Hilt gerado é anexado ao ciclo de vida do objeto Application e fornece dependências a ele. Além disso, ele é o componente pai do app, o que significa que outros componentes podem acessar as dependências fornecidas.
Injetar dependências na classe principal do App
Depois que o Hilt é configurado na classe Application e um componente no nível do aplicativo está disponível, ele pode fornecer dependências para outras classes do Android que tenham a anotação @AndroidEntryPoint:
Main Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private lateinit var navHostController: NavHostController
private val noteViewModel: NoteViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotesTheme {
navHostController = rememberNavController()
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
NavScreen(navHostController = navHostController, noteViewModel = noteViewModel)
}
}
}
}
}
Criar Repositório
Uma classe de repositório abstrai o acesso a várias fontes de dados. O repositório não faz parte das bibliotecas dos Componentes da arquitetura, mas é uma prática recomendada para a separação e arquitetura do código. Uma classe de repositório fornece uma API limpa para acesso aos dados no restante do aplicativo.
Por que usar um repositório?
Um repositório gerencia consultas e permite usar vários back-ends. No exemplo mais comum, o repositório implementa a lógica para decidir se precisa buscar dados de uma rede ou usar resultados armazenados em cache em um banco de dados local.
Como implementar o repositório
Crie um arquivo de classe do Kotlin chamado NoteRepo e cole o código a seguir nele:
class NoteRepo @Inject constructor(
private val noteDao: NoteDao
){
fun getAllNotes(): Flow<List<NoteModel>> {
return noteDao.getAllNotes()
}
suspend fun insertNote(note: NoteModel) {
return noteDao.insertNote(note = note)
}
suspend fun updateNote(note: NoteModel) {
return noteDao.updateNote(note = note)
}
suspend fun deletetNote(note: NoteModel) {
return noteDao.deletetNote(note = note)
}
}
Classe Selada da aplicativo
Vamos criar uma classes seladas Kotlin e aproveitá-las para gerenciar os estados da nossa aplicação.
Uma subclasse de uma classe selada pode ter várias instâncias. Isso permite que objetos de classes seladas contenham estado. Nesse casos quatros estados possíveis: parado, carregando, sucesso e erro.
sealed class ResultState<out T>{
object Idle: ResultState<Nothing>()
object Loading:ResultState<Nothing>()
data class Sucess<out T>(val data: T): ResultState<T>()
data class Error(val exception: Throwable): ResultState<Nothing>()
}
Criar Viewl Model
O ViewModel atua como um intermediário entre a Model e a View. Ele é responsável por fornecer os dados necessários para a View e processar as ações do usuário.
O ViewModel encapsula a lógica de apresentação e transformação de dados, permitindo que a View seja agnóstica em relação à lógica de negócios subjacente.
Ele pode conter propriedades observáveis que notificam a View quando os dados mudam, permitindo uma ligação de dados eficiente.
viewModelScope é um CoroutineScope predefinido que está incluído nas extensões KTX ViewModel. Todas as corrotinas precisam ser executadas em um escopo. Um CoroutineScope gerencia uma ou mais corrotinas relacionadas.
launch é uma função que cria uma corrotina e envia a execução do corpo funcional para o agente correspondente.
Dispatchers.IO indica que essa corrotina deve ser executada em uma linha de execução reservada para operações de E/S.
HiltViewModel O HiltViewModel é uma anotação específica do Hilt que pode ser usada para marcar classes ViewModel. Ela permite a injeção de dependência em ViewModels de maneira transparente. É o utilizaremos neste projeto.
@HiltViewModel
class NoteViewModel @Inject constructor(
private val noteRepo: NoteRepo
): ViewModel() {
val id: MutableState<Int> = mutableStateOf(0)
val title: MutableState<String> = mutableStateOf("")
val description: MutableState<String> = mutableStateOf("")
private val _getNotes = MutableStateFlow<ResultState<List<NoteModel>>>(ResultState.Idle)
val getNotes: StateFlow<ResultState<List<NoteModel>>> = _getNotes
private val _selectedNote: MutableStateFlow<NoteModel?> = MutableStateFlow(null)
val selectedNote: StateFlow<NoteModel?> = _selectedNote
//listar Anotações
fun getNotes(){
viewModelScope.launch {
val result = try {
noteRepo.getAllNotes().collect{
_getNotes.value = ResultState.Sucess(it)
}
}catch (error: Exception){
ResultState.Error(error)
}
Log.d("RESULTSATE","$result")
}
}
//inserir anotações
private fun insertNote(){
viewModelScope.launch(Dispatchers.IO){
val note = NoteModel(
title = title.value,
description = description.value
)
noteRepo.insertNote(noteModel =note)
}
}
//atualizar anotações
private fun updateNote(){
viewModelScope.launch(Dispatchers.IO){
val note = NoteModel(
id = id.value,
title = title.value,
description = description.value
)
noteRepo.updateNote(noteModel = note)
}
}
//deletar anotações
private fun deleteNote(){
viewModelScope.launch(Dispatchers.IO){
val note = NoteModel(
id = id.value,
title = title.value,
description = description.value
)
noteRepo.deleteNote(noteModel = note)
}
}
//selecionar Anotação
fun getSelectNoteID(noteID: Int){
viewModelScope.launch {
noteRepo.selectNoteID(noteID = noteID).collect{
_selectedNote.value = it
}
}
}
//Set Anotação Selecionada nos campos
fun updateNotesFields(selectedNote: NoteModel?){
if (selectedNote !=null){
id.value = selectedNote.id
title.value = selectedNote.title
description.value = selectedNote.description
}else{
//limpar os campos:
id.value = 0
title.value = ""
description.value = ""
}
}
//validar campos
fun validateFields():Boolean{
return title.value.isNotEmpty() && description.value.isNotEmpty()
}
//eventos o banco
fun dbHandle(action: String): String{
var result = action
when(result){
"INSERT" ->{
insertNote()
}
"UPDATE"->{
updateNote()
}
"DELETE"->{
deleteNote()
}else ->{
result = "NO_EVENT"
}
}
return result
}
Criar View Home do App Ou Tela Principal do App
A View é a camada de apresentação do aplicativo, que é responsável por exibir os dados ao usuário e capturar as interações do usuário, como toques, cliques e gestos.
Ela não deve conter lógica de negócios e, em vez disso, se concentra em representar visualmente os dados do aplicativo.
As Views geralmente consistem em componentes da interface do usuário, como botões, campos de texto, listas e gráficos.
package br.com.gilbercs.notes.ui.screens
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import br.com.gilbercs.notes.R
import br.com.gilbercs.notes.ui.components.HomeFab
import br.com.gilbercs.notes.ui.screens.home.HomeContent
import br.com.gilbercs.notes.ui.theme.ELEVATION_BAR
import br.com.gilbercs.notes.viewmodel.NoteViewModel
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun HomeScreen(
navController: NavController,
noteViewModel: NoteViewModel){
LaunchedEffect(key1 = Unit){
noteViewModel.getNotes()
}
val getAllNotes by noteViewModel.getNotes.collectAsState()
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
modifier = Modifier
.fillMaxWidth(),
elevation = ELEVATION_BAR,
backgroundColor = MaterialTheme.colors.surface,
title = { Text(text = stringResource(id = R.string.home_app_bar), fontWeight = FontWeight.Bold)}
)
},
content = {it
HomeContent(
getAllNotes = getAllNotes,
navController = navController,
onDelete = {event, note ->
noteViewModel.dbHandle(event)
noteViewModel.updateNotesFields(selectedNote = note)
})
},
floatingActionButtonPosition = FabPosition.Center
,
floatingActionButton = {
HomeFab(navClick = navController)
}
)
}
Criar View HomeContent
O HomeContent é um componente de interface do usuário que vai representa os items na tela inicial. É projetado para fornecer uma experiência de usuário atraente e informativa logo no momento em que o aplicativo é aberto. Pode incluir uma variedade de elementos, como:
Lista de Conteúdo: Abaixo do AppBar, o HomeContent geralmente exibe uma lista de conteúdo relevante para a tela inicial. Isso pode incluir notícias, atualizações de status, produtos em destaque, ou qualquer outro tipo de informação importante para o usuário.
Botões de Ação: O HomeContent pode conter botões ou ícones de ação que permitem aos usuários realizar tarefas específicas, deletar, atualizar, criar um novo item ou acessar áreas específicas do aplicativo.
package br.com.gilbercs.notes.ui.screens.home
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import br.com.gilbercs.notes.data.model.NoteModel
import br.com.gilbercs.notes.ui.screens.EmptyContent
import br.com.gilbercs.notes.ui.theme.*
import br.com.gilbercs.notes.util.ResultState
import kotlinx.coroutines.launch
import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox
@Composable
fun HomeContent(
getAllNotes: ResultState<List<NoteModel>>,
navController: NavController,
onDelete: (String,NoteModel) -> Unit){
val listNotes: List<NoteModel>
if (getAllNotes is ResultState.Sucess){
listNotes = getAllNotes.data
if (listNotes.isEmpty()){
EmptyContent()
}else{
LazyColumn{
items(items = listNotes){note->
Card(modifier = Modifier
.fillMaxSize()
.padding(all = ALL_PADDING),
elevation = CARD_ELEVATION,
shape = RoundedCornerShape(size = CARD_SHAPE)
){
SwipeNote(note = note, navController = navController) {
onDelete(
"DELETE",
note
)
}
}
}
}
}
}
}
@Composable
fun SwipeNote(
note: NoteModel,
navController: NavController,
onSwipe: () -> Unit ={}){
val scope = rememberCoroutineScope()
//swipe
val deleteSwipe = SwipeAction(
onSwipe = {
scope.launch {
onSwipe()
}
},
icon = {
Icon(
modifier = Modifier
.padding(ALL_PADDING)
.size(ICON_SWIPE),
imageVector = Icons.Default.DeleteForever,
contentDescription = "",
tint = Color.White)
},
background = Color.Red
)
val updateSwipe = SwipeAction(
onSwipe = {
navController.navigate("note_screen/${note.id}")
},
icon = {
Icon(
modifier = Modifier
.padding(ALL_PADDING)
.size(ICON_SWIPE),
imageVector = Icons.Default.EditNote,
contentDescription = "",
tint = Color.White)
},
background = MaterialTheme.colors.primaryVariant
)
SwipeableActionsBox(
swipeThreshold = ICON_THRESHOLD,
startActions = listOf(updateSwipe),
endActions = listOf(deleteSwipe)
) {
NoteItem(note = note)
}
}
@Composable
fun NoteItem(note: NoteModel){
//declaração de variavel
var expanded by remember{ mutableStateOf(false) }
Row(modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.surface)
.padding(all = ALL_PADDING)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = note.title, fontSize = TITLE, fontWeight = FontWeight.Bold)
if (expanded){
Text(text = note.description, fontSize = TEXT_DEFAULT)
}
}
IconButton(onClick = { expanded =! expanded }) {
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (expanded){
"Menos"
}else{"Mais"})
}
}
}
Cria view Cadastro
package br.com.gilbercs.notes.ui.screens.note
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.navigation.NavController
import br.com.gilbercs.notes.data.model.NoteModel
import br.com.gilbercs.notes.viewmodel.NoteViewModel
@Composable
fun NoteScreen(
navController: NavController,
noteViewModel: NoteViewModel,
selected: NoteModel?){
//declaração da variaveis
var title by noteViewModel.title
var description by noteViewModel.description
Scaffold(
topBar = {
if (selected==null){
CreateNoteTopBar(navController = navController, noteViewModel = noteViewModel)
}else{
UpdateNoteTopBar(navController = navController, noteViewModel = noteViewModel)
}
},
){it
NoteContent(
title = title,
onTitle = {title = it},
description = description,
onDescription = {description = it})
}
}
Criar NoteContent
package br.com.gilbercs.notes.ui.screens.note
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import br.com.gilbercs.notes.R
import br.com.gilbercs.notes.ui.theme.ALL_PADDING
import br.com.gilbercs.notes.ui.theme.SPACER_HEIGHT
import br.com.gilbercs.notes.ui.theme.TEXT_DEFAULT
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun NoteContent(
title: String,
onTitle:(String)-> Unit,
description: String,
onDescription:(String) ->Unit
){
val keyboardController = LocalSoftwareKeyboardController.current
Column(modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.surface)
.padding(ALL_PADDING)) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = title,
onValueChange = {onTitle(it)},
label = { Text(text = stringResource(id = R.string.note_title), fontSize = TEXT_DEFAULT)},
placeholder = { Text(text = stringResource(id = R.string.place_holder_note_title), fontSize = TEXT_DEFAULT)},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
)
)
Spacer(modifier = Modifier.height(SPACER_HEIGHT))
OutlinedTextField(
modifier = Modifier.fillMaxSize(),
value = description,
onValueChange = {onDescription(it)},
label = { Text(text = stringResource(id = R.string.note_description), fontSize = TEXT_DEFAULT)},
placeholder = { Text(text = stringResource(id = R.string.place_holder_note_description), fontSize = TEXT_DEFAULT)},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
}
)
)
}
}
Criar NoteTopApp do App
O topo da tela normalmente contém um AppBar que pode conter o logotipo ou título do aplicativo, bem como botões de ação, como pesquisa ou menu.
package br.com.gilbercs.notes.ui.screens.note
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import br.com.gilbercs.notes.R
import br.com.gilbercs.notes.ui.theme.ELEVATION_BAR
import br.com.gilbercs.notes.viewmodel.NoteViewModel
@Composable
fun CreateNoteTopBar(navController: NavController, noteViewModel: NoteViewModel){
val context = LocalContext.current
TopAppBar(
modifier = Modifier.fillMaxWidth(),
backgroundColor = MaterialTheme.colors.surface,
elevation = ELEVATION_BAR,
title = { Text(text = stringResource(id = R.string.create_app_bar))},
navigationIcon = {
IconButton(onClick = {
navController.navigate("home_screen"){
popUpTo("home_screen"){
inclusive = true
}
}
}) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(id = R.string.icon_arrow_back))
}
},
actions = {
IconButton(onClick = {
if (noteViewModel.validateFields()){
val insertResult = noteViewModel.dbHandle("INSERT")
if (insertResult.equals("INSERT")){
Toast.makeText(context,"Gravado com sucesso!!",Toast.LENGTH_LONG).show()
navController.navigate("home_screen")
}else{
Toast.makeText(context,"Erro na Gravação!!",Toast.LENGTH_LONG).show()
}
Log.d("RESULTINSERT","$insertResult")
}else{
Toast.makeText(context,"Preencha os campos!!",Toast.LENGTH_LONG).show()
}
}) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(id = R.string.icon_check))
}
}
)
}
@Composable
fun UpdateNoteTopBar(navController: NavController, noteViewModel: NoteViewModel){
val context = LocalContext.current
TopAppBar(
modifier = Modifier.fillMaxWidth(),
backgroundColor = MaterialTheme.colors.surface,
elevation = ELEVATION_BAR,
title = { Text(text = stringResource(id = R.string.update_app_bar))},
navigationIcon = {
IconButton(onClick = {
navController.navigate("home_screen"){
popUpTo("home_screen"){
inclusive = true
}
}
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(id = R.string.icon_arrow_back))
}
},
actions = {
IconButton(onClick = {
if (noteViewModel.validateFields()){
val updateResult = noteViewModel.dbHandle("UPDATE")
if (updateResult.equals("UPDATE")){
Toast.makeText(context,"Atualizado com sucesso!!",Toast.LENGTH_LONG).show()
navController.navigate("home_screen")
}else{
Toast.makeText(context,"Erro na Gravação!!",Toast.LENGTH_LONG).show()
}
Log.d("RESULTINSERT","$updateResult")
}else{
Toast.makeText(context,"Preencha os campos!!",Toast.LENGTH_LONG).show()
}
}) {
Icon(
imageVector = Icons.Default.EditNote,
contentDescription = stringResource(id = R.string.icon_check))
}
}
)
}
Criar Empty Content
Vamos desenvolver componente de interface do usuário que atua como um marcador de ausência de conteúdo em uma parte específica da tela do aplicativo. Ele vai ser usado quando não há dados para exibir ou quando uma lista ou seção está vazia.
package br.com.gilbercs.notes.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SentimentVeryDissatisfied
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import br.com.gilbercs.notes.R
import br.com.gilbercs.notes.ui.theme.ICON_EMPTY
@Composable
fun EmptyContent(){
Column(modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.surface),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
modifier = Modifier
.size(ICON_EMPTY),
imageVector = Icons.Default.SentimentVeryDissatisfied,
contentDescription = stringResource(id = R.string.icon_empty),
tint = MaterialTheme.colors.primary
)
Text(
text = "\n\n" + stringResource(id = R.string.text_empty))
}
}
Conclusão
O desenvolvimento de aplicativos Android com código limpo, arquitetura MVVM, Kotlin, Jetpack Compose e Android Studio é uma maneira eficaz de criar aplicativos de alta qualidade que atendem às expectativas dos usuários. A adoção de boas práticas de desenvolvimento, incluindo a separação de preocupações, testes e documentação adequada, contribui para a manutenção de um código limpo e de fácil manutenção. Ao seguir essas práticas, você pode criar aplicativos que são mais eficientes, escaláveis e fáceis de manter à medida que crescem e evoluem.
Top comments (1)
Android Development Jobs Profile and Salary