torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

【Flutter】ListViewで子要素がListViewの内側に表示されたか検知する

こんにちは、とりかつ(https://twitter.com/torikatsu923)です。 いきなりですが、FlutterにはListViewという大変便利なWidgetがあります。

api.flutter.dev

ListView+ListTileを使うことで数分で以下のようなUIを作ることができます。

f:id:torikatsu923:20210730005502p:plain:h600

このような画面をさくっと実装できるのはFlutterのとてもいいところだと思います。

最近、ListViewの要素が画面内に表示されたかどうか知りたいケースがありました。 そこで今回の記事は、ListViewの要素がListViewの内側に表示されたことを検知する方法について紹介します。

今回紹介する方法を使うと、ListViewの内側に表示されている間だけ色を変えるみたいな実装も実現可能です。

f:id:torikatsu923:20210730233808g:plain:h600

コード

今回の記事で紹介するコードは以下のリポジトリです。 github.com

方針

ListViewの要素がListView内に表示されたかどうかは、ListViewの表示領域とListViewの要素の座標を比較することで判断することができます。 図にすると以下のようになります。 f:id:torikatsu923:20210730012837p:plain:h600 要素がListViewの内側にあるとき、要素の下端のy座標はListViewの下端のy座標よりも小さくなります。 逆に要素がListViewの外側にあるとき、要素の下端のy座標はListViewの下端のy座標よりも大きくなります。

これをまとめると、以下のようになります。

要素がListViewの内側にあるときの条件: `要素の下端のy座標 <= ListViewの下端のy座標`  
要素がListViewの外側にある時の条件  : `要素の下端のy座標 > ListViewの下端のy座標`

この判定をListViewのスクロールイベントが発生するごとに行います。
そうすることで、ListViewの要素がListViewの内側に完全に入ったことを検知することができます。

実際にこれを実装していきます。実装手順は以下の通りです。

  1. ListViewの要素の下端の座標を取得する
  2. ListViewの要素で、ListViewの表示領域の下端の座標を取得する
  3. 1、2で取得した座標を使って、要素がListViewの領域内に表示されているかどうかの判定を行う
  4. 3の判定をスクロールイベントが発生するたびに行う

1. ListViewの要素の下端の座標を取得する

まずはListViewの要素の下端の座標を取得します。下端の座標はRenderObjectから取得します。 FlutterはWidget、Element、RenderObjectの3つのツリーでUIを構築しているのですが、RenderObjectは実際に描画された時のサイズや座標を持っています。

https://flutter.dev/images/arch-overview/trees.png

任意のWidgetに対応するcontext経由で以下のように取得することができます。 とってくるRenderObjectの実体はRenderBoxなので、取得するついでにキャストしています。

visibility_detector_list_item.dart

    final renderBox = context.findRenderObject() as RenderBox?;

次にRenderBoxからグローバル座標を取得します。 グローバル座標の取得にはRenderBoxのlocalToGlobal()を使用します。

api.flutter.dev

今回は単にグローバル座標を取得したいだけなので、Offset.zeroを渡してあげます。 ここまでのコードは以下のようになります。

visibility_detector_list_item.dart

    final renderBox = context.findRenderObject() as RenderBox?;
    if (renderBox == null) return;
    final globalOffset = renderBox.localToGlobal(Offset.zero);

2. ListViewの要素で、ListViewの座標を取得する

1の手順と同様にcontext経由でRenderObject経由でListViewの座標を取得します。 問題はどうやってListViewの要素からListViewのcontextにアクセスするかです。 やり方はいろいろあると思いますが、今回はGlobalKeyを使います。 ListViewを含むWidget側でGlobalKeyを生成し、ListViewのコンストラクタに渡します。 さらに、ListViewの要素側でこのGlobalKeyを受け取れるようにしてあげます。

visibility_detectable_list_view.dart

  final listViewKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemBuilder: (_, index) =>
          ListItem(index: index, listViewKey: listViewKey),
      separatorBuilder: (_, __) => const Divider(height: 0),
      itemCount: 100,
      key: listViewKey, // pass global key
    );
  }

ListView側では、GlobalKeyのcurrentContext経由でcontextにアクセスし、1の手順と同様にListViewのグローバル座標を取得します。 ここまでのコードは以下のようになります。

visibility_detectable_list_item.dart

    final renderBox = context.findRenderObject() as RenderBox?;
    final listViewRenderBox =
        widget.listViewKey.currentContext?.findRenderObject() as RenderBox?;

    if (renderBox == null || listViewRenderBox == null) return;
    
    final globalOffset = renderBox.localToGlobal(Offset.zero);
    final listViewGlobalOffset = listViewRenderBox.localToGlobal(Offset.zero);

3. 1、2で取得した座標を使って、要素がListViewの領域内に表示されているかどうかの判定を行う

ListViewの要素がListView内にあるかどうかの条件は冒頭で紹介しました。 条件分岐にはウィジェットの最下端のグローバル座標が必要になるので、まずはその座標を計算します。

1、2でlocalToGlobal(Offset.zero)を使って取得したグローバル座標は、ウィジェットの左上の座標になります。 以下の図のようなイメージです。

f:id:torikatsu923:20210730224141p:plain:h600

ウィジェットの再下端の座標は、1、2で取得したグローバル座標にウィジェットの高さを足すことで算出できます。 ウィジェットの高さはRenderBox.size.heightで取得することができます。

それぞれのウィジェットの再下端を取得できたら、あとは冒頭で紹介した条件に突っ込むだけです。 ここまでのコードは以下のようになります。

    final renderBox = context.findRenderObject() as RenderBox?;
    final listViewRenderBox =
        widget.listViewKey.currentContext?.findRenderObject() as RenderBox?;

    if (renderBox == null || listViewRenderBox == null) return;

    final listItemBottomPosition =
        renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height;

    final listViewBottomPosition =
        listViewRenderBox.localToGlobal(Offset.zero).dy +
            listViewRenderBox.size.height;

    if (listItemBottomPosition <= listViewBottomPosition) {
      // inside
    } else {
      // outside
    }

4. 3の判定をスクロールイベントが発生するたびに行う

3でつくったの判定のロジックをスクロールイベントが発生するたびに行うことで、ListViewの要素ががListView内に表示された瞬間に、そのことを検知することができます。 そのためにはScrollControllerを使用します。 まず、ListViewにScrollControllerを設定します。さらにListViewの要素でScrollControllerのリスナに3で作った処理を設定します。

visibility_detectable_list_item.dart

class _VisibilityDetectableListItemState
    extends State<VisibilityDetectableListItem> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    WidgetsBinding.instance?.addPostFrameCallback(
      (_) => widget.controller.addListener(_onScroll),
    );
  }

  @override
  void dispose() {
    super.dispose();
    widget.controller.removeListener(_onScroll);
  }

_onScroll()は3で作った処理をまとめた関数です。

ここで大事なのは、リスナを設定するタイミングです。 ListViewの要素がListView内に描画されているかどうかの条件判定はcontextにアクセスする必要があります。 画面がレンダリングされる前にcontextにアクセスできないので、didChangeDependenciesWidgetBinding.instance?.addPostFrameCallback()を使ってリスナを設定しています。

また、リスナを開放ことも非常に重要です。 ListViewは画面内に入りそうな要素を必要に応じて作成・破棄します。なので、画面がスクロールされると要素が破棄されることがあります。 要素が破棄された場合、onScrollは存在しなくなります。 リスナを解放しないとスクロールイベントが発生した時、存在しないonScroll()を呼び出そうとしてメモリリークが起きます。 そのため、disposeで確実にScrollController.removeListener()してあげます。

実装は以上になります!これで以下のように画面内に表示されたことを検知できるようになりました!

f:id:torikatsu923:20210730233808g:plain:h600

おわりに

今回はグローバル座標を基準に、ListViewの要素がListView内に表示されたかどうかを検知する方法を紹介しました。 localToGlobal()にListViewのRenderObjectを渡すことで、ListViewとListViewの要素の相対的な座標で判定を行うこともできそうです。

ListViewで子要素がListViewの内側に表示されたか検知する方法に番兵を用いる方法もありました。 qiita.com

番兵を用いる方法は非常にシンプルでとっつきやすいです。 ですが、管理するWidgetの数が増えてしまいます。また、番兵を用いるとbuildが呼ばれたタイミングで初めてListViewで子要素がListViewの内側に表示されたかを検知することができるのですが、Widgetが破棄されていない等の理由で微妙に扱いづらい部分があります。 その点今回の記事で紹介した方法は汎用性がかなり高いため、安定した動作を求めるならこの記事で紹介した実装方法がおすすめです!

【Flutter】いい感じのローディング画面を表示したい!

こんにちは、とりかつ(@torikatsu923)です。 よくアプリでこんな感じのローディング画面を見かけると思います。

f:id:torikatsu923:20210714014700g:plain:h400

アプリ起動時にただ真ん中でぐるぐるするのを見ているより、いい感じのローディング画面が表示されていたら、きっとユーザは退屈しなくて済むと思います。

このいい感じのエフェクトのことをShimmerと呼ぶそうです。 そこで、今回の記事はShimmerローディング画面を実装する方法を紹介していきます。

リポジトリは以下になります。 github.com

実装の流れ

  1. UIのスケルトンをつくる
  2. ケルトン全体をグラデーションで塗りつぶす
  3. アニメーションをつける

UIのスケルトンをつくる

まず、アニメーションとかが無いシンプルなローディング画面を作ってみます。 今回は以下のような画面を作っていきます。

f:id:torikatsu923:20210713044630p:plain:h400

Row,Column,Containerを組み合わせて、以下のようになりました。 わかりやすいよう、あえて黄色を設定しています。

f:id:torikatsu923:20210713044943p:plain:h400

アウトラインはこのようになっています。

f:id:torikatsu923:20210713045035p:plain:h400

UIのスケルトンを実装した段階のサンプルはこちらです。 github.com

ケルトン全体をグラデーションで塗りつぶす

次に黄色の部分をメタリックな感じのグラデーションで塗りつぶします。 今から作成する黄色の部分を塗りつぶすウィジェットShimmerとします。

塗りつぶしにはShaderMaskというウィジェットを使用します。 ShaderMaskは、子要素のうちcolorが設定されている部分をShaderMaskで指定された条件で塗りつぶします。 api.flutter.dev

