Développement d'un système de commande en libre-service par QR code avec Android Studio

Un système de commande en libre-service permet d'optimiser l'efficacité opérationnelle d'un restaurant et d'améliorer l'expérience client dans un contexte numérique. Cet article explore la conception et l'implémentation d'un tel système sur la plateforme Android Studio, couvrant les flux de commande, le paiement, l'appel du personnel, ainsi que les aspects techniques fondamentaux.

1. Choix de l'architecture et conception modulaire

L'adoption d'une architecture MVC (Modèle-Vue-Contrôleur) offre une base solide pour une application Android maintenable. L'objectif est de séparer clairement la logique métier, la présentation et les interactions utilisateur. L'architecture modulaire facilite les tests, les mises à jour et l'ajout de nouvelles fonctionnalités.

Principes de conception appliqués :

  • Découplage : Réduction des dépendances entre composants via l'injection de dépendances.
  • Extensibilité : Définition d'interfaces claires pour permettre l'ajout de nouveaux modules (ex. : intégration d'un nouveau système de paiement).
  • Testabilité : Chaque module (gestion des utilisateurs, traitement des commandes, gestion des paiements) peut être testé de manière isolée.

2. Intégration et logique des codes QR

Les codes QR servent de point d'entrée pour initier une session de commande. Le système génère un code unique par table, encodant un identifiant de session sécurisé. Lors du scan, l'application récupère cet identifiant pour charger le menu correspondant et lier la commande à la table.

Implémentation de la lecture et génération de codes QR :

// Génération d'un code QR contenant un identifiant de session
fun generateSessionQrCode(sessionId: String, width: Int, height: Int): Bitmap {
    val qrCodeWriter = QRCodeWriter()
    val bitMatrix = qrCodeWriter.encode(sessionId, BarcodeFormat.QR_CODE, width, height)
    val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
    for (x in 0 until width) {
        for (y in 0 until height) {
            bitmap.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
        }
    }
    return bitmap
}

// Traitement du résultat du scan
fun handleQrCodeScan(rawResult: String): Session? {
    return if (isValidSessionId(rawResult)) {
        SessionRepository.getSession(rawResult)
    } else {
        null
    }
}

Considérations de sécurité :

  • Les identifiants de session sont des jetons temporaires à usage unique, signés cryptographiquement.
  • L'intégrité du contenu du code QR est vérifiée par le serveur avant d'autoriser le chargement du menu.
  • La validité temporelle du jeton est limitée pour prévenir les réutilisations malveillantes.

3. Stratégie de gestion des données

La persistence des données est assurée par une combinaison de stockage local (pour les données hors ligne et le cache) et d'une base de données cloud (pour la synchronisation en temps réel et le stockage principal).

Exemple d'interaction avec la base de données locale (SQLite) :

class MenuDatabaseHelper(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {

    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL("""
            CREATE TABLE $TABLE_DISH (
                $COL_ID TEXT PRIMARY KEY,
                $COL_NAME TEXT NOT NULL,
                $COL_PRICE REAL NOT NULL,
                $COL_CATEGORY TEXT,
                $COL_AVAILABILITY INTEGER DEFAULT 1
            )
        """)
    }

    suspend fun fetchAllDishes(): List<dish> = withContext(Dispatchers.IO) {
        val dishes = mutableListOf<dish>()
        readableDatabase.query(TABLE_DISH, null, null, null, null, null, null).use { cursor ->
            while (cursor.moveToNext()) {
                dishes.add(
                    Dish(
                        id = cursor.getString(cursor.getColumnIndex(COL_ID)),
                        name = cursor.getString(cursor.getColumnIndex(COL_NAME)),
                        price = cursor.getDouble(cursor.getColumnIndex(COL_PRICE)),
                        category = cursor.getString(cursor.getColumnIndex(COL_CATEGORY)),
                        isAvailable = cursor.getInt(cursor.getColumnIndex(COL_AVAILABILITY)) == 1
                    )
                )
            }
        }
        dishes
    }
}</dish></dish>

Pour la synchronisation et le backup, l'intégration d'un service cloud comme Fierbase Realtime Database permet de maintenir les données cohérentes entre les différents terminaux clients et le serveur restaurant.

4. Interface utilisateur et flux de commande

L'interface utilisateur est conçue selon les principes du Material Design pour garantir intuitivité et cohérence. L'utilisation de RecyclerView pour les listes de plats et ConstraintLayout pour des mises en page adaptatives assure un rendu optimal sur différentes tailles d'écran.

Flux utilisateur typique :

  1. L'application démarre et affiche l'écran d'accueil avec une option pour scanner un code QR.
  2. Après le scan réussi, l'interface affiche le menu du restaurant associé à la session.
  3. L'utilisateur navigue par catégories, sélectionne des articles et les ajoute à son panier.
  4. Le panier affiche un récapitulatif avec les quantités modifiables et le total calculé.
  5. La commande est confirmée, ce qui déclenche le processus de paiement.
  6. Une fois le paiement validé, la commande est transmise au système du restaurant (kitchen display, imprimante).

Code illustratif pour la gestion du panier dans un ViewModel :

class OrderViewModel : ViewModel() {
    private val _currentOrder = MutableLiveData<mutablelist>>(mutableListOf())
    val currentOrder: LiveData<list>> = _currentOrder.map { it.toList() }
    private val _orderTotal = MutableLiveData(0.0)
    val orderTotal: LiveData<double> = _orderTotal

    fun addDishToOrder(dish: Dish, quantity: Int = 1) {
        val currentItems = _currentOrder.value ?: mutableListOf()
        val existingItem = currentItems.find { it.dish.id == dish.id }
        if (existingItem != null) {
            existingItem.quantity += quantity
        } else {
            currentItems.add(OrderItem(dish, quantity))
        }
        _currentOrder.value = currentItems
        recalculateTotal()
    }

    private fun recalculateTotal() {
        val total = _currentOrder.value?.sumOf { it.dish.price * it.quantity } ?: 0.0
        _orderTotal.value = total
    }
}</double></list></mutablelist>

5. Intégration des paiements et sécurité globale

L'intégration d'une passerelle de paiement nécessite une interaction sécurisée entre l'application cliente, le serveur du restaurant et le fournisseur de services de paiement (ex. : Stripe, PayPal). Les détails de la carte ne transitent jamais directement par l'application ; celle-ci initie une transaction via une API sécurisée côté serveur.

Mesures de sécurité transversales :

  • Tout le trafic réseau utilise le protocole HTTPS avec TLS 1.2 ou supérieur.
  • Les données sensibles (tokens d'authentification, informations de sesion) sont chiffrées au repos dans le Keystore Android.
  • L'authentification des utilisateurs et l'autorisation des actions critiques se basent sur des jetons JWT avec une durée de vie limitée.
  • Des tests de sécurité, y compris des tests de pénétration et d'analyse de vulnérabilités statiques (SAST), sont intégrés au pipeline de développement.

6. Déploiement et maintenance

Le système est déployé via une combinaison de téléchargement direct (pour les tiblettes du restaurant) et de distribution restreinte sur le Google Play Store. Un suivi des performances et des crashs via Firebase Crashlytics permet d'identifier et de résoudre rapidement les problèmes en production. Des mises à jour régulières sont publiées pour corriger des bogues et introduire des améliorations de fonctionnalités, garantissant ainsi la pérennité du système.

Étiquettes: Android kotlin Material Design QR Code sqlite

Publié le 22 juillet à 13h21