- Upgrade react-native-android-widget 0.20.1 -> 0.21.0 (expedited
WorkManager upstream, clickable-area ripple corner fix)
- patch-package on the lib:
1. WIDGET_CLICK bypasses WorkManager and runs the headless JS task
directly on the ReactHost (goAsync + WorkManager fallback), removing
two Room writes + worker dispatch from every tap and WorkManager
init from the cold path
2. WEBP_LOSSLESS (effort 0) encoding instead of PNG q100 on API 30+,
several times faster for widget-sized bitmaps
3. Copy identical light/dark collection item files instead of
compressing every row bitmap twice when the config is single-mode
- Extract dependency-light src/services/widgetState.ts so the headless
click handler no longer pulls DB/drizzle/date-fns at cold start
- Smoke tests: lock the light import graph + patch/postinstall wiring
- Bump v1.6.5 (versionCode 17) for the preview build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
Simpl-Liste
Application mobile de gestion de tâches minimaliste par La Compagnie Maximus.
Bundle ID : com.lacompagniemaximus.simpliste — Scheme : simplliste
Stack
- React Native 0.81 + Expo SDK 54 (New Architecture)
- Expo Router 6 (file-based, typed routes)
- TypeScript 5.9 (strict)
- NativeWind 4.2 (Tailwind CSS 3.4 pour RN)
- Drizzle ORM 0.45 + expo-sqlite 16 (SQLite local,
simpliste.db) - Zustand 5 + AsyncStorage (state UI persisté)
- i18next 25 + react-i18next (FR/EN, français par défaut)
- lucide-react-native (icônes)
- date-fns 4 (dates)
Scripts
npm start # Expo dev server
npm run android # Lancer sur Android
npm run ios # Lancer sur iOS
Structure
app/
├── _layout.tsx # Root stack (fonts, migrations, theme)
├── (tabs)/
│ ├── _layout.tsx # Tab bar (3 onglets)
│ ├── index.tsx # Inbox
│ ├── lists.tsx # Toutes les listes
│ └── settings.tsx # Paramètres + gestion tags
├── task/
│ ├── new.tsx # Création de tâche (modal)
│ └── [id].tsx # Détail/édition tâche
└── list/
└── [id].tsx # Détail d'une liste
src/
├── components/
│ ├── FilterMenu.tsx # Modal filtres (bottom sheet)
│ ├── SortMenu.tsx # Modal tri (bottom sheet)
│ └── task/
│ ├── TagChip.tsx # Pill tag réutilisable
│ └── TaskItem.tsx # Rangée de tâche
├── db/
│ ├── client.ts # Config SQLite + Drizzle
│ ├── schema.ts # Tables : lists, tasks, tags, task_tags
│ ├── migrations/ # SQL migrations (auto-appliquées au démarrage)
│ └── repository/
│ ├── lists.ts # CRUD listes + ensureInbox()
│ ├── tags.ts # CRUD tags + relations task-tag
│ └── tasks.ts # CRUD tâches, filtres, tri, sous-tâches
├── i18n/
│ ├── index.ts # Init i18next (détection locale appareil)
│ ├── fr.json # Traductions françaises
│ └── en.json # Traductions anglaises
├── lib/
│ ├── priority.ts # Helpers couleurs priorité
│ ├── recurrence.ts # Types récurrence + calcul prochaine occurrence
│ ├── uuid.ts # Wrapper expo-crypto randomUUID
│ └── validation.ts # Validation UUID pour deep links
├── services/
│ ├── calendar.ts # Sync expo-calendar
│ ├── icsExport.ts # Export .ics + partage
│ ├── notifications.ts # Planification expo-notifications
│ └── widgetSync.ts # Sync tâches + thème vers widget Android
├── stores/
│ ├── useSettingsStore.ts # Thème, locale, notifs, calendrier
│ └── useTaskStore.ts # État tri/filtre
├── theme/
│ └── colors.ts # Palette centralisée (bleu, crème, terracotta)
└── widgets/
├── TaskListWidget.tsx # Composant widget Android (3 tailles, dark mode)
└── widgetTaskHandler.ts # Handler headless pour actions widget
Base de données
4 tables SQLite gérées par Drizzle ORM. Migrations auto-appliquées via useMigrations().
- lists : id, name, color, icon, position, is_inbox, timestamps
- tasks : id, title, notes, completed, priority (0-3), due_date, list_id → lists, parent_id (sous-tâches), position, recurrence, calendar_event_id, timestamps
- tags : id, name, color, timestamps
- task_tags : task_id → tasks (CASCADE), tag_id → tags (CASCADE)
Générer une migration
npx drizzle-kit generate
Puis mettre à jour src/db/migrations/migrations.js si nécessaire.
Conventions
- Français par défaut, anglais en second. Toutes les chaînes visibles dans
fr.json/en.json - Dark mode : résolu localement avec
isDarkternary (pas de NativeWind dark: prefix) - Composants : headers custom par écran (pas de React Navigation header)
- Polling 500ms : les écrans rechargent les données par intervalle (pas de live queries)
- Pas de state global pour les tâches : chargées depuis SQLite par écran
- Icônes via lucide-react-native, polices Inter (400/500/600/700)
- UUID générés via
expo-crypto - Sous-tâches = tâches avec
parentIdnon-null
Palette de couleurs
| Token | Valeur |
|---|---|
| bleu | #4A90A4 |
| crème | #FFF8F0 |
| terracotta | #C17767 |
| vert | #8BA889 |
| sable | #D4A574 |
| violet | #7B68EE |
| rouge | #E57373 |
| teal | #4DB6AC |
Couleurs sombres : fond #1A1A1A, surface #2A2A2A, bordure #3A3A3A, texte #F5F5F5, secondaire #A0A0A0
Widget Android
3 tailles configurées dans app.json (plugin react-native-android-widget) :
- SimplListeSmall (2×2) — Compteur de tâches + bouton ajout
- SimplListeMedium (2×4) — Liste de 4 tâches avec indicateur couleur de liste
- SimplListeLarge (4×4) — Liste de 8 tâches
Sync des données
widgetSync.tslit les tâches depuis SQLite et les cache dans AsyncStorage (widget:state)- Le thème est lu depuis AsyncStorage (
simpl-liste-settings→state.theme), résolu sisystemviaAppearance.getColorScheme() widgetTaskHandler.tsgère le rendu headless (quand l'app n'est pas ouverte) en lisant la clé consolidéewidget:statewidgetState.tscontient l'état widget + accesseurs AsyncStorage. Module volontairement léger (pas de DB/drizzle/date-fns) : c'est le chemin critique du cold start headless. Le smoke test verrouille son graphe d'import.- Les couleurs du widget suivent la même palette que l'app (voir
LIGHT_COLORS/DARK_COLORSdansTaskListWidget.tsx) - Un debounce de 600ms sur
TOGGLE_EXPANDempêche les double-taps d'annuler l'expansion
Latence des interactions (expand/collapse, toggle)
Chemin d'un tap : broadcast → RNWidgetProvider → task headless JS → renderWidget → bitmap → RemoteViews. Optimisations en place :
- Render optimiste dans
widgetTaskHandler.ts:renderWidgetavant la persistance AsyncStorage et le write DB - Patchs natifs (
patches/react-native-android-widget+<version>.patch, appliqués parpostinstall→ patch-package, donc aussi en build EAS) :- Les
WIDGET_CLICKcourt-circuitent WorkManager (RNWidgetClickTask.java: exécution directe sur le ReactHost via goAsync, fallback WorkManager). Les autres actions (UPDATE/ADDED/RESIZED) gardent WorkManager expedited (upstream 0.21.0) - Compression WEBP_LOSSLESS effort 0 au lieu de PNG q100 (API 30+) dans
RNWidgetImageProvider.writeImage - Dédup light/dark des items de collection quand la config est unique : copie de fichier au lieu d'une seconde compression
- Les
- En cas d'upgrade de la lib : régénérer le patch (
npx patch-package react-native-android-widget) après avoir réappliqué les 3 modifications ; le postinstall échoue bruyamment si le patch ne s'applique plus - Mesurer (build dev sur appareil) :
adb logcat -s ReactNativeJS | grep '\[widget\]'— chaque étape du handler est instrumentée en__DEV__. Le plancher incompressible reste le cold start du runtime JS headless (~0,5-1,5s si le process est mort) ; à chaud, viser < 300ms
Clés AsyncStorage utilisées par le widget
| Clé | Contenu |
|---|---|
widget:state |
WidgetState sérialisé JSON (tasks, isDark, expandedTaskIds) |
simpl-liste-settings |
Store Zustand persisté (contient state.theme, state.widgetPeriodWeeks) |
Build & déploiement
Profiles EAS dans eas.json :
- development — APK avec dev client
- preview — APK de distribution directe (hors Play Store)
- production — AAB pour le Play Store,
autoIncrement: truesurversionCode
Commandes de build
npx eas-cli build --platform android --profile preview --non-interactive # APK
npx eas-cli build --platform android --profile production --non-interactive # AAB
Important : eas n'est pas installé globalement, utiliser npx --yes eas-cli (pas npx eas).
Processus de release
- Bumper
versiondansapp.jsonETpackage.json - Bumper
android.versionCodedansapp.json— doit être strictement supérieur au versionCode du dernier build publié. Android refuse d'installer un APK avec un versionCode égal ou inférieur. Vérifier le dernier versionCode avec :
⚠️npx --yes eas-cli build:list --platform android --limit 1 --json 2>/dev/null | jq '.[0].appBuildVersion'autoIncrement: truedans eas.json ne s'applique qu'au profilproduction. Pour le profilpreview(APK), le versionCode vient directement deapp.json— il faut le mettre à jour manuellement. - Commit le bump de version, tag, push
- Build preview (APK) :
npx --yes eas-cli build --platform android --profile preview --non-interactive - Télécharger l'APK et créer la release sur Forgejo :
# Récupérer l'URL de l'APK npx --yes eas-cli build:list --platform android --limit 1 --json 2>/dev/null | jq -r '.[0].artifacts.buildUrl' # Télécharger curl -L -o simpl-liste-vX.Y.Z.apk "<url>" # Créer la release curl -X POST ".../api/v1/repos/maximus/simpl-liste/releases" -d '{"tag_name":"vX.Y.Z",...}' # Attacher l'APK curl -X POST ".../releases/{id}/assets?name=simpl-liste-vX.Y.Z.apk" -F "attachment=@fichier.apk" - Le bouton « Vérifier les mises à jour » dans l'app utilise l'endpoint
/releases/latestet propose le téléchargement de l'asset.apk
Repo Forgejo
- URL :
https://git.lacompagniemaximus.com/maximus/simpl-liste - Remote git :
origin(push via HTTPS avec token dans~/.git-credentials) - Issues : utilisées pour le suivi des bugs/features
- Releases : distribution APK avec assets attachés
Mises à jour in-app
Le bouton dans Paramètres > À propos appelle GET /api/v1/repos/maximus/simpl-liste/releases/latest (repo public, pas d'auth nécessaire). Compare release.tag_name (ex: v1.0.1) avec Constants.expoConfig.version. Si différent, affiche une Alert avec le changelog (release.body) et un lien vers le premier asset .apk trouvé.