Application Android - Informations Officielles¶
Télécharger cette page en Markdown
Architecture¶
Entity Room¶
Fichier : data/local/entities/InfoOfficielleEntity.kt
@Entity(tableName = "informations_officielles")
data class InfoOfficielleEntity(
@PrimaryKey
val id: Long,
@ColumnInfo(name = "mairie_id")
val mairieId: Long,
val titre: String,
val description: String,
@ColumnInfo(name = "type_info")
val typeInfo: TypeInfo,
val latitude: Double,
val longitude: Double,
val adresse: String?,
@ColumnInfo(name = "rayon_metres")
val rayonMetres: Int = 1000,
val statut: StatutInfo,
@ColumnInfo(name = "date_debut_publication")
val dateDebutPublication: String?,
@ColumnInfo(name = "date_publication")
val datePublication: String?,
@ColumnInfo(name = "date_expiration")
val dateExpiration: String?,
@ColumnInfo(name = "created_at")
val createdAt: String,
@ColumnInfo(name = "updated_at")
val updatedAt: String
)
enum class TypeInfo {
TRAVAUX,
EVENEMENT,
ALERTE,
ANNONCE
}
enum class StatutInfo {
BROUILLON,
PROGRAMME,
PUBLIE,
EXPIRE,
DESACTIVE
}
DAO¶
Fichier : data/local/dao/InfoOfficielleDao.kt
@Dao
interface InfoOfficielleDao {
@Query("""
SELECT * FROM informations_officielles
WHERE statut = 'PUBLIE'
ORDER BY created_at DESC
""")
fun getPublishedInfos(): Flow<List<InfoOfficielleEntity>>
@Query("SELECT * FROM informations_officielles WHERE id = :id")
suspend fun getById(id: Long): InfoOfficielleEntity?
@Query("""
SELECT * FROM informations_officielles
WHERE statut = 'PUBLIE'
AND type_info = :type
ORDER BY created_at DESC
""")
fun getByType(type: TypeInfo): Flow<List<InfoOfficielleEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(infos: List<InfoOfficielleEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(info: InfoOfficielleEntity)
@Query("DELETE FROM informations_officielles")
suspend fun deleteAll()
@Query("DELETE FROM informations_officielles WHERE id = :id")
suspend fun deleteById(id: Long)
}
Repository¶
Fichier : data/repository/InfoOfficielleRepository.kt
class InfoOfficielleRepository(
private val infoDao: InfoOfficielleDao,
private val api: UrbafixApi,
private val context: Context
) {
/**
* Flow des infos publiées (source locale)
*/
fun getPublishedInfos(): Flow<List<InfoOfficielleEntity>> {
return infoDao.getPublishedInfos()
}
/**
* Synchronise les infos depuis l'API
*/
suspend fun syncInfosFromApi(
latitude: Double,
longitude: Double,
radius: Int = 10000
) {
try {
val response = api.getInfosOfficielles(latitude, longitude, radius)
if (response.success) {
// Mapper DTO vers Entity
val entities = response.infos.map { dto ->
InfoOfficielleEntity(
id = dto.id,
mairieId = dto.mairie.id,
titre = dto.titre,
description = dto.description,
typeInfo = TypeInfo.valueOf(dto.typeInfo),
latitude = dto.latitude,
longitude = dto.longitude,
adresse = dto.adresse,
rayonMetres = dto.rayonMetres,
statut = StatutInfo.PUBLIE,
dateDebutPublication = null,
datePublication = dto.datePublication,
dateExpiration = dto.dateExpiration,
createdAt = dto.datePublication ?: "",
updatedAt = dto.datePublication ?: ""
)
}
// Remplacer les données locales
infoDao.deleteAll()
infoDao.insertAll(entities)
Log.d(TAG, "Sync infos: ${entities.size} infos récupérées")
}
} catch (e: Exception) {
Log.e(TAG, "Erreur sync infos: ${e.message}")
}
}
/**
* Récupère une info par ID
*/
suspend fun getInfoById(id: Long): InfoOfficielleEntity? {
return infoDao.getById(id)
}
companion object {
private const val TAG = "InfoOfficielleRepo"
}
}
API Retrofit¶
Fichier : data/remote/api/UrbafixApi.kt
interface UrbafixApi {
@GET("api/get_infos.php")
suspend fun getInfosOfficielles(
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double,
@Query("rayon") rayon: Int = 10000
): InfosResponse
}
data class InfosResponse(
val success: Boolean,
val count: Int,
val infos: List<InfoOfficielleDto>
)
data class InfoOfficielleDto(
val id: Long,
val titre: String,
val description: String,
@SerializedName("type_info")
val typeInfo: String,
val latitude: Double,
val longitude: Double,
val adresse: String?,
@SerializedName("rayon_metres")
val rayonMetres: Int,
@SerializedName("date_publication")
val datePublication: String?,
@SerializedName("date_expiration")
val dateExpiration: String?,
val distance: Double?,
val mairie: MairieDto,
val photos: List<String>
)
data class MairieDto(
val id: Long,
val nom: String
)
UI Components¶
InfoOfficielleCard¶
Fichier : ui/components/InfoOfficielleCard.kt
@Composable
fun InfoOfficielleCard(
info: InfoOfficielleEntity,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val (emoji, color) = when (info.typeInfo) {
TypeInfo.TRAVAUX -> "🚧" to Color(0xFFF59E0B)
TypeInfo.EVENEMENT -> "🎉" to Color(0xFF8B5CF6)
TypeInfo.ALERTE -> "⚠️" to Color(0xFFEF4444)
TypeInfo.ANNONCE -> "📢" to Color(0xFF3B82F6)
}
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Row(modifier = Modifier.padding(16.dp)) {
// Barre colorée à gauche
Box(
modifier = Modifier
.width(4.dp)
.height(80.dp)
.background(color, RoundedCornerShape(2.dp))
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
// Badge type
Surface(
color = color.copy(alpha = 0.1f),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = "$emoji ${info.typeInfo.name}",
style = MaterialTheme.typography.labelSmall,
color = color,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
// Titre
Text(
text = info.titre,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
// Description
Text(
text = info.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
// Dates
info.dateExpiration?.let { expiration ->
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Expire le ${formatDate(expiration)}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline
)
}
}
}
}
}
HomeViewModel Integration¶
Fichier : ui/screens/home/HomeViewModel.kt
class HomeViewModel(
private val repository: IncidentRepository,
private val infoRepository: InfoOfficielleRepository,
private val context: Context
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
// Observer les infos officielles
viewModelScope.launch {
infoRepository.getPublishedInfos().collect { infos ->
_uiState.update { it.copy(infosOfficielles = infos) }
}
}
}
fun syncAll(latitude: Double, longitude: Double) {
viewModelScope.launch {
// Sync incidents
repository.syncIncidentsFromApi(latitude, longitude)
// Sync infos officielles
infoRepository.syncInfosFromApi(latitude, longitude)
}
}
fun selectInfo(info: InfoOfficielleEntity) {
_uiState.update {
it.copy(
selectedInfo = info,
showInfoDetail = true
)
}
}
fun closeInfoDetail() {
_uiState.update {
it.copy(
selectedInfo = null,
showInfoDetail = false
)
}
}
}
data class HomeUiState(
val incidents: List<IncidentGroup> = emptyList(),
val infosOfficielles: List<InfoOfficielleEntity> = emptyList(),
val selectedInfo: InfoOfficielleEntity? = null,
val showInfoDetail: Boolean = false,
// ... autres champs
)
HomeScreen Integration¶
Fichier : ui/screens/home/HomeScreen.kt
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column {
// Carte avec markers incidents + infos
MapView(
incidents = uiState.incidents,
infos = uiState.infosOfficielles,
onInfoClick = { viewModel.selectInfo(it) }
)
// Liste combinée
LazyColumn {
// Infos officielles en premier (priorité)
items(uiState.infosOfficielles) { info ->
InfoOfficielleCard(
info = info,
onClick = { viewModel.selectInfo(info) }
)
}
// Incidents
items(uiState.incidents) { group ->
IncidentGroupCard(
group = group,
onClick = { /* ... */ }
)
}
}
}
// Detail sheet
if (uiState.showInfoDetail && uiState.selectedInfo != null) {
InfoDetailScreen(
info = uiState.selectedInfo!!,
onDismiss = { viewModel.closeInfoDetail() }
)
}
}
Marqueurs carte¶
Fichier : utils/MarkerUtils.kt
fun createInfoMarker(
mapView: MapView,
info: InfoOfficielleEntity,
onClick: () -> Unit
): Marker {
val color = when (info.typeInfo) {
TypeInfo.TRAVAUX -> Color(0xFFF59E0B)
TypeInfo.EVENEMENT -> Color(0xFF8B5CF6)
TypeInfo.ALERTE -> Color(0xFFEF4444)
TypeInfo.ANNONCE -> Color(0xFF3B82F6)
}
val marker = Marker(mapView).apply {
position = GeoPoint(info.latitude, info.longitude)
title = info.titre
snippet = info.description.take(100)
icon = createColoredMarkerDrawable(mapView.context, color)
setOnMarkerClickListener { _, _ ->
onClick()
true
}
}
// Cercle de rayon
val circle = Polygon(mapView).apply {
points = Polygon.pointsAsCircle(
GeoPoint(info.latitude, info.longitude),
info.rayonMetres.toDouble()
)
fillPaint.color = color.copy(alpha = 0.2f).toArgb()
outlinePaint.color = color.toArgb()
outlinePaint.strokeWidth = 2f
}
mapView.overlays.add(circle)
mapView.overlays.add(marker)
return marker
}
Database Migration¶
Fichier : data/local/AppDatabase.kt
@Database(
entities = [
IncidentEntity::class,
PhotoEntity::class,
MairieEntity::class,
InfoOfficielleEntity::class // NEW
],
version = 5, // Increment version
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun incidentDao(): IncidentDao
abstract fun photoDao(): PhotoDao
abstract fun mairieDao(): MairieDao
abstract fun infoOfficielleDao(): InfoOfficielleDao // NEW
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"urbafix_database"
)
.fallbackToDestructiveMigration() // Dev only
.build()
.also { INSTANCE = it }
}
}
}
}
Converters¶
Fichier : data/local/Converters.kt
@TypeConverters
class Converters {
@TypeConverter
fun toTypeInfo(value: String): TypeInfo = TypeInfo.valueOf(value)
@TypeConverter
fun fromTypeInfo(type: TypeInfo): String = type.name
@TypeConverter
fun toStatutInfo(value: String): StatutInfo = StatutInfo.valueOf(value)
@TypeConverter
fun fromStatutInfo(statut: StatutInfo): String = statut.name
}
Dépendances¶
// build.gradle.kts
dependencies {
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// UnifiedPush
implementation("com.github.UnifiedPush:android-connector:2.4.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}