便利にしたつもりが赤線だらけ — Dartで「名前付きオプション引数を足したら tear-off が壊れた」話

自作の電子帳簿リネーマ(レシートPDFを規則通りの名前に変えて年月フォルダに振り分けるMacアプリ)に、小さな改善を入れました。

保存が終わるとスナックバーに「📁 開く」ボタンが出るのですが、これが保存先のルートフォルダを開いていました。開いた先で 2026/07 と2階層潜るのが地味に面倒。せっかく年月フォルダに振り分けているのだから、保存した年月フォルダを直接開いてほしい

やることは単純に見えました。フォルダを開く関数にサブパスを渡せるようにするだけです。

目次

Before

Future<void> _openSaveDirectory() async {
  // 保存先ルートを Finder で開く
  await Process.run('open', [saveDir.path]);
}

呼び出し側はこう。

// スナックバーのボタン
action: SnackBarAction(
  label: '📁 開く',
  onPressed: _openSaveDirectory,
),

// 保存先パスの表示をクリックしたとき
onTap: _saveDirectory.isEmpty ? null : _openSaveDirectory,

どちらも関数名をそのまま渡しています。いわゆる tear-off(関数参照渡し)です。

After — オプション引数を足した

Future<void> _openSaveDirectory({String? subPath}) async {
  String openPath = saveDir.path;
  if (subPath != null && subPath.isNotEmpty) {
    final target = Directory('${saveDir.path}/$subPath');
    // 存在しなければルートにフォールバック
    if (await target.exists()) openPath = target.path;
  }
  if (Platform.isWindows) {
    openPath = openPath.replaceAll('/', r'\');
  }
  await Process.run('open', [openPath]);
}

スナックバー側で subPath を渡せば完成、のはずでした。

onPressed: () => _openSaveDirectory(subPath: '$year/$month'),

で、なぜか別の場所が赤くなる

触ってもいない onTap: の行でコンパイルエラー。

The argument type 'Future<void> Function({String? subPath})'
can't be assigned to the parameter type 'void Function()?'

言われてみればその通りで、名前付きオプション引数を1個足しただけで関数の「型」が変わっているのです。

  • 変更前:Future<void> Function()
  • 変更後:Future<void> Function({String? subPath})

onTap が要求しているのは VoidCallback、つまり void Function()。引数を取らない関数しか受け付けません。「省略できるんだから引数なしと同じでしょ」と人間は思いますが、型としては別物です。

呼び出し時に省略できることと、型が一致することは違う話なんですね。

直し方

ラムダで包むだけです。

onTap: _saveDirectory.isEmpty
    ? null
    : () => _openSaveDirectory(),

() => _openSaveDirectory() は引数を取らない関数なので、めでたく void Function() になります。

学んだこと

既存の関数にオプション引数を足すのは「後方互換な変更」ではない。呼び出しには互換だが、tear-off で渡している箇所は壊れる。

引数を足したのに、関係なさそうな場所がエラーになったら、そこは tear-off を疑うといいです。

プログラミング歴の浅い自分にはなかなか気づけない筋道でしたが、一度腹落ちすると「関数も値であり、値には型がある」という感覚が少し身についた気がします。

ちなみに Security-Scoped Resource(macOSサンドボックスでユーザーが選んだフォルダにアクセスする仕組み)の方は、アクセス権はブックマークのルート(保存先)に対して取得し、実際に開くのはその配下のサブパスで問題なく動きました。ここは素直でひと安心。


小さな改善ですが、「保存 → 開く → もうそこが目的のフォルダ」になったのは想像以上に快適です。自分のためのアプリは、こういう1クリックの削減が一番うれしい。

目次