smart_measurer
Mesurez la taille réelle d'un widget Flutter et réagissez-y, sans le flash
visible typique d'un GlobalKey associé à addPostFrameCallback écrit à la
main.
Le package expose quatre widgets, du plus simple au plus complet.
| Widget | Usage | Délai d'une frame |
|---|---|---|
MeasuredDecoration |
Dessiner un décor (fond, bordure) dépendant de la taille, sans reconstruire de widgets | Non |
SimpleMeasurer |
Un décor qui s'adapte à la taille d'un enfant, celui-ci étant superposé automatiquement | Oui |
SmartMeasurer |
Contrôle complet : vous insérez vous-même measuredChild où vous le souhaitez |
Oui |
SmartMeasurerGroup |
Mesurer plusieurs enfants à la fois, par exemple pour aligner des largeurs | Oui |
Installation
dependencies:
smart_measurer: ^1.0.3
import 'package:smart_measurer/smart_measurer.dart';
Le délai d'une frame (une limitation incontournable)
Flutter exécute toujours la construction avant la mise en page au sein d'une
même frame. Pour que builder retourne un Widget qui dépende de la taille
de l'enfant, cette taille doit avoir été mesurée lors d'une frame
précédente. C'est une contrainte du pipeline de Flutter, et non une limite
propre à ce package : tout mécanisme qui retourne un Widget arbitraire en
fonction d'une taille mesurée subit le même délai.
Si vous avez seulement besoin de dessiner, et non de construire de nouveaux
widgets, en fonction de la taille, utilisez plutôt MeasuredDecoration, qui
n'a aucun délai.
SmartMeasurer
C'est le widget le plus complet. measuredChild doit être inséré
exactement une fois dans l'arbre retourné par builder.
Widget buildSmartMeasurerExample() {
return SmartMeasurer(
child: const Text('Hello World'),
builder: (context, measuredChild, size, isPrecise, constraints) {
return Container(
width: size.width + 24,
height: size.height + 24,
decoration: BoxDecoration(
color: isPrecise ? Colors.blue.shade100 : Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
child: measuredChild,
);
},
);
}
Estimation
Entre un changement de contraintes et la prochaine mesure précise, la taille
est estimée (isPrecise vaut false). Cela garantit qu'aucune taille
périmée n'est jamais présentée comme précise.
- Si
estimateBuilderest fourni, il est utilisé pour l'estimation. S'il lève une exception ou retourne uneSizeinvalide (NaN, infinie ou négative), l'erreur est capturée, avec un avertissement en mode debug, et l'estimation par défaut prend le relais. - Sinon, l'estimation suit
estimateStrategy:zero: toujoursSize.zerotant qu'aucune mesure précise n'est arrivée ;previousSize(comportement par défaut) : réutilise la dernière taille précise si elle satisfait toujours les contraintes actuelles, sinonSize.zero;constraints: utilise les contraintes maximales bornées, avec 0 sur tout axe non borné ;custom: signale queestimateBuilderest censé toujours fournir une estimation valide ; à défaut,Size.zeroest utilisé, avec un avertissement.
useConstraintsAsInitialEstimate est conservé pour la compatibilité
ascendante, mais il est ignoré dès que estimateStrategy est défini.
Coût de performance
Chaque mesure précise déclenche un setState interne, qui reconstruit tout
ce que retourne builder. Si builder construit un sous-arbre coûteux mais
par ailleurs statique, enveloppez-le dans un RepaintBoundary, ou extrayez-le
dans un widget const séparé. sizeChangeThreshold et notifyDebounce
permettent aussi de réduire la fréquence des reconstructions lors de mises en
page bruyantes ou qui changent rapidement.
Paramètres principaux
| Paramètre | Rôle |
|---|---|
estimateBuilder |
Fournit une estimation personnalisée, prioritaire sur estimateStrategy |
keepPreviousSizeOnChildChange |
Réutilise la dernière taille précise comme estimation quand child change (true par défaut) |
sizeChangeThreshold |
Delta minimal, en pixels logiques, requis pour déclencher une reconstruction |
onSizeChanged |
Appelé à chaque mesure précise commise, indépendamment de builder |
notifyDebounce |
Regroupe les mesures rapprochées en une seule notification |
animationDuration / animationCurve |
Anime la transition entre deux tailles précises |
controller |
SmartMeasurerController externe pour lire la taille hors de builder |
unconstrained |
Enveloppe measuredChild dans un UnconstrainedBox |
ignorePointerDuringEstimate |
Désactive les interactions tant que isPrecise vaut false |
placeholderBuilder |
Affiché tant qu'aucune mesure précise n'a jamais été obtenue |
debugPaintEstimatedSize |
Bordure rouge ou verte de debug, selon que la mesure est estimée ou précise |
constrainToAvailableSpace |
Contraint le résultat à l'espace disponible du parent |
scrollable / scrollDirection |
Enveloppe le résultat dans un SingleChildScrollView |
debugLabel |
Étiquette de la GlobalKey interne, utile en debug avec plusieurs SmartMeasurer imbriqués |
SimpleMeasurer
Version simplifiée : l'enfant est automatiquement superposé sur le décor
retourné par builder. Vous n'écrivez que le décor.
Widget buildSimpleMeasurerExample() {
return SimpleMeasurer(
child: const Text('Hello'),
builder: (context, size, isPrecise, constraints) {
return Container(
width: size.width + 16,
height: size.height + 16,
color: Colors.amber.shade100,
);
},
);
}
L'enfant est toujours mesuré à sa taille naturelle : il n'est jamais
contraint par la taille du décor retourné, même quand ce décor est lui-même
dérivé de la taille mesurée, grâce à un UnconstrainedBox interne.
SimpleMeasurer transmet la plupart des options facultatives de
SmartMeasurer (sizeChangeThreshold, notifyDebounce, animationDuration,
controller, placeholderBuilder, debugPaintEstimatedSize,
scrollable/constrainToAvailableSpace, etc.) — voir le tableau ci-dessus.
SmartMeasurerGroup
Ce widget mesure plusieurs widgets à la fois et renvoie leurs tailles sous
forme de liste, dans le même ordre que children. Il est plus léger que
SmartMeasurer : pas de stratégie d'estimation, pas d'animation, pas de
seuil, juste la mesure de N widgets et le rapport de leur taille.
Widget buildSmartMeasurerGroupExample() {
return SmartMeasurerGroup(
children: const [
Text('Court'),
Text('Un peu plus long'),
Text('Moyen'),
],
builder: (context, measuredChildren, sizes, allPrecise, constraints) {
final double maxWidth = sizes.isEmpty
? 0.0
: sizes.map((s) => s.width).reduce((a, b) => a > b ? a : b);
return Wrap(
spacing: 8,
children: [
for (final child in measuredChildren)
Container(
width: allPrecise ? maxWidth + 24 : null,
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
alignment: Alignment.center,
child: child,
),
],
);
},
);
}
measuredChildren correspond un à un à children, et chaque élément doit
être inséré exactement une fois. sizes utilise Size.zero pour toute
entrée pas encore mesurée. allPrecise n'est vrai qu'une fois que chaque
enfant a rapporté une mesure réelle pour la passe de mise en page en cours.
MeasuredDecoration
Ce widget n'a aucun délai d'une frame : painter ne construit pas de
Widget, il dessine directement sur un Canvas pendant la phase de peinture,
une fois que la taille de l'enfant est déjà connue dans la même passe de mise
en page.
Widget buildMeasuredDecorationExample() {
return MeasuredDecoration(
painter: (canvas, size, constraints) {
final paint = Paint()..color = Colors.deepPurple.shade100;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(-12, -12, size.width + 24, size.height + 24),
const Radius.circular(12),
),
paint,
);
},
child: const Text('Sans délai'),
);
}
En contrepartie, painter ne peut pas produire de nouveaux widgets
interactifs, comme des boutons ou du texte sélectionnable : il ne peut que
dessiner (formes, dégradés, ombres...). Pour un sous-arbre de widgets réactif
à la taille, utilisez plutôt SmartMeasurer. onSizeChanged permet de
réagir aux changements de taille sans provoquer de reconstruction.
SmartMeasurerController
Il s'agit d'un handle externe vers la taille courante d'un SmartMeasurer,
pour du code qui doit lire la taille mesurée en dehors de builder, par
exemple pour piloter un widget frère, un décalage de défilement, ou un
gestionnaire d'état.
class ControllerExample extends StatefulWidget {
const ControllerExample({super.key});
@override
State<ControllerExample> createState() => _ControllerExampleState();
}
class _ControllerExampleState extends State<ControllerExample> {
final SmartMeasurerController controller = SmartMeasurerController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
SmartMeasurer(
controller: controller,
child: const Icon(Icons.star, size: 48),
builder: (context, measuredChild, size, isPrecise, constraints) {
return measuredChild;
},
),
ListenableBuilder(
listenable: controller,
builder: (context, _) => Text('${controller.size}'),
),
],
);
}
}
Pensez à appeler controller.dispose() vous-même : SmartMeasurer se
contente de le lire, il ne le possède jamais et ne le libère donc jamais à
votre place.
Tests
Les notifications de mesure sont livrées via
SchedulerBinding.addPostFrameCallback. Dans les tests de widgets, la
séquence est donc la suivante :
pumpWidget()exécute la première frame : une taille estimée est construite, puis la mise en page se déroule et programme le post-frame callback.- Un premier
pump()supplémentaire exécute la deuxième frame : le post-frame callback se déclenche et commet la mesure précise viasetState. - Un second
pump()supplémentaire, quand il apparaît, laisse un effet post-frame additionnel, commeSmartMeasurerController.notifyListeners, se stabiliser.
void main() {
testWidgets('SmartMeasurer starts estimated and becomes precise',
(tester) async {
bool? lastIsPrecise;
await tester.pumpWidget(
MaterialApp(
home: SmartMeasurer(
child: const SizedBox(width: 120, height: 40),
builder: (context, measuredChild, size, isPrecise, constraints) {
lastIsPrecise = isPrecise;
return measuredChild;
},
),
),
);
// Première frame : aucune mesure n'est encore arrivée.
expect(lastIsPrecise, isFalse);
await tester.pump();
expect(lastIsPrecise, isTrue);
});
}
Dépannage : « isn't defined » pour SmartMeasurerGroup ou SmartMeasurerController
Si votre projet ne trouve pas ces symboles après un flutter pub get
classique, vérifiez ceci, dans l'ordre :
- Le fichier
lib/smart_measurer.dartdoit contenir tout le code du package, et non une version tronquée. Si c'est un fichier « barrel » qui se contente de ré-exporter un fichiersrc/, vérifiez que l'exportne restreint pas la liste avec unshowincomplet : les quatre classes publiques (SmartMeasurer,SimpleMeasurer,SmartMeasurerGroup,MeasuredDecoration), ainsi queSmartMeasurerController, doivent toutes y figurer. - Le champ
name:depubspec.yamldoit valoirsmart_measurer, afin queimport 'package:smart_measurer/smart_measurer.dart'pointe bien vers le bon package. - Si le projet contient un dossier
example/, sachez qu'il s'agit d'un package Flutter à part entière, avec son propre cache. Lancezflutter clean && flutter pub getaussi bien à la racine que depuisexample/. - Redémarrez l'Analysis Server de votre IDE : il arrive qu'il conserve en
cache un ancien arbre de symboles, même après un
pub getréussi.
Libraries
- smart_measurer
- Measure a widget's real size and react to it — with estimation during the one-frame measurement delay, and a zero-delay variant for pure drawing.