Dart: enfin une solution

#dev #dart

J’ai assez pesté sur le paysage de développement front-end dans ma vie, je ne vais pas refaire une tartine maintenant. J’ai 5 projets perso avec une UI qui ne build plus, dans différentes version de

Il est clair aujourd’hui que dans un monde avec une architecture décentralisée, les applications mobiles seront un pilier de cette nouvelle architecture de part le simple nombre de téléphone répartie sur la surface du globe.

Dart accompagné de Flutter pourrait être une solution à mon problème de front-end. Le language a l’air propre, l’architecture simple et pensé pour le front mobile. Essayons

Première étape: Installer flutter

flutter doctor doit renvoyer une checklist valide:

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[] Flutter (Channel stable, 3.13.2, on Manjaro Linux 6.1.55-1-MANJARO, locale en_US.UTF-8)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Chrome - develop for the web
[] Linux toolchain - develop for Linux desktop
[] Android Studio (version 2022.3)
[] Connected device (2 available)
[] Network resources

• No issues found!

Deuxième étape: Configurer vim

Il y a pas mal de plugins pour flutter, et je ne m’y connais malheureusement pas assez pour juger la qualité:

Allons-y avec flutter-tools.nvim qui a plus d’activité sur Github. Bon, c’est un critère.

Plug 'dart-lang/dart-vim-plugin'
Plug 'nvim-lua/plenary.nvim'
Plug 'stevearc/dressing.nvim' " optional for vim.ui.select
Plug 'akinsho/flutter-tools.nvim'

require("flutter-tools").setup {} -- use defaults

et :PlugInstall

Troisième étape: Hello world

zéparti:

$ flutter create hello_flutter

La cli suggère d’executer flutter run. OK ?

