DEV Community

Gilber
Gilber

Posted on

Desenvolvimento de Aplicativos com Kotlin, Room e Jetpack Compose: Uma Jornada para Interfaces de Usuário Modernas e Eficientes

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.
Image description

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

Image description
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
}
Enter fullscreen mode Exit fullscreen mode
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'
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
)

Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
                }
            }
        }
    }
}  
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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>()
}
Enter fullscreen mode Exit fullscreen mode

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
    }


Enter fullscreen mode Exit fullscreen mode

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)
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

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"})
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

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})
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
                }
            )
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

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))

    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
geekstalk profile image
Geeks Talk