torikatsu.dev

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

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がいいとか紙がいいとかではなく、それぞれに良いところと悪いところがあって、うまく使い分けていくべきだと改めて感じました。

Haskellの門を叩いてみた

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

最近、私の中で関数型パラダイムのブームが来ています。 その流れでHaskellを触りました。 そこで、今回の記事はHaskellを触る経緯と、学習していて感動したところについて話したいと思います。

Haskellを触る経緯

冒頭でもお伝えしたように、私の中では関数型パラダイムがブームです。 最近、普段JSとか触ってる自分の耳にも、ReactのFunctionalComopnent最高だぜとかテストしやすいぜとかいう声が聞こえるようになりました。

私は、一度気になったものは、気になって気になってしょうがない性なので、空いてる時間に細々と、関数型パラダイムに入門することにしました。

関数型の入門にあたって、初めはJSとかで関数型の雰囲気を掴もうとしてました。 でも関数型やるならゴリゴリの関数型言語やればいいやんってなって、Haskellの門を叩いてみることにしました。

普段、新しい言語とか触るときは、公式docとかでさらっとやって「完全に理解した」状態にしてました。 Haskellもそうしようと思っていましたが、あえて入門書を買うことにしました。

あえて入門書を買うことにした理由として、関数型パラダイムを基礎から固めたいという思いがあります。 私が今まで触ってきた言語は、java, kotlin, dart,jsあたりで、どれもオブジェクト指向の考え方を持ち越すことができました。 しかし、Haskellは純粋な関数型言語ということでパラダイムが異なります。 思想を基礎から固めるには、しっかりと構造化された情報でインプットを行う必要があると考えられます。 それゆえ、入門書を買うという選択をしました。

幸い、オブジェクト指向とかを触ってきた人向けの「すごい Haskell 楽しく学ぼう!」という本があったのでこの本にしました。

www.amazon.co.jp

まだ全部読み切ってはいないですが、解説がとても丁寧な点と、オブジェクト指向の思想と混乱しそうな箇所がしっかりとフォローされていた点がとても最高な本です。

Haskellで感動したこと

Haskellの感動したところは、数学っぽいことが自然な形でコードに落とし込めるところです。

例えば、10以下の偶数の集合を表現したいときは、以下のようになるかと思います。

{0, 2, 4, 6, 8, 10}

さらに、次のようにも書くことができます。

{ x | x≤10, xは偶数 }

ここで、10以下の偶数のリストをHaskellで作ると、次のようになります。

[ x | x <-[0..11], x `mod` 2 == 0 ]

実際に実行してみると、確かに[0, 2, 4, 6, 8, 10]が出力されます。

f:id:torikatsu923:20210127223508p:plain

集合とリストは異なりますが、集合の表記とコードが非常に似ています。

このように非常に自然な表現でコードに落とし込むことができるところがHaskellのいいところだなと思います。

おわりに

まだ学習し始めたばかりで、Haskellのいいところを全て吸収できたわけではありません。 ですが、これは見方を変えると、入門したてにもかかわらず、いい!って思えるところが見つかるプログラミング言語だといえます。

まだHaskell触ったことないって人は、ぜひHaskellを触ってみてはいかがでしょうか。

Logicool mx ergoを導入してみて

お久しぶりです。 とりかつです。

先日、Logicool mx ergoというトラックボールを導入ました。 今回の記事は、その使用感とデスク環境のアップデート状況について話をしたいと思います。

f:id:torikatsu923:20210124231934j:plain

また、以前、以下の記事で私のデスク環境を紹介させていただきました。 もしよければご覧になってください。 torikatsu923.hatenablog.com

目次

  • logicool mx ergo導入の経緯
  • 実際に使ってみて
  • 置いてみて(見た目)
  • おわりに

logicool mx ergo導入の経緯

私は、ITエンジニアとして働いており、その職の性質上机に向かい続ける時間が長くなる傾向があります。 そこで、少しでもQOLを向上させるために、デスクまわりの環境が快適になるよう様々なアイテムを導入してきました。

私のデスク環境を整えていく方針として、自身の生産性に対して投資をするというものがありました。 また、別の軸として、PC操作をキーボードで完結させるというものもありました。

