Android: Stamps y Visage SDK en Flutter

Esta guía documenta cómo integrar las librerías Android: Stamps SDK y Visage SDK de JAAK en un proyecto Flutter.

Prerrequisitos

  1. Flutter instalado (versión recomendada: 3.x o superior)
  2. JDK 17 instalado
  3. Android Studio o herramientas de Android configuradas
  4. Dispositivo Android físico (los SDKs no funcionan bien en emuladores)

Crear Proyecto Flutter

Si aún no tienes un proyecto Flutter, créalo:

flutter create nombre_proyecto
cd nombre_proyecto

Configuración de pubspec.yaml

Archivo: pubspec.yaml

El pubspec.yaml NO requiere dependencias especiales para esta integración, ya que usamos Platform Channels nativos.

name: nombre_proyecto
description: Proyecto Flutter con Stamps y Visage SDK

environment:
  sdk: ^3.9.2

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

Nota: No se necesitan plugins de Flutter adicionales porque la integración se hace completamente mediante Platform Channels.


Versiones Requeridas

IMPORTANTE: Estas versiones deben coincidir con el proyecto Android de referencia para garantizar compatibilidad.

Versiones del Proyecto Android Original:

  • Kotlin: 1.9.22
  • Hilt: 2.46
  • Java: VERSION_17 (JDK 17)
  • minSdk: 24
  • compileSdk: 35
  • Gradle Plugin: 8.3.2 (compatible con 8.9.1)
  • Stamps SDK: 1.0.0.beta
  • Visage SDK: 1.0.0.beta

Paso 1: Configurar Repositorios Maven

Archivo: android/build.gradle.kts

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
        maven { url = uri("https://us-maven.pkg.dev/jaak-platform/jaak-android") }
    }
}

Paso 2: Configurar Plugins en settings.gradle.kts

Archivo: android/settings.gradle.kts

plugins {
    id("dev.flutter.flutter-plugin-loader") version "1.0.0"
    id("com.android.application") version "8.9.1" apply false
    id("org.jetbrains.kotlin.android") version "1.9.22" apply false
    id("com.google.dagger.hilt.android") version "2.46" apply false
}

Paso 3: Configurar build.gradle.kts de la App

Archivo: android/app/build.gradle.kts

Plugins:

plugins {
    id("com.android.application")
    id("kotlin-android")
    id("kotlin-kapt")
    id("com.google.dagger.hilt.android")
    id("dev.flutter.flutter-gradle-plugin")
}

Configuración Android:

android {
    namespace = "com.example.tu_app"
    compileSdk = 35 // o flutter.compileSdkVersion

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_17.toString()
    }

    defaultConfig {
        minSdk = 24
        targetSdk = 33
        // ... resto de configuración
    }
}

Dependencias:

dependencies {
    // AndroidX Core
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("androidx.fragment:fragment-ktx:1.6.2")
    implementation("androidx.activity:activity-ktx:1.8.2")

    // Hilt - Inyección de dependencias (REQUERIDO)
    implementation("com.google.dagger:hilt-android:2.46")
    kapt("com.google.dagger:hilt-android-compiler:2.46")

    // SDKs de JAAK
    implementation("com.jaak.stampssdk:jaakstamps-sdk:1.0.0.beta")
    implementation("com.jaak.visagesdk:jaakvisage-sdk:1.0.0.beta")

    // Google ML Kit
    implementation("com.google.android.gms:play-services-mlkit-face-detection:17.1.0")
    implementation("com.google.android.gms:play-services-location:21.0.1")

    // CameraX
    implementation("androidx.camera:camera-camera2:1.4.1")
    implementation("androidx.camera:camera-lifecycle:1.4.1")
    implementation("androidx.camera:camera-core:1.4.1")
    implementation("androidx.camera:camera-view:1.4.1")
    implementation("androidx.camera:camera-video:1.4.1")

    // Guava
    implementation("com.google.guava:guava:32.1.3-android")
}

Paso 4: Configurar AndroidManifest.xml

Archivo: android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permisos -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"/>

    <!-- Características de hardware -->
    <uses-feature android:name="android.hardware.camera.any" />
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

    <application
        android:name=".MyApplication"
        tools:replace="android:label,android:name"
        ...>

        <!-- MainActivity principal -->
        <activity android:name=".MainActivity" ... />

        <!-- Actividades para SDKs -->
        <activity
            android:name=".StampsActivity"
            android:exported="false"
            android:screenOrientation="portrait" />

        <activity
            android:name=".VisageActivity"
            android:exported="false"
            android:screenOrientation="portrait" />

        <!-- FileProvider -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true"
            tools:replace="android:authorities">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

