2025~26年 冬休みOSS活動ログ

25年末に一年分を振り返ろうとしたら、思っていた以上に量があって断念したので、今後は定期的にまとめてみる所存。

対応したPR一覧

冬休みに入る時に、Xで「直ると嬉しいIssue募集中」と書いたらいくつか候補をいただいたので、それをベースに取り組んでみた。面白かった (知らないissueばっかだった) ので、今後も機会があればやりたい。

github.com

でいただいたやつ。実は、ちゅーやんさんが貼っているコードがほぼ答えで、裏を取ったらさっと直せる系だった。大変だったのは想定外に flex_test.dart が落ちたので、色々と調査してコードを修正した点。

原因は、コメントを書いた2018年10月時点の実装だとFlutterが独自実装していたが、2019年ごろにSkiaに処理を委譲済みになっていた。また、Skiaも (コードをざっとみる限り) 2019年ごろには問題を解消しているようだったので、TODOの解消を忘れていた状態だった。 flex_test.dart が落ちていたのは、 TextBaseline.alphabetic が固定でセットされてしまっていたので、それを前提としたコードになってしまっていたためだった。古いTODOは、早めに直さないとややこしいことになる典型例。

今回解消したIssueが、また別のIssueのブロッカーになっているらしいので、連鎖的に解消が進むといいな〜と思っている。

github.com

困っているというか、Crashlyticsを眺めていると気になる案件あったな〜で探したのがこれ。再現する簡易アプリを作っていじっていたら、通常の処理でも想定しないログが出力されたので、これは直す対象にしてもいいでしょう……! と取り組んでみた。

原因というか、これは「処理を実装した時の想定が、実際に引き起こされる事象を想定できていなかった」のではないか、と思っている。というのも、テストケースを見ると、この振る舞いは意図通りに見える *1 。しかし、今の実装だとImageの errorBuilder を実装していても、dispose後に起きたExceptionが FlutterError.onError に流れてしまう。

zenn.dev

FlutterError.onError は、大抵の場合「開発者が処理するべきであったのに、処理しなかったからフレームワークに流れた」Exceptionなどを通知するためのもので、Flutter的にはfatalなerrorとして扱われる *2 。また、back keyなどで読み込み中のImage Widgetがdisposeされるのは通常あり得ることで、それが監視ログなどに流れてくるとログを圧迫して大変困る。

こういった事情があるので、提案も込みでPRとした。ダメなら、もう仕様であることが明らかになるので、ignoreをみんなで書くことになるはず。

github.com

PageViewの cacheExtent を追加するもの。Issueも古く、実は過去に修正が試みられたが閉じられた経緯がある。cacheExtent を追加すること自体は難しくない。一方で、アクセシビリティを保持するのが大変に難しい。

FlutterのListViewで語られる内容ととして、ListViewの cacheExtentiOSのVoiceOverをサポートしている、というものがある。これは「画面の中にない要素であっても、Semanticsが生成され、VoiceOverで選択できるようになっている」ことを指している。

仕組みを追ってみると、Flutter frameworkでSemanticsを作る対象としてマークされる箇所か、Flutter engine側で isFocusable の判定がされる箇所で対応する方法があった。Flutter engine側で対応する方がいいかなと思ったが、思った以上にFlutterのWidgetiOSのセマンティクスとしてマークされていない状態だったので、「横スクロールのPageViewは弾くが、横スクロールのListViewは弾かない」などの処理をengine側で実現できる未来が見えず、Flutter framework側で対応することにした。

github.com

このPRを用意するにあたり、SemanticsについてFlutter frameworkとiOSの両方で知識がついたのは嬉しかった。 2025年12月中に終わらせるんだ……! と頑張っていたので、ギリギリ間に合って嬉しかった覚えがある。

github.com

年も明けたし、1つぐらい手をつけておこう! と取り組んでみたのがこちら。やってみるとFlutter Webの 歪み とでもいうべきものを踏んでおり、学びになりつつうーむとなってしまった。いいのか新年。

原因は、大きく分けて2つ (もしかしたら3つ) あった。それぞれが独立して問題を引き起こしているはず。

  1. line-heightやletter-spacing、word-spacingがFlutter frameworkがweb engineに伝達されていない
  2. フォーカスが外れるたびに <textarea> が生成されるので、最後に編集した時の scrollTopが維持されていない
  3. cssの初期化時に margin='0' が抜けている

