Une introduction à la notion de Thème et plus spécifiquement à la notion de Styles de Texte (TextStyle & TextTheme).

Difficulté: Intermédiaire


Introduction

Une des plus grandes joies lorsque l’on débute en Flutter, c’est cette liberté de pouvoir modifier l’affichage au fur et à mesure que l’on développe. Cela permet de faire des essais, de répondre en temps réel à des demandes des clients, de voir en temps réel ce à quoi quelque chose pourrait ressembler… et une fois que l'écran ou le composant sur lequel on travaille nous satisfait, on passe au suivant et ainsi de suite.

Sur certains écrans, on aimera avoir un titre en gras et italique d’une taille 28, en bleu puis sur un autre, on définira une taille de 26, mais pas italique et pourquoi pas en vert foncé. Enfin, vous voyez de quoi je parle…

Flutter fournit une classe que vous connaissez sans doute, appelée TextStyle, qui vous permet de définir le style d’un texte à afficher à l’aide du Widget Text.

Une utilisation non-contrôlée de cette classe conduit souvent au problèmes suivants:

  • manque d’harmonie

    Les styles diffèrent d’un écran à l’autre, d’un composant à l’autre…

  • difficulté d’adaptation

    Le fait d’avoir des dizaines de styles définis de manière arnarchique partout dans votre application rendra toute modification demandée par vos clients très laborieuse.

Après quelques temps, on se dit qu’avec la prochaine application, on fera mieux. Alors, on commence à créer une classe qui contiendra la liste de styles des textes et on leur donnera un nom. Tout se passe très bien et, très consciencieusement, au début de l’application, on se limite aux styles que l’on a définis mais souvent, on commence à en créer de plus en plus et, même si on se force à n’utiliser que ces styles, à nouveau, notre application perd de son harmonie. Ceci est d’autant vrai lorsque vous réutilisez des Widgets que vous avez créés d’une application à l’autre…

Qui n’a pas été confronté à cela?

D’un autre côté, on sait qu’il existe une notion de Theme en Flutter. On y jette un coup d’oeil mais au début, on se demande comment l’utiliser et l’on se dit que l’on ne pourra pas tout obtenir en s’y limitant.

Longue introduction…

Cet article a pour but de

  • démystifier cette notion de TextTheme
  • d’expliquer et quand, par qui ils sont utilisés (vous trouverez un fichier Excel qui fait l’inventaire de leur utilisation)
  • comment les réutiliser dans son propre code
  • comment et sous quelles conditions les modifier localement
  • comment définir vos propres styles de manière contrôlée.

Informations de base

Avant d’entrer dans les détails, revenons d’abord aux quelques notions de base, qui seront utilisées plus loin dans cet article.

Terminologie

Pour une explication beaucoup plus complète sur la notion de typographie peut être trouvée dans cet article.

Glyphes

En typographie, un glyphe est un symbole élémentaire destiné à représenter un caractère lisible (ref: wikipedia ).

En termes très simples, un glyphe est un caractère (a, b, D, W …), un chiffre (0, 1, 2 …), une ponctuation (;!.), un symbole ($ £ @) … un morceau de texte à afficher sur votre écran.

Font (= Police)

Dans notre monde informatique, une police (= Font) peut être comprise comme un fichier numérique contenant la définition d’un ensemble de glyphes liés graphiquement avec une taille, un poids et un style spécifiques. Un font définit entre autres, la forme, la taille de chacun de ses glyphes.


Font Size (= Taille de la police)

La taille de police (= Font Size) est exprimée en pixels logiques (Android: sp, iOS: pt, Web: rem) et définit la taille des glyphes.

Bon à savoir:
Il n'y a pas de taux de conversion entre 'sp' (Android) et 'pt' (iOS), alors qu'il existe un taux de conversion pour le Web. Ce taux de conversion est de 0,0625. Par exemple, une taille de police de 10sp (Android) sera 10pt (iOS) et 0,625rem (Web).



