Raccourcis d'application et tuiles de Paramètres rapides sur Android : enregistrer de l'eau sans ouvrir l'application
Un guide pratique de l'API dynamique ShortcutManager et de TileService sur Android — comment permettre à une action en un tap de contourner complètement l'application, avec les pièges qui font trébucher la plupart des implémentations.
La version la plus rapide de n’importe quelle action d’application est celle qui n’ouvre jamais l’application. La boucle centrale de Hydrame — enregistrer un verre d’eau — se produit des dizaines de fois par jour, et demander à quelqu’un de déverrouiller son téléphone, de trouver l’icône, d’attendre un démarrage à froid, puis d’appuyer sur un bouton, ça fait trois étapes de trop pour quelque chose qui devrait n’en prendre qu’une. Android offre deux façons de ramener cela à un seul tap : les raccourcis d’application sur l’écran d’accueil et une tuile de Paramètres rapides (Quick Settings Tile). Ils résolvent le même problème depuis des points d’entrée différents, et les deux sont plus simples à implémenter qu’il n’y paraît. Voici comment chacun fonctionne réellement.
Deux points d’entrée, deux rôles différents
Les raccourcis d’application vivent sous l’icône du launcher — un appui long fait apparaître un menu avec les actions que vous avez définies. Ils sont faits pour un petit ensemble d’actions à haute fréquence, liées à cette application précise, découvrables par quiconque a déjà l’icône sous les yeux.
Une tuile de Paramètres rapides vit dans le panneau de notifications, à côté du Wi-Fi et du Bluetooth. Elle sert à une seule action que l’on veut avoir disponible de n’importe où, sans avoir à chercher une icône — plus proche d’un bouton matériel que d’une fonctionnalité d’application.
Hydrame propose les deux : un raccourci pour « Log 250ml » et « Log 500ml » sous l’icône du launcher, et une tuile de Paramètres rapides qui enregistre une quantité par défaut d’un seul tap depuis le panneau de l’écran verrouillé. Ce sont deux API distinctes, deux chemins d’enregistrement distincts, et cela vaut la peine de les construire séparément plutôt que d’essayer de les unifier.
Raccourcis dynamiques : ShortcutManagerCompat
Les raccourcis existent en deux variantes — statiques (déclarés en XML, fixés à la compilation) et dynamiques (poussés à l’exécution, capables de changer selon l’état de l’application). Un « Log 250ml » fixe n’a pas besoin de changer, donc le statique fonctionnerait, mais les raccourcis dynamiques permettent aux quantités de refléter ce que la personne boit réellement — les deux tailles qu’elle utilise le plus, pas deux tailles devinées par l’application.
val shortcut = ShortcutInfoCompat.Builder(context, "log_250ml")
.setShortLabel("250ml")
.setLongLabel("Log 250ml of water")
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_glass))
.setIntent(
Intent(context, LogIntakeReceiverActivity::class.java).apply {
action = ACTION_LOG_INTAKE
putExtra(EXTRA_AMOUNT_ML, 250)
}
)
.build()
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut250, shortcut500))
L’intent doit cibler une Activity, pas un BroadcastReceiver — c’est la seule contrainte stricte imposée par l’API de raccourcis, contrairement au pattern des actions de notification où un receiver est le bon choix. La recommandation même de Google est de rendre cette Activity aussi invisible que possible : pas de layout, finish() immédiatement après l’écriture.
class LogIntakeReceiverActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val amount = intent.getIntExtra(EXTRA_AMOUNT_ML, 0)
lifecycleScope.launch {
intakeDao.logIntake(amount, timestamp = System.currentTimeMillis())
Toast.makeText(this@LogIntakeReceiverActivity, "Logged ${amount}ml", Toast.LENGTH_SHORT).show()
finish()
}
}
}
Donnez-lui un thème transparent (Theme.Material3.DayNight.NoActionBar surchargé avec windowIsTranslucent) et la transition se lit comme une confirmation système, pas comme le lancement d’une application — le tout se résout en moins d’une demi-seconde sur un appareil milieu de gamme.
Rang et plafond : les raccourcis ne sont pas un menu
ShortcutManagerCompat.setDynamicShortcuts() remplace l’ensemble complet à chaque appel, et le système plafonne le nombre — getMaxShortcutCountPerActivity() — généralement autour de quatre ou cinq selon le launcher. Poussez-en plus et les excédents sont silencieusement abandonnés, pas mis en file d’attente. L’ordre compte aussi : les raccourcis s’affichent dans l’ordre où vous les passez, donc la quantité que la personne enregistre le plus doit venir en premier, pas en dernier.
L’autre piège est d’appeler setDynamicShortcuts() trop souvent. Chaque appel est censé correspondre à un vrai changement d’état — les habitudes d’un utilisateur qui évoluent sur plusieurs semaines, pas à chaque lancement d’application. Réécrire les deux mêmes raccourcis à chaque démarrage à froid ne fait rien d’autre que vous coûter un appel système ; vérifiez si l’ensemble a réellement changé avant de le pousser.
La tuile de Paramètres rapides : TileService
Une tuile est un service, pas une Activity — Android contrôle son cycle de vie, et vous n’obtenez qu’un seul point d’entrée qui compte vraiment : onClick().
class LogWaterTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
qsTile?.apply {
label = "Log water"
icon = Icon.createWithResource(this@LogWaterTileService, R.drawable.ic_tile_glass)
state = Tile.STATE_ACTIVE
updateTile()
}
}
override fun onClick() {
super.onClick()
val pendingResult = goAsync()
lifecycleScope.launch {
try {
intakeDao.logIntake(defaultAmountMl(), timestamp = System.currentTimeMillis())
} finally {
pendingResult.finish()
}
}
}
}
goAsync() apparaît ici pour la même raison que dans un BroadcastReceiver : onClick() est censé retourner rapidement, et une écriture Room sur le thread principal saccaderait l’animation de fermeture du panneau. La tuile doit être déclarée dans le manifeste avec la permission BIND_QUICK_SETTINGS_TILE, et — c’est le point qui piège les gens — elle n’apparaît nulle part automatiquement. L’utilisateur doit la faire glisser parmi ses tuiles actives depuis l’écran d’édition du panneau. TileService.requestListeningState() peut inciter Android 13+ à suggérer de l’ajouter, mais il n’y a aucun moyen de forcer son placement, et aucune raison de croire que la plupart des utilisateurs s’en donneront la peine. Considérez la tuile comme une fonctionnalité pour utilisateurs avancés, pas comme un chemin d’interaction principal.
Garder l’état de la tuile honnête
Une tuile qui affiche toujours la même icône, indépendamment de l’état de l’application, paraît cassée la première fois qu’elle ne l’est pas. Si l’objectif quotidien de Hydrame est déjà atteint, onStartListening() est l’endroit pour le refléter :
override fun onStartListening() {
super.onStartListening()
lifecycleScope.launch {
val goalMet = intakeDao.todayTotal() >= goalDao.currentGoal()
qsTile?.apply {
state = if (goalMet) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE
subtitle = if (goalMet) "Goal reached" else null
updateTile()
}
}
}
onStartListening() se déclenche chaque fois que le panneau contenant votre tuile devient visible, pas sur une minuterie, donc cette lecture est peu coûteuse et à jour par construction — pas de polling, pas de tâche en arrière-plan pour la garder synchronisée.
Pourquoi ça vaut la peine d’être construit
Aucune de ces deux API n’est difficile une fois que l’on connaît les deux règles qui ne sautent pas aux yeux dans la documentation : les raccourcis ont besoin d’une Activity même pour une simple écriture en arrière-plan, et une tuile a besoin de goAsync() pour la même raison qu’un receiver d’action de notification. Ce qu’elles apportent est disproportionné par rapport au code — un appui long ou un tap sur le panneau qui remplace le lancement complet de l’application pour la poignée d’actions que quelqu’un répète chaque jour. C’est le genre de friction dont les utilisateurs ne remarquent pas consciemment la disparition — seulement son absence si on la leur retire. Pour toute application avec une ou deux actions dominantes — enregistrer quelque chose, démarrer quelque chose, basculer quelque chose — ça vaut l’après-midi qu’on y consacre.
// À lire aussi
D’autres notes du journal
L'API In-App Review d'Android : demander une note sans être pénible
Un guide pratique de l'API In-App Review de Google — comment elle fonctionne réellement, quand la déclencher, et pourquoi la popup classique « notez-nous » nuit discrètement à votre note sur le Play Store.
Les types de services au premier plan sur Android en 2026 : choisir celui auquel votre fonctionnalité correspond vraiment
Un guide pratique des restrictions de types de services au premier plan d'Android — dataSync, mediaPlayback, specialUse, shortService — et comment choisir le bon sans se faire tuer ou rejeter.
StateFlow vs SharedFlow dans Jetpack Compose : modéliser l'état UI sans le bug de replay
Quand utiliser StateFlow et quand utiliser SharedFlow dans un ViewModel Compose — et le bug d'événement unique qui apparaît quand on les confond.