Paso 5: Crear MyApplication.kt con Hilt

Archivo: android/app/src/main/kotlin/com/example/tu_app/MyApplication.kt

package com.example.tu_app

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication : Application()

Paso 6: Crear Actividades para SDKs

StampsActivity.kt

Archivo: android/app/src/main/kotlin/com/example/tu_app/StampsActivity.kt

package com.example.tu_app

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.jaak.stampssdk.sdk.StampsSDK
import com.jaak.stampssdk.ui.adapter.StampsListener
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class StampsActivity : AppCompatActivity(), StampsListener {
    private lateinit var stampsSDK: StampsSDK

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        stampsSDK = StampsSDK(this, this)
        val documentType = intent.getIntExtra("documentType", 1)
        stampsSDK.startStamps(documentType)
    }

    override fun onErrorStamps(text: String) {
        val resultIntent = Intent()
        resultIntent.putExtra("error", text)
        setResult(Activity.RESULT_CANCELED, resultIntent)
        finish()
    }

    override fun onSuccessStamps(
        typeProcess: Int,
        frontOriginalUri: Uri?,
        frontCropUri: Uri?,
        backOriginalUri: Uri?,
        backCropUri: Uri?
    ) {
        val resultIntent = Intent()
        resultIntent.putExtra("typeProcess", typeProcess.toString())
        resultIntent.putExtra("frontOriginalUri", frontOriginalUri?.toString())
        resultIntent.putExtra("frontCropUri", frontCropUri?.toString())
        resultIntent.putExtra("backOriginalUri", backOriginalUri?.toString())
        resultIntent.putExtra("backCropUri", backCropUri?.toString())
        setResult(Activity.RESULT_OK, resultIntent)
        finish()
    }
}

VisageActivity.kt

Archivo: android/app/src/main/kotlin/com/example/tu_app/VisageActivity.kt

package com.example.tu_app

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.jaak.visagesdk.ui.view.VisageSDK
import com.jaak.visagesdk.ui.adapter.VisageListener
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class VisageActivity : AppCompatActivity(), VisageListener {
    private lateinit var visageSDK: VisageSDK

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        visageSDK = VisageSDK(this, this)
        visageSDK.startVisage()
    }

    override fun onErrorVisage(text: String) {
        val resultIntent = Intent()
        resultIntent.putExtra("error", text)
        setResult(Activity.RESULT_CANCELED, resultIntent)
        finish()
    }

    override fun onSuccessVisage(typeProcess: Int, uri: Uri?) {
        val resultIntent = Intent()
        resultIntent.putExtra("typeProcess", typeProcess.toString())
        resultIntent.putExtra("videoUri", uri?.toString())
        setResult(Activity.RESULT_OK, resultIntent)
        finish()
    }
}

Paso 7: Modificar MainActivity.kt

Archivo: android/app/src/main/kotlin/com/example/tu_app/MainActivity.kt

package com.example.tu_app

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

@AndroidEntryPoint
class MainActivity : FlutterActivity() {
    private val STAMPS_CHANNEL = "com.example.stamps/channel"
    private val VISAGE_CHANNEL = "com.example.visage/channel"
    private val STAMPS_REQUEST_CODE = 1001
    private val VISAGE_REQUEST_CODE = 1002