ShaderMaskのshaderCallbackにはShaderを返す関数を指定してあげる必要があります。 今回はLinerGradient.createShaderでShaderを作成してあげます。 まずはいい感じのグラデーションになるようLinerGradientを作成します。設定する値は好みで問題ありません。

        final gradient = LinearGradient(
          colors: [
            Color(0xFFEBEBF4),
            Color(0xFFF4F4F4),
            Color(0xFFEBEBF4),
          ],
          stops: [
            0.1,
            0.3,
            0.4,
          ],
        );

次にgradient.createShaderShaderを作成してあげるのですが、この作り方が少し特殊になります。 今回Shimmerの効果をかけたいのはShimmerで囲われたエリアです。 Shimmerの子要素は動的に変わるため、画面が描画されるまでShimmer効果をかける位置や高さが分かりません。 なので、Shimmer.buildが実行されるタイミングでShimmerの効果をかける位置や高さを取得してあげる必要があります。

以下はbuild内で位置や高さを取得している部分です。

    // shimmerのRenderObjectを取得する
    // shimmerが一度もbuildされていない場合はRenderObjectが取得できない
    final shimmerBox = context.findRenderObject() as RenderBox?;
    if (shimmerBox == null) {
      // shimmerのRenderObjectが存在しなければ何もないウィジェットを返す
      return SizedBox();
    }

    // Offset.zeroをRenderObjectの座標系に変換する
    // つまり、RenderObjectのleftとtopを取得する
    final offsetWithinShimmer =
        shimmerBox.localToGlobal(Offset.zero, ancestor: shimmerBox);

    // グラデーションの範囲をShimmerと同じ範囲にする
    final shimmerRect = Rect.fromLTWH(
      offsetWithinShimmer.dx,
      offsetWithinShimmer.dy,
      shimmerBox.size.width,
      shimmerBox.size.height,
    );

これを使ってgradient.createShader(shimmerRect)をすると以下のようになります。

f:id:torikatsu923:20210714013258p:plain:h400

黄色で塗りつぶされていたところに灰色のグラデーションがかかっていることがわかります。 ここまでの実装は以下になります。 github.com

アニメーションをつける

最後にアニメーションをつけていきます。 LinerGradientのコンストラクタにはtransformを指定することができます。 ここには、自作のGradientTransformを渡してあげます。 以下は自作のGradientTransformになります。 ここで大事なのは、0-1のdoubleを受け取って、その値に応じて_SlidingGradientTransform.transformが返す値が変化するようにすることです。

class _SlidingGradientTransform extends GradientTransform {
  const _SlidingGradientTransform(this.slidePercent);

  final double slidePercent;

  @override
  Matrix4? transform(Rect bounds, {TextDirection? textDirection}) =>
      Matrix4.translationValues(bounds.width * slidePercent, 0, 0);
}

これをLinerGradientのコンストラクタに渡してあげます。 ここで、_SlidingGradientTransformのコンストラクタに適当な値を渡して挙動を見てみます。

0.1のとき 0.4のとき
f:id:torikatsu923:20210714013258p:plain:h400 f:id:torikatsu923:20210714014040p:plain:h400

渡した値が大きくなるにつれて、グラデーションの色が明るいところが右へ移動していることがわかります。 ここにAnimationControllerを渡すといい感じのローディング画面ができ上がります。

今回のAnimationControllerの実装は以下のようになっています。

    _controller = AnimationController.unbounded(vsync: this)
      ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1500))
      ..addListener(() {
        setState(() {});
      });

これを設定すると以下のようなアニメーションができました!

f:id:torikatsu923:20210714014731g:plain:h400

あとはグラデーションの傾きをいい感じに調整するとこの記事の冒頭のようなShimmer効果が出来上がります。

おわりに

今回はShimmer効果の実装方法を紹介しました。 Shimmerは効果をつけたいときは以下のような公開されているパッケージを使う方法もあります。

pub.dev

Shimmerの実装量は100行程度なので、パッケージに頼らずに自前で実装してしまうのも多いにアリですね!

それではよいFlutterライフを!

【Flutter】riverpod 1.0.0-dev.0の変更点まとめ

こんにちは、とりかつ((@torikatsu923)https://twitter.com/torikatsu923)です。

今朝、riverpodの1.0.0-dev.0が公開されました!

以前記事で触れましたが、4月の半ばごろにriverpodの作者の方からriverpodに破壊的変更が入るかもしれないというアナウンスがありました。 torikatsu923.hatenablog.com

Changelogを確認したところ、今回のアップデートはそのアナウンスを受けてのものと考えて良さそうです。 個人的に欲しかった機能がつまっているアップデートなので、使うのがとても楽しみです!

今回のアップデートは変更点が20個程含まれており、破壊的変更も多く含まれています。 pub.dev

そこで今回の記事ではriverpodの変更点についてまとめていきます。

アップデートの方針の考察

今回のアップデートのキーは「Providerへアクセスする方法を統一する」だと思われます。 初めてriverpodを触る時、Providerをreadする方法の複雑さにウッとなる方も多いと思います。 https://riverpod.dev/assets/images/reading-eddc30e98d777456a442ddd5b181eb2a.svg

以前の作者さんの(アナウンス)https://github.com/rrousselGit/river_pod/issues/335や、change logでproviderへのアクセス周りで破壊的変更が多く入っていることから、 アップデートにはProviderへアクセスする方法を統一し、よりシンプルにするにする目的があると考えられます。

実際に変更後はref.read, ref.watch, ref.listenによってProviderへのアクセス、UIでの状態の監視、状態変更の監視が可能になっています。

以下ではこの点を踏まえつつ、変更点を追っていきます。

変更点一覧

useProviderの削除

HookWidgetのuseProviderがなくなり、Widget refを使用してProviderへアクセスするように変更が入っています。 HookConsumerWidgetを継承することで、buildの引数にWidget refを受け取ることができるようになります。

Before

class Example extends HookWidget {
  @override
  Widget build(BuildContext context) {
    useState(...);
    int count = useProvider(counterProvider);
    ...
  }
}

After

class Example extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    useState(...);
    int count = ref.watch(counterProvider);
    ...
  }
}

pub devより引用

ConsumerWidgetのbuildメソッドでWidget refを受け取るようにする変更

従来は以下のようにbuildメソッドでScopedReaderを受け取っていましたが、変更後はWidgetRefを受け取るようになりました。 変更後はProviderの変更をwatchしたい際はref.watchとする必要があります。

Before

class A extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(someProvider);
  }
}

After

class A extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(someProvider);
  }
}

ProviderListenerを非推奨にする変更

WidgetReflistenという状態の変更を監視する関数ができました。その関係で、今まで状態の変更を監視する際に使用されていたProviderListenerが非推奨になりました。

この変更により、今までWidgetをネストしなければならなかったところをとてもシンプルに記述することができるようになりました!

Before

class A extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderListener(
      provider: someProvider,
      onChange: (context, state, child) { ... },
      child: Container( ... ),
    );
  }
}

After

class A extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen(someProvider, (state) {
       // something
    }
  }
}

ConsumerStatefulWidget, ConsumerStateの追加

StatefulWidgetとConsumerWidgetが合体したConsumerStatefulWidgetが追加されました。 StatefulWidgetのライフサイクルにフックしてProviderの処理を走らせたいが、状態の管理はProviderに行わせたいみたいな場合、いままではStatefulWidgetでWidgetを定義して、build内でConsumerで括ってあげる必要がありました。 今回のConsumerStatefulWidgetの追加で、Consumerで括る必要がなくなったため、よりシンプルに記述できるようになりました。

watchでselectでstateの特定の値の変更のみ検知できるようにする変更

userのフィールドにusernamestatusがあるとします。 以下のコードではstatusに変更があった場合はリビルドされず、usernameに変更があった場合にリビルドされるようになりました。

final userProvider = StateNotifierProvider<UserController, User>(...);

Consumer(
  builder: (context, ref, _) {
    bool userName = ref.watch(userProvider.select((user) => user.name));
  },
)

グローバルな状態とリビルドについて悩んでいたため、この変更は個人的にとても嬉しいです!

overrideWithValueの削除

今まで.familyで作成したProviderはoverrideする必要がありました。その際値のみをオーバーライドするoverrideWithValueとProviderごとオーバーライドするoverrideWithProviderの2種類の方法がありました。 しかし、今回の変更でoverrideWithProviderに一本化されたようです。

ProviderObserverが変更前と変更後の2種類の状態を受け取れるようにする変更

ProviderObserverが変更の前後の状態を受け取れるように変更が入りました。 これによって状態の差分をみたりする処理が書きやすくなりました。

ProviderObserver.mayHaveChangedの削除

ProviderObserver.mayHaveChangedが削除されました。 これは破壊的変更ですが、個人的にはあまり使っていない機能だったため影響はありませんでした。

ref.listenの追加

先で触れたようにref.listenが追加されました。 バージョンアップ後はダイアログの表示等はこれを使用することになります。

ProviderReferenceを非推奨にする変更

ProviderReferenceが非推奨になり、代わりにProviderBaseが採用されました

全てのProviderがProviderRefBaseをパラメータとして受け取るようにする変更

今までProviderは以下のようにrefを受け取っていましたが、これがProviderRefBaseに変更されました。

Provider((ref) => ...);

この変更でrefがstateというプロパティを持つようになり、ref.state++のようにすることで状態を変更することができるようになりました。 また、StateProviderのrefにはcontrollerというプロパティが追加され、ref.controller....でStateControllerにアクセスできるようになりました。

ProviderReference.mountedの削除

ProviderReference.mountedが削除されました。代わりにonDisposeを使用することで似たような挙動を実現できるようです。

Provider<T>((ref) {
  var mounted = true;
  ref.onDispose(() => mounted = false);
});

ProviderContainerのdebugproviderValuesとdebugProviderElementsの削除

ProviderContainerのdebugProviderValuesとdebugProviderElementsが削除され、 ProviderContainer.getAllProviderElementsを使用できるようになりました

StreamProviderのlast, stream, futureがプロバイダーの再構築の回数に依存しないようにする変更

関連するProviderの再構築の回数に関係ないfuture, streamを外部に公開するようになりました。

Providerの値が同じ時、Providerをオーバーライドできるようにする変更