理解するためには、Flutter Webにおける文字入力の仕組みを把握する必要がある。みなさんご存知の通り、Flutter WebはCanvaskitを利用し、文字を描画している。この文字描画は、いわばCanvasの上で描画した画像であり、ブラウザにおける <input><textarea> ではない。しかしブラウザでは <input><textarea> にしか反応しない操作がいくつもある。 このため、Flutter Webでは見えない <input><textarea> の要素を、Canvaskit上で描画している文字とxyが同じになるように重ねている。この仕組みを利用することで、 TextField の各種操作は実現されている。

元となるIssueのように、IMEの変換候補がズレるということは、見えている TextField の文字の位置と見えていない <textarea> の文字の位置がずれていることを意味する。ので、原因を探ってみると、特に line-height の反映がされていないことが問題だった。 line-height は未設定の場合、ブラウザが 1.2 程度を設定するらしい。いい感じに揃う倍率を確かめてみると、おおよそ 1.1 であったので、行数が増えれば増えるほどズレることになる。

この line-height などを渡すようメソッドを拡張すればよい……のだが、これも一苦労となる。というのも、Flutterのテストケースには「壊れると困る3rdパーティーパッケージ」のテストケースを走らせるもの (customer_testing) がある。この中に、super_editorが含まれている。

pub.dev

このパッケージには TextInputConnection をimplementsしたコードが含まれている。このため、公開されたAPIを更新すると、super_editorにとってのbreaking changeとなる。

github.com

結果として、次のようなコードを書いてbreaking changeを避けてみた。Dartのextensionを使えば、公開済みのinterfaceを段階的に更新できるはず。

/// Extension to add [setStyleWithMetrics] to [TextInputConnection].
extension TextInputConnectionExt on TextInputConnection {
  /// Send text styling information.
  ///
  /// This information is used by the Flutter Web Engine to change the style
  /// of the hidden native input's content. Hence, the content size will match
  /// to the size of the editable widget's content.
  void setStyleWithMetrics({
    required String? fontFamily,
    required double? fontSize,
    required FontWeight? fontWeight,
    required TextDirection textDirection,
    required TextAlign textAlign,
    double? letterSpacing,
    double? wordSpacing,
    double? lineHeight,
  }) {
    assert(attached);

    TextInput._instance._setStyle(
      fontFamily: fontFamily,
      fontSize: fontSize,
      fontWeight: fontWeight,
      textDirection: textDirection,
      textAlign: textAlign,
      letterSpacing: letterSpacing,
      wordSpacing: wordSpacing,
      lineHeight: lineHeight,
    );
  }
}

line-height などのプロパティを増やすだけなのに、とても大変だった……と終わるかと思ったのだが、より厄介な問題があった。

先に説明した <textarea> だが、実はフォーカスが当たるたびに新規に生成されている。しかし、現状「破棄された <textarea> から新規に生成される <textarea> に対して、同じ TextField を参照しているから値を復元する」仕組みがない。このため、常に全ての値がリセットされている。

勘の良い方ならすでにお気付きだと思うが、ここで scrollTop の問題が生じていた。 TextFieldmaxLines を指定した状態で複数の改行が入力されると、 <textarea> は内部的にスクロールする。この値が scrollTop になる。この値が変わることで、例えば maxLines が5のテキストにおいて、改行してもカーソルが移動しても、IMEの変換候補の場所は正しく表示されている。 *3 しかし、フォーカスが外れて再度当たると、この scrollTop0 に戻る。結果として、フォーカスを外してから当て直すと、非常に変な位置にIMEの変換候補が表示されることになる。

この問題の対応としては、Flutter frameworkからscroll時のoffsetを送信するか、Flutter engineにて scrollTop を復元できるようにする必要がある。だが、前者はframeworkの修正が必要になるため懸念が多く、後者はTextField と対応する <textarea> を紐づけるkeyが存在しないためワークアラウンド的な対応が必要になる。現時点では後者で対応してみたが、より根本的に前者で対応する必要があるかもしれない。

なお、Flutter Webの文字入力についてはPlatform Viewを使うことで解消する計画もある。

