【Flutter】ListViewで子要素がListViewの内側に表示されたか検知する
こんにちは、とりかつ(https://twitter.com/torikatsu923)です。 いきなりですが、FlutterにはListViewという大変便利なWidgetがあります。
ListView+ListTileを使うことで数分で以下のようなUIを作ることができます。
このような画面をさくっと実装できるのはFlutterのとてもいいところだと思います。
最近、ListViewの要素が画面内に表示されたかどうか知りたいケースがありました。 そこで今回の記事は、ListViewの要素がListViewの内側に表示されたことを検知する方法について紹介します。
今回紹介する方法を使うと、ListViewの内側に表示されている間だけ色を変えるみたいな実装も実現可能です。
コード
今回の記事で紹介するコードは以下のリポジトリです。 github.com
方針
ListViewの要素がListView内に表示されたかどうかは、ListViewの表示領域とListViewの要素の座標を比較することで判断することができます。 図にすると以下のようになります。 要素がListViewの内側にあるとき、要素の下端のy座標はListViewの下端のy座標よりも小さくなります。 逆に要素がListViewの外側にあるとき、要素の下端のy座標はListViewの下端のy座標よりも大きくなります。
これをまとめると、以下のようになります。
要素がListViewの内側にあるときの条件: `要素の下端のy座標 <= ListViewの下端のy座標` 要素がListViewの外側にある時の条件 : `要素の下端のy座標 > ListViewの下端のy座標`
この判定をListViewのスクロールイベントが発生するごとに行います。
そうすることで、ListViewの要素がListViewの内側に完全に入ったことを検知することができます。
実際にこれを実装していきます。実装手順は以下の通りです。
- ListViewの要素の下端の座標を取得する
- ListViewの要素で、ListViewの表示領域の下端の座標を取得する
- 1、2で取得した座標を使って、要素がListViewの領域内に表示されているかどうかの判定を行う
- 3の判定をスクロールイベントが発生するたびに行う
1. ListViewの要素の下端の座標を取得する
まずはListViewの要素の下端の座標を取得します。下端の座標はRenderObjectから取得します。 FlutterはWidget、Element、RenderObjectの3つのツリーでUIを構築しているのですが、RenderObjectは実際に描画された時のサイズや座標を持っています。
任意のWidgetに対応するcontext経由で以下のように取得することができます。 とってくるRenderObjectの実体はRenderBoxなので、取得するついでにキャストしています。
visibility_detector_list_item.dart
final renderBox = context.findRenderObject() as RenderBox?;
次にRenderBoxからグローバル座標を取得します。
グローバル座標の取得にはRenderBoxのlocalToGlobal()
を使用します。
今回は単にグローバル座標を取得したいだけなので、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)
を使って取得したグローバル座標は、ウィジェットの左上の座標になります。
以下の図のようなイメージです。
ウィジェットの再下端の座標は、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にアクセスできないので、didChangeDependencies
でWidgetBinding.instance?.addPostFrameCallback()
を使ってリスナを設定しています。
また、リスナを開放ことも非常に重要です。
ListViewは画面内に入りそうな要素を必要に応じて作成・破棄します。なので、画面がスクロールされると要素が破棄されることがあります。
要素が破棄された場合、onScrollは存在しなくなります。
リスナを解放しないとスクロールイベントが発生した時、存在しないonScroll()を呼び出そうとしてメモリリークが起きます。
そのため、dispose
で確実にScrollController.removeListener()
してあげます。
実装は以上になります!これで以下のように画面内に表示されたことを検知できるようになりました!
おわりに
今回はグローバル座標を基準に、ListViewの要素がListView内に表示されたかどうかを検知する方法を紹介しました。
localToGlobal()
にListViewのRenderObjectを渡すことで、ListViewとListViewの要素の相対的な座標で判定を行うこともできそうです。
ListViewで子要素がListViewの内側に表示されたか検知する方法に番兵を用いる方法もありました。 qiita.com
番兵を用いる方法は非常にシンプルでとっつきやすいです。 ですが、管理するWidgetの数が増えてしまいます。また、番兵を用いるとbuildが呼ばれたタイミングで初めてListViewで子要素がListViewの内側に表示されたかを検知することができるのですが、Widgetが破棄されていない等の理由で微妙に扱いづらい部分があります。 その点今回の記事で紹介した方法は汎用性がかなり高いため、安定した動作を求めるならこの記事で紹介した実装方法がおすすめです!
【Flutter】いい感じのローディング画面を表示したい!
こんにちは、とりかつ(@torikatsu923)です。 よくアプリでこんな感じのローディング画面を見かけると思います。
アプリ起動時にただ真ん中でぐるぐるするのを見ているより、いい感じのローディング画面が表示されていたら、きっとユーザは退屈しなくて済むと思います。
このいい感じのエフェクトのことをShimmerと呼ぶそうです。 そこで、今回の記事はShimmerローディング画面を実装する方法を紹介していきます。
リポジトリは以下になります。 github.com
実装の流れ
UIのスケルトンをつくる
まず、アニメーションとかが無いシンプルなローディング画面を作ってみます。 今回は以下のような画面を作っていきます。
Row,Column,Containerを組み合わせて、以下のようになりました。 わかりやすいよう、あえて黄色を設定しています。
アウトラインはこのようになっています。
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.createShader
でShader
を作成してあげるのですが、この作り方が少し特殊になります。
今回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)
をすると以下のようになります。
黄色で塗りつぶされていたところに灰色のグラデーションがかかっていることがわかります。 ここまでの実装は以下になります。 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のとき |
---|---|
渡した値が大きくなるにつれて、グラデーションの色が明るいところが右へ移動していることがわかります。
ここにAnimationController
を渡すといい感じのローディング画面ができ上がります。
今回のAnimationController
の実装は以下のようになっています。
_controller = AnimationController.unbounded(vsync: this) ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1500)) ..addListener(() { setState(() {}); });
これを設定すると以下のようなアニメーションができました!
あとはグラデーションの傾きをいい感じに調整するとこの記事の冒頭のようなShimmer効果が出来上がります。
おわりに
今回はShimmer効果の実装方法を紹介しました。 Shimmerは効果をつけたいときは以下のような公開されているパッケージを使う方法もあります。
Shimmerの実装量は100行程度なので、パッケージに頼らずに自前で実装してしまうのも多いにアリですね!
それではよいFlutterライフを!
【Flutter】riverpod 1.0.0-dev.0の変更点まとめ
こんにちは、とりかつ((@torikatsu923)https://twitter.com/torikatsu923)です。
今朝、riverpodの1.0.0-dev.0が公開されました!
I have published a dev-release of Riverpod v1:
— Remi Rousselet (@remi_rousselet) 2021年6月20日
riverpod 1.0.0-dev.0
Go give it a try 🎉https://t.co/OWGQ6evu4O
以前記事で触れましたが、4月の半ばごろにriverpodの作者の方からriverpodに破壊的変更が入るかもしれないというアナウンスがありました。 torikatsu923.hatenablog.com
Changelogを確認したところ、今回のアップデートはそのアナウンスを受けてのものと考えて良さそうです。 個人的に欲しかった機能がつまっているアップデートなので、使うのがとても楽しみです!
今回のアップデートは変更点が20個程含まれており、破壊的変更も多く含まれています。 pub.dev
そこで今回の記事ではriverpodの変更点についてまとめていきます。
- アップデートの方針の考察
- 変更点一覧
- useProviderの削除
- ConsumerWidgetのbuildメソッドでWidget refを受け取るようにする変更
- ProviderListenerを非推奨にする変更
- ConsumerStatefulWidget, ConsumerStateの追加
- watchでselectでstateの特定の値の変更のみ検知できるようにする変更
- overrideWithValueの削除
- ProviderObserverが変更前と変更後の2種類の状態を受け取れるようにする変更
- ProviderObserver.mayHaveChangedの削除
- ref.listenの追加
- ProviderReferenceを非推奨にする変更
- 全てのProviderがProviderRefBaseをパラメータとして受け取るようにする変更
- ProviderReference.mountedの削除
- ProviderContainerのdebugproviderValuesとdebugProviderElementsの削除
- StreamProviderのlast, stream, futureがプロバイダーの再構築の回数に依存しないようにする変更
- Providerの値が同じ時、Providerをオーバーライドできるようにする変更
- ref.refreshの呼び方変更
- ref.onDisposeのタイミング変更
- Providerの状態の再計算のタイミング変更
- ProviderContainer.pumpの追加
- familyとdisposeを併用した時に状態が残る問題の修正
- おわりに
アップデートの方針の考察
今回のアップデートのキーは「Providerへアクセスする方法を統一する」だと思われます。 初めてriverpodを触る時、Providerをreadする方法の複雑さにウッとなる方も多いと思います。
以前の作者さんの(アナウンス)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を非推奨にする変更
WidgetRef
にlisten
という状態の変更を監視する関数ができました。その関係で、今まで状態の変更を監視する際に使用されていた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のフィールドにusername
とstatus
があるとします。
以下のコードでは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チェックをすることがあると思います。
一見正しそうに見えますが、実際にやってみるとコンパイルエラーになります。
エラーの内容は以下の通りです。
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;
のようにしてみます。
すると、、、
エラーが消えました!!🎉🎉
Tips2 !
を使って握り潰す
どこからどうみてもnullでない値なのにnullの可能性があるなんて言われた時には!
(bang operator)を使用することもできます。
以下のようにコードを変更してみると、、、
エラーが消えました!!🎉🎉
実はこの方法、一つ注意することがあります。
MyInteger
のコードの例ではvalue
はfinal
のため変更されることがないので問題が起きません。
しかし、value
がfinal
出ない時は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)が表示されて、データの取得が終わると動画一覧が表示されます。
大したことなさそうに見えます。 とりあえず動くものを作ろうとすると、FutureBuilder使って初期ローディングを行うだけです。
一見問題なさそうに見えますが、MVVM的なアーキテクチャを目指そうとした時、以下のような課題感があると思いました。
- StateNotifierを経由せずにUIの更新を行っている
- UIにエラーハンドリングの詳細ロジックが漏れる
StateNotifierを経由せずにUIの更新を行っている
UIは常にStateNotifierが持っている状態をもとにUIのレンダリングを行うべきです。 もう少し抽象的な言い方をすると、UIは状態を引数に取る関数によって構築されるべきです。 この考えはFlutterのdocsでも述べられています。
上記の例では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重ダウンロードの修正が入ったようです。 この修正を手元のプロジェクトで有効にするためには以下の手順を踏めば良さそうです。
- index.htmlを削除する
flutter create .
を実行する
また、アクセシビリティ周りの機能も充実しました。
dev tool のアップデート
devtoolでProviderを確認できるようになりました🎉
The devtool for Provider is out 🎉 pic.twitter.com/BIL6ERymOP
— Remi Rousselet (@remi_rousselet) 2021年5月19日
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でハンドリングしたいことが結構あります。
dartにはJavaでいうthrowsのような構文がないため、どんなエラーを吐くのかを知りたいときはメソッドの内部使用を見る必要があります。 しかし、これではメソッドを呼び出す側がメソッドがどんな型のエラーを吐くかを知ることになります。 これでは、せっかくレイヤーごとに分離しているのに、レイヤ間が密結合になってしまいます。 また、エラーハンドリング漏れも十分に考えられます。
そこで、Resultを使用する方法が考えられます。 ntaoo.hatenablog.com
Resultは値に対して成功、失敗の文脈を持たせることができるため、エラーハンドリングの漏れを防ぐことができます。
さらに、独自のResultクラスとエラークラスを定義して、各エラーをアプリケーションに依存したエラーへ変換することで、よりすっきりした構成にすることが可能です。 github.com
【riverpod】画面遷移時にStateNotifierへ値を渡す方法
こんにちは、とりかつ(@torikatsu923)です。
普段私はFlutterで開発しており、riveropd+StateNotifier+freezedでMVVMライクに状態管理をしています。 先日、画面遷移を実装する際、画面遷移時にStateNotifierで作ったViewModelのコンストラクタへ渡そうとしてうまく渡せずとても苦戦しました。 そこで、今回の記事では画面遷移時にViewModelへ値を渡す方法について紹介していこうと思います。
リポジトリ
実装はこちらのリポジトリになります。 github.com
作ろうとしているもの
今回は以下のようなアプリを作ろうとしています。
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
を使った初期化処理をコンストラクタで行うことができない
一応動くものを作るときはこれでもいいかもしれません。 しかし、それなりの規模感のものを作ろうとしたとき、このような実装はいつか負の遺産になりえます。
問題点の解決方法
前述の問題点を回避するためには、text
をPopUpViewModel
のコンストラクタで受け取って初期化する必要があります。
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の作者のツイートを見つけました。
If this is about "I don't want to pass the Family key all around the widget tree", a solution is to create an extra provider that exposes the combination of the family + the key
— Remi Rousselet (@remi_rousselet) 2020年7月2日
This can be combined with `ProviderScope.overrides` for fancy stuffhttps://t.co/Dkq7YForRB pic.twitter.com/cqi1HdCLPK
どうやらScopeedProviderを使うことで動的にProviderを初期化できるようです。
さらに、providerへアクセスできる範囲を制限できるので、見せたくないViewからtext
を参照することができなくなります。
ScopedProviderを使うように修正
修正の流れは以下のようになります。
- StateNotiferをfamily修飾子を使用して作成する
- 未実装の
ScopedProvider
を作成する PopUpPage
をNavigator.push
するとき、ProviderScope
でPopUpPage
を包む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.
3. PopUpPage
をNavigator.push
するとき、ProviderScope
でPopUpPage
を包む
次に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
側で以下のようにPopUpPage
をNavigator.push
すれば期待通りに動作します。
final text = context.read(homeViewModelProvider).text; Navigator.push(context, PopUpPage.createPageRoute(text));
おわりに
今回の記事では、riverpodを使ってMVVMな実装をしているときに、画面遷移時にViewModelへ値をいい感じに渡す方法について紹介しました。 最初は少しトリッキーな実装のように感じていましたが、いろいろな課題点をクリアして、スッキリ実装できるriverpodの柔軟さに改めて驚きました。
それではよい開発ライフを!
Flutterの文脈におけるアーキテクチャと状態管理のパターンと状態管理の手法の違い
こんにちは、とりかつ(@torikatsu923)です。 Flutterは状態管理の手法が数多くあり、状態管理の手法は日々進化しています。 それゆえ、最新の情報に敏感になる必要があり、私は日々色々な記事を読み漁っています。
そんな中でふと、Flutterの状態管理周りの記事は、アーキテクチャと状態管理のパターンと状態管理の手法が一緒くたに論じられることが多いと思いました。 そこで今回の記事では、Flutterという文脈でアーキテクチャと状態管理のパターンと状態管理の手法の違いについて整理していこうと思います。
アーキテクチャと状態管理のパターンと状態管理の手法の違い
私は違いについて以下のように考えています。
用語 | 説明 |
---|---|
アーキテクチャ | アプリケーションのプログラム構造 |
状態管理のパターン | アプリケーションの状態を管理するためのプログラム構造 |
状態管理の手法 | 状態管理のパターンを実現するための実装手段 |
これらは階層構造になっています。と言ってもピンと来づらいのでで図にしてみました。
アーキテクチャにはさまざまなものがあります。 フロントエンドのアプリケーションにおいて、もっとも関心が高いことは、複雑なアプリケーションの状態をどのように管理するかということです。 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側でボタンのコールバックにCounterController
のincrement
を紐付けようと以下のコードを書きました。
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 intent
とpayment method
を用いて決済を確定させます。
支払い方法をとてもざっくり捉えると、支払いのセッションID(payment intent
)と支払い方法(payment method
)を用いて支払いを確定(payemnt confirm
)させます。
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-Type
がapplication/json
ではなく、application/x-www-form-urlencoded
だということです。
私はjsonと勘違いして時間を溶かしまくりました()
jsonではないのでpayment_method_types
はpayment_method_types[]
になることに注意です。
payment methodの作成
payment method
を作成する際は、Stripe SDKに用意されたメソッドを使用します。
その場でクレジットカード情報を入力してpayment method
を作成する場合はStripePayment.paymentRequestWithCardForm
を、既存のクレジットカード情報(CreditCardクラス)を利用する場合はStripePayment..createPaymentMethod
を使用します。
StripePayemnt.paymentRequestWithCardForm
を利用するといい感じのクレジットカード情報入力画面を表示してくれます。
以下はいい感じのクレジットカード情報入力画面を表示しつつ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
適用すると以下のようになります。
マルチライナーのおかげで、コマンドが見やすくなります。 さらにディレクトリ表示まわりもすっきりとしていて、作業に集中できそうですね。
設定編
ここではシンタックスハイライトとオートサジェスチョンを設定していきます。
シンタックスハイライト
シンタックスハイライトは、コマンドをいい感じにハイライトしてくれる機能です。
シンタックスハイライトoffの時 シンタックスハイライトonの時 コマンドが緑色でハイライトされているのがわかります。 また、タイポした時は赤色にハイライトしてくれたりします。 シンタックスハイライトがあると、視覚的にコマンドの構造を理解することができて、ターミナルの作業効率が上がります。
オートサジェスチョン
オートサジェスチョンはコマンド入力中に、historyから入力候補をうっすら表示してくれる機能です。 コマンド入力中に右キー(arrow_right or C-f)を押すことで、うっすら表示されているコマンドを入力することができます。
オートサジェスチョンがoffのとき オートサジェスチョンが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は入力をリストで表示し、選択されたアイテムを返却するという、非常にシンプルコマンドです。 花御さんも、シンプルなものほど応用が効いて強いことを認めています。
インストール
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
tac
はcat
を逆順にやってくれるコマンドです。
私のPCはOSXなのでtacコマンドは存在しません。
tac
コマンドを実装する方法もあったのですが、少し面倒だったので、今回はtail -r
を使ってあげます。
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
の部分ですがこのままだと数字が入力されてうまく機能しません。
上の画像だと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-E
はEmacsの行の最後にカーソルを移動するキーバインドと重なってしまうため、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
いい感じになりました。
終わりに
ターミナルを快適に使えそうな状態になったので、今回の記事はここまでにします!
zshの設定系で``zinitや
z、
ghq with peco```を考えたのですが、現時点で困ってることがなかったので一旦保留にしました。
どれも便利そうだったので、環境構築が落ち着いてから試してみたいと思います。
次回の記事では、tmuxやvimの設定をしていこうと思います。
M1 mac の環境構築 その1
こんにちは、とりかつです。 最近、intel macからm1 macに乗り換えました!
my new gear... pic.twitter.com/Fkb6WYRlrC
— Torikatsu (@torikatsu923) 2021年2月26日
乗り換えに当たって環境構築をしていたのですが、せっかくなので環境構築の手順をドキュメント代わりに記事にまとめてみようと思います。
環境構築の方針は以下の通りです。
- 極力apple cilicon nativeで動くものを選択する
- flutterの開発ができるようになる
- ポータビリティを高めるため、ミニマルな構成にすることを心がける
今回の記事で、全ての環境は完成していません。 とりあえず現時点で最低限必要なものをインストールしていく形になります。
またなにか環境構築が必要になったタイミングで記事にまとめていこうと思います。
※ m1対応状況は日々更新されています。もし「これ対応がアップデートされたよ」とかあればコメントしていただければ幸いです。
- Xcodeのインストール
- Homebrewのインストール
- chromeのインストール
- nodeのインストール
- vscodeのインストール
- Android studioのインストール
- flutter sdkのインストール
- おわりに
Xcodeのインストール
まずはAppStoreからXcodeをインストールしましょう。 こいつはサイズがクソでかいです。そのため、インストールにはかなりの時間がかかります。 環境構築タスクを並列で処理できるように、最初にXcodeのインストールを行います。
Homebrewのインストール
Homebrewがないと何も始まりません。とりあえず脳死で入れます。
m1 mac発売直後は動かないみたいな話を聞いていたのですが、最近M1対応したという風の噂を聞きました。 brew.sh
homebrewのインストールにはcommand line tools for xcodeが必要だということなので先に入れておきます。
xcode-select --install
ということで以下のページのインストール用のワンライナーをシェルで実行します。 brew.sh
インストールの後半で「/opt/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
vscodeのインストール
vscodeはinsider版がm1対応しているという話を聞いたことがあるので、insider版をインストールします。
\
brew install visual-studio-code-insiders
Flutterの開発が快適に行えるよう、インストールが完了したら以下の拡張機能を入れておきます。()
marketplace.visualstudio.com marketplace.visualstudio.com marketplace.visualstudio.com
Android studioのインストール
モバイルアプリの開発をする上でAndroidSturioは欠かせません。 パッケージは通常とbeta、canaryがあります。どれもRosseta2上で動かす必要がありそうです。
今回は通常盤を入れます。 betaやcanaryの違いについては以下のページを参考にしました。 medium.com
brew install android-studio
インストール後はAndroidStudioを起動して、よしなに設定を進めます。
初期設定がが完了したらとりあえずEmulatorが動くか確認します。
やはり、まだm1チップには対応していないようです。なのでしばらくは実機を使う必要がありそうです。 どうしてもEmulatorを触りたいんだ!って人は以下でm1対応のpreview版が配布されているのでそちらから...
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
をみてみましょう。
なんということでしょう!set
のt
は省略可能ではありませんか!
ということで、先程のキーストロークは以下の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個後ろのアルファベットに置き換える暗号化方式です。
a~mはn~zに変換されます。 これを利用すれば入力回数を減らせそうです。
iabcdefghijklm<Esc>y0g??P
キーストロークは21です。 ついに直打ちよりも少ないキーストロークで入力できるようになりました! さりげなく、rot13を使ってアルファベットを入力できたら、合コンでモテモテ間違いなしですね!
コピペしちゃう
最後の方法はa~zをコピペしちゃう方法ですw
手元でvimを開いている方は以下のヘルプを表示してみてください。
:h <_
...
...
...
なんということでしょう!カーソルの2行下にa~zの文字列が存在するではありませんか!
ということで、コピペ戦略を実践してみます。
:h<_<CR>jjYZZP
なんと!!わずか11回の入力でa~zの文字列を入力することに成功しました! タイポの心配もなく、安定してa~zを入力できる最高の方法ですねw
おわりに
今回の記事ではvimでa~zを入力する方法をいくつか紹介してみました。その中で最小キーストロークは11でした。
最後のコピペの方法を初めて知った時、「そんなのありかよwww」ってなりました。 目から鱗とはまさにこのことですね。
vimでa~zを最速入力する方法がヘルプからコピペで、そんなのありかよってなった
— Torikatsu (@torikatsu923) 2021年2月14日
もしかしたら、もっと早い入力方法があるかもしれませんね。
それではよいvimライフを!
macでkindleで本読んでみた感想
こんにちは、とりかつです。 私は普段紙の本派だったのですが、たまたまkindleを使う機会がありました。
そこで、kindle使ってみて感じたこととか書いていこうと思います
macのkindleアプリは...
私はmacでkindleアプリを使って本を読んでいるのですが、この「kindle for mac」が激重です。 どれぐらい重いかというと、ページをめくる際にエンターキーを押してからページが捲られるまでに1秒以上かかります。 また、検索する時は入力が終わってから2~3秒遅れて検索が完了し、検索結果のスクロールは操作してから0.5~2秒ほど遅れます。 快適に本が読めるとは言えません。
アクティビティモニタから検索時のCPU使用率をみてみたところ90%超えでした
また、ページをめくった時のCPU使用率は60%を超えていました。
この原因として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がいいとか紙がいいとかではなく、それぞれに良いところと悪いところがあって、うまく使い分けていくべきだと改めて感じました。