StreamProvider<User>があった時、Provider<AsyncValue<User>>でオーバーライドすることができるようになりました。

ref.refreshの呼び方変更

ref.refreshの呼び方がref.container.refreshに変更されました。

ref.onDisposeのタイミング変更

Providerの依存関係の変更がわかったらすぐにonDisposeが呼ばれるようになりました。

Providerの状態の再計算のタイミング変更

Providerの依存関係が変更されてリスナがいるとき、次の読み取りまで状態の再計算を待たなくなりました。

ProviderContainer.pumpの追加

ProviderContainer.pumを使うことで、awaitのように簡単にproviderが状態の変更を通知するまで待つことができるようになりました。

familyとdisposeを併用した時に状態が残る問題の修正

familyとdisposeを一緒に使用した際状態が残ってしまう問題がありましたが、今回のアップデートで解決されたようです。

おわりに

今回の記事ではriverpodの変更点を追っていきました。 Providerへのアクセスする際の構文がrefを経由して行われるように統一されたので、より使いやすくなったと感じています! またいくつかの変更によってシンプルに処理をかけるようになったのもとても嬉しいです!

それではよい開発ライフを!

null-safetyとお友達になるTips

こんにちは、とりかつ((@torikatsu923)https://twitter.com/torikatsu923)です。

Flutterで開発をする際はDartを書くことになります。DartにはNNBD(null-safety) があり、Flutter側でもnull-safetyの対応がどんどん進んでいます。 null-safetyのおかげで、nullが入りこむことによって起こるバグを見つけやすくなりました。 私もFlutterの開発をする際はnull-safetyをonにしているのですが、このnull-safetyの恩恵にあやかるためにはちょっとした工夫が必要になります。 今回の記事では、そんなnull-safetyと最高のお友達になるためのtipsを2つ紹介します。

note 今回の記事は以下のdocsの更新を受けてのものです。 一時ソースをサクッと確認したい方は以下からお願いします。 dart.dev

nullチェックしてるのにnullの可能性があるとコンパイラに怒られるケース

Dartのコードを書いていると、以下のようにnullチェックをすることがあると思います。

一見正しそうに見えますが、実際にやってみるとコンパイルエラーになります。 f:id:torikatsu923:20210619164656p:plain

エラーの内容は以下の通りです。

The method 'toDouble' can't be unconditionally invoked because the receiver can be 'null'.
Try making the call conditional (using '?.') or adding a null check to the target ('!').dart(unchecked_use_of_nullable_value)

ざっくり訳すと「valueはnullの可能性があるからtoDoubleは呼び出せないよー」となります。 手前でnullチェックをしているのにvalueがnullの可能性があるとはどういうことでしょうか、、、

Tips1 ローカル変数に切り出す

実はこれ、一度ローカル変数に切り出すことでうまく切り抜けることができます。 試しに以下のようにfinal v = value;のようにしてみます。

すると、、、

f:id:torikatsu923:20210619165402p:plain エラーが消えました!!🎉🎉

Tips2 !を使って握り潰す

どこからどうみてもnullでない値なのにnullの可能性があるなんて言われた時には!(bang operator)を使用することもできます。 以下のようにコードを変更してみると、、、

f:id:torikatsu923:20210619171016p:plain

エラーが消えました!!🎉🎉

実はこの方法、一つ注意することがあります。 MyIntegerのコードの例ではvaluefinalのため変更されることがないので問題が起きません。 しかし、valuefinal出ない時は7行目でnullチェックをした後に値が書き換えられる可能性があります。 もし値が書き換えられてnullが入り込んでしまうとクラッシュしてしまいます。 bang operatorは簡潔に記述することができますが、使い方によっては思わぬバグを作り込む原因になってしまうため、使用する際には十分に注意を払う必要があります。

個人的には少し冗長ですが1の方法が安心感があって好みです。

終わりに

今回はnull-safetyとお友達になるTipsを2つ紹介しました。 Kotlinとか触ってからdartを触るとなんでnullableと判定されちゃうん、、、というケースがちょくちょくあって初めはびっくりしました。 しかし、ちゃんと付き合い方を知れば心強い味方になってくれます!

それでは良い開発ライフを!

【Flutter】MVVMなアーキテクチャで初期化処理をしたい!

こんにちは、とりかつ(@torikatsu923)です。 私は趣味でFlutterアプリの開発をしていて、MVVMライクなアーキテクチャをriverpod+StateNotifier+freezedで状態管理をしています。

この状態管理手法は非常に便利ですが、初期化処理をする際にすこし詰まりました。 そこで、今回はMVVM的なアーキテクチャを目指すときの画面の初期化処理のやり方について書いていきます。

はじめに

Flutterでアプリを開発していると、「画面表示時にサーバからデータの取得をしつつ初期ローディング画面を表示したい」みたいなケースが結構あります。 このケースの例としてYoutubeアプリが挙げられます。 Youtubeアプリはアプリを起動するとホームが表示されます。 最初にホームが表示されるときは初期ローディングエフェクト(Shimmer)が表示されて、データの取得が終わると動画一覧が表示されます。

f:id:torikatsu923:20210530004824p:plain
初期ローディング(shimmer)

f:id:torikatsu923:20210530004850p:plain
初期データ取得後画面

大したことなさそうに見えます。 とりあえず動くものを作ろうとすると、FutureBuilder使って初期ローディングを行うだけです。

一見問題なさそうに見えますが、MVVM的なアーキテクチャを目指そうとした時、以下のような課題感があると思いました。

  • StateNotifierを経由せずにUIの更新を行っている
  • UIにエラーハンドリングの詳細ロジックが漏れる

StateNotifierを経由せずにUIの更新を行っている

UIは常にStateNotifierが持っている状態をもとにUIのレンダリングを行うべきです。 もう少し抽象的な言い方をすると、UIは状態を引数に取る関数によって構築されるべきです。 この考えはFlutterのdocsでも述べられています。

https://flutter.dev/assets/development/data-and-backend/state-mgmt/ui-equals-function-of-state-54b01b000694caf9da439bd3f774ef22b00e92a62d3b2ade4f2e95c8555b8ca7.png

上記の例ではStateNotifierの状態の変更とFutureBuilderのfutureの結果の2つの要素によってUIが構築されます。 上記の例は状態によってUIを管理できておらず、将来的にアプリケーションの状態管理が複雑になることが考えられます。

UIにエラーハンドリングの詳細ロジックが漏れる

上記の例ではinitialize()がどういった値を返すのか、どういったエラーをハンドリングする必要があるのかをUI側のコードに記述する必要があります。 これは「画面の初期化処理」というイベントのロジックそのものと考えられます。 StateNotifierを採用してMVVM的なアーキテクチャを目指す一つの理由に、UIとロジックの分離が挙げられます。 これより、「画面の初期化処理」はViewModel側(StateNotifier)に記述し、「初期化処理」によって生み出された「状態」によって以下の画面を出し分ける必要があります。

  • 初期ローディング画面
  • 初期化処理完了後画面
  • エラー画面

課題解決の方法

前述の課題を解決するためには以下を達成する必要があります。

  • 画面の出しわけを状態に基づくようにする
  • 初期化処理をUI構築時に呼び出せるようにする

これらを実装に反映させていきます。

画面の出しわけを状態に基づくようにする

初期ローディングはbool initializingという状態として考えることができます。 なので、StateNotifierが管理する状態にinitializingというフィールドを追加し、UI側を以下のように変更します。 また、エラーは「エラーが起きている」という状態として考えることができます。 なので、Error errorというフィールドを追加します。 そしてUI側のコードを以下のように変更します。

UI側のコードが状態飲みに依存するようになり、どのような状態の時にどのようなUIが構築されるのか見通しがよくなりました。

ここでは2つのフラグによってUIを管理していますが、enumでUIの状態の種類を定義し、switchを使用する方法もあると思います。 このとき、それぞれのフラグの概念をenumにまとめていいかどうか注意する必要があります。

初期化処理をUI構築時に呼び出せるようにする

初期化処理をUI構築時に呼び出す方法にはいくつかあります。 とりあえず以下のような方法を思いつきました。 (これらは全てアンチパターンです。課題感の前提を共有するためにあえて流れを追うように書いていきます。)

  • buildの最初に呼び出す
  • StatelessWidgetのコンストラクタで呼び出す
  • StatefulWidgetのinitStateで呼び出す

しかし、これらは全て失敗します。 まず、buildの最初に呼び出す方法ですが、これだとWidgetがrebuildされるたびに初期化処理が走ります。 そのため、状態に変更が入るたびに初期化処理が発火してしまいます。 初期化処理はウィジェットのライフサイクルに合わせたいため、この方法では目的が達成できません。

次にStatelessWidgetのコンストラクタで呼び出す方法を試しました。 コンストラクタ内でinitializeを呼び出す時ProviderContainerを使用してStateNotifierを取得します。 しかし、コンストラクタでProviderContainerが参照するStateNotifierとbuild内でcontextを使用して参照するStateNotifierは別のインスタンスです。 コンストラクタ内で初期化処理を行ってもbuild内で参照するStateNotifierは初期化処理が行われていないため、失敗します。

次にStatefulWidgetのinitStateで呼び出す方法を試しました。 initState内ではcontextを参照することができません。それゆえ、この方法は失敗します。 contextを参照したいときはinitState直後に呼ばれるdidChangeDependenciesを利用するそうです。

以上の失敗を踏まえ、以下2つの方法が候補として考えられます。

  • StateNotifierのコンストラクタで呼び出す
  • didChangeDependenciesで呼び出す

StateNotifierのコンストラクタで呼び出す

以下のようにして呼び出します。

class ViewModel extends StateNotifier<SomeState> {
  ViewModel(): super(SomeState()) {
    _initialize();
  }

  Future<void> _initialize() async {
    ...
  }
}

これはうまく機能します。 初期化処理はViewModelのライフサイクルの開始時に行いたいため、StateNotifierのコンストラクタで初期化処理を行うのは理にかなっています。 また、初期化処理をViewModel内に閉じ込めることもできます。 ですが、コンストラクタ内で非同期に状態を変更する関数を呼び出すため、ユニットテストがしづらくなります。

didChangeDependenciesで呼び出す

didChangeDependenciesではcontextを参照できるため以下のように初期化処理を行うことができます。 これにより、ページのライフサイクルの開始時に合わせて初期化処理を発火させることができます。 さらに、ViewModelのユニットテストが容易になります。 ですが、ViewModelの初期化処理を外部に公開する必要があります。

この2つの方法はどちらもメリットでメリットがあるので、どちらを選択するかは好みかと思います。 私はユニットテストをしやすくしたいため、didChangeDependenciesで初期化処理を呼び出す方が好みです。

これで以下の課題を解決することができました🎉

  • StateNotifierを経由せずにUIの更新を行っている
  • UIにエラーハンドリングの詳細ロジックが漏れる

おわりに

今回の記事では、MVVM的なアーキテクチャを目指すときにどのように初期化処理を行うかということについて述べました。 目指すアーキテクチャの姿は人それぞれであり、正解はありません。 もしこっちの方法がスマートになる!みたいなものがあればコメントにて教えていただけると幸いです。

それではよい開発ライフを!

【Flutter】今週のインプット 2021/5/17-23

こんにちは、とりかつ((@torikatsu923)https://twitter.com/torikatsu923)です。

今回の記事は、Flutterについてここ一週間(2021/5/17-23)でのキャッチアップをつらつら書いていこうと思います。

Flutter2.2

先日のGoogleIOにてFlutter2.2が発表されました。 medium.com

Flutter2.2の内容についてはここの記事がよくまとまっていました。 medium.com

Dart2.13

Flutter2.2ではDart2.13が利用可能です。 Dart2.13ではクラスのタイプエイリアスを定義することができます。

// いままでのtypedef
typedef VoidCallBack = void Function();

// 2.13で可能になったtypedef
typedef Json = Map<String, dynamic>

いままでJsonをパースする処理でMap<Map<String, Map<String, dynamic>>>とか書かなければならなかったところがすっきり書けるようになったので とても嬉しい機能です。

Flutter web

main.dart.jsの2重ダウンロードの修正が入ったようです。 この修正を手元のプロジェクトで有効にするためには以下の手順を踏めば良さそうです。

  1. index.htmlを削除する
  2. flutter create .を実行する

また、アクセシビリティ周りの機能も充実しました。

dev tool のアップデート

devtoolでProviderを確認できるようになりました🎉

riverpodのProviderは絶賛対応中だそうです。

riverpod

docsの更新

riverpod.dev 0.13.0 -> 0.14.0のStateNotifierまわりの破壊的変更に伴う、マイグレーションガイドが追加されていました。

どうやらcliマイグレーションを行うことができるようです。

$ dart pub global activate riverpod_cli
$ riverpod migrate

大幅なアップデート

こちらのIssueを眺めていると、riverpodの書き方の大幅なアップデートが入ることがわかりました。 github.com

個人的に大きいと感じていることは、ProviderListener周りのアップデートです。

いままで、値の変更に応じてダイアログを表示したいようなケースではProviderListernerを使って、 以下のようにウィジェットを囲む必要がありました。

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ProviderListener<Something>(
      provider: anotherProvider,
      onChange: (context, value) {}
    );
  }
}