En Flutter, la taille de police par défaut est définie à 14.

Une même valeur de taille de police peut donner différentes dispositions, selon la police utilisée!

Chaque police définit sa propre taille de référence, appelée ‘em', sur la base de laquelle chaque glyphe est conçu.

En conséquence, il n’est pas rare d’avoir une mise en page différente en utilisant 2 polices différentes, même en définissant la même taille de police!

Pour plus d’informations à ce sujet, consultez cet article.


Baseline

La baseline est une ligne invisible sur laquelle repose une ligne de texte.

Pour visualiser quelle est la base de référence, considérez un cahier que vous avez utilisé à l'école et dans lequel vous deviez écrire votre texte, en prenant une ligne horizontale comme référence pour dessiner vos lettres. La baseline est cette ligne de référence horizontale de votre cahier.

La baseline est une notion très importante en Material Design, car elle est utilisée pour mesurer la distance verticale entre le texte et un élément.


Letter-Spacing

Le letter-spacing fait référence à l’espace à insérer entre chaque lettre (glyphe) dans un morceau de texte.


Weight (= épaisseur)

Le weight fait référence à l’ épaisseur du trait de la police. Les weights courants sont:

  • light
  • regular
  • medium
  • bold

Cependant, il peut en exister d’autres et Flutter définit les suivants:

  • FontWeight.w100 (le moins épais)
  • FontWeight.w200 (extra-light)
  • FontWeight.w300 (light)
  • FontWeight.w400 (normal / regular / plain)
  • FontWeight.w500 (medium)
  • FontWeight.w600 (semi-bold)
  • FontWeight.w700 (bold)
  • FontWeight.w800 (extra-bold)
  • FontWeight.w900 (le plus épais)

TextSpan

En Flutter, lorsque vous utilisez le widget Text, ce dernier est rendu sous la forme d’un TextSpan (dans un widget RichText). Un span est une quantité d’espace qui a des dimensions.


Height (= hauteur)

En Flutter, le height (textStyle.height) définit un rapport à appliquer à la taille de police (= fontSize) pour donner la hauteur exacte du TextSpan qui affiche le texte.

Il faut noter que chaque police définit sa propre “hauteur de police par défaut” (= font metrics default height). Cela explique également pourquoi la hauteur d’un TextSpan peut également différer d’une police à l’autre, même en définissant la même hauteur de police.

L’image suivante illustre les variations que vous pouvez observer, en utilisant différentes polices, en définissant le même fontSize.

Chaque ligne de cette image donne le résultat définissant la hauteur comme suit:

  • ligne 1: la hauteur n’est pas définie

    la hauteur de TextSpan correspond à la “hauteur par défaut des métriques de police” (= font metrics default height)

  • ligne 2: la hauteur est 1

    la hauteur de TextSpan correspond exactement à 1.0 * fontSize

  • ligne 3: la hauteur est de 0,8

    la hauteur de TextSpan correspond exactement à 0.8 * fontSize

Variations basées sur la police et la hauteur
Variations basées sur la police et la hauteur

Le code pour produire ceci est le suivant:

Variations basées sur Font et height (code)
Variations basées sur Font et height (code)

Cette image illustre assez bien les notions que j'ai essayé d'expliquer ci-dessus:
  • chaque police a une hauteur de boîte différente, même en utilisant le même paramètre fontSize;
  • chaque police a une ligne de base différente. Aucune des lettres ne “touche” la même ligne horizontale

Cela conduit à la conclusion initiale suivante:

L’utilisation de plusieurs polices peut entraîner des alignements incohérents et un manque d’harmonie de mise en page.


Notion de TextTheme

Flutter définit une série de styles typographiques. Ces styles sont référencés par les différents widgets qui utilisent la notion de texte.

Ces styles de texte (Typography 2018) sont les suivants:

English-likeChinese, Japanese, KoreanArabic, Farsi, Hindi, Thai
NameSizeWeightLSSizeWeightSizeWeight
headline196w300-1.596w10096w400
headline260w300-0.560w40060w400
headline348w4000.0 48w40048w400
headline434w4000.2534w40034w400
headline524w4000.0 24w40024w400
headline620w5000.1521w50021w700
subtitle116w4000.1517w40017w400
subtitle214w5000.1015w50015w500
bodyText116w4000.5 17w40017w700
bodyText214w4000.2515w40015w400
button 14w5000.7515w50015w700
caption 12w4000.4 13w40013w400
overline 10w4001.5 11w40011w400
(*) LS signifie LetterSpacing



L’image suivante vous donne un aperçu de ces styles de texte pour “English-like".

2018 Material TextStyles
2018 Material TextStyles

Note

À partir de la version 1.13.8 de Flutter, certains des noms qui étaient utilisés dans la version 2014 de Material Design sont devenus obsolètes. Cela explique pourquoi vous pourriez remarquer de nombreux avertissements (= warnings) émis par le Lint lorsque vous éditez un code plus ancien.

Jusqu'à présent, l’ancien nom fonctionne toujours car un “mapping” interne est assuré, mais je vous recommande de passer aux nouveaux noms dès que possible.

Cependant, la typographie par défaut utilisée lors de l’initialisation du ThemeData est toujours 2014?!? Je suppose que cela sera corrigé sous peu.

Outre les caractéristiques physiques des glyphes (fontSize, fontWeight, letterSpacing), nous devons également prendre en compte les couleurs par défaut, qui sont définies en fonction du type de plate-forme et de la luminosité.

Le tableau suivant donne les “noms” des normes correspondantes:

PlatformiOS / macOSAndroid / FuchsiaWindowsLinux
BrightnesslightCupertinoMountainViewRedmondHelsinki
dark

Le tableau suivant indique les couleurs et les familles de polices par défaut correspondantes:

StylesBrightnessfontFamily
DarkLightCupertinoMountainViewRedmondHelsinki
headline1black54white70.SF UI DisplayRobotoSegoe UIRoboto
headline2black54white70
headline3black54white70
headline4black54white70
headline5black87white
headline6black87white
subtitle1black87white.SF UI Text
subtitle2blackwhite
bodyText1black87white
bodyText2black87white
buttonblack87white
captionblackwhite70
overlineblack54white

Quand les utiliser?

Voici un bref résumé des recommandations faites par Material Design.

Headlines

Il existe 6 headlines (headline1 à headline6) qui sont les plus grands textes à afficher à l'écran.

Ils doivent être réservés à des textes courts et importants tels que:

  • titres
  • chiffres

Subtitles

Il existe 2 subtitles (subtitle1 et subtitle2) qui sont plus petits que les headlines.

Ils devraient être réservés aux textes d’importance moyenne mais toujours relativement courts.

BodyText

Il existe 2 bodytexts (bodyText1 et bodyText2). Ils sont plus petits que subtitles et devrait généralement être utilisé pour des textes plus longs, tels que des descriptions.

Caption & Overline

Caption et overline (texte avec une ligne au-dessus) sont les plus petits.

Ils doivent être réservés pour annoter des images ou pour introduire un titre.

Button

Button doit être réservé à un appel à des actions, telles que:

  • button
  • tabs
  • navigation bars

L’image suivante montre un exemple d’utilisation de ces styles:

Example of use
Example of use

Comment Flutter compose-t-il ses TextThemes?

Lorsque vous démarrez votre application, tout commence dès que vous définissez le MaterialApp.

