gitでcherrypickの使い所

新機能を実装したりバグフィックスのためにあれこれ試したりする時、main or masterブランチとは別のブランチを作り、そのブランチで作業するものだ。この作業ブランチで実装した内容、つまり1つ以上のコミット全てをmainに取り込みたい時は、作業ブランチをmainブランチにmergeすれば良い。

作業ブランチのcommitのうち一部だけmainブランチに取り込みたい場合もある。その時はそのcommitのみcherrypickで取り込めば良い。また私は以下のようなケースでもcherrypickを使う。

作業ブランチをmainブランチにmergeすると多くのconflictが生じる場合がある。conflict解決作業が複雑である時、mergeではなくcherrypickを使う。具体的には以下のように作業する。

  • 作業ブランチから取り込みたいcommitのうち、古い方から新しい順に1つずつcherrypickでmainブランチに取り込んでいく
  • conflictが起きたら修正する

これだと1commit単位の作業になるのでマージミスを減らせそうである。また10commitのcherrypickのうちconflictするのは1〜2commitだけだったということもあるだろう。

dart/flutterでキャンセル可能な非同期処理を実装する

公式ドキュメントにサンプルがない?ので結構ハマった。ググって出てきたサンプルもなんか上手くいかなかった。試行錯誤して多分これで大丈夫そうなのでメモしておく。

CancelableOperationを使う

キャンセル不要の非同期処理の実装はCompletionを使うことが多いので、CancelableCompletionを使うべきかと思ったけど、どう使っていいかわからなかった。あれこれ試してCancelableOperationですっきり実装できたので、こちらを使う。ちなみにCancelableOperation#fromFutureを使うことになるが、ドキュメントにこれを使うとCancelableCompletionを使うのと同じようなことだと書いてあった。

CancelableOperation.fromFuture constructor - CancelableOperation - async library - Dart API

Calling this constructor is equivalent to creating a CancelableCompleter and completing it with result.

CancelableOperation#fromFuture

CancelableOperationを生成する際は、CancelableOperation#fromFutureで生成するのが基本。

  • 第一引数に非同期処理を行うFutureを渡す。以後、メイン処理と呼ぶ。
  • 第二引数のonCancelでキャンセル処理を待つFutureを渡す。以後、キャンセル待ち処理と呼ぶ。

メイン処理

とりあえずわかりやすいサンプルを書く。

  • 1秒待つループを10回繰り返して約10秒待つ処理をする
    • 本来は時間のかかる処理をする。通信とかファイルロードとか
  • 戻り値は正常終了したらtrueを返すようにする
    • 本来は非同期処理で返したい結果を返す。結果が必要なければvoidでいい
    • キャンセルした時はnullを返す。別に値を返しても良いが、いずれにせよキャンセルした時の結果は仕組み上参照されないだろう
bool _isCancelRequested = false;

Future<bool?> _mainFuture() {
   for (int i = 0; i < 10; i++) {
     await Future.delayed(Duration(seconds: 1));
     if (_isCancelRequested) return null;
   }

   return true;
}

最後まで終了する前に、_isCancelRequested = trueになれば、途中で処理が終わることになる。

キャンセル待ち処理

キャンセル処理で覚えておくこと

  • 非同期処理外からはCancelableOperation#cancelをコールしてキャンセルする
  • キャンセル処理終了まで時間がかかることもある
  • CancelableOperation#cancelはawaitするとキャンセル終了を待つことができる
  • ただしそのためには自分でキャンセル終了待ちの非同期処理を実装する必要がある
  • キャンセル終了までの非同期処理はCancelableOperation#fromFutureの引数onCancelに渡す
CancelableOperation? _operation;

Future<void> _waitCancelCompletion() {
    // 並行して走っているmainFutureの終了を待つ
    return Future.doWhile(() async {
      // 待ち時間は適当に・・・
      await Future.delayed(const Duration(milliseconds: 300));
      return !(_operation.isCanceled || _operation.isCompleted);
    });
}

上記をラッピングした非同期処理クラスを作る

実際にビルドして動かしてはないです。が、やるべきことはわかるはず。

import 'dart:async';

import 'package:async/async.dart';
import 'package:flutter/foundation.dart';

/// 非同期処理の結果。キャンセルしたかどうかもわかるようにしている
class AsyncCancelableResult {
  final bool isCanceled;
  final bool? value;

  const AsyncCancelableResult({required this.isCanceled, required this.value});
}

/// CancelableOperationのラッパークラス
class SampleCancelableOperation {
  /// ここの<bool>は非同期処理が正常終了した時、返したい型にする
  late CancelableOperation<bool> _operation;
  bool _isCancelRequested = false;

