Sistema de almoxarifado no Android

 

Para criar um sistema de almoxarifado no Android, a estrutura principal deve focar em gestão de estoque (entradas e saídas), cadastro de produtos e rastreabilidade.

Arquitetura Recomendada

Para um aplicativo moderno e escalável em Kotlin:

  • Interface (UI): Jetpack Compose (design moderno e reativo).

  • Banco de Dados Local: Room Database (armazenamento de dados local e offline).

  • Scanner: CameraX + ML Kit Barcode Scanning (para leitura de códigos de barras/QR Code de produtos).

  • Arquitetura: MVVM (Model-View-ViewModel).

Módulos Principais do Sistema

  1. Cadastro de Produtos: Nome, código de barras/SKU, categoria, quantidade mínima, localização (prateleira/corredor).

  2. Movimentação de Estoque:

    • Entrada: Adição de novos itens ao estoque.

    • Saída: Baixa de itens para uso interno ou entrega.

  3. Scanner de Código de Barras: Para rápida conferência e busca de produtos.

  4. Relatórios e Alertas: Notificação ou destaque visual quando um produto atinge o estoque mínimo.

Estrutura de Banco de Dados (Room)

Kotlin
@Entity(tableName = "produtos")
data class Produto(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val codigoBarras: String,
    val nome: String,
    val quantidade: Int,
    val quantidadeMinima: Int,
    val localizacao: String
)

@Entity(tableName = "movimentacoes")
data class Movimentacao(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val produtoId: Int,
    val tipo: String, // "ENTRADA" ou "SAIDA"
    val quantidade: Int,
    val dataHora: Long = System.currentTimeMillis()
)