Les paramètres les plus importants liés au Theme qui peuvent être définis au niveau du widget MaterialApp sont:

  • theme

    Habituellement, définit le thème light (= clair).

  • themeDark

    Souvent oublié mais disponible, définit un deuxième thème pour fournir une version sombre de l’interface utilisateur. Ce themeDark peut également être utilisé pour les plates-formes qui permettent à l’utilisateur de sélectionner un “mode sombre” au niveau du système.

  • themeMode

    Ce paramètre détermine lequel des 2 thèmes utiliser [par défaut: ThemeMode.system]. Cela signifie que le thème à utiliser dépendra des paramètres utilisateur.

    Cependant, il est possible de forcer le thème, indépendamment des paramètres utilisateur, en le forçant à ThemeMode.light ou ThemeMode.dark.

  • fontFamily

    Ce paramètre définit le fontFamily par défaut à utiliser, s’il est omis dans une définition.

  • typography

    Ce paramètre (par défaut: Typography.material2014) est utilisé comme référence pour définir les textTheme, primaryTextTheme et accentTextTheme.

  • locale & localizationsDelegates

    Généralement oubliés en termes d’impact sur les textTheme(s), ces paramètres sont très importants car ils définissent les variations en termes de baseline, fontSize, fontWeight (voir plus loin) .


Comment Flutter détermine-t-il le «thème» à utiliser?

Voici l’extrait du code source ( j’ai modifié la dernière ligne du code pour faciliter la lecture ), où widget fait référence à l’instance MaterialApp:

final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
ThemeData theme;

if (widget.darkTheme != null) {
  final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
  if (mode == ThemeMode.dark ||
      (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark)) {
    theme = widget.darkTheme;
  }
}

theme ??= widget.theme ?? ThemeData(brightness: Brightness.light);

Ainsi, Flutter invoque la “factory” ThemeData qui initialise certains textStyles:

  • 3 TextTheme principaux:

    • textTheme
    • primaryTextTheme
    • accentTextTheme
  • 19 indirects:

Namedefault
toggleButtonsTheme.textStyletextTheme.bodyText2
inputDecorationTheme.labelStyletextTheme.subtitle1
inputDecorationTheme.helperStyletextTheme.caption
inputDecorationTheme.hintStyletextTheme.caption
inputDecorationTheme.errorStyletextTheme.caption
inputDecorationTheme.prefixStyletextTheme.caption
inputDecorationTheme.suffixStyletextTheme.caption
inputDecorationTheme.counterStyletextTheme.caption
sliderTheme.valueIndicatorTextStyletextTheme.bodyText1
tabBarTheme.labelStyletextTheme.bodyText1
tabBarTheme.unselectedLabelStyletextTheme.bodyText1
tooltipTheme.textStyletextTheme.bodyText2
chipTheme.labelStyletextTheme.bodyText1
chipTheme.secondaryLabelStyletextTheme.bodyText1
navigationRailTheme.unselectedLabelTextStyletextTheme.bodyText1
navigationRailTheme.selectedLabelTextStyletextTheme.bodyText1
snackBarTheme.contentTextStyletextTheme.subtitle1
popupMenuTheme.textStyletextTheme.subtitle1
bannerTheme.contentTextStyletextTheme.bodyText2

Le ‘cupertinoOverrideTheme’ est un thème spécial utilisés par certains widgets Cupertino.

Wouaw! Tant de TextStyles

Quelle est la différence entre ‘textTheme’, ‘primaryTextTheme’ et ‘accentTextTheme’?

En considérant la définition par défaut, il n’y a aucune différence en termes de fontSize, fontFamily, height, fontWeight, letterSpacing, decoration…

Des différences peuvent être définies lorsque le texte contraste avec son conteneur. La plupart du temps, cela se limitera à la notion de couleurs.


Quand sont-ils utilisés et par quels Widgets du Framework Flutter?

Il y a quelque temps, afin de mieux comprendre comment et quand ces TextStyles prédéfinis étaient utilisés par le Flutter Framework, j’ai commencé à lire le code source entier et à faire l’inventaire. Voici ma feuille Excel personnelle qui donne un résumé de la façon dont TextStyles sont utilisés par les principaux widgets Flutter (version 1.17):

