Architecture Agents¶
Pattern MVVM¶
L'application suit l'architecture Model-View-ViewModel avec une stratégie offline-first.
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¶
- Lecture : Toutes les données affichées proviennent de Room (source de vérité)
- Écriture : Les modifications sont d'abord sauvées localement avec
syncStatus = PENDING - Synchronisation : WorkManager synchronise périodiquement avec le serveur
Flux de données¶
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 :
- Ajout du header
Authorization: Bearer {token} - Détection de l'expiration du token
- Refresh automatique si token expiré
- 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
)