    private var stampsResult: MethodChannel.Result? = null
    private var visageResult: MethodChannel.Result? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        // Configurar canal para Stamps
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, STAMPS_CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "startStampsCapture" -> {
                        stampsResult = result
                        val documentType = call.argument<Int>("documentType") ?: 1
                        startStampsActivity(documentType)
                    }
                    else -> result.notImplemented()
                }
            }

        // Configurar canal para Visage
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VISAGE_CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "startVisageDetection" -> {
                        visageResult = result
                        startVisageActivity()
                    }
                    else -> result.notImplemented()
                }
            }
    }

    private fun startStampsActivity(documentType: Int) {
        val intent = Intent(this, StampsActivity::class.java)
        intent.putExtra("documentType", documentType)
        startActivityForResult(intent, STAMPS_REQUEST_CODE)
    }

    private fun startVisageActivity() {
        val intent = Intent(this, VisageActivity::class.java)
        startActivityForResult(intent, VISAGE_REQUEST_CODE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
            STAMPS_REQUEST_CODE -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val result = mapOf(
                        "success" to true,
                        "typeProcess" to data.getStringExtra("typeProcess"),
                        "frontOriginalUri" to data.getStringExtra("frontOriginalUri"),
                        "frontCropUri" to data.getStringExtra("frontCropUri"),
                        "backOriginalUri" to data.getStringExtra("backOriginalUri"),
                        "backCropUri" to data.getStringExtra("backCropUri")
                    )
                    stampsResult?.success(result)
                } else {
                    val errorMessage = data?.getStringExtra("error") ?: "Error desconocido en Stamps"
                    stampsResult?.error("STAMPS_ERROR", errorMessage, null)
                }
                stampsResult = null
            }

            VISAGE_REQUEST_CODE -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val result = mapOf(
                        "success" to true,
                        "typeProcess" to data.getStringExtra("typeProcess"),
                        "videoUri" to data.getStringExtra("videoUri")
                    )
                    visageResult?.success(result)
                } else {
                    val errorMessage = data?.getStringExtra("error") ?: "Error desconocido en Visage"
                    visageResult?.error("VISAGE_ERROR", errorMessage, null)
                }
                visageResult = null
            }
        }
    }
}

Paso 8: Crear file_paths.xml

Archivo: android/app/src/main/res/xml/file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="cache" path="." />
    <files-path name="files" path="." />
    <external-path name="external" path="." />
    <external-cache-path name="external_cache" path="." />
    <external-files-path name="external_files" path="." />
</paths>



Paso 9: Crear Servicios Dart

StampsService

Archivo: lib/services/stamps_service.dart

import 'package:flutter/services.dart';

class StampsService {
  static const MethodChannel _channel = MethodChannel('com.example.stamps/channel');

  static Future<Map<String, dynamic>> startStampsCapture({int documentType = 1}) async {
    try {
      final result = await _channel.invokeMethod('startStampsCapture', {
        'documentType': documentType,
      });
      if (result is Map) {
        return Map<String, dynamic>.from(result);
      }
      throw Exception('Formato de respuesta inválido');
    } on PlatformException catch (e) {
      throw Exception('Error en Stamps: ${e.message}');
    }
  }
}

VisageService

Archivo: lib/services/visage_service.dart

import 'package:flutter/services.dart';

class VisageService {
  static const MethodChannel _channel = MethodChannel('com.example.visage/channel');

  static Future<Map<String, dynamic>> startVisageDetection() async {
    try {
      final result = await _channel.invokeMethod('startVisageDetection');
      if (result is Map) {
        return Map<String, dynamic>.from(result);
      }
      throw Exception('Formato de respuesta inválido');
    } on PlatformException catch (e) {
      throw Exception('Error en Visage: ${e.message}');
    }
  }
}

Paso 10: Crear Modelos Dart

StampsResult

Archivo: lib/models/stamps_result.dart

class StampsResult {
  final bool success;
  final String? typeProcess;
  final String? frontOriginalUri;
  final String? frontCropUri;
  final String? backOriginalUri;
  final String? backCropUri;

  StampsResult({
    required this.success,
    this.typeProcess,
    this.frontOriginalUri,
    this.frontCropUri,
    this.backOriginalUri,
    this.backCropUri,
  });

  factory StampsResult.fromMap(Map<String, dynamic> map) {
    return StampsResult(
      success: map['success'] ?? false,
      typeProcess: map['typeProcess'],
      frontOriginalUri: map['frontOriginalUri'],
      frontCropUri: map['frontCropUri'],
      backOriginalUri: map['backOriginalUri'],
      backCropUri: map['backCropUri'],
    );
  }

  Map<String, dynamic> toMap() {
    return {
      'success': success,
      'typeProcess': typeProcess,
      'frontOriginalUri': frontOriginalUri,
      'frontCropUri': frontCropUri,
      'backOriginalUri': backOriginalUri,
      'backCropUri': backCropUri,
    };
  }

  @override
  String toString() {
    return 'StampsResult(success: $success, typeProcess: $typeProcess, frontCropUri: $frontCropUri)';
  }
}

VisageResult

Archivo: lib/models/visage_result.dart

class VisageResult {
  final bool success;
  final String? typeProcess;
  final String? videoUri;

  VisageResult({
    required this.success,
    this.typeProcess,
    this.videoUri,
  });

  factory VisageResult.fromMap(Map<String, dynamic> map) {
    return VisageResult(
      success: map['success'] ?? false,
      typeProcess: map['typeProcess'],
      videoUri: map['videoUri'],
    );
  }