TextStyle Excel Cheatsheet

Cliquez pour télécharger

En un coup d'œil, nous voyons directement que headline1 n’est jamais utilisé, headline(2-5) ne sont utilisés que par les Dialogs (TimePicker, DatePicker, About …) et le plus ceux utilisés sont headline6 (ancien: title), subtitle1, bodyText1, bodyText2, caption et bien sûr, button.


Attention... valeurs ou règles codées en dur...
Malgré le fait que l'équipe Flutter ait essayé autant que possible de laisser aux développeurs la liberté de définir leurs propres thèmes, le code source actuel de Flutter Framework contient malheureusement encore quelques valeurs et règles codées en dur. J'ai essayé de les répertorier dans la feuille Excel.

Qu’en est-il des thèmes de texte définis par l’utilisateur?

Jusqu'à présent, nous avons vu les principes du TextTheme tels que définis par Flutter, basés sur la norme Material Design. Cela veut-il dire que nous ne pouvons appliquer aucun changement?

Pas du tout. Cependant, en fonction de la façon dont les thèmes de texte sont utilisés dans tous les Widgets de base, fournis par Flutter, il devient évident que:

Si vous souhaitez harmoniser votre mise en page, vous devez également utiliser autant que possible les noms de style suivants dans votre code:

  • headline6
  • subtitle1
  • subtitle2
  • bodyText1
  • bodyText2
  • caption
  • button

Par conséquent, comment pouvons-nous définir nos propres TextStyles sans enfreindre les règles et l’harmonie?

Règle 1: Soyez conscient des impacts …

Lorsque vous définissez votre thème d’application, sachez que votre définition sera utilisée par d’autres widgets!

Par exemple, si vous définissez textTheme.subtitle1, vous devez vous rappeler que cela aura potentiellement un impact sur les widgets suivants:


  • TimePicker
  • AlertDialog
  • PaginatedDataTable
  • CircleAvatar
  • DropdownButton
  • ExpansionTile
  • GridTitleBar
  • InputDecorator
  • ListTile
  • PopupMenu
  • SnackBar
  • TextField
  • TextFormField
  • …et tous les Widgets dérivés !

C’est là que ma Excel Cheatsheet pourrait être utile!

Règle 2: Utilisez Theme.of(context) dans vos Widgets

Lorsque vous définissez vos propres widgets, n’oubliez pas d’utiliser le Theme.of(context) pour référencer un textStyle existant.

Par exemple, supposons que vous construisez un nouveau widget Cartouche qui doit afficher un titre, une image et une étiquette.

Une mauvaise pratique serait de l'écrire sans tenir compte des styles existants et de l'écrire comme ceci:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Cartouche extends StatelessWidget {
  const Cartouche({
    Key key,
    this.title,
    this.caption,
    this.assetImage,
    this.size,
    this.padding = const EdgeInsets.all(8.0),
  }) : super(key: key);

  final String title;
  final String caption;
  final String assetImage;
  final Size size;
  final EdgeInsets padding;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Container(
        width: size.width,
        height: size.height,
        padding: padding,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              title,
              textAlign: TextAlign.center,
              style: TextStyle(
                  fontSize: 16.0, 
                  fontWeight: FontWeight.bold, 
                  letterSpacing: 0.15,
                ),
            ),
            Container(width: size.width - padding.horizontal * 2.0, child: Image.asset(assetImage)),
            Text(
              caption,
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 10.0, letterSpacing: 0.4),
            ),
          ],
        ),
      ),
    );
  }
}