  Future<AsyncCancelableResult> start() {
    final completer = Completer<AsyncCancelableResult>();
    final operation = CancelableOperation<bool>.fromFuture(
        _mainFuture(),
        onCancel: () => _waitCancelCompletion()
    );
    // thenで処理開始前にセットしておく
    _operation = operation;

    operation.then((value) {
      // mainProcessが正常終了した時、ここに来る
      // valueはmainProcessの戻り値。つまりtrue
      completer.complete(AsyncCancelableResult(isCanceled: false, value: value));
    }, onError: (e, s) {
      // mainProcessで例外をthrowした時、ここに来る
      completer.completeError(e);
    }, onCancel: () {
      // cancelをコールし、_waitCancelCompletionが終了した時、ここに来る
      completer.complete(AsyncCancelableResult(isCanceled: true, value: null));
    });

    return completer.future;
  }

  Future<bool?> _mainFuture() {
    for (int i = 0; i < 10; i++) {
      await Future.delayed(Duration(seconds: 1));
      if (_isCancelRequested) return null;
    }

     return true;
  }

  /// キャンセルしたい時に呼ぶ
  /// awaitで呼ぶとキャンセル終了するまで待つ
  Future<void> cancel() async {
    if (_isCancelRequested || _operation.isCanceled || _operation.isCompleted) return;

    _isCancelRequested = true;
    // このawaitは_waitCancelCompletion()の終了を待つ
    await _operation.cancel();
  }

  Future<void> _waitCancelCompletion() {
    return Future.doWhile(() async {
      await Future.delayed(const Duration(milliseconds: 300));
      return !(_operation.isCanceled || _operation.isCompleted);
    });
  }
}

SampleCancelableOperationを以下のようにして使うことができる。

SampleCancelableOperation? _operation;

// どこかの関数処理
try {
 _operation = SampleCancelableOperation();
  final result = await operation.start();
  if (result.isCanceled) {
    // キャンセルした時の処理
  } else {
    // 正常した時の処理
    // trueが出力されるはず
    print(result.value!);
  }
} catch (e) {
  // _mainFutureから例外をthrowするとここに来る
}

// 上とは別のどこかの関数処理
// キャンセルし、その終了まで待つ
await _operation?.cancel();

Dart/Flutterのisolateを理解

isolate = dartのマルチスレッド的なやつ。

  • main()はmain isolate
  • 子のisolateを生成した親のisolateは子のisolateの終了を待つことができる
  • 親子isolate間でping pong的な相互通信もできる

worker isolateで1秒かかる処理を待つ

import 'dart:isolate';

void main() async {
  debugPrint("${DateTime.now()} start");
  final result = await _runInBackground();
  debugPrint("${DateTime.now()} $result");
  debugPrint("${DateTime.now()} end");
}

Future<String> _runInBackground() async {
  // 以下の3行はおまじないと思うことにする
  // 大事なのは_runInIsolateで実際のバックグラウンド処理をするということ
  final p = ReceivePort();
  await Isolate.spawn(_runInIsolate, p.sendPort); // worker isolateの生成
  return await p.first as String;
}

Future<void> _runInIsolate(SendPort p) async {
  // 1秒待つ。実際の実装ではここで時間のかかる処理をする。
  await Future.delayed(const Duration(seconds: 1));
  const result = "worker isolate終了";
  Isolate.exit(p, result);
}

これくらいならisolateを使うまでもないはず。次のような相互通信などで必要になる。

isolateで相互通信する

やること

  • mainのバックグラウンド処理からメッセージを投げると遅延してworker isolateから応答が来る
  • mainのバックグラウンド処理がもういいよとworker isolateに伝えるまで、そのやりとりが続く
  • mainは1回ごとのworker isolateの応答を受け取りprintする
import 'dart:isolate';
import 'package:async/async.dart';

void main() async {
  debugPrint("${DateTime.now()} start ping pong");
  // await forで通信が終わるまでループする
  await for (final message in _pingPongInBackground()) {
    debugPrint("${DateTime.now()} $message");
  }
  debugPrint("${DateTime.now()} end ping pong");
}

// 戻り値は、Stream<1回の通信で返す値の型>、お尻はasyncではなくasync*と書く
Stream<String> _pingPongInBackground() async* {
  final p = ReceivePort();
  await Isolate.spawn(_runInWorkerIsolate, p.sendPort);

  // 呼び出し元のawait forに順次値を返すStreamを生成
  final stream = StreamQueue<dynamic>(p);
  // 最初にworker isolateから通信するためのPortを受け取る(Aと対応)
  SendPort sendPort = await stream.next;

  // worker isolateとの相互通信を10回繰り返す
  for (var i = 0; i < 10; i++) {
    // pingをworker isolateに投げる
    sendPort.send("ping $i");
    // worker isolateの応答を待つ
    String pong = await stream.next;
    // Streamへの返却値はyieldで返す
    // main()のawait forで順次受け取る
    yield pong;
  }

  // worker isolateにもうpingは送らないから終了していいよと伝える
  sendPort.send(null);

  // Stream終了。main()のawait forループを終了させる
  await stream.cancel();
}