これがバージョンアップによって以下のように書くことができるようです。

class _StatefulExampleState extends State<StatefulExample> {
  @override
  Widget build(BuildContext context) {
    A value = watch(a);
    listen<A>(a, (value) {
    });

  }
}

ウィジェットをネストする必要がなくなったため、よりフラットにかけるようになりました。

Flutterにおけるエラーハンドリング

Flutterで以下のようなMVVMライクな構造にしようとしたとき、DataレイヤのエラーをViewModelでハンドリングしたいことが結構あります。 https://miro.medium.com/max/700/1*Uj5dnm3RTQ89uidDpcObHw.jpeg

dartにはJavaでいうthrowsのような構文がないため、どんなエラーを吐くのかを知りたいときはメソッドの内部使用を見る必要があります。 しかし、これではメソッドを呼び出す側がメソッドがどんな型のエラーを吐くかを知ることになります。 これでは、せっかくレイヤーごとに分離しているのに、レイヤ間が密結合になってしまいます。 また、エラーハンドリング漏れも十分に考えられます。

そこで、Resultを使用する方法が考えられます。 ntaoo.hatenablog.com

Resultは値に対して成功、失敗の文脈を持たせることができるため、エラーハンドリングの漏れを防ぐことができます。

さらに、独自のResultクラスとエラークラスを定義して、各エラーをアプリケーションに依存したエラーへ変換することで、よりすっきりした構成にすることが可能です。 github.com

【riverpod】画面遷移時にStateNotifierへ値を渡す方法

こんにちは、とりかつ(@torikatsu923)です。

普段私はFlutterで開発しており、riveropd+StateNotifier+freezedでMVVMライクに状態管理をしています。 先日、画面遷移を実装する際、画面遷移時にStateNotifierで作ったViewModelのコンストラクタへ渡そうとしてうまく渡せずとても苦戦しました。 そこで、今回の記事では画面遷移時にViewModelへ値を渡す方法について紹介していこうと思います。

リポジトリ

実装はこちらのリポジトリになります。 github.com

作ろうとしているもの

今回は以下のようなアプリを作ろうとしています。

f:id:torikatsu923:20210424165822p:plain
作ろうとしているアプリ

HomePageで入力したテキストが、PopUpPageで表示されるというシンプルなアプリです。

最初の実装の概要

初め、PopUpPageのViewModelと状態クラスの実装は以下のようにしていました。

class PopUpViewModel extends StateNotifier<PopUpState> {
  PopUpViewModel(): super(PopUpState());

  void setText(String text) {
    state = state.copyWith(text: text);
  }

  String get text => state.text;
}

@freezed
abstract class PopUpState with _$PopUpState {
  factory PopUpState({
    @Default('') String text,
  }) = _PopUpState;

  PopUpState._();
}

実装はとてもシンプルでsetTextで入力された文字列を受け取って、stateへセットします。

一方viewからは以下のように文字列をセットします。

class PopUpPage extends ConsumerWidget {
  const PopUpPage(this.text, {Key? key}) : super(key: key);

  final String text;

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    context.read(popUpViewModelProvider.notifier).setText(text);