それゆえ、chromeにvimiumという拡張機能を導入し、マウスレスブラウジングを実現したり、 vimを導入して黒い画面からなんでも操作できるようにするみたいな工夫をしてきました。 chrome.google.com

f:id:torikatsu923:20210124232305p:plain
黒い画面の見た目もいろいろいじりました

それゆえ、トラックボールの導入はPC操作をキーボードで完結させるというポリシー登坂するため、導入について非常に悩みました。 これについて、会社の先輩の先輩に相談したところ、「マウスとキーボードのいいとこ取りして共存するのもいいかも」という話をいただきました。 たしかに、真に求めるべきものは効率であり、先入観や自身の価値観で便利なものを導入しないのことは、かえって作業の効率を落としかねないです。

例えばvimでインサートモードの時に、カーソル以降の文字を全て消したい時は、esc→$→xなんてせずに、インサートモードのままctrl+kした方が効率が...(ry おっと、だれかがきたようだ...

こんな経緯で、トラックボールの導入を決意しました。

一口にトラックボールには様々な種類があります。 その種類の多さゆえに、アイテム選びには非常に迷いました。

そこで、以下の条件で絞り込みをしました。 - 所持しているデスク周りのアイテムと親和性の高いデザイン - Bluetoothで接続先を複数設定できる - 机に対するグリップ力

この条件でいろいろ見ていった結果Logicool mx ergoに辿り着きました。 www.logicool.co.jp お値段は1.4万円ほどで非常にリーズナブルな価格帯です(?)

私にとっての決め手は以下の4つです。

  • 色が黒色で、所持しているHHKBと相性がよかったこと
  • 角度調整が可能なこと
  • Bluetoothで充電回数が極端に少ないこと
  • Logicoolというブランドの信頼性

実際につかってみて

最初は、トラックボール自体、使うのが初めてだったので、新しい操作にかなり戸惑いました。 しかし、2、3日触っていたらかなり手に馴染みました。

トラックボールの設定で、スクロール速度を最高にしました。 さらに、Mac本体の設定でポインタの移動速度を最速にしました。

f:id:torikatsu923:20210124234604p:plain
スクロール速度の設定

その結果爆速ポインタ移動が実現しました! ディスプレイの移動も親指で少し回すだけで端から端まで移動できちゃいます! 最初は早すぎて「使えるか!」とか思ってましたが、今ではMacトラックパッドのポインタ移動がとても遅く感じてしまう体になってしまいました。 また、トラックボールの滑り具合やボタンのクリックした時のフィードバックはとても快適です。

mx ergoを購入する前の懸念点として、机に対するグリップ力がありました。 折角性能のいいトラックボールでも、固定しずらければ、デスクのフォームが崩れたりかえって疲れてしまったりする原因になります。 ですが、この心配は取り越し苦労でした。 mx ergoの底面にはゴムがついており、このグリップ力が高いおかげで、ズレとは無関係の、快適な操作を提供してくれました!

mx ergoのすごいところは他にもあります。 それは角度調整が可能だというところです。 mx ergoは本体の角度を、0度と20度の2種類に調整できます。

f:id:torikatsu923:20210124235656p:plain
https://www.logicool.co.jp/ja-jp/products/mice/mx-ergo-wireless-trackball-mouse.910-005183.html?crid=7より
f:id:torikatsu923:20210124235704p:plain
https://www.logicool.co.jp/ja-jp/products/mice/mx-ergo-wireless-trackball-mouse.910-005183.html?crid=7より

この20度という角度がとても絶妙な角度なのです。 絶妙であると考えられる理由として、鉛筆の持ち方との共通点が考えられます。

トンボ鉛筆によると正しい鉛筆の持ち方の定義について以下のようにあります。

「5本の指の関節(15箇所)と、手首の関節を自由に屈伸、屈折することができ、手と腕に余計な力が動くことなく、筆記具を自在に動かしてあらゆる基本線を思うように書ける持ち方」と定義されています。 tombow-ippo.jp https://tombow-ippo.jp/assets/img/howto/point03.png

つまり、私たちの手が最もリラックスした状態が20度という角度なのです。 これよりmx ergoは、手が疲れないよう、非常に考えられた設計がされたトラックボールであるといえます。

実際に使ってみて、ペンを握るような感覚で非常にリラックスして作業をすることができました。

デスクにおいてみて(見た目)

このmx ergoですが、Bluetoothのおかげで机の上がとてもシンプルになりました。

f:id:torikatsu923:20210124231934j:plain

HHKBのコンパクトさと相俟って、デスク上の雑念が存在しない空間作りに貢献してくれているのがわかると思います。 落ち着いたマットな黒色も、高級感を演出していて気分がいいです。

おわりに

今回の記事では、logicool mx ergoというトラックボールを導入した経緯とその使用感についてお話をさせていただきました。 それでは快適なデスクライフを!

JSの+について考える

こんにちは、とりかつです。 最近JSのコードを読むことが多いです。 とあるリポジトリのJSのコードを読んでいたら以下のような文がありました。

var a = +new Date();

+new Date()...??? なんじゃこりゃ...

と言うことで今回の記事ではjsの+について軽く調べてわかったことをまとめていきます。

+の種類

+ 記号を使用する演算子はいくつかの種類があります。

演算子 名前
+ 単項正値演算子
++ インクリメント
+ 加算
+= 加算代入

それぞれ見ていきます。

単項正値演算子

構文は以下のようになります。

+ ○

この記事の冒頭で出てきた+ は単項正値演算子です。 オペランドが数値以外の場合、数値に変換する役割を持っています。

+ 10 // -> 10

new Date()は数値変換するとミリ秒を取得できます。

f:id:torikatsu923:20201204005839p:plain

なので、冒頭のコードはミリ秒を取得するコードになります。

f:id:torikatsu923:20201204010021p:plain

インクリメント

インクリメントはオペランドに1を加算して返します。 インクリメントはオペランドの前後どちらにもつけることができます。 ですが、動きは異なります。

前置した場合は、インクリメントしてから値を返していることがわかります。

f:id:torikatsu923:20201204010511p:plain

後置した場合は、値を返してからインクリメントしていることがわかります。 f:id:torikatsu923:20201204010657p:plain

加算

オペランドを足し合わせた値または文字列連結した結果を返却します。

f:id:torikatsu923:20201204011213p:plain

オペランドの型が違う時、JSによって暗黙の型変換が行われます。

f:id:torikatsu923:20201204011317p:plain

暗黙の型変換には優先順位?みたいなものがあります。

f:id:torikatsu923:20201204011608p:plain
対応表

引用:https://jsprimer.net/basic/implicit-coercion/

また、加算演算子は左から右に評価されます。 なので、式の項の順序(これは正確な言い回しではないかもしれないです。)によって結果が変わります。 これ覚えとくと、いつか何かに使える気がします。

f:id:torikatsu923:20201204011838p:plain

加算代入

右辺のオペランドを変数の値と加算し、結果を変数に代入します。

f:id:torikatsu923:20201204012142p:plain

加算代入は代入系の演算子で優先度が3です。 対して、加算、減算の優先度は14、剰余、乗算、除算の優先度は15です。

なので+= ( 10 - 3 ) みたいにかかなくても良さそうです。

f:id:torikatsu923:20201204015351p:plain

おわりに

ここまでJSの+についてまとめてきました。 +に限らずJSの演算子による表現は幅が広いです。

f:id:torikatsu923:20201204012856p:plain

(どこでこれ使うかわかりませんがこんなこともできちゃいます)

また、新しい発見があったらまとめてみたいなと思います。

Vueで直接DOM生成してクラス割り当てようとしたらできなかった話

こんにちは、とりかつです。 今回は、VueでDOMを生成してクラスを割り当てようとしたら、スタイルが適用されなくてハマりました。 そこで、ハマリから得られた知見を共有したいと思います。

やろうとしていたこと

SFCで以下のようなことをしようとしていました。 付与するクラスinner_elementのスタイルは、SFC内の<css scoped>で定義してあります。

  1. mounted()のタイミングで動的に<span></span>を生成 1 生成した要素にnewElementというクラスを付与
  2. $refsの子要素として追加

コードにすると以下のようになります。

mounted() {
  const el = this.$refs.hoge as HTMLElement
  const newElement = document.createElement('span')
  newElement.className = "inner_element"
  el.append(newElement)
}

いざ実行してみると<span>はしっかり追加されており、class="inner_elementも確認することができました。 しかし、スタイルは適用される気配がありません。 うまくいかない原因はなんでしょうか。

なんでVueでVanilaJS使ってDOMを追加しようとしとるねんってことはあえて考えないようにしましょう。

VueのCSSが読み込まれるタイミング

スタイルが適用されなくて悩んでる時にscoped cssの仕様についてふと思い出しました。

scoped 属性をもつ <style> タグを利用するとき、その CSS は現在のコンポーネントの要素にのみ適用されます。これは Shadow DOM のスタイルのカプセル化に似ています。いくつか注意点がありますが、polyfill は必要ありません。PostCSS を使用して以下を変換することによって実現しています[^1]

同ページには以下のようなスニペットも掲載されていました。(一部抜粋)

example[data-v-f3f3eg9] {
  color: red;
}

要約すると、SFCのスコープ付きCSSのクラス名はコンパイルされて、.inner_element.inner_element[data-v-jfoieh]のようになるといった感じです。

では、CSSコンパイルされるタイミングはいつでしょうか。 以下はVue.js公式ドキュメントに掲載されていたライフサイクルの図です。[^2]

f:id:torikatsu923:20201127210526p:plain
Vueライフサイクル

この図からはSFCテンプレートがコンパイルされるタイミングがcreated()beforeMount()の間であることが読み取れます。 ですが、CSSコンパイルされるタイミングは読み取ることができませんでした。 推測ではありますが、この間でCSSコンパイルされている可能性がありそうです。

ここで気づいたのですが、CSSコンパイルされるのと同時にコンパイルによって付与されたID的なものがVueによって生成されるテンプレート内の要素にも付与されるようです。 以下のようなSFCを考えます。

<templete>
  <button class="button">
</templete>
<style scoped>
  .button {
    /* some style */
  }
</style>

Vueによってマウントされたとき、.button.button[data-v-horekjkd]となるとします。 このとき、Vueによって生成されるbuttonは次にようになります。

<...>
  <button data-v-horekjkd class="button"></button>
</...>

このようにVueによって生成されたIDはHTMLのタグに反映?されるようです。

実際にインスペクタを見てみるとv-data-.....buttonに付与されていることがわかります。

f:id:torikatsu923:20201127203001p:plain
buttonタグ内にdata-v-24921c38が存在していることが確認できます。


少し冗長になってしまいました。 ここまでわかったことをまとめます。

  • スコープ付きCSSはVueによってコンパイルされ、一意なIDが付与される
  • IDはテンプレート内のタグにも付与される
  • CSSコンパイルされるタイミングははっきりとわからない
  • テンプレートはcreated()beforeMounted()の間でコンパイルされ、このタイミングでIDが付与される

CSSコンパイルされるタイミングをはっきりと掴むことができませんでした。 しかし、CSSコンパイルで付与されるIDがテンプレート内のタグに付与されることと、テンプレートがコンパイルされるタイミングは明らかになりました。 これによりmounted()で生成した要素にスタイルが付与されなかったことの説明がつきそうです。

CSSコンパイルされるタイミングは追々調べようと思います。

まとめ

当初、私はmounted()<span class="inner_element"></span>を生成していました。 しかし、この時点でSFCCSSクラス名はコンパイルされ.inner_element[v-data-hgoejdklgha]のように変化していました。 また、テンプレートのコンパイルも終了していました。 スタイルを適用するには、クラスに付与されたIDをテンプレートの要素にも付与する必要がありましたが、私の生成したspanはIDが付与されていませんでした。 これによってスタイルが適用されなかったと考えられます。

おわりに

ここまで、Vueのmounted()でVanilaJSによって生成した要素にスタイルが適用されない原因について考えてきました。

思い返してみると... Vueでクラス名をバインドする際は、

:class="{classA: true, classB: false}"

みたいに書く必要がありました。 いままで、以下のように書けたらいいのにな。なんだか気持ち悪いなとか思ってました。

$refs.class = "classC"

ですが、テンプレートがコンパイルされるタイミングでIDが付与されることを考慮すると、 あらかじめクラス名をバインドしておいて、vueのdata等の状態の切り替えで、クラスの付与のアクティブ/パッシブを切り替えに納得が行きました。


[^1] https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html [^2] https://jp.vuejs.org/v2/guide/instance.html

JavascriptのIIFEについて考える

こんにちは、とりかつです。 最近、Javascriptで以下のようなコードを見かけました。

(function() {
  console.log("hogehoge");
})();

調べていくとこれはどうやら即時実行関数式(IIFE: Immediately Invoked Function Expression)[^1]というものだそうです。 私はぱっと見でIIFEの書き方が理解できませんでした。 なので、今回の記事ではこのIIFEを完全に理解したいと思います。

原点回帰

最初に関数の定義について振り返ります。 JSで関数式は以下のように定義, 実行できます。

// 関数の定義
var a = function() {
  return "done";
}

// 関数の実行
a();

aの後ろの()は関数呼び出し演算子です。 関数呼びだし演算子の構文は...(/* 引数 */)です。[^2]

関数の実行の流れは以下のような感じかと考えました。

  1. aが関数の実態と置き換わる
  2. 関数が実行される
  3. 関数の返り値の"done"に置き換わる

関数実行のイメージ
関数実行のイメージ
(もしかしたらこの説明は少し語弊があるかもしれません。あくまでイメージ程度で捉えていただければと思います。)

関数の実行時に変数が関数の実態と置き換わるイメージから、関数の定義と同時に関数の呼び出しをおこなうことができそうな感じがします。 やってみます。

関数を即時事項してみる
関数を即時実行してみる

... だめでした。 エラーにはUncaught SyntaxError: Function statements require a function nameとありました。 どうやら現在の書き方では関数式ではなく関数定義文として認識されていて、関数名が必要だと怒られているようです。

関数の定義を関数式として認識させる

ではどうしたら関数式として認識してもらえるようになるのでしょうか。 私は関数式という名前から関数を式として認識させればいいと考えました。 具体的には+, -, !のような演算子を関数の定義の前につければいいといった感じです。

プログラミング言語において式はオペレータとオペランドから成り立っています。 たとえば以下のような式を考えます。

1 + 6

この式では16オペランドで、+がオペレータです。 JSで関数定義を式として認識させるためには、JSエンジンに関数の定義がオペランドだよと教えてあげられれば良さそうです。

なのでオペレータを使って関数定義をオペランドとして認識できるようにします。

// 例
+ function() {
  return "done";
}

実行してみると...

関数をオペランドとして認識させる
関数をオペランドとして認識させる

うまくいきました。 現時点では関数呼び出しはしていないのでNaNが返ってきていることがわかります。 次に関数呼び出し演算子を使って関数を呼び出してみます。

関数の即時実行
関数の即時実行
うまくいきました! これで関数を定義して即時実行することができるようになりました。

グループ化演算子を使用する

関数の定義を式として認識させるためには、関数の定義がオペランドであることを明示すればよさそうでした。 JSのグループ化演算子(...)は「式での評価の優先順位を制御」[^3]する役割をもっています。

グループ化演算子は中に式が入ることが想定されています。 これよりグループ化演算子の中で関数を定義すれば、その関数は関数式として扱われると考えられます。 実際にやってみます。

グループ化演算子を使って関数を定義する
グループ化演算子を使って関数を定義する
グループ化演算子を使って関数を式として定義することができました。

もちろん、即時実行も可能です。

グループ化演算子を用いて関数を即時実行する

グループ化演算子を使う理由

ここまで、演算子を用いた関数式の定義と関数の即時実行について考えてきました。 私がIIFEを理解できなかったポイントは、なぜグループ化演算子を用いるのかという点です。 なので最後になぜグループ化演算子を用いるのか考えていきます。

グループ化演算子は静的なスコープをもっているようです。 そのため、以下のケースではエラーを吐かれます。

グループ化演算子ありの場合

このようにグループ化演算子内で宣言する前の変数にはアクセスできないようです。 これは名前空間を切り分けることに役立ちそうです。

実際の開発では複数人で開発を進めていきます。さらにさまざまなパッケージを使用することが考えられます。 なので、名前空間は非常に重要となってきます。

以上より、JSファイルが読み込まれた際に名前空間を切り分ける目的でグループ化演算子が用いられていると考えられます。 逆に名前空間を切り分ける必要がないばあいに関数を即時実行したければ使用する演算子はなんでもいいかもしれません。

おわりに

今回はJSのIIFEを切り口にJSの関数について考えました。

個人的に、演算子を用いることで関数を式として認識させるという部分は大きな再発見でした。

参考

vuex-module-decorators使用時のエラーについて

お久しぶりです。 最近、Vuexをクラススタイルで記述することを可能にするvuex-module-decoratorsを使う機会があったのですが、ActionDecoratorParamsについて気になったので記事を書きました。

vuex-module-decoratorsとは

VueをTypescriptで書こうとすると、絶妙なところでTSの恩恵を受けられないといったケースがあると思います。 例えばVueのPropstypestringの配列を指定しようとするケースを考えます。

私ははじめ以下のように指定しました。

props: {
  items: string[]
}

しかし、この書き方はどうやらできないようです。 これはpropstypeは以下のものに制限されているためです。[^1]

  • String
  • Number
  • Boolean
  • Aarray
  • Object
  • Date
  • Function
  • Symbol

そこでArrayを指定しようと考えました。

props: {
  items: Array
}

しかしこれではany[]となってしまい、いまいちです。 どうやらPropTypeを使用することでstring[]を認識させることができるようですが、さくっと型を指定できないのはどこかもどかしく感じました。[^2]


少し話が逸れてしまいました。 先ほどの例のように、素のVueではTypescriptの恩恵にあやかるために一手間加える必要があります。 このような経験はVuex+Typescriptでも経験しました。

そこで、VuexでゴリゴリにTypescriptの恩恵にあやかれるように使用したのがvuex-module-decoratorsです。

github.com

情報を漁っていくと以下のような記述もありました。[^3]

最も人気のあるアプローチの1つは vuex-module-decorators です。- ガイドを参照してください。

モジュール自体も軽量で、コード行数もそこまで多くないです。 そのため内部実装の把握にそこまでコストがかからないと感じたので使ってみることにしました。

@Actionで謎のエラーが大量に出る

使ってみるなかで、私は以下のようなコードを書きました。

@Action
public getItemFromLocalStorage(): string {
  const item = localStorage.getItem("item")  as string
  if(!item) {
    throw new Error("item is not exist")
  }
  return item
}

このコードはlocalStorageから"item"というキーでアイテムを取得し、もし存在しなければエラーを投げるというコードです。

このコードが正常に動作するか確認するためにテストをしていたのですが、エラーを投げるか確認するテストを組んでいるときに以下のようなエラーに遭遇しました。

ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access 
                this.someMutation() or this.someGetter inside an @Action?
                That works only in dynamic modules.
                If not dynamic use this.context.commit("mutationName", payload)

先ほどのコードでは単にError("item is not exist")を投げるだけだったため、急に発生した予期しないエラーに思わず声が出てしまいました。

調べてみるとvuex-module-decoratorsは、エラーが発生した場合に独自のエラ〜メッセ〜ジを全力で投げつけてくるそうです。 ですがActionデコレータにrowError: trueを渡せば[^4]、ちゃんと元のエラーメッセージを優しく(個人調べ)投げつけてくれるそうです。

@Action({ rowError: true })

ここで、個人的になぜrowError: trueで元のエラーを投げてくれるのか気になったのでコードをよんでみることにしました。

Actionのコードリーディング

まず、@Actionの定義元にジャンプしてみます

/**
 * Parameters that can be passed to the @Action decorator
 */
export interface ActionDecoratorParams {
    commit?: string;
    rawError?: boolean;
    root?: boolean;
}
export declare function Action<T, R>(target: T, key: string | symbol, descriptor: TypedPropertyDescriptor<(...args: any[]) => R>): void;
export declare function Action<T>(params: ActionDecoratorParams): MethodDecorator

どうやらActionにはActionDecoratorParamsなるものを渡せるそうです。

ですが、これ以上定義元ジャンプで遡るのが厳しかったので大人しくGithubリポジトリを見てみることにします。 コードを見ていくと/src/action.tsが怪しそうです。

コードは以下のようになっていました。ビンゴっぽいです。 github.com

こちらのコードのインターフェースの宣言のところには以下のようにコメントしてあるだけだったので、actionDecoratorFactoryrowErrorがどのように使われているか確認していきます。

/**
 * Parameters that can be passed to the @Action decorator
 */

コードを読んでいくと以下の部分に行き着きました。

throw rawError
          ? e
          : new Error(
              'ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access ' +
                'this.someMutation() or this.someGetter inside an @Action? \n' +
                'That works only in dynamic modules. \n' +
                'If not dynamic use this.context.commit("mutationName", payload) ' +
                'and this.context.getters["getterName"]' +
                '\n' +
                new Error(`Could not perform action ${key.toString()}`).stack +
                '\n' +
                e.stack
            )

https://github.com/championswimmer/vuex-module-decorators/blob/08249c58ab60dd26a993c995d8a00c15c7222a48/src/action.ts#L47

先ほどの章でコメントした謎のエラーに似た文字列を発見することができました。 どうやらrawErrorの値によって独自のエラーを投げるか、素のエラーを投げるか切り替えているそうです。

おわりに

今回の記事では@Action({ rawError: true })とする方法を紹介しました。 READMEによると一括で設定を指定できるようです。

import { config } from 'vuex-module-decorators'
// Set rawError to true by default on all @Action decorators
config.rawError = true

一個一個rawErrorをしていくよりも一箇所で指定できた方が何かと便利そうな感じがします。

リンク

Riverpodのドキュメントを意訳してみた2

お久しぶりです。今回は前回に引き続きriverpodを読んでいたので、メモ代わりに意訳をまとめました。

今回まとめたのは、以下の二つです。

riverpod.dev

riverpod.dev

Providerをreadする

このガイドでは、どうやってProviderを使用するのかを知ることができる

providerをreadする方法はいくつかあって、基準・規格・仕様によって少し違う 手短に知るには以下の図にそってどのproviderをreadするかみてね

どこでProviderをreadするか? - テストやDart-Only-PackageでreadするならProviderContainer.read

  • Widgetの中

    • Dialog表示のためにlisteningするならProviderListener
    • onPressedを覗くbuildメソッドの中でreadしなければContext.read
    • flutter-hooksを使うならuseProvider
    • flutter-hooksを使わないならConsumer
  • 他のProviderの中

    • providerの更新に伴ってproviderによって公開された値が、再生成されるべきか
      • べきならProviderReference.watch
      • ちがうならproviderReference.read

つぎにそれぞれ個別の場合をみながらどうやって動作するのか紹介します

このガイドでは以下のproviderで考えます。

final counterProvider = StateProvider((ref) => 0);

readの方法を決定する

listenしたいProviderによって、値をlistenする方法がいくつかあります

たとえば、以下のStreamProviderについてだと、

final userProvider = Streamprovider<User>(...);

userProviderをreadするとき、以下のような方法が可能である。

  • userProvider自身で同期的に現在の状態をreadする
Widget build(BuildContext context, ScopeReader watch) {
  AsyncValue<User> user = watch(userProvider);

  return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
  );
}
  • Streamの連携に伴って変更される値をreadするならuserProvider.stream