  Map<String, dynamic> toMap() {
    return {
      'success': success,
      'typeProcess': typeProcess,
      'videoUri': videoUri,
    };
  }

  @override
  String toString() {
    return 'VisageResult(success: $success, typeProcess: $typeProcess, videoUri: $videoUri)';
  }
}

Checklist de Integración

Configuración Android

  • Versiones de Kotlin (1.9.22), Hilt (2.46) y Java (17) coinciden
  • Repositorios Maven configurados en android/build.gradle.kts
  • Plugins de Hilt agregados en android/settings.gradle.kts
  • Dependencias agregadas en android/app/build.gradle.kts
  • Permisos agregados en AndroidManifest.xml
  • MyApplication.kt creado con @HiltAndroidApp
  • StampsActivity.kt y VisageActivity.kt creados con @AndroidEntryPoint
  • MainActivity.kt actualizado con Platform Channels completos
  • file_paths.xml creado en res/xml/

Código Flutter/Dart

  • Servicios Dart creados (lib/services/stamps_service.dart, lib/services/visage_service.dart)
  • Modelos Dart creados (lib/models/stamps_result.dart, lib/models/visage_result.dart)
  • Integración en main.dart o widget principal

Validación

  • Proyecto compila sin errores
  • Dispositivo Android físico conectado
  • Permisos de cámara funcionando
  • SDKs responden correctamente

🚧

Notas Importantes

  1. Hilt es OBLIGATORIO: Los SDKs de Stamps y Visage requieren Hilt para inyección de dependencias.

  2. Versiones deben coincidir: Para evitar conflictos, usa las mismas versiones que el proyecto Android de referencia.

  3. Dispositivo físico requerido: Los SDKs requieren cámara real, no funcionan bien en emuladores.

  4. Java 17: Asegúrate de tener JDK 17 instalado en tu sistema.


Problemas Comunes

Error: Hilt Activity must be attached to @HiltAndroidApp Application

Solución: Verifica que:

  • MyApplication.kt tenga @HiltAndroidApp
  • AndroidManifest.xml use android:name=".MyApplication"
  • StampsActivity y VisageActivity tengan @AndroidEntryPoint
Error: Cannot access AppCompatActivity

Solución: Verifica que las dependencias de AndroidX estén agregadas en build.gradle.kts

Error de compilación con Kotlin

Solución: Asegúrate de usar Kotlin 1.9.22 exactamente

Error: PlatformException Stamps_ERROR o VISAGE_ERROR

Posibles causas:

  • Permisos de cámara no concedidos
  • SDK no inicializado correctamente
  • Dispositivo emulador (usar dispositivo físico)

Solución:

  1. Verificar permisos en tiempo de ejecución
  2. Verificar que Hilt esté configurado correctamente
  3. Revisar logs de Android para detalles específicos
Error: MissingPluginException

Causa: Method Channels no están registrados correctamente

Solución: Verificar que MainActivity.kt tenga el código completo de configuración de canales

Error: NoClassDefFoundError para clases de Jaak

Causa: Dependencias de Jaak no están disponibles

Solución:

  1. Verificar que los repositorios Maven estén configurados
  2. Limpiar y reconstruir el proyecto: flutter clean && flutter pub get
  3. Verificar conectividad a repositorios Maven
Error: Aplicación se cierra al usar SDKs

Causa: Usualmente problemas de permisos o configuración de Hilt

Solución:

  1. Revisar AndroidManifest.xml tiene todos los permisos
  2. Verificar que todas las Activities tengan @AndroidEntryPoint
  3. Verificar que MyApplication tenga @HiltAndroidApp

Tips de Debugging

  1. Usar Android Studio Logcat para ver errores detallados
  2. Verificar permisos antes de usar cada SDK:
    import 'package:permission_handler/permission_handler.dart';
    
    if (await Permission.camera.request().isGranted) {
      // Usar SDK
    }
  3. Test en dispositivo físico - los emuladores no soportan bien estos SDKs

Estructura de Archivos del Proyecto Flutter

Después de completar la integración, tu proyecto debería tener esta estructura:

nombre_proyecto/
├── android/
│   ├── app/
│   │   ├── src/main/
│   │   │   ├── kotlin/com/example/tu_app/
│   │   │   │   ├── MainActivity.kt
│   │   │   │   ├── MyApplication.kt
│   │   │   │   ├── StampsActivity.kt
│   │   │   │   └── VisageActivity.kt
│   │   │   ├── res/xml/
│   │   │   │   └── file_paths.xml
│   │   │   └── AndroidManifest.xml
│   │   └── build.gradle.kts
│   ├── build.gradle.kts
│   └── settings.gradle.kts
├── lib/
│   ├── models/
│   │   ├── stamps_result.dart
│   │   └── visage_result.dart
│   ├── services/
│   │   ├── stamps_service.dart
│   │   └── visage_service.dart
│   └── main.dart
└── pubspec.yaml

Ejecutar el Proyecto

1. Instalar dependencias de Flutter:

flutter pub get

2. Conectar dispositivo Android físico con USB Debug activado

3. Verificar que el dispositivo sea detectado:

flutter devices

4. Ejecutar la aplicación:

flutter run

O desde Android Studio:

  • Abre el proyecto Flutter
  • Selecciona el dispositivo
  • Presiona Run ▶️

Uso en el Código Flutter

Ejemplo completo de uso:

Archivo: lib/main.dart

import 'package:flutter/material.dart';
import 'services/stamps_service.dart';
import 'services/visage_service.dart';
import 'models/stamps_result.dart';
import 'models/visage_result.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Jaak SDK Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Stamps & Visage SDK'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _statusMessage = 'Presiona un botón para comenzar';
  bool _isLoading = false;

  // Capturar documento con Stamps
  Future<void> _capturarDocumento() async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Iniciando captura de documento...';
    });

    try {
      final result = await StampsService.startStampsCapture(documentType: 1);
      final stampsResult = StampsResult.fromMap(result);

      if (stampsResult.success) {
        setState(() {
          _statusMessage = 'Documento capturado exitosamente!\n'
              'Tipo: ${stampsResult.typeProcess}\n'
              'Imagen frontal: ${stampsResult.frontCropUri != null ? 'Sí' : 'No'}\n'
              'Imagen trasera: ${stampsResult.backCropUri != null ? 'Sí' : 'No'}';
        });
      } else {
        setState(() {
          _statusMessage = 'Error en la captura del documento';
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  // Detección facial con Visage
  Future<void> _deteccionFacial() async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Iniciando detección facial...';
    });

    try {
      final result = await VisageService.startVisageDetection();
      final visageResult = VisageResult.fromMap(result);

      if (visageResult.success) {
        setState(() {
          _statusMessage = 'Detección facial exitosa!\n'
              'Tipo: ${visageResult.typeProcess}\n'
              'Video: ${visageResult.videoUri != null ? 'Capturado' : 'No disponible'}';
        });
      } else {
        setState(() {
          _statusMessage = 'Error en la detección facial';
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'Demo de integración Jaak SDKs',
                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 30),

              // Botón Stamps
              SizedBox(
                width: double.infinity,
                height: 50,
                child: ElevatedButton.icon(
                  onPressed: _isLoading ? null : _capturarDocumento,
                  icon: const Icon(Icons.document_scanner),
                  label: const Text('Capturar Documento (Stamps)'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                    foregroundColor: Colors.white,
                  ),
                ),
              ),

              const SizedBox(height: 16),

              // Botón Visage
              SizedBox(
                width: double.infinity,
                height: 50,
                child: ElevatedButton.icon(
                  onPressed: _isLoading ? null : _deteccionFacial,
                  icon: const Icon(Icons.face),
                  label: const Text('Detección Facial (Visage)'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                    foregroundColor: Colors.white,
                  ),
                ),
              ),

              const SizedBox(height: 30),

              // Indicador de carga
              if (_isLoading)
                const CircularProgressIndicator(),

              const SizedBox(height: 20),

              // Mensaje de estado
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.grey[300]!),
                ),
                child: Text(
                  _statusMessage,
                  style: const TextStyle(fontSize: 14),
                  textAlign: TextAlign.center,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Repositorio de Ejemplo

📦 Puedes apoyarte con un ejemplo funcional en el siguiente repositorio:

Ir a Github

Resumen

Esta integración permite usar los SDKs nativos de Android (Stamps y Visage) desde Flutter usando Platform Channels. Las versiones deben coincidir con el proyecto Android original para garantizar compatibilidad.

👍

Puntos clave:

  • ✅ No requiere plugins de Flutter adicionales
  • ✅ Usa Platform Channels nativos
  • ✅ Requiere Hilt (inyección de dependencias)
  • ✅ Requiere dispositivo Android físico
  • ✅ Las versiones deben coincidir con el proyecto Android de referencia