github.com

しかし、現在の自分の理解では、Platform Viewに変えても問題は解決しない。というか、現時点でline-heightなどの反映が漏れているので、より広範にUIを壊すことにつながってしまう。Platform Viewに置き換えても問題がないということは、逆説的に言えば、置き換えずともIMEのポジションなどが正しく表示できることを指している。 ((DOMでFlutterの TextField で描画したい通りの描画ができている必要があるので)) マージまでは遠いかもしれないが、日本語での入力が壊れている点でもあるので、なんとかモチベーションを保って解消まで持ち込みたい。

今回の工夫点

冬休みの頭に、Geminiのプランが安いって話題になっていたので導入してみた。

ので、ちょうどいい機会なのでAntigravityを導入。今回作ったPRは、ほぼGeminiとペアプロして調査&記述している。

antigravity.google

やりとりから推測させて作り、微妙な点を手直しして、現在運用しているルールは次のとおり。Flutterのソースコードを読んで、より良いものを考えて実装する必要があるので、通常のアプリ開発には向かないかもしれない。自分の活動は、主にOSS作ったりFlutter SDKにパッチを送ったりなので、だいたいこれでいいんじゃないか感がある。

2026年1月6日現在はこれ。

# ANTIGRAVITY PARTNER RULES

SUPERSEDES ALL OTHER INSTRUCTIONS

## 1. ROLE DEFINITION
Role: Principal/Lead Engineer Partner
Mindset: Critical, Defensive, Semantic-focused

## 2. CORE DIRECTIVES
These directives must be followed for every interaction.

### 2.1 Critical Engineering
* Challenge Logic: Do not accept instructions blindly. Verify technical validity first.
* Defensive Implementation: Always handle edge cases, null states, and errors explicitly.
* Root Cause Analysis: Investigate issues at the OS/Engine level rather than applying surface fixes.

### 2.2 Communication Protocol
* Discussion First: Clarify ambiguity before writing any code.
* Hypothesis Driven: Formulate and state a logical hypothesis before implementation.
* Stop and Think: If an approach fails, pause to re-evaluate assumptions.

### 2.3 Technical Validation
* Standard Compliance: Prioritize standard specifications over custom extensions unless rigorously justified.
* Context Verification: Validate that proposals are technically supported in the target environment before implementing.

## 3. LANGUAGE & OUTPUT
* Thinking Process: English
* Response: Japanese
* Artifacts: Japanese (Draft)

## 4. PROHIBITED BEHAVIORS
* DO NOT Write code without a clear logical basis.
* DO NOT Ignore potential null or undefined states.
* DO NOT Provide "quick fixes" without understanding the root cause.

GEMINI.md · GitHub

進め方としては、まずissueをベースに問題だと想定していることの概要を伝える。その上で コードから 原因を推測させるようにしている。コードを中心に据えることで、「確かにそうなっている」や「いや、その理屈ならこうなるべきだ」などの判断をしやすくしている。また、調査に時間がかかるので、何かの片手間に情報を整理させておき、まとまった時間が取れた時に読み込んで考えることもしやすい。

FlutterのソースコードをGeminiに与えることに抵抗はないし、与えることでGeminiがFlutter SDKに詳しくなるなら万々歳だしで、今の所損らしい損は感じていない。

感想

AIツールの導入について。

かけた時間に対しての成果はよく出た、はず。冬休みはそこそこ長かったのだが、子供と外に行ったり遊んだり、DartのHooksについてYoutube Liveやったりしていたので、それなりに他のことにも時間はかけていた。というか、子供とのあれこれが大半だった。どんどん活発になっているので、今後Flutter SDKにかけられる時間は減る見込み。

その意味で、この冬休み中にペアプロ風のやり方を覚えられたのは、非常によかったように思う。1年プランで契約したので、26年はこのスタイルで進めてみたい。


ブログをまとめて書くのが大変だったので、次回からは下書きしながらPRを作ろうと思う。何事もコツコツやるしかないっすね。

*1:dispose後に起きたExceptionがFlutterError.onErrorに流れるようになっている

*2:ことが多い。当然、開発者側で分岐させることはできる

*3:先述の問題で、ズレやすいので気づかれにくかったが