![CDATA[
HarmonyOS NEXT (API 24) et son framework ArkUI offrent un modèle déclaratif moderne pour construire des interfaces réactives. Cet article décortique la réalisation d'une application minimaliste nommée « Vive la compréhension », conçue comme un espace de lecture contemplatif. L'objectif technique est double : maîtriser les composants fondamentaux d'ArkUI (Column, Row, List, ForEach, @State, @Builder) et démontrer comment une palette sombre et chaleureuse peut supporter un récit émotionnel.
- Architecture du projet et navigation
Sous le modèle Stage, chaque page est enregistrée dans main_pages.json. Le projet se présente ainsi :
AppEmpathie/
├── build-profile.json5
├── entry/
│ ├── build-profile.json5
│ └── src/main/
│ ├── ets/pages/
│ │ ├── Accueil.ets
│ │ └── ComprehensionPage.ets
│ ├── resources/
│ └── module.json5
Le fichier de routes :
{
"src": [
"pages/Accueil",
"pages/ComprehensionPage"
]
}
L'écran d'accueil utilise un simple Column centré et un bouton d'orange chaleureux pour lancer la page principale :
import router from '@ohos.router';
@Entry
@Component
struct Accueil {
build() {
Column({ space: 20 }) {
Text('Bienvenue').fontSize(28).fontColor('#F5F0EB');
Button('Vive la compréhension →')
.fontColor('#F5F0EB')
.backgroundColor('#E8843A')
.onClick(() => {
router.push({ url: 'pages/ComprehensionPage' });
});
}
.width('100%')
.height('100%')
.backgroundColor('#1A1612')
.justifyContent(FlexAlign.Center);
}
}
- Modèle de données et gestion de l'état
Le contenu est modélisé par une interface TypeScript. Deux états suffisent pour piloter l'interface : l'indice de la carte active et un booléen indiquant si la citation est affichée.
interface PerspectiveCard {
titre: string;
icone: string;
accroche: string;
corps: string;
citation: string;
teinte: string;
}
Les états réactifs :
@State activeIndex: number = -1;
@State citationVisible: boolean = false;
Le tableau de cartes est stocké dans une propriété privée, car il ne change pas au cours de la session :
private cartes: PerspectiveCard[] = [
{
titre: 'Écouter vraiment',
icone: '👂',
accroche: 'La compréhension commence par l’oreille',
corps: 'L’écoute attentive est le premier acte d’empathie. Elle demande de suspendre le jugement avant de formuler une réponse.',
citation: 'On passe deux ans à apprendre à parler, et le reste de sa vie à apprendre le silence.',
teinte: '#E8843A'
},
{
titre: 'Changer de regard',
icone: '🔄',
accroche: 'Voir le monde avec les yeux des autres',
corps: 'Adopter le point de vue d’autrui ne signifie pas renoncer au nôtre, mais enrichir notre vision des situations.',
citation: 'Pour comprendre quelqu’un, il faut marcher un moment dans ses souliers.',
teinte: '#5B9279'
},
{
titre: 'Franchir les générations',
icone: '👨👧',
accroche: 'Comprendre le temps qui sépare les âges',
corps: 'Chaque génération porte les marques de son époque. Accueillir ces différences désamorce les conflits familiaux.',
citation: 'Aimer, c’est regarder ensemble dans la même direction, malgré les distances.',
teinte: '#8B6FA0'
},
{
titre: 'Accueillir la diversité',
icone: '🌍',
accroche: 'Célébrer les cultures sans les fusionner',
corps: 'La rencontre entre les cultures amplifie la richesse du monde quand elle repose sur le respect mutuel.',
citation: 'Apprenons à admirer notre propre beauté et celle des autres, pour grandir ensemble.',
teinte: '#4A8DB7'
},
{
titre: 'Se réconcilier',
icone: '🪞',
accroche: 'La compréhension commence par soi',
corps: 'On pardonne souvent plus facilement aux autres qu’à soi-même. La paix intérieure nourrit la patience envers autrui.',
citation: 'Accepter ses failles est la plus belle façon de laisser entrer la lumière.',
teinte: '#D4899B'
}
];
- Mise en page du détail
La page principale est organisée en trois zones verticales : barre de navigation, entête et liste scrollable, pied de page. La zone centrale utilise List pour bénéficier d’un défilement natif et d’une meilleure séparation des responsabilités qu’un simple Column enveloppé dans un Scroll.
@Entry
@Component
struct ComprehensionPage {
@State activeIndex: number = -1;
@State citationVisible: boolean = false;
private cartes: PerspectiveCard[] = [ /* ... */ ];
build() {
Column() {
this.navigationBar();
this.headerBlock();
Divider().color('#2A221A');
if (this.activeIndex === -1) {
this.emptyPrompt();
}
List({ space: 14 }) {
ForEach(this.cartes, (item: PerspectiveCard, index: number) => {
this.cardWidget(item, index);
}, (item: PerspectiveCard) => item.titre)
}
.padding({ left: 16, right: 16, bottom: 14 })
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring);
this.footerBlock();
}
.width('100%')
.height('100%')
.backgroundColor('#1A1612');
}
@Builder
navigationBar() {
Row() {
Text('← Retour')
.fontSize(15)
.fontColor('#A09080')
.onClick(() => router.back());
Text('').layoutWeight(1);
}
.width('100%')
.padding(12);
}
@Builder
headerBlock() {
Column({ space: 4 }) {
Text('Vive la compréhension')
.fontSize(34)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0EB');
Text('LONG LIVE UNDERSTANDING')
.fontSize(13)
.fontColor('#E8843A')
.letterSpacing(3);
Divider().width('60%').color('#E8843A33').margin({ vertical: 8 });
Text('Accueillir l’autre par la compréhension')
.fontSize(13)
.fontColor('#A09080')
.letterSpacing(1);
}
.width('100%')
.padding({ vertical: 8 });
}
@Builder
emptyPrompt() {
Column({ space: 6 }) {
Text('☀️').fontSize(24);
Text('Touchez une carte pour explorer un point de vue')
.fontSize(14)
.fontColor('#7A6C5A')
.textAlign(TextAlign.Center);
Text('Retouchez la même carte pour révéler la citation')
.fontSize(12)
.fontColor('#6A5C4A')
.textAlign(TextAlign.Center);
}
.padding(20)
.width('100%');
}
@Builder
footerBlock() {
Column() {
Divider().width('80%').color('#2A221A');
Text('Toute chose porte une fissure, c’est par elle que pénètre la lumière.')
.fontSize(12)
.fontColor('#6A5C4A')
.textAlign(TextAlign.Center)
.margin({ top: 12 });
}
.width('100%')
.padding(16)
.backgroundColor('#0A0808');
}
}
- Carte interactive et états visuels
Chaque carte dispose de trois états : initiale, sélectoinnée et dépliée. Le @Builder cardWidget centralise le rendu et l’interaction. Un appui sur une carte non active la sélectionne et affiche automatiquement la citation ; un nouvel appui sur la même carte bascule l’affichage du passage. L’animation est englobée dans animateTo pour lisser les transitions de couleur et d’ombre.
@Builder
cardWidget(item: PerspectiveCard, index: number) {
ListItem() {
Column() {
Row() {
Text(item.icone).fontSize(22).margin({ right: 8 });
Text(item.titre)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0EB');
}.width('100%');
if (this.activeIndex === index) {
Text(item.accroche)
.fontSize(13)
.fontColor(item.teinte)
.margin({ top: 6 });
Divider().color(item.teinte + '66').margin({ vertical: 8 });
Text(item.corps)
.fontSize(14)
.fontColor('#C5BDB0')
.lineHeight(22);
if (this.citationVisible) {
Divider().color('#4A4036').margin({ vertical: 8 });
Text('Réflexion')
.fontSize(11)
.fontColor(item.teinte);
Text(item.citation)
.fontSize(13)
.fontColor('#E8DCC8')
.fontStyle(FontStyle.Italic);
}
}
}
.width('100%')
.padding(16)
.backgroundColor(this.activeIndex === index ? '#2E2822' : '#221E1A')
.border({
width: 1,
color: this.activeIndex === index ? item.teinte + '66' : '#3D342C',
radius: 16
})
.shadow(this.activeIndex === index ? {
radius: 16,
color: item.teinte + '33',
offsetX: 0,
offsetY: 6
} : undefined)
.onClick(() => {
animateTo({ duration: 280, curve: Curve.EaseOut }, () => {
if (this.activeIndex === index) {
this.citationVisible = !this.citationVisible;
} else {
this.activeIndex = index;
this.citationVisible = true;
}
});
})
}
}
La logique diffère de l’implémentation initiale en deux points : elle utilise List/ListItem au lieu d’une colonne scrollable manuelle, et elle encapsule les mutations d’état dans animateTo. Ces choix n’affectent pas la sémantique mais améliorent la fluidité et la maintenabilité.
- Langage visuel et système de couleurs
Le fond brun très sombre (#1A1612) crée une atmosphère intime, tandis que l’orange chaud (#E8843A) joue le rôle de marque. Les cartes adoptent chacune une teinte thématique propre.
| Dimension | Teinte | Sens |
|---|---|---|
| Écouter vraiment | #E8843A | Accueil, chaleur |
| Changer de regard | #5B9279 | Croissance, recul |
| Franchir les générations | #8B6FA0 | Trensition, nuance |
| Accueillir la diversité | #4A8DB7 | Ouverture, horizon |
| Se réconcilier | #D4899B | Douceur, introspection |
La hiérarchie typographique repose sur trois tailles principales : 34 vp pour le titre principal, 20 vp pour les titres de carte et 14 vp pour le corps de texte. Les interlignes et les lettres espacées sont ajustées pour donner du souffle au contenu.
- Bonnes pratiques pour API 24
- Stage Model : enregistrer chaque page dans
main_pages.jsonet utiliser le module@ohos.router. - Navigation : préférer
router.push({ url: 'pages/ComprehensionPage' })àpushUrldans les projets API 24. - État réactif : utiliser
@Statepour les données modifiables par l’UI et garder les listes statiques dans des propriétés privées. - Composants réutilisables : encapsuler les fragments d’UI avec
@Builderet fournir une fonction de clé àForEachpour optimiser le diff. - Adaptation : employer
layoutWeightpour remplir l’espace restant etletterSpacingpour rythmer les titres. - Animations : entourer les changements d’état visuels de
animateToafin d’éviter les transitions brutales.
Commande de construction pour un HAP de débogage :
hvigorw assembleHap --no-daemon --build-mode debug
- Stack technique
| Élément | Valeur |
|---|---|
| Framework | HarmonyOS NEXT (API 24) |
| Langage | ArkTS (TypeScript) |
| Framework UI | ArkUI déclaratif |
| Pages principales | Accueil, ComprehensionPage |
| Composants clés | List, ListItem, ForEach, @State, @Builder |
| Construction | hvigorw assembleHap |