    return Scaffold(......

最初の実装の問題点

これで一応動作はします。 しかし、この実装には以下のような問題があります。

  • ビルド後にsetTextをするので、余分なビルドが走ってしまう
  • setTextが公開されているので、いつでもtextを変更できてしまう
  • viewModel側で、textを使った初期化処理をコンストラクタで行うことができない

一応動くものを作るときはこれでもいいかもしれません。 しかし、それなりの規模感のものを作ろうとしたとき、このような実装はいつか負の遺産になりえます。

問題点の解決方法

前述の問題点を回避するためには、textPopUpViewModelのコンストラクタで受け取って初期化する必要があります。

class PopUpViewModel extends StateNotifier<PopUpState> {
  PopUpViewModel({ required String text }): super(PopUpState(text: text));

  String get text => state.text;
}

@freezed
abstract class PopUpState with _$PopUpState {
  factory PopUpState({
    required String text,
  }) = _PopUpState;

  PopUpState._();
}

こうすることで、ViewModelの初期化時にtextをセットできるので、先ほどの問題点は解消されます。

新たな問題点

しかし、ここで新たな問題が発生しました。 それはいかにしてProviderを初期化するかということです。 riverpodのProviderはグローバル変数として定義されます。それゆえViewやViewModelよりもはるかに長いライフサイクルを持ちます。 そのため、以下のような実装ができません。

final popUpViewModelProvider =
StateNotifierProvider<PopUpViewModel, PopUpState>(
        (ref) => PopUpViewModel()); // コンストラクタへtextを渡す必要があるのでコンパイルエラーが出る

また、グローバルなProviderにtextをセットしたとき、他のViewからもtextへアクセスすることが可能になります。 値を他のViewに見せたくないときにこれだと困ってしまいます。

解決策

困って色々ググっていたら、以下のriverpodの作者のツイートを見つけました。

どうやらScopeedProviderを使うことで動的にProviderを初期化できるようです。 さらに、providerへアクセスできる範囲を制限できるので、見せたくないViewからtextを参照することができなくなります。

ScopedProviderを使うように修正

修正の流れは以下のようになります。

  1. StateNotiferをfamily修飾子を使用して作成する
  2. 未実装のScopedProviderを作成する
  3. PopUpPageNavigator.pushするとき、ProviderScopePopUpPageを包む
  4. ProviderScope内でScopedProviderの実装をオーバーライドする

1. StateNotiferをfamily修飾子を使用して作成する

familyを使って以下のようにstateNotifierProviderを作成します。 こうすることでStateNotifierProvider作成時にtextをコンストラクタに渡すことができるようになります。

final popUpViewModelFamily =
    StateNotifierProvider.family<PopUpViewModel, PopUpState, String>(
        (ref, text) => PopUpViewModel(text: text));

2. 未実装のScopedProviderを作成する

以下のように未実装のScopedProviderを作成します。

final popUpViewModelProvider =
    ScopedProvider<StateNotifierProvider<PopUpViewModel, PopUpState>>(
        (ref) => throw Error());

ここでエラーを投げるようにするのは、オーバーライドされていないpopUpViewModelProviderが思わぬ使い方をされないようにするためです。

APIリファレンスでもエラーを投げるよう実装することが推奨されていました。

Note: We made our ScopedProvider throw by default, as our list items by design requires an index to be specified. Another possibility would be to return null and have the item handle the null scenario.

pub.dev

3. PopUpPageNavigator.pushするとき、ProviderScopePopUpPageを包む

次にPopUpPageにstaticなMaterialPageRouteを作成するメソッドを生やしていきます。

  static MaterialPageRoute<PopUpPage> createPageRoute(String text) {
    return MaterialPageRoute(builder: (BuildContext context) {
      return ProviderScope(
        child: PopUpPage(),
      );
    });
  }

4. ProviderScope内でScopedProviderの実装をオーバーライドする

3で生やしたメソッド内で、overrideAsを使ってpopUpViewModelProviderをオーバーライドします。

  static MaterialPageRoute<PopUpPage> createPageRoute(String text) {
    return MaterialPageRoute(builder: (BuildContext context) {
      return ProviderScope(
        overrides: [
          popUpViewModelProvider
              .overrideAs((watch) => popUpViewModelFamily(text)),
        ],
        child: PopUpPage(),
      );
    });
  }

以上で実装は完了です。 あとは、HomePage側で以下のようにPopUpPageNavigator.pushすれば期待通りに動作します。

final text = context.read(homeViewModelProvider).text;
Navigator.push(context, PopUpPage.createPageRoute(text));

おわりに

今回の記事では、riverpodを使ってMVVMな実装をしているときに、画面遷移時にViewModelへ値をいい感じに渡す方法について紹介しました。 最初は少しトリッキーな実装のように感じていましたが、いろいろな課題点をクリアして、スッキリ実装できるriverpodの柔軟さに改めて驚きました。

それではよい開発ライフを!

Flutterの文脈におけるアーキテクチャと状態管理のパターンと状態管理の手法の違い

こんにちは、とりかつ(@torikatsu923)です。 Flutterは状態管理の手法が数多くあり、状態管理の手法は日々進化しています。 それゆえ、最新の情報に敏感になる必要があり、私は日々色々な記事を読み漁っています。

そんな中でふと、Flutterの状態管理周りの記事は、アーキテクチャと状態管理のパターンと状態管理の手法が一緒くたに論じられることが多いと思いました。 そこで今回の記事では、Flutterという文脈でアーキテクチャと状態管理のパターンと状態管理の手法の違いについて整理していこうと思います。

アーキテクチャと状態管理のパターンと状態管理の手法の違い

私は違いについて以下のように考えています。

用語 説明
アーキテクチャ アプリケーションのプログラム構造
状態管理のパターン アプリケーションの状態を管理するためのプログラム構造
状態管理の手法 状態管理のパターンを実現するための実装手段

これらは階層構造になっています。と言ってもピンと来づらいのでで図にしてみました。

f:id:torikatsu923:20210420233213p:plain
図解

アーキテクチャにはさまざまなものがあります。 フロントエンドのアプリケーションにおいて、もっとも関心が高いことは、複雑なアプリケーションの状態をどのように管理するかということです。 Flutterはフロントエンドのフレームワークなので、Flutterという文脈では、アーキテクチャ=状態管理のパターンと捉えることができます。 この切り口で線引きをすると、MVVM、MVC、Flux等が該当するアーキテクチャになります。

そして、MVVMやMVC、Fluxのようなフロントエンドのアーキテクチャを実現するための手段としてriverpodやbloc、fluxパッケージがあると考えることができます。

おわりに

短いですが、Flutterの文脈におけるアーキテクチャと状態管理のパターンと状態管理の手法の違いについて整理してみました。 この記事が状態管理の記事を読み解く手助けになれば幸いです。

本記事は個人的な考え方による部分が強いです。その点についてご了承ください。 記事の質を向上させ、正しい知見を広めたいと考えているので、もしご意見等あればコメントいただけると幸いです。

riverpodでStateNotifierの取得方法が変わった件について

はじめに

こんにちは、とりかつ(@torikatsu923)です。

私はStateNotifier+Riverpod+Freezedを使用してMVVMライクにFlutterアプリを開発しています。 4月の頭にriverpodが0.13.1+1から0.14.0にアップデートされたのですが、このアップデートでは破壊的変更が入っていました。

この破壊的変更に気づかず、いつものようにRiverpodのproviderからStateNotifierを取得しようとしたところ、うまく取得できずに沼にハマりました。

この記事を執筆したのは4/19日ですが、どうやら公式docはこの破壊的変更に対応していないようです。 なので、今回はバージョンアップに伴う仕様変更の一部について解説をしたいと思います。

手っ取り早く確認したい方はpub devのchangelogを参照してください。

はまったこと

ここではカウンターアプリを例として取り上げます。

初めに以下のようなCounterControllerというコントローラがあるとします。 このコントローラはカウンターのカウント数の状態と、カウントを一個増やすincrementというメソッドを持っています。

counter_controller.dart

class CounterController extends StateNotifier<int> {
  Counter(): super(0);

  /// カウントを一個増やす
  void increment() => state++;
}

final counterProvider = StateNotifierProvider((ref) => Counter());

UI側でボタンのコールバックにCounterControllerincrementを紐付けようと以下のコードを書きました。

app.dart

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final CounterController counter = context.read(counterProvider);
  
    return RaisedButton(
      onPressed: counter.increment,
      child: Text('increment'),
    );
  }
}

一見問題なさそうなコードに見えます。 いままでならこれで問題なく動いたのですが、v0.14.0の破壊的変更によってこの方法は使えなくなりました。

まず、context.readの戻り値がdynamicになってしまうのでapp.dartの以下の行がコンパイルエラーになってしまいます。

    final CounterController counter = context.read(counterProvider);// return dynamic

これを解決するためにStateNotifierProviderに型パラメータを設定する必要があります。 1個目の型パラメータにはStateNotifierの型を、2個目の型パラメータにはStateNotifierが保持する型を持たせる必要があります。 修正後のCounterControllerは以下のようになります。

counter_controller.dart

class CounterController extends StateNotifier<int> {
  Counter(): super(0);

  /// カウントを一個増やす
  void increment() => state++;
}

/// 型パラメータの追加
final counterProvider = StateNotifierProvider<CounterController, int>((ref) => Counter());

型パラメータを追加することでcontext.readの戻り値がdynamicではなくなりました。 しかし、まだコンパイルエラーは消えません。 app.dartの以下のコードではCounterControllerを取得したいのですが、context.readの戻り値はintになってしまいます。

    final CounterController counter = context.read(counterProvider);// return int type

これではincrementへアクセスすることができません。 これも破壊的変更の影響です。 stateではなくCounterController(StateNotifier)へアクセスしたい場合はnotifierを追加する必要があります。 以下は修正後のコードになります。

    final CounterController counter = context.read(counterProvider.notifier);// return CounterController type

counterProviderのすぐ後ろにnotifierがついていることがわかると思います。

これで、無事にボタンとCounterController.incrementを紐づけることができました!

おわりに

今回はriverpodの破壊的変更について紹介しました。 もし間違っている部分があればご連絡いただければ幸いです。

flutterはとても便利なフレームワークですが、新しい分枯れたライブラリが少ないです。 そのため日々、変更を追っていかないと一瞬で置いてけぼりにされてしまうので、注意をしなければと改めて思いました。

それでは、よい開発ライフを!

flutter+Stripeでクレジットカード決済をさくっと作る

こんにちは、とりかつ@torikatsu923です。 今回はFlutter+Stripeでサクッとクレジットカード決済を実装する方法を紹介したいと思います

はじめに

個人開発をしていると、自身のプロダクトを収益化したい欲に駆られることがあります。 お金を扱う関係上、決済システムには高い厳密さとセキュリティ要件が求められます。 それゆえ、決済システムを自前で実装しようとした場合、決済の導入はハードルが高いものとなります。


世の中には決済システムを提供してくれるサービス(PaaS)が存在します。 PaaSを使用することで、プロダクトへ決済を導入するハードルを大きく下げることができます。 今回の記事では、Stripeを使用してFlutter製アプリに決済を導入する方法を紹介していきます。 本記事ではわかりそうでわからない、でもわかった気になれるぐらいにざっくりと解説をしていきます。 正確な解説を求める方は以下の公式ドキュメントを参照することをお勧めします。 stripe.com

リポジトリ

サンプルは以下のリポジトリです。 github.com

Stripeの決済の流れ

Stripeの登場人物は以下の3つです。

payment intent

とても雑に理解すると、様々な情報を含んだ決済のセッションIDのようなやつです。

payment method

クレジットカードや銀行振り込みなど具体的な支払い方法です。

confirm payment

payment intentpayment methodを用いて決済を確定させます。

支払い方法をとてもざっくり捉えると、支払いのセッションID(payment intent)と支払い方法(payment method)を用いて支払いを確定(payemnt confirm)させます。 https://stripe.com/img/docs/payments/accept-a-payment-web.png stripe.com

Flutterでの実装

FlutterでStripeの決済を実装する際は以下のコミュニティ公式パッケージを使用します。 pub.dev

Stripe SDKの初期化

初めにStripeの公開可能キーを使用してStripeのSDKを初期化します。

ここではStripeがテスト用に用意している以下の公開可能キーを使用します。

pk_test_51IflCeLOGgN8A203ILklq6uYJPxz2bB2gH1IavQ9C1SEg9sU1cCCYJRlJzt3ZbIF6jJ6zvFwebYNvHvwz4BZVOs400iX7GJNBn
const publishableKey =
        'pk_test_51IflCeLOGgN8A203ILklq6uYJPxz2bB2gH1IavQ9C1SEg9sU1cCCYJRlJzt3ZbIF6jJ6zvFwebYNvHvwz4BZVOs400iX7GJNBn';
    StripePayment.setOptions(
      StripeOptions(
        publishableKey: publishableKey,
        merchantId: 'Test',
        androidPayMode: 'test',
      ),
    );

payment intentの取得

まず、https://api.stripe.com/v1/payment_intentsへpostし、payment intentを取得します。 payment intentの取得にはheaderにStripeのシークレットキーを含める必要があります。 今回はテスト用に用意された以下のシークレットキーを使用します。

sk_test_51IflCeLOGgN8A203RTe6H6aYhGl5drdcPpZJ9B936U3QRHCDuUUWtjQdi4Kud3HWXXpg3YJjdRrVpNem9aNYOacr00uWaWRM6p

以下はpostしてpayment intentを取得するコードです。

    final paymentEndpoint = Uri.https('api.stripe.com', 'v1/payment_intents');
    const secretKey =
        'sk_test_51IflCeLOGgN8A203RTe6H6aYhGl5drdcPpZJ9B936U3QRHCDuUUWtjQdi4Kud3HWXXpg3YJjdRrVpNem9aNYOacr00uWaWRM6p';

    final headers = <String, String>{
      'Authorization': 'Bearer $secretKey',
      'Content-Type': 'application/x-www-form-urlencoded',
    };

    final body = <String, dynamic>{
      'amount': '2000',
      'currency': 'jpy',
      'payment_method_types[]': 'card',
    };

    final response = await http.post(
      paymentEndpoint,
      headers: headers,
      body: body,
    );

    final paymentIntent = jsonDecode(response.body);

