パルカワ2

最近はFlutterをやっています

flutter test --coverageが遅い

 $ flutter --version
Flutter 2.5.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision ffb2ecea52 (9 weeks ago) • 2021-09-17 15:26:33 -0400
Engine • revision b3af521a05
Tools • Dart 2.14.2

原因

github.com

一時的な対応

github.com

根本対応

Flutterのmasterには対応が入ってるっぽい

github.com

じぶんはどうしたか

stableから変更したくなかったので一時的な対応を行った。こういうスクリプトを書いてcoverageを取得したい時にscriptを実行して作成されたファイルを実行するようにした。

import 'dart:io';

Future<void> main() async {
  final dir = Directory('test');
  final files = <File>[];
  await for (final file in dir.list(recursive: true)) {
    if (file is File && file.path.contains('_test.dart')) {
      files.add(file);
    }
  }

  files.sort((a, b) => a.path.compareTo(b.path));

  final imports = files.map((file) {
    final basename = file.path.split('/').last;
    final name = basename.replaceAll('.dart', '');
    // ignore: prefer_single_quotes
    return """
import '${file.path.replaceFirst('test/', '')}' as $name;
""";
  }).join('');

  final tests = files.map((file) {
    final name = file.path.split('/').last.replaceAll('.dart', '');
    return '''
  group(
    '$name',
    $name.main,
  );
''';
  }).join('');

  final body = """
import 'package:test/test.dart';
$imports
void main() {
$tests
}
""";

  final testFile = File('test/coverage.dart');
  await testFile.writeAsString(body);
}
dart test/generate_coverage_testfile.dart
flutter test --coverage test/coverage.dart

coverageを実行するのに17分ほどかかっていたのが1分で終わるようになった。すごい。

fitbit charge 5を買った

体調が良くもないが悪くもないずっと曇りみたいな状態だけど人生とはそんなもんだろと思ってあんま気にしていなかった。ところが同僚から体調が悪そうなので心配と言われたりしていたのでちゃんとしようという気持ちになってきた。ただどうにかしたいけどどうしようという感じだったので、よし!とりあえずトラッキングだ!計測だ!と思ってちょうど予約開始されたfitbit charge 5を買った。届いたのは発売されてから1週間くらい経ってからだった。

実際睡眠を測ってみるとこんな感じだった。5時間寝てれば十分でしょとか思ってたんだけどそうじゃないようで、6-8時間寝るのがいいって書いてた。あとベッドに入ってから5時間寝たらOKみたいな気持ちでいたけど、寝てるつもりでも起きてるときはあるようで、そういうのを差し引くともっと寝たほうがよいというのはわかった。

f:id:hisaichi5518:20211009112319j:plain

今はこういう状態

  • 早めに寝るために寝る準備を早めに終わらせる
  • 2度寝、3度寝してもいいから長時間寝る
  • 無限にパソコンしがちなので寝る前はパソコンをしない
  • 寝る前にコーヒーを飲まない
  • 朝に光を浴びる

みんな当たり前にやってそう。運動とかしたほうがいいんだろうけど、頑張ると続かないので頑張らない方針でいく。

Sorted³ 使い始めた

もともとSunsamaを使っていたけど、全然使いこなしてないのに月2000円くらい払っていたので良い感じのがあれば乗り換えるかという気持ちでいたらSorted³ というのを見つけたので使い始めた。

www.sortedapp.com

Sorted³のいいところは、雑に楽にスケジュールが決めやすい。

朝に今日やりたいことをタスクに分けて順番を決めてオートスケジュールを実行するといい感じに今日のスケジュールを決めてくれる。

support.sortedapp.com

f:id:hisaichi5518:20211008204930p:plain

僕は雑なので実際この通りに実行しなくても良いと思っていて散歩とか日を浴びるために10分くらい外にいたりするだけだったりするので全然沿ってないこともある。

次のタスクのタイミングになると通知が来るので、その時点でまだ前のタスクをやっていたら想定より遅れているので、そのときは終わってないタスクの時間を伸ばしてまたオートスケジュールすればよい。これで朝に今日やると決めたことが何時くらいに終わるか大体わかって無限に働かずに済む。

f:id:hisaichi5518:20211008205448p:plain

他にも指定したタスクの開始時間を一気に早めたり遅めたりとか出来て便利なのと買い切りなので安い。

support.sortedapp.com

不満なところがあるとしたら、こういうので頑張ってくれという気持ち

  • タスクのタイトル一覧をコピペできないこと
    • 仕事する日は朝Slackにやることを共有しているのでほしい 問い合わせた
  • Androidアプリがない
    • iPhoneを買ったのでちゃんと乗り換える
  • 完了したタスクのふりかえりが出来ないこと
    • まあでも、僕は毎週ふりかえりをやるような人間ではないんだよな…

バーン感が足りない

上期の自己評価を書いてる時に半年を振り返って、この半年は大体仕事をこなしただけでつまらなかったということに自分で気づいてハッとなった。

気づくのが遅すぎるというのは置いといて、なんでつまらなかったか考えてみるとやりたいことが出来ていないみたいなのは多少なりともあるんだけど、目の前のタスクに追われてやることを自分で小さく収めていたからだなと思った。