// worker isolateの処理
Future<void> _runInWorkerIsolate(SendPort p) async {
  final commandPort = ReceivePort();
  // 最初に自分と通信するためのPortを渡す(A)
  p.send(commandPort.sendPort);

  // ping待機ループ
  await for (final ping in commandPort) {
    if (ping is String) {
      // ping受信を検知
      // 1秒遅延。実際の実装ではここで通信処理など時間のかかる処理をする
      await Future.delayed(const Duration(seconds: 1));
      // 応答を返す
      p.send("pong ${DateTime.now()} for $ping");
    } else if (ping == null) {
      // 通信終了していいよと言われたらループを抜ける
      break;
    }
  }

  Isolate.exit();
}

iOS、Androidのフルページ対応(100dvhとか)

2023年1月現在、端末の画面高さぴったりに表示したい時、CSSだけでiOSAndroid両方対応するのは無理っぽい。JavaScriptも使わないといけないようだ。さもなくばposition: absolute; bottom: 0; 指定した要素がアドレスバーの裏側に回ったりする。

iOSAndroid個別に対応する場合は以下でうまくいくようだ。

iOSの場合

bodyタグ下などルートとなる親要素のstyleを以下のようにする。

.root {
  height: 100vh;
  height: 100dvh;
}

こうすると100dvhに対応していないブラウザは、100dvhの指定は無視され100vhが適用される。100dvhに対応しているブラウザは100vhを100dvhの指定で上書きする。

Androidの場合

100vhではうまくいかない。JavaScriptでwindow.innerHeightをルート要素のheightにセットするのが確実らしい。多分、これでPCブラウザもうまくいくはず。

iOSAndroid両方対応する

以上のことを踏まえて両方対応するなら、以下のようにすればいいはず。

<html>
  <body>
    <div id="root" class="root">        
      <main class="main">
        <!-- メインコンテンツ -->
      </main>
    </div>
  </body>
</html>

<style>
.root {   
  height: 100vh;
  height: 100dvh;

  // 端末下からはみ出た部分はスクロールで見られるようにしたい場合
  overflow: scroll;
}

.main {
   // rootの高さに合わせたい場合
   height: 100%;
}
</style>

<script>
// ブラウザ生JavaScriptではなくES2015とかの記述
window.addEventListener('load', (event) => {
    const isIPhone = () => {
      const ua = navigator.userAgent;
      const regexp = /iPhone.+Mobile/g;
      return regexp.exec(ua) !== null;
    };

    const setFullPageHeight = () => {
      // iPhoneはCSSの指定のままで良い
      if (isIPhone()) return;
      // Android、その他はwindowの高さに合わせる
      document.getElementById("root").style.height = `${window.innerHeight}px`;
    };

    setFullPageHeight();

    // ウィンドウリサイズ時に高さを再設定
    let timeoutId;
    window.addEventListener('resize', () => {
      clearTimeout(timeoutId);
      timeoutId = window.setTimeout(setFullPageHeight, 200);
    });
});
</script>

Macでfvmをインストール

公式サイトでbrewでインストールするように書かれているが、Xcodeのcommand line toolsが最新ではないとか怒られたり面倒。

Error: Your Command Line Tools are too outdated.

同じく公式サイトの別のインストール方法として書かれているdartで入れた方が良さそう。

dart pub global activate fvm

こっちだと何も怒られずすんなり入った。あとは以下のようにインストール後出力される警告通りパスを通してすぐ使えるように。

export PATH="$PATH":"$HOME/.pub-cache/bin"

 

Nuxt + TypeScript + Google Maps API

問題発生

Nuxt + TypeScriptで、Google Maps APIを使おうとするとエラーに遭遇。

例えばこのようにgoogle.maps.*を使おうとすると

let map: google.maps.Map

このようにエラーが出る。

Cannot find namespace 'google'.

解決方法

@types/googlemaps Nodeパッケージをインストール

yarn add @types/googlemaps --dev

Nuxtプロジェクトのルートディレクトリにある tsconfig.json を修正。

"types": [
     "@types/node",
     "@nuxt/types",
     "@types/googlemaps"  これを追加
]

これでエラーが出なくなりました。

ただGoogleではない第三者のDefinitelyTypedという組織がメンテナンスしていることに注意。この組織はいろんなライブラリの型定義のパッケージを作ってますが、精度が低いと指摘している記事も見かけました。

https://github.com/DefinitelyTyped/DefinitelyTyped#readme

でもGoogle本家のドキュメントで、このパッケージのインストールを紹介している。

https://developers.google.com/maps/documentation/javascript/using-typescript

大丈夫かな?問題が起きたらまた考えよう。

tabBarItem.titleよりtitleが優先される問題

UIViewControllerで以下のようにセットし、それをUITabController#viewControllersに追加する。

title = "全てのエリア"
tabBarItem.title = "投稿"

この場合、タブの下に表示されるタイトルは、titleの方が優先されるようである。ちなみにtitleはNavigationBarにも表示される。

titleはセットせずに、以下のようにどこに表示するのか明示的にした方が良さそうである。

navigationBarItem.title = "全てのエリア"  // NavigationBarのタイトル
tabBarItem.title = "投稿"  // Tabのタイトル