Une meilleure approche serait de réutiliser la définition existante comme suit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Cartouche extends StatelessWidget {
  const Cartouche({
    Key key,
    this.title,
    this.caption,
    this.assetImage,
    this.size,
    this.padding = const EdgeInsets.all(8.0),
  }) : super(key: key);

  final String title;
  final String caption;
  final String assetImage;
  final Size size;
  final EdgeInsets padding;

  @override
  Widget build(BuildContext context) {
    
    final TextTheme textTheme = Theme.of(context).textTheme;

    return Card(
      child: Container(
        width: size.width,
        height: size.height,
        padding: padding,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(
              title,
              textAlign: TextAlign.center,
              style: textTheme.subtitle1.copyWith(fontWeight: FontWeight.bold),
            ),
            Container(width: size.width - padding.horizontal * 2.0, child: Image.asset(assetImage)),
            Text(
              caption,
              textAlign: TextAlign.center,
              style: textTheme.caption,
            ),
          ],
        ),
      ),
    );
  }
}

Explication:

  • ligne # 20: nous récupérons le TextTheme en cours
  • ligne # 33: nous utilisons le thème de base subtitle1 et nous le forçons à bold
  • ligne # 39: nous utilisons le thème de base caption

Cela garantit que si vous modifiez la définition du theme à un stade ultérieur, vous conserverez l’harmonie.

Règle 3: Envisagez de créer un Theme dédié pour un nouveau package

Supposons que vous souhaitiez créer un nouveau package (disons un nouveau RadialMenu).

Comme ce dernier ne fera (certainement) pas partie du framework Flutter standard et pourrait nécessiter des paramètres de mise en page très spécifiques, il pourrait être très judicieux de créer un thème spécifique pour ce package. Cela donnera aux développeurs la possibilité de le personnaliser, sans prendre de risques d’impact sur les autres.

Bien sûr, il sera hautement recommandé de prendre en compte les valeurs par défault, basées sur les normes Material Design.

Par exemple, vous pourriez écrire quelque chose comme:

class RadialMenuTheme extends InheritedTheme {
  const RadialMenuTheme({
    Key key,
    @required this.data,
    @required Widget child,
  }) : super(key: key, child: child);

  final RadialMenuThemeData data;

  ...
}

class RadialMenuThemeData {
  const RadialMenuThemeData({
    this.menuTextStyle,
    ...
  });

  final TextStyle menuTextStyle;
  ...
}

Ensuite, pour l’utiliser (et supposons que le développeur lui appliquera n’importe quel style):

@override
Widget build(BuildContext context){
  return RadialMenuTheme(
    data: RadialMenuThemeData(
      menuTextStyle: TextStyle(fontWeight: FontWeight.w500),
    ),
    child: RadialMenu(
      menuColor: Colors.red,
    ),
  );
}

Au niveau la méthode build() du RadialMenu, vous pourriez envisager quelque chose comme:

@override
Widget build(BuildContext context){
  final ThemeData theme = Theme.of(context);
  final RadialMenuThemeData radialMenuThemeData = RadialMenuTheme.of(context) ?? RadialMenuThemeData();
  TextStyle menuTextStyle = radialMenuThemeData.menuTextStyle ?? theme.textStyle.subtitle1;

  if (widget.menuColor != null){
    menuTextStyle = menuTextStyle.copyWith(color: widget.menuColor);
  }

  ...
}

Bien sûr, ce n’est pas un exemple complet mais j’espère que cela vous donne une idée d’une façon de gérer les thèmes.


Qu’en est-il de la classe TextStyle ()?

Cela signifie-t-il que vous devez proscrire l’utilisation de la classe TextStyle?

Pas du tout mais, en tant que recommandation, je suggère de:

  • essayez de vous abstenir d’utiliser TextStyle de manière anarchique.

  • essayer de limiter son utilisation à la définition de

    • color (si vraiment c’est nécessaire)
    • decoration
    • weight
  • essayez d'éviter le codage en dur

    • fontSize
    • fontFamily
    • letterSpacing

Est-il possible d'étendre le ThemeData ?