自分で小さく収めるとはなにかというと期限があるタスクに関して期限を優先して、やることを削ぎ落としてとにかく小さく実装することにして「本来はこうあるべきだけどしょうがないよね…」と言ってたなと思った。そうなると本来あるべき挑戦もなくて、ただただ期限までにタスクをこなすマシーンになっていたんだと思う。もちろん、期限は大事で守られるべきなんだけど、今いる会社は不確実だけどバーンと大胆な意思決定を推奨しているはずでそこに沿えてなかったな〜とおもった。

なので今日から「しょうがないからこう実装するか…」って話してたのを捨てて、より良い形で実装することにした。大胆な意思決定から生まれる挑戦をやっていく。

Riverpod を使ったアーキテクチャについて考える

前回この記事でRiverpodを使ったMVUアーキテクチャについて書いた。

hisaichi5518.hatenablog.jp

が、いくつか問題があるなと気づいた。

  • UIに関する処理を書く場所がない
  • 複数のStateを同時に扱う場合が考慮されていない(ビジネスロジックを書く場所がない)

なので、ViewModelを挟むのがよいのではと考えた。

// lib/screens/home/_home_view_model.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../providers/providers.dart';

final viewModelProvider = Provider((ref) => HomeViewModel(ref.read));

final navigationProvider = StateProvider((ref) => NavigationState.none);

/// 値を持たせたい場合は、freezedなどを使う
enum NavigationState {
  none,
  showSnackbar,
}

class HomeViewModel {
  HomeViewModel(this._read);

  final Reader _read;

  Future<void> increment() async {
    final countValue = _read(counterNotifierProvider);

    final count = countValue.data?.value;
    if (count == null) {
      return;
    }
    final newCount = count + 1;

    /// LocalStorageのデータも新しいデータで更新
    final sharedPreference = await _read(sharedPreferenceProvider.future);
    await sharedPreference.setInt('counter', newCount);

    /// counter以外、他のStateを更新しても問題ない

    /// 処理を行ったらStateの更新
    /// CounterNotifierのupdateを呼び出してStateを更新
    await _read(counterNotifierProvider.notifier).update(newCount);

    /// 更新後にSnackbarを表示する
    _read(navigationProvider).state = NavigationState.showSnackbar;
  }
}
// lib/screens/home/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../providers/providers.dart';
import '_home_view_model.dart';

class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<StateController<NavigationState>>(navigationProvider, (value) {
      switch (value.state) {
        case NavigationState.showSnackbar:
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('更新されました')),
          );
          break;
        default:
          break;
      }
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Consumer(builder: (context, ref, _) {
          /// 初期値の読み込みやStateが変更されたらrebuildなどwatchすれば行われる
          final count = ref.watch(counterNotifierProvider);

          /// 非同期でカウント数を読み込むので、loading, errorなどをちゃんと表現する
          return count.map(
            data: (data) => Text('${data.value}'),
            loading: (_) => const CircularProgressIndicator(),
            error: (_) => const Text('On Error'),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        /// ここでStateを更新するのではなくViewModelを経由する
        onPressed: () => ref.read(viewModelProvider).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

本来ViewModelは、UIに関する処理のみを担ってビジネスロジックは別のクラスであるUseCaseなどが担うべきな気はするけど、毎回UseCaseを作るのはめんどくさいので気にしていない。ビジネスロジックが大きくなってきたり共通化したい場合にUseCaseに分けるなどすればよいと思う。

Flutter/Dartでcustom lint ruleを作りたい

推奨されないコードを機械的に発見出来る部分は人間がコードレビューなどで指摘するのではなくLinterで気づきたい。 なので、プロジェクトごとにLinterのCustom Ruleを作りたいのだが、DartのLinterはCustom Ruleに対応していない。

github.com

このイシューに書かれてある通り、linterコマンドを自分で作る方法とanalyzer_pluginを利用する方法がある。

linterコマンドを自分で作る

この方法は実装が簡単というメリットがあり、IDE上ではエラーや警告が表示されないというデメリットがある。

linter/linter.dart at master · dart-lang/linter · GitHub

linter.dart の中で自分で作ったRuleを登録してあげて実行する。まあこういう感じで出来る

Future main(List<String> args) async {
  final enableRules = [
    MyCustomRule(),
  ];
  for (final rule in enableRules) {
    Analyzer.facade.register(rule);
  }
  await cli.run([
    '--packages=.packages',
    '--rules=${enableRules.map((r) => r.name).join(",")}',
    'lib',
    'test',
  ]);
}

analyzer_pluginを利用する方法

この方法はIDE上でもエラーや警告が表示されるというメリットがあり、実装やデバッグがめんどくさいというデメリットがある。

analyzer_plugin | Dart Packageを利用する。

めんどくさい実装をメンテし続けるのはつらいので、dart_code_metricsというライブラリをforkして自前のルールを追加するのはどうかと思ってやってみた。

試しに追加したのは、 DateTime.utc() とかを使うとエラーにするRule

github.com

これでDateTime.utc(2020) とかするとIDE上でエラーになる。ただまあ、forkなのであんまりやりたくないなと思っている。