ここでハマったのが、Content-Typeapplication/jsonではなく、application/x-www-form-urlencodedだということです。 私はjsonと勘違いして時間を溶かしまくりました() jsonではないのでpayment_method_typespayment_method_types[]になることに注意です。

payment methodの作成

payment methodを作成する際は、Stripe SDKに用意されたメソッドを使用します。

その場でクレジットカード情報を入力してpayment methodを作成する場合はStripePayment.paymentRequestWithCardFormを、既存のクレジットカード情報(CreditCardクラス)を利用する場合はStripePayment..createPaymentMethodを使用します。

StripePayemnt.paymentRequestWithCardFormを利用するといい感じのクレジットカード情報入力画面を表示してくれます。

f:id:torikatsu923:20210417022857p:plain
いい感じのクレジットカード情報入力画面

以下はいい感じのクレジットカード情報入力画面を表示しつつpayment methodを作成するコードです。

final paymentMethod = await StripePayment.paymentRequestWithCardForm(
  CardFormPaymentRequest(),
);

以下は既存のクレジットカード情報を利用してpayment methodを作成するコードです。

final paymentMethod = await StripePayment.createPaymentMethod(
      PaymentMethodRequest(card: creditCard),
);

必要に応じて使い分けてください。

決済の確定

決済を確定させるにはStripePayment.confirmPaymentを使用します。 以下はそのコードです。

final confirmResult = await StripePayment.confirmPaymentIntent(
  PaymentIntent(
    clientSecret: paymentIntent['client_secret'],
    paymentMethodId: paymentMethod.id,
  ),
);

決済周りの処理は以上になります。 あとはエラーハンドリングや決済中にローディング画面を表示したり、決済結果を知らせるスナックバーの表示をしたりして完成になります。

おわりに

今回の記事ではFlutterにStripeの決済を導入する方法を紹介しました。 シンプルな形のものしか実装していませんが、数ステップで安心に決済を導入できるPaaSはすごいなと改めて思いました。

それでは良い開発ライフを

Flutterのネイティブ広告をKotlinで実装する

はじめに

先日、Google Mobile Adsのflutterプラグインが発表されました。 developers.google.cn

先祖のプラグインにあたるAdmobは非常に使いづらかった印象だったので、ついにきたか!と非常にわくわくしました。 早速実装をしようと以下の公式docを参照してみたのですが、ネイティブ広告のAndroidのサンプルコードがjavaでした。

私が開発中のプロダクトはKotlinを採用しているので、公式サンプルをKotlinに読み替えつつ、ネイティブ広告の実装方法を共有したいと思います。

サンプル

今回紹介するサンプルは以下のリポジトリです。 https://github.com/torikatsupg/googleads_mobile_sdk_sample

ネイティブ広告以外の広告のサンプルもあります。もし何かの参考になれば幸いです。

実装

下準備

依存関係の追加

まずはパッケージの依存関係をpubspec.yamlに追加します。 pub.devのinstallingを参考に、適宜バージョンを読み替えてください。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
+  google_mobile_ads: ^0.11.0+1

Admob IDの追加

次にAdmobのIDをAndroidManifest.xmlに追加する必要があります。

今回はサンプルの実装なので以下のIDを使用します。

ca-app-pub-3940256099942544~3347511713 

適宜各自のIDに読み替えてください。

android/main/src/AndroidManifest.xml

<manifest>
    <application>

        .........

+        <meta-data
+            android:name="com.google.android.gms.ads.APPLICATION_ID"
+            android:value="ca-app-pub-3940256099942544~3347511713 "/>
    <application>
<manifest>

SDKの初期化

最後にアプリ起動時にSDKの初期化をするようにします。 SDKの初期化は以下によって行います。

MobileAds.instance.initialize();

公式docによると初期化のタイミングはアプリ起動前がいいそうです。 今回は、run()で初期化します。

run.dart

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:googleads_mobile_sdk_sample/app.dart';

run() {
  WidgetsFlutterBinding.ensureInitialized();
+  MobileAds.instance.initialize();

  return runApp(
    ProviderScope(
      child: App(),
    ),
  );

ネイティブ広告(Android)を実装する

ネイティブ広告の実装はざっくりとFlutter側の実装とネイティブ側の実装に別れます。

Flutter側の実装

ネイティブ広告は、Flutter側でウィジェットとして扱います。 なので、広告ウィジェットを作成する関数を定義し、UIに広告を組み込みます。

初めにlistenerを作成します。 とりあえず表示したいだけなら、広告の読み込みが完了したときのonAdLoadedと、onAdClosedでのリスナの開放ぐらいを実装しておけばいいと思います。

native_page.dart

  Widget _buildNativeAd() {
    final listener = AdListener(
      onAdLoaded: (Ad ad) => ,
      onAdFailedToLoad: (Ad ad, LoadAdError error) {
        print('Ad failed to load: $error');
      },
      onAdOpened: (Ad ad) => print('Ad opened.'),
      onAdClosed: (Ad ad) => print('Ad closed.'),
      onApplicationExit: (Ad ad) => print('Left application.'),
      onNativeAdClicked: (NativeAd ad) => print('Ad clicked.'),
      onNativeAdImpression: (NativeAd ad) => print('Ad impression.'),
    );

   ......

listenerの作成が終わったら、広告を作成します。 広告の作成には以下の4つを渡してあげる必要があります。

  • 広告ユニットID 今回はサンプルとして用意されたca-app-pub-3940256099942544/2247696110を使用します。
  • このあとAndroid側で登録する任意のFactoryID(文字列)
  • AdRequestインスタンス
  • 先ほどつくったリスナ

実装は以下のようになります。

native_page.dart

  ......

  final myNative = NativeAd(
      adUnitId: 'ca-app-pub-3940256099942544/2247696110',
      factoryId: 'adFactoryExample',
      request: AdRequest(),
      listener: listener,
    );
    myNative.load();
    return AdWidget(ad: myNative);

広告の実装ができたら、広告の読み込みを開始しつつAdWidgetに広告を渡して、広告ウィジェットを作成します。

native_page.dart

  ......

  myNative.load();
  return AdWidget(ad: myNative);
}

Flutter側の実装は以上になります。

ネイティブ側での実装

初めに広告のUIを作成します。 Androidでは、UIをxml形式で記述してきます。 ここでandroid:id="...のように、Viewの要素にIDがいくつか指定されていることがわかります。 あとでこのIDをレイアウトにアクセスするときに使用します。

android/src/main/res/layout/my_native_ad.xml

<com.google.android.gms.ads.formats.UnifiedNativeAdView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/ad_headline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/ad_body"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
</com.google.android.gms.ads.formats.UnifiedNativeAdView>

次にNativeAdFactoryを継承した、広告を作成するクラスを定義します。 今回はNativeAdFactoryExampleというクラス名にします。

android/src/main/kotlin/com/example.googleads_mobile_sdk_sample/NativeAdFactoryExample.kt

class NativeAdFactoryExample(private val layoutInflater: LayoutInflater) : NativeAdFactory {

    override fun createNativeAd(nativeAd: UnifiedNativeAd?,
                                customOptions: MutableMap<String, Any>?): UnifiedNativeAdView {
        val adView = layoutInflater.inflate(R.layout.my_native_ad, null)
                as UnifiedNativeAdView
        val headlineView = adView.findViewById<TextView>(R.id.ad_headline)
        val bodyView = adView.findViewById<TextView>(R.id.ad_body)

        headlineView.text = nativeAd?.headline
        bodyView.text = nativeAd?.body

        adView.setBackgroundColor(Color.YELLOW)

        adView.setNativeAd(nativeAd)
        adView.headlineView = headlineView
        adView.bodyView = bodyView
        return adView
    }
}

ここでやっていることは、レイアウトのxmlを読み込んできて、xmlに定義したID(android:id="...)を頼りに、各コンテンツを埋め込んで広告のUIを作成するといった感じです。

次に、Flutter側で定義したFactoryIDと、先ほど作成したFactoryクラスの紐付けを行います。

android/src/main/kotlin/com/example/googleads_mobile_sdk_sample/MainActivity.kt

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        flutterEngine.plugins.add(GoogleMobileAdsPlugin());
        super.configureFlutterEngine(flutterEngine)

+        GoogleMobileAdsPlugin.registerNativeAdFactory(
+                flutterEngine,
+                "adFactoryExample",
+                NativeAdFactoryExample(layoutInflater)
+        )

    }

```GoogleMobileAdsPlugin.registerNativeAdFactory```の引数に、Flutter側で定義した```adFactoryExample```というFactoryIDと、先ほど作成した```NativeAdFactoryExample```を渡しているのがわかります。

さらに、アプリ終了時に広告の削除処理を行うために```cleanUpFlutterEngine```を以下のようにオーバーライドします。

android/src/main/kotlin/com/example/googleads_mobile_sdk_sample/MainActivity.kt

......

override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
    super.cleanUpFlutterEngine(flutterEngine)
    GoogleMobileAdsPlugin.unregisterNativeAdFactory(
            flutterEngine, "adFactoryExample"
    )
}
これでネイティブ広告を表示できるようになりました!👏

[f:id:torikatsu923:20210413010107p:plain]

## おわりに
今回はKotlinでネイティブ広告を実装する方法を紹介しました。
本当はIOS側の実装をswiftに読み替えて、一緒に紹介する予定でした。しかし、IOS開発やobjective-cに詳しくないために、ランタイムエラーを回避することができず断念しました。
サンプルをみていて気づいたことがありましたらissueをあげていただけると嬉しいです。

それでは良い開発ライフを!

M1 mac の環境構築 その2 ターミナル編

こんにちは、とりかつです。 前回に引き続き、M1 macのターミナルの環境構築を進めていきます。

前回の記事 ↓ torikatsu923.hatenablog.com

ポータビリティをと作業効率のバランスがいい感じになるようにターミナルの環境構築を進めていきます。

prezto

インストール編

まずはzshの設定をいい感じにできるようにしていきます。 今回はpreztoをチョイスしました。 github.com

readmeにしたがって、導入していきます。 すでにzshの設定ファイルが存在するとlnエラーが出てインストールがうまくできません。 なので、あらかじめzshの設定ファイルはどこかに退避させておきます。 必要なファイルを退避させたら残っているファイルを全て消しておきます。