Comme le ThemeData fait partie des classes de base du Framework Flutter, il est uniquement géré par l'équipe Flutter et par conséquent, il n’est pas possible de l'étendre. Néanmoins, rien ne vous empêche de créer votre propre ApplicationExtendedTheme , qui pourrait alors contenir tout ce que vous voulez!

Où devrait-il être placé ?

Lorsque Flutter effectue le rendu d’un MaterialApp, beaucoup de Widgets sont automatiquement générés et insérés dans la sous-arbrorescence (= Widget Tree) de ce MaterialApp. Un de ces nombreux Widgets est le Theme.

Tout comme ce Theme est impacté par l’information locale que vous fournissez au MaterialApp et qu’idéalement, votre ApplicationExtendedTheme va se baser sur les définitions générées par le Theme, il est préférable de l’insérer avant toutes vos pages.

MaterialApp Widget a une propriété builder très pratique qui pourrait être utilisée à cette fin. La signature de cette fonction est:


Widget Function(BuildContext context, Widget child)

L’argument child correspond à tout ce qui doit être rendu en dessous (généralement vos écrans).

Nous savons donc maintenant où placer notre widget ApplicationExtendedTheme.

Comment le construire ?

Comme l’objectif est de le rendre disponible à tous les widgets de votre application, il serait formidable de pouvoir le récupérer, en utilisant l’instruction habituelle “of(context)".

Partie 1: Le Widget ‘ApplicationExtendedTheme’

Par conséquent, nous le construirons comme un StatelessWidget, qui contiendra un InheritedTheme pour contenir la définition supplémentaire du thème.


class ApplicationExtendedTheme extends StatelessWidget {
  const ApplicationExtendedTheme({
    Key key,
    this.data,
    @required this.child,
  })  : assert(child != null),
        super(key: key);

  final ApplicationExtendedThemeData data;
  final Widget child;

  static ApplicationExtendedThemeData of(BuildContext context) {
    final _InheritedTheme inheritedTheme = 
                context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
    return inheritedTheme?.theme?.data ?? ApplicationExtendedThemeData.fromContext(context);
  }

  @override
  Widget build(BuildContext context) {
    return _InheritedTheme(
      theme: this,
      child: child,
    );
  }
}

Explication:

Comme nous pouvons le voir, ce Widget permet de stocker la définition de ApplicationExtendedThemeData (si fourni).

La fonction of renvoie cette définition (si disponible) ou demande sa composition en fonction du BuildContent si aucune n’a pu être trouvée.

Partie 2: Le widget InheritedTheme sous-jacent

Le deuxième widget interne est là pour contenir ApplicationExtendedThemeData pour une réutilisation ultérieure ou pour demander (puis stocker) les données à produire, si elles ne sont pas existantes ou différentes.


class _InheritedTheme extends InheritedTheme {
  const _InheritedTheme({
    Key key,
    @required this.theme,
    @required Widget child,
  })  : assert(theme != null),
        super(key: key, child: child);

  final ApplicationExtendedTheme theme;

  @override
  Widget wrap(BuildContext context, Widget child) {
    final _InheritedTheme ancestorTheme = context.findAncestorWidgetOfExactType<_InheritedTheme>();
    return identical(this, ancestorTheme) 
      ? child 
      : ApplicationExtendedTheme(data: theme.data, child: child);
  }

  @override
  bool updateShouldNotify(_InheritedTheme old) => theme.data != old.theme.data;
}
Partie 3: La classe ApplicationExtendedThemeData

Voici un exemple d’une telle classe, qui génère 2 styles de texte.


class ApplicationExtendedThemeData with Diagnosticable {
  ApplicationExtendedThemeData();

  //
  // Style spécial pour les boutons qui sont
  // désactivés et présents dans un AppBar
  //
  TextStyle appBarDisabledButtonTextStyle;

  //
  // Style spécial pour les messages où
  // l'utilisateur est invité à prendre
  // une décision importante
  TextStyle userImportantDecisionMessageTextStyle;