Widget build(BuildContext context, ScopeReader watch) {
  Stream<User> user = watch(userProvider.stream);
}
  • 最新の値で発火されるFutureで解決される値を取得するには、userProvider.lastを使用する
Widget build(BuildContext context, Scopedreader watch) {
  Future<User> user = watch(userProvider.last);
}

さらなる情報はAPIリファレンスみてね

ProviderをWidgetの中で使う

この章では、ProviderをWidgetの中で使う方法について知ることができる

ConsumerWidget

ConsumerWidgetStatelessWidgetのような基底クラスだ。そして、providerをlistenすることができる

class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, Scopedreader watch) {
    int count = watch(counterProvider).state;

    return Scaffold(
      appBar: AppBar(title: const Text('Counter example'));
      body: Center(
        child: Text('$count'),
      ),
    );
  }
}

この例では、counterProviderとcounterが変更される旅にTextがリビルドされるだろう。

counterProviderの値によって気づくことは、関数がwatchを呼ぶことだ。ConsumerWidgetで作られた関数は私たちのproviderをlistenし、値の変更が公開されるとリビルドする。

Caution

ConsumerWidgetの引数を渡すwatch()は```onPressedのような関数では非同期で呼ばないべきだ。

もしユーザのイベントのレスポンスをproviderをreadする必要があるなr、myProvider.read(BuildContext)を使用してください

Consumer

Consumerはアプリケーション内でデータを扱う特定のウィジェットの再描画のパフォーマンスを最適化することができるConsumerWidget

たとえば、ConsumerWidgetのスニペットを更新以下のコードでは、まえもってTextのみが再描画されることを認識できる。

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Consumer(
          builder: (context, watch, child) {
            int count = watch(counterProvider).state;
            return Text('$count');
          },
        ),
      ),
    );
  }
}

context.read(myProvider)

ときには、listenする値が存在しないこともある。例えば、オブジェクトはonPressedでのみ必要な場合。

私たちはConsumerを使うことができる

Consumer(builder: (context, watch, _) {
  StateController<int> counter = watch(counterProvider);
  return RaisedButton(
    onPressed: () => counter.state++,
    child: Text('increment');
  )
});

しかし、これは効果的ではない。providerのlistenによって、counterが変更される旅にRaisedButtonが再描画される。counterが実際にbuildに使われない場合でさえ。

解決方法としてcontext.read(myProvider)が存在する。

これを使うことで前述のコードを以下のようにリファクターできる。

@override
Widget build(BuildContext context) {
  retur RaisedButton(
    onPressed: () => context.read(counterProvider).state++,
    child: Text('increment'),
  );
}

こうすることで、依然として、クリックすることで、ボタンはcounterをインクリメントする。しかし、もはやproviderをlistenしておらず、不必要な再描画を避けている。

context.readがみつからない。どうしたらいい?

もしcontext.readが見つからないなら、それはおそらく正しいパッケージをインポートしていないからだ。このメソッドを使うにはpackage:flutter_riverpodか、package:hooks_riverpodをインポートしなければならない。

Info

listenしているProviderによっては、これは必要ないかもしれない。 たとえば、StateNotifierProviderはそれ自身の状態のlistenを除く、StateNotifierを取得する方法が用意されている。

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

  void increment() => state++;
}

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

// ...
@override
Widget build(BuildContext context, ScopedReader watch) {
  final Counter counter = watch(counterProvider);

  return RaisedButton(
    onPressed: counter.increment,
    child: Text('increment'),
  );
}

Caution

context.readbuild()の中で呼ぶことを回避してください。もし、再描画を最適化したいなら、値の変更のlistenをProviderの中に変更してください。

ProviderListener

ときには、Providerの変更のあとでダイアログの表示やウィジェットツリーへのルートの挿入を行いたい場合もある。

ProviderListenerウィジェットを利用して以下のように実装してください

Widget build(BuildContext context) {
  return ProviderListener<StateController<int>>(
    provider: counterProvider,
    onChange: (counter) {
      if(counter.state == 5) {
        show Dialog(...);
      }
    },
    child: Whatever(),
  )
}

これできっとカウントが5になるとダイアログが表示されます。

Providerの中で他のProviderをreadする

一般的なProviderの作成方法として他のオブジェクトからオブジェクトを生成することがある。 例えば、UserRepositoryからUserControllerを生成したい時、それらは違ったProviderによって公開されているとする。

このシナリオはproviderがパラメータを受け取るためのrefを使うことによって可能になる

final userRepositoryProvider = Provider((ref) => UserRepository());

final userControllerProvider = StateNotifierProvider((ref) {
  return UserController(
    repository: ref.watch(userRepositoryProvider),
  );
});

CombineProvidersガイドでより多くの情報を得られる。例えば以下のように - 時間と共に変化する値からオブジェクトを作る時、どんなことがおこるか - 良い方法について