rm .zlogin.org && rm .zlogout.org && rm .zpreztorc && rm .zprofile &&rm .zshenv && rm .zshrc

preztoをクローンしてきます。

git clone --recursive https://github.com/sorin-ionescu/prezto.git "${ZDOTDIR:-$HOME}/.zprezto"

ターミナルで以下を実行します。

setopt EXTENDED_GLOB
for rcfile in "${ZDOTDIR:-$HOME}"/.zprezto/runcoms/^README.md(.N); do
  ln -s "$rcfile" "${ZDOTDIR:-$HOME}/.${rcfile:t}"
done

あらかじめ退避させておいた設定を、作成された.zshrcとかに移して完了です。

テーマの適用

preztoをインストールしたおかげで簡単にテーマを設定することができます。 今回はpureというテーマを設定します。

.zshrcに以下を追記するとpureが適用されます。

autoload -Uz promptinit
prompt pure

適用すると以下のようになります。

f:id:torikatsu923:20210228210422p:plain
pure theme

マルチライナーのおかげで、コマンドが見やすくなります。 さらにディレクトリ表示まわりもすっきりとしていて、作業に集中できそうですね。

設定編

ここではシンタックスハイライトとオートサジェスチョンを設定していきます。

シンタックスハイライト

シンタックスハイライトは、コマンドをいい感じにハイライトしてくれる機能です。

シンタックスハイライトoffの時

f:id:torikatsu923:20210228193155p:plain
syntax off
シンタックスハイライトonの時
f:id:torikatsu923:20210228193245p:plain
syntax on
コマンドが緑色でハイライトされているのがわかります。 また、タイポした時は赤色にハイライトしてくれたりします。
f:id:torikatsu923:20210228193418p:plain
syntax on with illegal command
シンタックスハイライトがあると、視覚的にコマンドの構造を理解することができて、ターミナルの作業効率が上がります。

オートサジェスチョン

オートサジェスチョンはコマンド入力中に、historyから入力候補をうっすら表示してくれる機能です。 コマンド入力中に右キー(arrow_right or C-f)を押すことで、うっすら表示されているコマンドを入力することができます。

オートサジェスチョンがoffのとき

f:id:torikatsu923:20210228193814p:plain
autosuggestion off
オートサジェスチョンがonのとき
f:id:torikatsu923:20210228193912p:plain
autosuggestion on

わざわざgrepする必要がないので思考を邪魔することなくコマンドを入力できて便利ですね。

設定の追記

30行目あたりに以下の2行を追記してシンタックスハイライトとオートサジェスチョンを設定します。

'syntax-highlighting' \
'autosuggestions' \

追加後の.zpreztorc

 32 zstyle ':prezto:load' pmodule \
 33   'environment' \
 34   'terminal' \
 35   'editor' \
 36   'history' \
 37   'directory' \
 38   'spectrum' \
 39   'utility' \
 40   'completion' \
 41   + 'syntax-highlighting' \
 42   + 'autosuggestions' \
 43   'prompt'

'prompt'より前に書かないとうまく動かないみたいな噂を聞いたので、一応'prompt'の前に追加しました。

peco

ターミナルで効率よく作業をしていく上で、強力な履歴検索機能が欲しくなりました。 ツールはいろいろあるのですが、いろいろ調べて行ってpecoを導入することにしました。

pecoってなんぞや

pecoは入力をリストで表示し、選択されたアイテムを返却するという、非常にシンプルコマンドです。 花御さんも、シンプルなものほど応用が効いて強いことを認めています。 https://i0.wp.com/mannga-brothers.com/wp-content/uploads/2019/03/IMG_2696.jpg?resize=634%2C720&ssl=1

インストール

homebrewを使ってインストールします。

brew install peco

設定

pecoの導入に当たって以下の記事を参考にしました。 qiita.com 記事にしたがってpecoの設定を進めていきます。

peco自体の設定

zshrcに以下を追記します。 これはC-Rで履歴をgrepできるようにする設定です。

記事には以下のようにあります。しかし、このままではうまく動いてくれなかったので少しいじります。

function peco-history-selection() {
    BUFFER=`history -n 1 | tac  | awk '!a[$0]++' | peco`
    CURSOR=$#BUFFER
    zle reset-prompt
}

zle -N peco-history-selection
bindkey '^R' peco-history-selection

taccatを逆順にやってくれるコマンドです。 私のPCはOSXなのでtacコマンドは存在しません。

f:id:torikatsu923:20210228204510p:plain
do not exist tac

tacコマンドを実装する方法もあったのですが、少し面倒だったので、今回はtail -rを使ってあげます。

historygrepしてる時のイメージ

f:id:torikatsu923:20210228201003p:plain
grep history with peco

cdrの設定

過去のディレクトリ移動履歴もgrepできたら便利だと感じたのでcdrを設定していきます。

~/.zshrc

if [[ -n $(echo ${^fpath}/chpwd_recent_dirs(N)) && -n $(echo ${^fpath}/cdr(N)) ]]; then
    autoload -Uz chpwd_recent_dirs cdr add-zsh-hook
    add-zsh-hook chpwd chpwd_recent_dirs
    zstyle ':completion:*' recent-dirs-insert both
    zstyle ':chpwd:*' recent-dirs-default true
    zstyle ':chpwd:*' recent-dirs-max 1000
    zstyle ':chpwd:*' recent-dirs-file "$HOME/.cache/chpwd-recent-dirs"
fi

次にpecoの設定を追記します。 記事では以下のようにありますが、これも少しいじります。

function peco-cdr () {
    local selected_dir="$(cdr -l | sed 's/^[0-9]\+ \+//' | peco --prompt="cdr >" --query "$LBUFFER")"
    if [ -n "$selected_dir" ]; then
        BUFFER="cd ${selected_dir}"
        zle accept-line
    fi
}
zle -N peco-cdr
bindkey '^E' peco-cdr

まず、selected_dirの部分ですがこのままだと数字が入力されてうまく機能しません。

f:id:torikatsu923:20210228205157p:plain
select directory in peco
f:id:torikatsu923:20210228205259p:plain
now working peco with cdr

上の画像だとcd ~/desktopとしたいところがcd 1 ~/desktopとなってしまっています。 これを回避するためにawkを使ってディレクトリのパスだけ引っこ抜きます。

- BUFFER="cd ${selected_dir}"
+BUFFER="cd `echo $selected_dir | awk '{print$2}'`"

さらに、returnしたらそのままcdが実行されてしまうのは自分の好みではなかったので、return時にコマンドを入力するよう設定を変更します。

- zle accept-line
+ CURSOR=$#BUFFER
+ zle reset-prompt

また、C-EEmacsの行の最後にカーソルを移動するキーバインドと重なってしまうため、C-Gにリマップします。

最終的に以下のようになりました。

function peco-cdr () {
  local selected_dir="$(cdr -l | sed 's/^[0-9]\+ \+//' | peco --prompt="cdr >" --query "$LBUFFER")"
  if [ -n "$selected_dir" ]; then
    BUFFER="cd `echo $selected_dir | awk '{print$2}'`"
    CURSOR=$#BUFFER
    zle reset-prompt
  fi
}
zle -N peco-cdr
bindkey '^G' peco-cdr

いい感じになりました。

f:id:torikatsu923:20210228205940p:plain
work well in peco
f:id:torikatsu923:20210228205918p:plain
work well

終わりに