  //
  // Construis les Themes sur base des thèmes déjà définis
  //
  factory ApplicationExtendedThemeData.fromContext(BuildContext context) {
    final ThemeData themeData = Theme.of(context);

    final ApplicationExtendedThemeData theme = ApplicationExtendedThemeData();

    //
    // (Exemple) Définition du style du buton
    //
    theme.appBarDisabledButtonTextStyle = themeData.textTheme.button
        .copyWith(
          fontStyle: FontStyle.italic,
        )
        .apply(
          decoration: TextDecoration.lineThrough,
          fontSizeFactor: 0.8,
        );

    //
    // (Exemple) Définition du style du message
    //
    theme.userImportantDecisionMessageTextStyle = themeData.primaryTextTheme.bodyText1
        .copyWith(
          fontWeight: FontWeight.w800,
        )
        .apply(
          fontSizeFactor: 1.2,
          decoration: TextDecoration.underline,
        );

    return theme;
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is ApplicationExtendedThemeData &&
        other.appBarDisabledButtonTextStyle == appBarDisabledButtonTextStyle &&
        other.userImportantDecisionMessageTextStyle == userImportantDecisionMessageTextStyle;
  }

  @override
  int get hashCode => hashList([
        appBarDisabledButtonTextStyle,
        userImportantDecisionMessageTextStyle,
      ]);
}

Comme vous pouvez le voir, ces 2 styles sont basés sur la définition existante du Theme général et sont donc cohérents du point de vue de l’harmonie de la mise en page.

Partie 4: Injection du ApplicationExtendedThemeData via le ApplicationExtendedTheme

Comme indiqué précédemment, cela se fera au niveau de MaterialApp comme le montre l’extrait suivant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
return MaterialApp(
  title: 'Material Design Text demo',

  //
  // Theme
  //
  theme: ThemeData(
    // définition du Theme général
  ),

  //
  // Insère le ApplicationExtendedTheme
  //
  builder: (BuildContext context, Widget child){
    return ApplicationExtendedTheme(
      child: child,
    );
  },

  //
  // Page de démarrage
  //
  home: SplashPage(),
);
Partie 5: Comment l’utiliser?

Enfin, la façon de l’utiliser est très similaire au Theme habituel.

@override
Widget build(BuildContext context) {
  final ApplicationExtendedThemeData applicationExtendedThemeData 
              = ApplicationExtendedTheme.of(context);

  return RaisedButton(
    child: Text(
      'Mon beau bouton',
      style: applicationExtendedThemeData.appBarDisabledButtonTextStyle,
    ),
    onPressed: (){
      ...
    },
  );
}

Vraiment nécessaire?

Est-il vraiment nécessaire de faire tout cela, me demanderez-vous?

Croyez-moi, si vous avez besoin de développer un énorme projet et d’y revenir après un certain temps, le fait d’avoir tous les thèmes étendus, centralisés dans un seul endroit vous aidera grandement.

L’exemple que j’ai donné ci-dessus est très basique et limité à 2 TextStyles. Dans la pratique, je définis aussi parfois des thèmes spécifiques à une application et je les incorpore à la classe ApplicationExtendedThemeData et c’est très pratique!


Conclusions

J’espère vraiment que cette introduction rapide de la notion de TextStyle au travers de l’utilisation du Theme vous donne une idée de l’importance d’utiliser la notion de thèmes et la démystifie également.

La classe ApplicationExtendedThemeData est un moyen personnel que j’ai trouvé pour gérer de manière centralisée les styles d’application et les rendre cohérents avec les définitions du Theme. Cela m’aide à réduire considérablement les styles non contrôlés et me force à les avoir tous définis dans le même fichier. Il y a d’autres façons de le faire, mais c’est celle que j’utilise personnellement.

Restez à l'écoute pour de nouveaux articles, sous peu et, comme d’habitude, bon codage!