Aller au contenu

Architecture Agents

Pattern MVVM

L'application suit l'architecture Model-View-ViewModel avec une stratégie offline-first.

Architecture MVVM en couches : Screens vers ViewModels vers Use Cases vers Repositories, qui accèdent à Room/SQLCipher en local et à l'API Retrofit à distance

Structure du projet

fr/urbafix/agents/
├── data/
│   ├── local/
│   │   ├── entities/       # Entités Room
│   │   │   ├── AgentEntity.kt
│   │   │   ├── IncidentEntity.kt
│   │   │   ├── PhotoEntity.kt
│   │   │   ├── CommentEntity.kt
│   │   │   └── SyncQueueEntity.kt
│   │   ├── dao/            # Data Access Objects
│   │   │   ├── AgentDao.kt
│   │   │   ├── IncidentDao.kt
│   │   │   ├── PhotoDao.kt
│   │   │   ├── CommentDao.kt
│   │   │   └── SyncQueueDao.kt
│   │   ├── AppDatabase.kt
│   │   └── Converters.kt
│   ├── remote/
│   │   ├── api/AgentsApi.kt
│   │   ├── dto/            # Data Transfer Objects
│   │   ├── RetrofitClient.kt
│   │   ├── TokenManager.kt
│   │   └── JwtAuthInterceptor.kt
│   └── repository/
│       ├── AuthRepository.kt
│       ├── IncidentRepository.kt
│       └── SyncRepository.kt
├── domain/
│   └── model/
│       ├── IncidentStatus.kt
│       ├── IncidentPriority.kt
│       └── CollectiviteType.kt
├── ui/
│   ├── screens/
│   │   ├── login/
│   │   ├── dashboard/
│   │   ├── incidents/
│   │   ├── map/
│   │   └── profile/
│   ├── components/
│   └── theme/
├── workers/
│   └── SyncWorker.kt
└── utils/

Stratégie Offline-First

Principe

  1. Lecture : Toutes les données affichées proviennent de Room (source de vérité)
  2. Écriture : Les modifications sont d'abord sauvées localement avec syncStatus = PENDING
  3. Synchronisation : WorkManager synchronise périodiquement avec le serveur

Flux de données

Séquence offline-first : sauvegarde locale immédiate dans Room et mise à jour de l'UI, puis synchronisation différée vers l'API par WorkManager qui marque l'enregistrement SYNCED

Gestion des tokens JWT

TokenManager

class TokenManager(context: Context) {
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val sharedPreferences = EncryptedSharedPreferences.create(
        context,
        "urbafix_agents_secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    fun saveTokens(accessToken: String, refreshToken: String, expiresInSeconds: Long)
    fun getAccessToken(): String?
    fun getRefreshToken(): String?
    fun isTokenExpired(): Boolean
    fun clearTokens()
}

JwtAuthInterceptor

L'intercepteur OkHttp gère automatiquement :

  1. Ajout du header Authorization: Bearer {token}
  2. Détection de l'expiration du token
  3. Refresh automatique si token expiré
  4. Déconnexion si refresh échoue

WorkManager

SyncWorker

class SyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            // 1. Push actions en attente
            syncRepository.pushPendingActions()

            // 2. Pull nouvelles données
            incidentRepository.syncFromServer(collectiviteId)

            Result.success()
        } catch (e: Exception) {
            if (runAttemptCount < 3) {
                Result.retry()
            } else {
                Result.failure()
            }
        }
    }
}

Configuration

val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    15, TimeUnit.MINUTES
)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        WorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "sync_incidents",
        ExistingPeriodicWorkPolicy.KEEP,
        syncRequest
    )