ターミナルを快適に使えそうな状態になったので、今回の記事はここまでにします! zshの設定系で``zinitzghq with peco```を考えたのですが、現時点で困ってることがなかったので一旦保留にしました。 どれも便利そうだったので、環境構築が落ち着いてから試してみたいと思います。

次回の記事では、tmuxやvimの設定をしていこうと思います。

M1 mac の環境構築 その1

こんにちは、とりかつです。 最近、intel macからm1 macに乗り換えました!

乗り換えに当たって環境構築をしていたのですが、せっかくなので環境構築の手順をドキュメント代わりに記事にまとめてみようと思います。

環境構築の方針は以下の通りです。

  • 極力apple cilicon nativeで動くものを選択する
  • flutterの開発ができるようになる
  • ポータビリティを高めるため、ミニマルな構成にすることを心がける

今回の記事で、全ての環境は完成していません。 とりあえず現時点で最低限必要なものをインストールしていく形になります。

またなにか環境構築が必要になったタイミングで記事にまとめていこうと思います。

※ m1対応状況は日々更新されています。もし「これ対応がアップデートされたよ」とかあればコメントしていただければ幸いです。

Xcodeのインストール

まずはAppStoreからXcodeをインストールしましょう。 こいつはサイズがクソでかいです。そのため、インストールにはかなりの時間がかかります。 環境構築タスクを並列で処理できるように、最初にXcodeのインストールを行います。

Homebrewのインストール

Homebrewがないと何も始まりません。とりあえず脳死で入れます。

m1 mac発売直後は動かないみたいな話を聞いていたのですが、最近M1対応したという風の噂を聞きました。 brew.sh

homebrewのインストールにはcommand line tools for xcodeが必要だということなので先に入れておきます。

xcode-select --install

ということで以下のページのインストール用のワンライナーをシェルで実行します。 brew.sh

インストールの後半で「/opt/homebrewのパスがとおってねぇぞ」って怒られてしまいました。

f:id:torikatsu923:20210227193004p:plain
homebrewインストール中の出力

なので、~/.zshrcにパスを通します。

$ vim ~/.zshrc
typeset -U path PATH
path=(
    /opt/homebrew/bin(N-/) <-homebrewのパスです
    /usr/local/bin(N-/)
    $path
)

パスの最後に(N-/)とつけています。 これは存在しないディレクトリのパスを展開しないようにしてくれる便利なやつです。

解説はここが結構わかりやすかったです。 qiita.com

どうやら(/)ディレクトリが存在しない時エラーを吐いてくれるやつのようです。 例えばunknown/path配下にディレクトリが存在しない時、unknown/path(/)はエラーを吐きます。

この状態だと対象がシンボリックリンクだった時にエラーを吐かれてしまうので、シンボリックリンクだったときに実体の方を見てくれる-をつけます。

unknown/path(-/)

実際にエラーを吐かれてしまうのは問題があるので、エラーの時にパスの代わりに空文字を展開してくれるNを使います。

unknown/path(N-/)

chromeのインストール

普段私はvimiumというchrome拡張を使用しています。 これは、ブラウザでvimキーバインドによる操作を可能にする拡張機能です。 これを使うことで、キーボードのホームポジションから手を離すことなく快適なブラウジングをすることができます。 chrome.google.com

safariにもvimキーバインドを可能にしてくれる拡張は存在するのですが、chrome拡張のほうが動作の安定感があると私は感じています。 それゆえ、safariよりchromeが使いたいので、homebrewを使ってchromeをインストールします。

brew install google-chrome

nodeのインストール

私はwebアプリケーション開発をメインで行っています。 webアプリケーション開発において、nodejsとの関係は切っても切り離せません。 なので、とりあえずnodeをインストールします。 普通にnodejsを入れてもいいのですが、nodeのバージョンを切り替えたい時があったりするので、バージョン管理ができるようにします。 私がnodeのバージョン管理をする上で、管理ツールの候補に以下2つをあげました。 - nodebrew - nodenv

nodenvはディレクトリ単位でバージョンを管理できるみたいな噂を聞いたので、今回はnodenvを選択します。

brew intall nodenv

そして、パスを通しておきます。initも必要らしいのでeval ~~~も追加しておきます。

# .zshrc
path=(
  ...
  $HOME/.nodenv/bin(N-/)
  ...
)

eval "$(nodenv init -)

nodenvのインストールが終わったら、とりあえずglobalに最新バージョンのnodeを導入します。

nodenv install 15.10.0
nodenv global 15.10.0

f:id:torikatsu923:20210227202301p:plain
node -v

vscodeのインストール

vscodeはinsider版がm1対応しているという話を聞いたことがあるので、insider版をインストールします。

vscode insiders property
Appleシリコンという記述があることがわかります
\

brew install visual-studio-code-insiders

Flutterの開発が快適に行えるよう、インストールが完了したら以下の拡張機能を入れておきます。()

marketplace.visualstudio.com marketplace.visualstudio.com marketplace.visualstudio.com

Android studioのインストール

モバイルアプリの開発をする上でAndroidSturioは欠かせません。 パッケージは通常とbeta、canaryがあります。どれもRosseta2上で動かす必要がありそうです。

brew search android-studio
brew search android-studio

今回は通常盤を入れます。 betaやcanaryの違いについては以下のページを参考にしました。 medium.com

brew install android-studio

インストール後はAndroidStudioを起動して、よしなに設定を進めます。

初期設定がが完了したらとりあえずEmulatorが動くか確認します。

AVDマネージャのエラー

やはり、まだm1チップには対応していないようです。なのでしばらくは実機を使う必要がありそうです。 どうしてもEmulatorを触りたいんだ!って人は以下でm1対応のpreview版が配布されているのでそちらから...

androidstudio.googleblog.com

Flutterの開発ができるよう、以下のプラグインを入れておきます() - Idea Vim - Dart (Flutterのプラグインインストール時に一緒に入れ流ことができます) - Flutter

flutter sdkのインストール

flutterの開発にはsdkが必要です。 公式が丁寧なので、以下にしたがってインストールを進めます。 flutter.dev

まずはインストール先のフォルダを作成します。

mkdir ~/development && cd ~/development

クローンします

git clone https://github.com/flutter/flutter.git -b stable

パスを通します

# ~/.zshrc
path=(
  ...
  $HOME/development/flutter/bin(N-/)
  ...
)

flutter doctorで問題を潰していって完了です。

おわりに

とりあえずFlutterの開発ができそうな感じになったので今回はここまでにします。 次回の記事ではシェルをいい感じに使えるようにしていきます!

vimでアルファベット26文字を最小キーストロークで入力する方法

こんにちは、とりかつです。

みなさんはvimで以下のようにアルファベットを入力したくなったことがありませんでしょうか。ありますよね(圧)

abcdefghijklmnopqrstuvwxyz

ということで本記事ではvimでアルファベットを最小キーストロークで入力していく方法について深めていきます。

本記事ではノーマルモードから始まり、ノーマルモードに終わることを前提とします。

一番シンプルな方法

まずは、普通に打ってみましょう。

iabcdefghijklmnopqrstuvwxyz<Esc>

キーストロークは28です。 ですが、a~zまで間違えずに高速入力するのは至難の技です。 TypoScripterならなおさらです。

<C-a>でアルファベットをインクリメンタルする方法

vimでは以下のコマンドで、アルファベットを<C-a>でインクリメンタルできるようになります。

:set nf=alpha

(アルファベットをインクリメンタルする機会はあまりないうえに、数字をインクリメンタルできるほうがクソ便利なので、.vimrcに書くのはお勧めしないです。)

ということでやってみましょう。 キーストロークの合計は31です。愚直に打ち込むより少し長くなってしまいました。

:set nf=alpha<CR>ia<Esc><fd-61>qqylp<C-A>q24@q

ここで:h setをみてみましょう。

f:id:torikatsu923:20210214220858p:plain
:h set
なんということでしょう!settは省略可能ではありませんか! ということで、先程のキーストロークは以下の30文字になりました。

:se nf=alpha<CR>ia<Esc><fd-61>qqylp<C-A>q24@q

マクロ使わずに<C-a>

発想の転換です。 さっきはマクロを使っていましたが、今度はマクロを使わずに、アルファベットをインクリメンタルする方法をやってみます。

:se nf=alpha<CR>ia<Esc>Y25pVGg<C-A>k26gJ

キーストロークは27になりました! アルファベット直打ちに近づいてきました。

g??入力

今度は、g??を使うやり方をやってみます。 g??はRot13変換をしてくれるコマンドです。

詳しくは以下のヘルプを...

:h g??

rot13変換は以下のように、アルファベットを13個後ろのアルファベットに置き換える暗号化方式です。 https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/ROT13.png/640px-ROT13.png

a~mはn~zに変換されます。 これを利用すれば入力回数を減らせそうです。

iabcdefghijklm<Esc>y0g??P

キーストロークは21です。 ついに直打ちよりも少ないキーストロークで入力できるようになりました! さりげなく、rot13を使ってアルファベットを入力できたら、合コンでモテモテ間違いなしですね!

コピペしちゃう

最後の方法はa~zをコピペしちゃう方法ですw

手元でvimを開いている方は以下のヘルプを表示してみてください。

:h <_

...

...

...

f:id:torikatsu923:20210214222936p:plain
:h <_

なんということでしょう!カーソルの2行下にa~zの文字列が存在するではありませんか!

ということで、コピペ戦略を実践してみます。

:h<_<CR>jjYZZP

なんと!!わずか11回の入力でa~zの文字列を入力することに成功しました! タイポの心配もなく、安定してa~zを入力できる最高の方法ですねw

おわりに

今回の記事ではvimでa~zを入力する方法をいくつか紹介してみました。その中で最小キーストロークは11でした。

最後のコピペの方法を初めて知った時、「そんなのありかよwww」ってなりました。 目から鱗とはまさにこのことですね。

もしかしたら、もっと早い入力方法があるかもしれませんね。

それではよいvimライフを!

macでkindleで本読んでみた感想

こんにちは、とりかつです。 私は普段紙の本派だったのですが、たまたまkindleを使う機会がありました。

そこで、kindle使ってみて感じたこととか書いていこうと思います

mackindleアプリは...

 私はmackindleアプリを使って本を読んでいるのですが、この「kindle for mac」が激重です。 どれぐらい重いかというと、ページをめくる際にエンターキーを押してからページが捲られるまでに1秒以上かかります。 また、検索する時は入力が終わってから2~3秒遅れて検索が完了し、検索結果のスクロールは操作してから0.5~2秒ほど遅れます。 快適に本が読めるとは言えません。

アクティビティモニタから検索時のCPU使用率をみてみたところ90%超えでした

f:id:torikatsu923:20210209011724p:plain
Kindleで検索した時のCPU使用率

また、ページをめくった時のCPU使用率は60%を超えていました。

f:id:torikatsu923:20210209011921p:plain
ページをめくった時のCPU使用率

この原因としてmacのOSがあるかもしれません。 私はBigSureを使っているため、もしかしたらまだ未対応な可能性もあります。 (BigSure以外でKindleの動作を試していないので、憶測です。) 今後のアップデートによる改善を期待します。

もしかしたら、kindle for macが遅いのは、amazonさんの「kindle端末買えや」っていう圧力かもしれませんねw

レイアウトが変わる?

どうやら、本によってはページという概念がないものもあるようです。 上手い例えではありませんが、本全体が一つのマークダウンで記述されているイメージです。 それゆえ、ページのレイアウト(テキストが記述されている位置)がページを開きなおすたびに変わります。

私は、紙の本に慣れていて、本を読む時は「本の中盤あたりの右下ぐらいに書いてあったなー」といった感じで、テキストが記述されている位置を、記憶を引っ張り出してくるときのインデックスにするようにする癖があります。 それゆえ、テキストが記述された位置が変更されるという仕様は、紙の本を読む時と違う頭の使い方をしなければならないので少し負担になると感じました。

また、Kindleに限った話ではありませんが、電子書籍では指でページ感を掴むこともできないのでその辺の再適応みたいなステップも必要かなと感じました。

amazon primeの恩恵

ここまでの感想は、Kindleのネガティブな面によってしまいました。しかし、Kindleにもいいところはたくさんあります。 その一つにamazon primeによって読める本が存在することが挙げられます。

しばしば、本の選択は出会いのように語られることがあります。 「書店でぶらぶら歩いていて、たまたま目に止まった本を取り、購入し...」といった感じです。 従来の電子書籍は、目的の本があり、その本のタイトルで検索して購入するといったイメージで、目的の本以外に触れる機会がないように感じていました。 しかし、amazon primeに入っているとprimeによって読める本が常に入れ替わります。 そして、リコメンド欄に表示されます。

この仕組みのおかげで、電子書籍でありながら「本との出会い」という体験を得ることができます。 この点において、kindleは素晴らしいと感じました。

kindle unlimitedはprimeに比べて、より多くの本を読むことができるため、加入すればこの体験のクオリティはより上がると思われます。

おわりに

今回の記事ではKindleを使ってみて感じたこととかをつらつら書いてみました。 Kindleがいいとか紙がいいとかではなく、それぞれに良いところと悪いところがあって、うまく使い分けていくべきだと改めて感じました。