Passos para Desenvolvimento

  1. Configuração Inicial: Crie um projeto em Kotlin no Android Studio usando o template Empty Compose Activity.

  2. Dependências: Adicione no build.gradle.kts:

    • androidx.room:room-runtime e androidx.room:room-ktx

    • com.google.mlkit:barcode-scanning

    • androidx.camera:camera-view e androidx.camera:camera-lifecycle

  3. Persistência Local: Crie a estrutura de DAOs (Data Access Objects) para inserir, atualizar e listar os produtos no Room.

  4. Interface: Desenvolva telas simples:

    • Lista de estoque (com busca por nome e filtro por estoque baixo).

    • Formuário de cadastro/edição de produtos.

    • Tela de leitura de código de barras para dar baixa/entrada rápida.
       
      Aqui está uma implementação completa e estruturada utilizando Room, Kotlin Coroutines / Flow e a arquitetura recomendada MVVM para o sistema de almoxarifado.

      1. Entidades do Banco de Dados (Entity)

      Primeiro, definimos a tabela de produtos:

      Kotlin
      import androidx.room.Entity
      import androidx.room.PrimaryKey
      
      @Entity(tableName = "produtos")
      data class Produto(
          @PrimaryKey(autoGenerate = true) 
          val id: Int = 0,
          val codigoBarras: String,
          val nome: String,
          val quantidade: Int,
          val quantidadeMinima: Int,
          val localizacao: String
      )
      

      2. Data Access Object (DAO)

      O DAO contém as operações de leitura, escrita e atualização no banco de dados local. Usamos Flow para que a interface atualize automaticamente sempre que os dados mudarem.

      Kotlin
      import androidx.room.Dao
      import androidx.room.Insert
      import androidx.room.OnConflictStrategy
      import androidx.room.Query
      import androidx.room.Update
      import kotlinx.coroutines.flow.Flow
      
      @Dao
      interface ProdutoDao {
      
          @Insert(onConflict = OnConflictStrategy.REPLACE)
          suspend fun salvar(produto: Produto)
      
          @Update
          suspend fun atualizar(produto: Produto)
      
          @Query("SELECT * FROM produtos ORDER BY nome ASC")
          fun listarTodos(): Flow<List<Produto>>
      
          @Query("SELECT * FROM produtos WHERE codigoBarras = :codigo LIMIT 1")
          suspend fun buscarPorCodigoBarras(codigo: String): Produto?
      
          @Query("SELECT * FROM produtos WHERE quantidade <= quantidadeMinima")
          fun listarEstoqueBaixo(): Flow<List<Produto>>
      
          @Query("UPDATE produtos SET quantidade = quantidade + :qtd WHERE id = :id")
          suspend fun registrarEntrada(id: Int, qtd: Int)
      
          @Query("UPDATE produtos SET quantidade = quantidade - :qtd WHERE id = :id AND quantidade >= :qtd")
          suspend fun registrarSaida(id: Int, qtd: Int): Int // Retorna quantidade de linhas afetadas
      }
      

      3. Repositório (Repository)

      O repositório atua como a única fonte da verdade, abstraindo o DAO do restante da aplicação.

      Kotlin
      import kotlinx.coroutines.flow.Flow
      
      class AlmoxarifadoRepository(private val produtoDao: ProdutoDao) {
      
          val todosOsProdutos: Flow<List<Produto>> = produtoDao.listarTodos()
          val produtosEstoqueBaixo: Flow<List<Produto>> = produtoDao.listarEstoqueBaixo()
      
          suspend fun salvarProduto(produto: Produto) {
              produtoDao.salvar(produto)
          }
      
          suspend fun buscarPorCodigo(codigo: String): Produto? {
              return produtoDao.buscarPorCodigoBarras(codigo)
          }
      
          suspend fun darEntrada(produtoId: Int, quantidade: Int) {
              produtoDao.registrarEntrada(produtoId, quantidade)
          }
      
          suspend fun darSaida(produtoId: Int, quantidade: Int): Boolean {
              val linhasAfetadas = produtoDao.registrarSaida(produtoId, quantidade)
              return linhasAfetadas > 0 // Retorna true se deu baixa com sucesso
          }
      }
      

      4. ViewModel (ViewModel)

      O ViewModel gerencia os estados da interface e executa as regras de negócio em segundo plano usando Coroutines.
      Kotlin
      import androidx.lifecycle.ViewModel
      import androidx.lifecycle.viewModelScope
      import kotlinx.coroutines.flow.SharingStarted
      import kotlinx.coroutines.flow.StateFlow
      import kotlinx.coroutines.flow.stateIn
      import kotlinx.coroutines.launch
      
      class AlmoxarifadoViewModel(
          private val repository: AlmoxarifadoRepository
      ) : ViewModel() {
      
          // Transforma o Flow em StateFlow para fácil consumo no Jetpack Compose
          val listaProdutos: StateFlow<List<Produto>> = repository.todosOsProdutos
              .stateIn(
                  scope = viewModelScope,
                  started = SharingStarted.WhileSubscribed(5000),
                  initialValue = emptyList()
              )
      
          val alertaEstoqueBaixo: StateFlow<List<Produto>> = repository.produtosEstoqueBaixo
              .stateIn(
                  scope = viewModelScope,
                  started = SharingStarted.WhileSubscribed(5000),
                  initialValue = emptyList()
              )
      
          fun CadastrarOuAtualizar(produto: Produto) {
              viewModelScope.launch {
                  repository.salvarProduto(produto)
              }
          }
      
          fun realizarEntrada(produtoId: Int, quantidade: Int) {
              viewModelScope.launch {
                  if (quantidade > 0) {
                      repository.darEntrada(produtoId, quantidade)
                  }
              }
          }
      
          fun realizarSaida(produtoId: Int, quantidade: Int, onSuccess: () -> Unit, onError: (String) -> Unit) {
              viewModelScope.launch {
                  if (quantidade <= 0) {
                      onError("A quantidade deve ser maior que zero")
                      return@launch
                  }
                  
                  val sucesso = repository.darSaida(produtoId, quantidade)
                  if (sucesso) {
                      onSuccess()
                  } else {
                      onError("Quantidade indisponível no estoque")
                  }
              }
          }
      }
      
      1.Adicionar Dependências no Gradle:app/build.gradle.kts.
      Adicione o ML Kit Barcode Scanning e os módulos da CameraX ao projeto:

      Kotlin
      dependencies {
          // ML Kit Barcode Scanning
          implementation("com.google.mlkit:barcode-scanning:17.3.0")
      
          // CameraX
          val cameraVersion = "1.3.4"
          implementation("androidx.camera:camera-core:$cameraVersion")
          implementation("androidx.camera:camera-camera2:$cameraVersion")
          implementation("androidx.camera:camera-lifecycle:$cameraVersion")
          implementation("androidx.camera:camera-view:$cameraVersion")
      }
      
      2.Solicitar Permissão no AndroidManifest:AndroidManifest.xml.
      Declare o uso da câmera e o recurso de hardware:

      XML
      
      
      
      3.Criar o Analisador de Imagem com ML Kit:BarcodeAnalyzer.kt.
      Esta classe estende ImageAnalysis.Analyzer para converter os frames da câmera e enviá-los ao ML Kit:

      Kotlin
      import androidx.annotation.OptIn
      import androidx.camera.core.ExperimentalGetImage
      import androidx.camera.core.ImageAnalysis
      import androidx.camera.core.ImageProxy
      import com.google.mlkit.vision.barcode.BarcodeScanning
      import com.google.mlkit.vision.barcode.common.Barcode
      import com.google.mlkit.vision.common.InputImage
      
      class BarcodeAnalyzer(
          private val onBarcodeScanned: (String) -> Unit
      ) : ImageAnalysis.Analyzer {
      
          private val scanner = BarcodeScanning.getClient()
      
          @OptIn(ExperimentalGetImage::class)
          override fun analyze(imageProxy: ImageProxy) {
              val mediaImage = imageProxy.image
              if (mediaImage != null) {
                  val image = InputImage.fromMediaImage(
                      mediaImage,
                      imageProxy.imageInfo.rotationDegrees
                  )
      
                  scanner.process(image)
                      .addOnSuccessListener { barcodes ->
                          for (barcode in barcodes) {
                              barcode.rawValue?.let { codigo ->
                                  onBarcodeScanned(codigo)
                              }
                          }
                      }
                      .addOnCompleteListener {
                          imageProxy.close() // Importante fechar para receber o próximo frame
                      }
              } else {
                  imageProxy.close()
              }
          }
      }
      
      4.Construir o Composable do Scanner:BarcodeScannerView.kt.
      Utilize o AndroidView para acoplar o PreviewView da CameraX ao ecossistema do Jetpack Compose:

      Kotlin
      import android.view.ViewGroup
      import androidx.camera.core.CameraSelector
      import androidx.camera.core.ImageAnalysis
      import androidx.camera.core.Preview
      import androidx.camera.lifecycle.ProcessCameraProvider
      import androidx.camera.view.PreviewView
      import androidx.compose.foundation.layout.fillMaxSize
      import androidx.compose.runtime.Composable
      import androidx.compose.runtime.remember
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.platform.LocalContext
      import androidx.compose.ui.platform.LocalLifecycleOwner
      import androidx.compose.ui.viewinterop.AndroidView
      import androidx.core.content.ContextCompat
      import java.util.concurrent.Executors
      
      @Composable
      fun BarcodeScannerView(
          onBarcodeScanned: (String) -> Unit
      ) {
          val context = LocalContext.current
          val lifecycleOwner = LocalLifecycleOwner.current
          val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
      
          AndroidView(
              factory = { ctx ->
                  val previewView = PreviewView(ctx).apply {
                      layoutParams = ViewGroup.LayoutParams(
                          ViewGroup.LayoutParams.MATCH_PARENT,
                          ViewGroup.LayoutParams.MATCH_PARENT
                      )
                  }
      
                  val executor = ContextCompat.getMainExecutor(ctx)
                  cameraProviderFuture.addListener({
                      val cameraProvider = cameraProviderFuture.get()
      
                      val preview = Preview.Builder().build().also {
                          it.setSurfaceProvider(previewView.surfaceProvider)
                      }
      
                      val imageAnalysis = ImageAnalysis.Builder()
                          .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                          .build()
                          .also {
                              it.setAnalyzer(
                                  Executors.newSingleThreadExecutor(),
                                  BarcodeAnalyzer { codigo ->
                                      onBarcodeScanned(codigo)
                                  }
                              )
                          }
      
                      val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
      
                      try {
                          cameraProvider.unbindAll()
                          cameraProvider.bindToLifecycle(
                              lifecycleOwner,
                              cameraSelector,
                              preview,
                              imageAnalysis
                          )
                      } catch (e: Exception) {
                          e.printStackTrace()
                      }
                  }, executor)
      
                  previewView
              },
              modifier = Modifier.fillMaxSize()
          )
      }
      
      5.Integrar e Tratar Permissões na Tela Principal:TelaLeituraAlmoxarifado.kt.
      Lide com a solicitação da permissão de câmera antes de exibir a prévia:
      Kotlin
      import android.Manifest
      import android.content.pm.PackageManager
      import androidx.activity.compose.rememberLauncherForActivityResult
      import androidx.activity.result.contract.ActivityResultContracts
      import androidx.compose.foundation.layout.*
      import androidx.compose.material3.*
      import androidx.compose.runtime.*
      import androidx.compose.ui.Alignment
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.platform.LocalContext
      import androidx.compose.ui.unit.dp
      import androidx.core.content.ContextCompat
      
      @Composable
      fun TelaScannerAlmoxarifado(
          onCodigoLido: (String) -> Unit
      ) {
          val context = LocalContext.current
          var temPermissaoCamara by remember {
              mutableStateOf(
                  ContextCompat.checkSelfPermission(
                      context,
                      Manifest.permission.CAMERA
                  ) == PackageManager.PERMISSION_GRANTED
              )
          }
      
          val launcher = rememberLauncherForActivityResult(
              contract = ActivityResultContracts.RequestPermission()
          ) { isGranted ->
              temPermissaoCamara = isGranted
          }
      
          LaunchedEffect(Unit) {
              if (!temPermissaoCamara) {
                  launcher.launch(Manifest.permission.CAMERA)
              }
          }
      
          Box(modifier = Modifier.fillMaxSize()) {
              if (temPermissaoCamara) {
                  BarcodeScannerView(
                      onBarcodeScanned = { codigo ->
                          onCodigoLido(codigo)
                      }
                  )
              } else {
                  Column(
                      modifier = Modifier
                          .fillMaxSize()
                          .padding(16.dp),
                      horizontalAlignment = Alignment.CenterHorizontally,
                      verticalArrangement = Arrangement.Center
                  ) {
                      Text("A permissão da câmera é necessária para ler códigos de barras.")
                      Spacer(modifier = Modifier.height(8.dp))
                      Button(onClick = { launcher.launch(Manifest.permission.CAMERA) }) {
                          Text("Conceder Permissão")
                      }
                  }
              }
          }
      }