Thierry Chappuis
- Site perso: placepython.fr
- Professeur et chercheur à la Haute Ecole d'Ingénierie et d'Architecture de Fribourg
- Mentor Python et Django sur PlacePython.fr
- Présent sur X: @PlacePython
Objectifs
-
Transformer une application web Django/Flask/Whatever en app mobile hybride
-
Utiliser Turbo, Stimulus et Hotwire Native
-
Simplifier et accélérer le dev mobile pour les petites équipes
-
Réduire le temps de mise sur le marché (time to app store) de votre app
-
Créer une présence sur les App Stores Android et iOS avec peu de ressources
Pourquoi créer une application mobile avec votre projet Django ?
-
Répondre aux attentes des utilisateurs mobiles (majorité de vos visiteurs)
-
Augmenter la portée de votre app ou SaaS
-
Offrir une expérience utilisateur améliorée
-
S’adapter à l’évolution des comportements numériques
Qu’est-ce qu’une application mobile hybride ?
Qu’est-ce qu’une application mobile hybride ?
-
Une app entre le web et le natif
-
Une base de code pour plusieurs plateformes
-
Accès aux fonctionnalités natives de l’appareil
-
Déployée sur les App Stores comme une application native
-
Solution idéale pour des petites équipes ou des projets à budget limité
Qu'est-ce que Hotwire ?
Qu'est-ce que Hotwire ?
- Une vieille approche moderne du dev web :)
-
Créé par les développeurs de Ruby on Rails (37signals)
-
Technologie front-end par défaut de Ruby on Rails
-
Moins de JavaScript, plus de simplicité
-
Une solution agnostique, adaptable à votre framework back-end
Qu'est-ce que Hotwire ?
+
+
Qu’est-ce que Hotwire Native ?
- Hotwire du web au mobile
- Réutilisation de votre code existant
- Vos vues Django sont vos vues mobiles
- Usage ponctuel de pages iOS ou Android natives
- Usage ponctuel de composants natifs avec BridgeComponent (anciennent Strada)
Avantages de Hotwire Native par rapport aux applications natives, Flutter ou React Native
-
Réutilisation maximale des vues Django ou Flask
-
Moins de complexité et de nouvelles technologies à apprendre
-
Pas nécessairement d'API
-
Temps de développement plus court et maintenance simplifiée
-
Accès aux fonctionnalités natives
Avantages de Hotwire Native par rapport aux PWA (Progressive Web Apps)
-
Présence sur les App Stores (iOS et Android)
-
Accès complet aux fonctionnalités natives de l’appareil
-
Meilleure expérience utilisateur avec une intégration native
-
Compatibilité et support étendus sur toutes les plateformes
Contraintes sur le Back-end
Pré-requis: Turbo Drive et Turbo Frames
$ npm install @hotwired/turbo
// frontend/src/application/app.js
import "@hotwired/turbo"
Qu'est-ce que Turbo Drive ?
Une navigation rapide et sans rechargement de page:
-
Sans Turbo Drive : Chaque fois que l’utilisateur clique sur un lien ou soumet un formulaire, Django renvoie une nouvelle page HTML complète, et tout le navigateur est rechargé.
-
Avec Turbo Drive : Django génère toujours la page HTML complète, mais seuls les éléments qui ont changé sont actualisés, ce qui rend la navigation beaucoup plus rapide et fluide pour l’utilisateur.
Pourquoi Turbo Drive est important pour Hotwire Native
Turbo Drive est la pierre angulaire de la fluidité de Hotwire Native
- Navigation rapide et sans rechargement
-
Performance et économies de ressources
-
Turbo Drive gère la navigation sans rechargement, Hotwire Native transforme la navigation en navigation native
Qu'est-ce que Turbo Frame ?
Chargement de parties de la page:
-
Sans Turbo Frame : Si une petite partie de la page (comme une liste de commentaires ou un formulaire) doit être mise à jour, Django renvoie une nouvelle page complète, et le navigateur doit tout recharger.
-
Avec Turbo Frame : Découpe de la page en section (appelées Frames) qui sont gérés indépendemment par le back-end.
Qu'est-ce que Turbo Frame ?
<body>
<div id="navigation">Links targeting the entire page</div>
<turbo-frame id="message_1">
<h1>My message title</h1>
<p>My message content</p>
<a href="/messages/1/edit">Edit this message</a>
</turbo-frame>
<turbo-frame id="comments">
<div id="comment_1">One comment</div>
<div id="comment_2">Two comments</div>
<form action="/messages/comments">...</form>
</turbo-frame>
</body>
Turbo Frame et Hotwire Native
-
Actualisation partielle des sections spécifiques d’une page mobile
-
Optimisation des interactions et des performances sur mobile
-
Permet une expérience utilisateur plus fluide et modulaire: chaque frame interagit avec le serveur sans rechargement
-
Améliore l'UX liée gestion des formulaires
Notre première app mobile
XCode 15+ et Swift pour iOS
XCode 15+ et Swift pour iOS
XCode 15+ et Swift pour iOS
XCode 15+ et Swift pour iOS
XCode 15+ et Swift pour iOS
File → Add Packages Dependencies…
Add to package
// SceneDelegate.swift
import HotwireNative
import UIKit
let rootURL = URL(string: "https://hotwire-native-demo.dev")!
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private let navigator = Navigator()
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
window?.rootViewController = navigator.rootViewController
navigator.route(rootURL)
}
}
Modifier la navigation par défaut avec path-configuration.json
{
"settings": {},
"rules": [
{
"patterns": [
".*"
],
"properties": {
"context": "default",
"pull_to_refresh_enabled": true
}
},
{
"patterns": [
"/new$",
],
"properties": {
"context": "modal",
"pull_to_refresh_enabled": false
}
}
]
}
Et Android ? Utiliser Android studio et Kotlin
File → New → New Project…
Web + Native == Hybride
- Substituer des composants avec des composants natifs grace à BridgeComponent (anciennement Strada)
- Substituer des vues avec des vues natives
Créer des pages natives
Amélioration progressive :
Certaines vues stratégique méritent une expérience utilisateur native
Interception de l'URL et réimplémentation de la page (et juste de cette page) en Swift/Kotlin
Créer des pages natives
{
"settings": {},
"rules": [
{
"patterns": [
"/numbers$"
],
"properties": {
"uri": "hotwire://fragment/numbers",
"title": "Numbers"
}
}
]
}
import HotwireNative
import UIKit
/// A simple native table view controller to demonstrate loading non-Turbo screens
/// for a visit proposal
final class NumbersViewController: UITableViewController, PathConfigurationIdentifiable {
static var pathConfigurationIdentifier: String { "numbers" }
convenience init(url: URL, navigator: Router) {
self.init(nibName: nil, bundle: nil)
self.url = url
self.navigator = navigator
}
private var url: URL!
private unowned var navigator: Router?
override func viewDidLoad() {
super.viewDidLoad()
title = "Numbers"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
100
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let number = indexPath.row + 1
cell.textLabel?.text = "Row \(number)"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailURL = url.appendingPathComponent("\(indexPath.row + 1)")
navigator?.route(detailURL)
tableView.deselectRow(at: indexPath, animated: true)
}
}
Logique visuelle et comportement en Swift/Kotlin
Créer des composants natifs (BridgeComponent)
$ npm install -D @hotwired/stimulus
$ npm install -D @hotwired/hotwire-native-bridge
Contenu géré par Django
Bouton géré par Swift/Kotlin
via un BidgeComponent
// frontend/src/application/application.js
import "@hotwired/turbo"
import { Application } from "@hotwired/stimulus"
import "@hotwired/hotwire-native-bridge"
// Controllers
import MenuController from "../controllers/menu_controller.js"
// Bridge Components
import BridgeFormController from "../controllers/bridge/form_controller.js"
import BridgeMenuController from "../controllers/bridge/menu_controller.js"
import BridgeOverflowMenuController from "../controllers/bridge/overflow_menu_controller.js"
// Start Stimulus
window.Stimulus = Application.start()
// Register Controllers
Stimulus.register("menu", MenuController)
// Register Bridge Components
Stimulus.register("bridge--form", BridgeFormController)
Stimulus.register("bridge--menu", BridgeMenuController)
Stimulus.register("bridge--overflow-menu", BridgeOverflowMenuController)
// frontend/src/controllers/bridge/menu_controller.js
import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
import { BridgeElement } from "@hotwired/hotwire-native-bridge"
export default class extends BridgeComponent {
static component = "menu"
static targets = [ "title", "item" ]
show(event) {
if (this.enabled) {
event.stopImmediatePropagation()
this.notifyBridgeToDisplayMenu(event)
}
}
notifyBridgeToDisplayMenu(event) {
const title = new BridgeElement(this.titleTarget).title
const items = this.makeMenuItems(this.itemTargets)
this.send("display", { title, items }, message => {
const selectedIndex = message.data.selectedIndex
const selectedItem = new BridgeElement(this.itemTargets[selectedIndex])
selectedItem.click()
})
}
makeMenuItems(elements) {
const items = elements.map((element, index) => this.menuItem(element, index))
const enabledItems = items.filter(item => item)
return enabledItems
}
menuItem(element, index) {
const bridgeElement = new BridgeElement(element)
if (bridgeElement.disabled) return null
return {
title: bridgeElement.title,
index: index
}
}
}
Et du côté Swift ?
Et du côté Swift ?
//Brige/MenuController.swift
import Foundation
import HotwireNative
import UIKit
/// Bridge component to display a native bottom sheet menu,
/// which will send the selected index of the tapped menu item back to the web.
final class MenuComponent: BridgeComponent {
override class var name: String { "menu" }
override func onReceive(message: Message) {
guard let event = Event(rawValue: message.event) else {
return
}
switch event {
case .display:
handleDisplayEvent(message: message)
}
}
// MARK: Private
private var viewController: UIViewController? {
delegate.destination as? UIViewController
}
private func handleDisplayEvent(message: Message) {
guard let data: MessageData = message.data() else { return }
showAlertSheet(with: data.title, items: data.items)
}
private func showAlertSheet(with title: String, items: [Item]) {
let alertController = UIAlertController(
title: title,
message: nil,
preferredStyle: .actionSheet
)
for item in items {
let action = UIAlertAction(title: item.title, style: .default) { [unowned self] _ in
onItemSelected(item: item)
}
alertController.addAction(action)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alertController.addAction(cancelAction)
// Set popoverController for iPads
if let popoverController = alertController.popoverPresentationController {
if let barButtonItem = viewController?.navigationItem.rightBarButtonItem {
popoverController.barButtonItem = barButtonItem
} else {
popoverController.sourceView = viewController?.view
popoverController.sourceRect = viewController?.view.bounds ?? .zero
popoverController.permittedArrowDirections = []
}
}
viewController?.present(alertController, animated: true)
}
private func onItemSelected(item: Item) {
reply(
to: Event.display.rawValue,
with: SelectionMessageData(selectedIndex: item.index)
)
}
}
// MARK: Events
private extension MenuComponent {
enum Event: String {
case display
}
}
// MARK: Message data
private extension MenuComponent {
struct MessageData: Decodable {
let title: String
let items: [Item]
}
struct Item: Decodable {
let title: String
let index: Int
}
struct SelectionMessageData: Encodable {
let selectedIndex: Int
}
}
On pilote le contenu depuis l'application Django !
Quand passer à des écrans natifs avec Hotwire Native ?
Trois types d’écrans à envisager pour un rendu natif :
- Écran d’accueil natif : Permet un lancement rapide de l’application et une haute qualité d’affichage dès le début. Exemples : HEY et Basecamp utilisent des vues SwiftUI natives dès le lancement, avec des données en cache pour accélérer l’accès hors-ligne.
-
Cartes natives : Les cartes basées sur des solutions comme MapKit offrent une meilleure expérience utilisateur que celles rendues dans une vue web. Cela permet d’afficher des cartes interactives avec des pins, des overlays, et des itinéraires.
-
Écrans utilisant des APIs natives : Lorsque vous interagissez avec des APIs natives, comme HealthKit, AVFoundation ou CoreLocation.
Trois types d’écrans mieux adaptés à une vue web :
- Écrans qui changent souvent : Les écrans comme les paramètres ou préférences, qui subissent des modifications fréquentes, sont plus faciles à gérer via HTML. Les mises à jour web sont moins coûteuses que les mises à jour natives
-
Fonctionnalités CRUD classiques : Les opérations basiques comme les CRUD (Create, Read, Update, Delete), qui n’ajoutent pas de valeur spécifique à l’expérience utilisateur, peuvent rester dans une vue web pour gagner du temps de dev.
-
Contenus dynamiques complexes : Les listes d’éléments hétérogènes, comme un flux d’actualités, sont souvent plus faciles à rendre via Hotwire. Chaque type d’élément natif nécessiterait une vue et une validation distincte, alors qu’une vue web peut les traiter facilement sans passer par une mise à jour sur l’App Store.
L’objectif est d’avoir une application simplement acceptée sur l’App Store, avec les fonctionnalités minimales.
Pour le lancement initial, vous n’avez pas nécessairement besoin d’écrans natifs.
- Hotwire est mature et soutenu par une grosse communauté
- Une solution framework agnostique
- Ne nécessite pas d'API pour le back-end
- Amélioration progressive comme philosophie: aller vers des écrans natifs SI nécessaire, et QUAND nécessaire
De votre back-end à l'AppStore ?
L'essentiel de la logique est contrôlée par le back-end Python !
Copy of Comment mettre à jour les messages lorsqu'on travaille avec htmx
By Thierry Chappuis
Copy of Comment mettre à jour les messages lorsqu'on travaille avec htmx
Slides de présentation pour le mentorat Docstring.fr du mercredi 25 septembre
- 50