$ flutter run
Error 1 retrieving device properties for A209TPVG0201:
adb: insufficient permissions for device
See [http://developer.android.com/tools/device.html] for more information


Downloading android-arm-profile/linux-x64 tools...                 366ms
Downloading android-arm-release/linux-x64 tools...                 210ms
Downloading android-arm64-profile/linux-x64 tools...               287ms
Downloading android-arm64-release/linux-x64 tools...               215ms
Downloading android-x64-profile/linux-x64 tools...                 238ms
Downloading android-x64-release/linux-x64 tools...                 219ms
Launching lib/main.dart on A209TPVG0201 in debug mode...
Checking the license for package Android SDK Tools in /home/proullon/Android/Sdk/licenses
License for package Android SDK Tools accepted.
Preparing "Install Android SDK Tools (revision: 26.1.1)".
"Install Android SDK Tools (revision: 26.1.1)" ready.
Installing Android SDK Tools in /home/proullon/Android/Sdk/tools
"Install Android SDK Tools (revision: 26.1.1)" complete.
"Install Android SDK Tools (revision: 26.1.1)" finished.
Checking the license for package Android SDK Build-Tools 30.0.3 in /home/proullon/Android/Sdk/licenses
License for package Android SDK Build-Tools 30.0.3 accepted.
Preparing "Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)".
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" ready.
Installing Android SDK Build-Tools 30.0.3 in /home/proullon/Android/Sdk/build-tools/30.0.3
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" complete.
"Install Android SDK Build-Tools 30.0.3 (revision: 30.0.3)" finished.
Checking the license for package Android SDK Platform 33 in /home/proullon/Android/Sdk/licenses
License for package Android SDK Platform 33 accepted.
Preparing "Install Android SDK Platform 33 (revision: 3)".
"Install Android SDK Platform 33 (revision: 3)" ready.
Installing Android SDK Platform 33 in /home/proullon/Android/Sdk/platforms/android-33
"Install Android SDK Platform 33 (revision: 3)" complete.
"Install Android SDK Platform 33 (revision: 3)" finished.
Running Gradle task 'assembleDebug'...                            146.7s
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
adb: insufficient permissions for device
See [http://developer.android.com/tools/device.html] for more information
Installing build/app/outputs/flutter-apk/app-debug.apk...            5ms
Error: ADB exited with exit code 1
adb: insufficient permissions for device
See [http://developer.android.com/tools/device.html] for more information
Error launching application on A209TPVG0201.

Ah. Oui donc après reset des authorisations sur mon téléphone, il me propose enfin d’accepter la clef de mon desktop.

$ flutter run
Launching lib/main.dart on FP3 in debug mode...
Running Gradle task 'assembleDebug'...                           1,964ms
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
Syncing files to device FP3...                                      52ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

A Dart VM Service on FP3 is available at: http://127.0.0.1:33945/r-5Up-zbMTQ=/
I/Gralloc4(18322): mapper 4.x is not supported
W/Gralloc3(18322): mapper 3.x is not supported
The Flutter DevTools debugger and profiler on FP3 is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:33945/r-5Up-zbMTQ=/

Cool.

Depuis vim, :FlutterRun lance l’application sur mon téléphone, le hot reload on save fonctionne. Parfait.

Quatrième étape: PROD ! (Installer l’appli sur le téléphone)

Oh c’est génial. la cli de flutter permet de build un apk et de l’installer directement.

$ flutter build apk
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 1336 bytes (99.9% reduction).
Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Running Gradle task 'assembleRelease'...                            85.2s
✓  Built build/app/outputs/flutter-apk/app-release.apk (17.4MB).

$ flutter install
Installing app-release.apk to FP3...
Installing build/app/outputs/flutter-apk/app-release.apk...      2,974ms

Cinquième étape: Hello World en dart

On a pas écrit une ligne de code pour le moment. Faisons ça.

void main() {
    print("Hello, World!");
}
$ dart run main.dart
Hello, World!

Uh. ok.

Hoho, je vois un dart format. C’est une des plus grosse feature de Go pour moi.

$ dart format .
Formatted main.dart
Formatted 1 file (1 changed) in 0.12 seconds.

OK d’accord mais je voudrais un binaire moi:

$ dart compile exe main.dart
Generated: /home/proullon/work/src/github.com/proullon/hello_dart/main.exe

# proullon @ desktop in ~/work/src/github.com/proullon/hello_dart [11:04:00]
$ ls
╭───┬───────────┬──────┬─────────┬──────────────╮
# │   name    │ type │  size   │   modified   │
├───┼───────────┼──────┼─────────┼──────────────┤
0 │ main.dart │ file │    42 B │ a minute ago │
1 │ main.exe  │ file │ 5.3 MiB │ now          │
╰───┴───────────┴──────┴─────────┴──────────────╯

# proullon @ desktop in ~/work/src/github.com/proullon/hello_dart [11:04:03]
$ ./main.exe
Hello, World!

Ça c’est étrange quand même. Pas très UNIX. Pourquoi nommer main.exe, qui est par convention un binaire windows ?

Sixième étape: HTTP et JSON en dart

Ok, faisons une requête HTTP.

On regarde Dart HTTP doc.

Ahhh première erreur de compilation:

$ dart compile exe main.dart
Error: Couldn't resolve the package 'http' in 'package:http/http.dart'.
main.dart:1:8: Error: Not found: 'package:http/http.dart'
import 'package:http/http.dart' as http;
       ^
main.dart:11:29: Error: Method not found: 'get'.
  var response = await http.get(url);
                            ^^^
Error: AOT compilation failed
Generating AOT kernel dill failed!

Comme pour go get, il faut dart add:

$ dart pub add http
Could not find a file named "pubspec.yaml" in "/home/proullon/work/src/github.com/proullon/hello_dart".

Grml, un coup de dart create . me génère un pubspec.yaml mais aussi un tas de fichier non nécessaire, erf. En tous cas je peux ajouter une dépendance maintenant:

Resolving dependencies...
+ http 1.1.0
  lints 2.1.1 (3.0.0 available)
Changed 1 dependency!

Alors finalement:

import 'package:http/http.dart' as http;

void main() {
    getTodo();
}

void getTodo() async {
  print("Hello, World!");

  var url = Uri.http('127.0.0.1:4059', '/task/todo');
  var response = await http.get(url);

  if (response.statusCode != 200) {
      return;
  }

  print('Response status: ${response.statusCode}');
  print('Response body: ${response.body}');
}

Nous donne

$ ./main.exe
Hello, World!
Response status: 200
Response body: {...

Cool ! JSON maintenant.

(En attendant, le plugin Dart pour nvim est assez agréable:)

nvim-dart

Faisons une pause pour lire Core libraries tour. Je ne sais pas si c’est moins qui suis débile où si il n’y a pas de méthode simple pour déserialiser du JSON dans des objets dart.

On me dit que je peux implémenter une factory MyObject.fromJSON(Map<string, dynamic> data), mais je trouve que c’est un peu overkill pour des types builtin ?

Du coup on a une classe qui ressemble à ça:

class Task {
  Task({required this.id, required this.name});
  final String id;
  final String name;

  factory Task.fromJson(Map<String, dynamic> data) {
    final id = data['id'] as String;
    final name = data['name'] as String;
    return Task(id: id, name: name);
  }

  String toString() {
    return this.name;
  }
}

Et on peut récupérer notre liste en faisant:


  const JsonDecoder decoder = JsonDecoder();

  // convert JSON to Map<String, dynamic>
  var data = decoder.convert(response.body);
  // get 'task' dynamic element from map
  var list = data['tasks'];
  // create list of Task element
  List<dynamic> tasks = list.map((i) => Task.fromJson(i)).toList();
  for (final task in tasks) {
    print('Task: $task');
  }
}

Ah, on me souffle dans l’oreillette que mon implémentation de toString est pas idiomatique. On devrait apparemment écrire:

  @override
  String toString() {
    return name;
  }

OK.

Septième étape: Afficher une liste avec Flutter

On transforme un peu la fonction de fetch pour renvoyer une Future:

Future<List<dynamic>> fetchTasks() async {
  var url = Uri.http('127.0.0.1:4059', '/task/todo');
  var response = await http.get(url);

  if (response.statusCode != 200) {
    throw Exception('Failed to load todolist');
  }

  const JsonDecoder decoder = JsonDecoder();
  var data = decoder.convert(response.body);
  var list = data['tasks'];
  List<dynamic> tasks = list.map((i) => Task.fromJson(i)).toList();
  return tasks;
}

On change le body du widget pour afficher une ListView si la future est disponible:

        child: FutureBuilder<List<dynamic>>(
          future: fetchTasks(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return ListView.builder(
                itemCount: snapshot.data?.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(snapshot.data?[index].name ?? "task"),
                    subtitle: Text(snapshot.data?[index].id ?? "id"),
                  );
                },
              );
            } else if (snapshot.hasError) {
              return Text('snapshot error: ${snapshot.error}');
            }
            return const CircularProgressIndicator();
          },
        ),

Nice !

Voir Fetch data from the internet dans la doc.

Plus qu’à installer sur le téléphone:

$ flutter install
Installing app-release.apk to FP3...
Installing build/app/outputs/flutter-apk/app-release.apk...         5.1s

Dart est dans la lignée de C et Go, plutôt que Java et pour Kotlin, et ça me plait beaucoup. J’aime bien les éléments de langage fonctionnels qui ont été ajouté. Je pense que je vais regarder de plus près :)

Final checklist

  • Compile: [Y]
  • Static check: [Y]
  • Format: [V]
  • Good compiler errors: [V]
  • Modern standard library: [V]
  • Integration with industry standard: [V]
  • Cross-comp: [V]

nvim config

 Plug 'akinsho/flutter-tools.nvim'