torikatsu.dev

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

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

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

こんにちは。 私は、現在FlutterアプリにRiverpod導入を考えています。導入に先立ってRiverpodの公式ドキュメントを読みました。 いつかドキュメント全体をざっくり見返すときにあまり頭を使いたくないため、メモの意味も兼ねてドキュメントを意訳して記事にまとめておくことにしました。

今回、意訳したページは以下のリンク先です。

意訳は正確でないこともあるのであらかじめご了承ください。

riverpod.dev

Providerをなぜ使うのか

  • 様々な場所から状態へのアクセスを簡単にする
  • 状態の組み合わせを単純にする
  • パフォーマンスを最適化する
  • テスタビリティを向上させる
  • 簡単に統合する

Providerの生成

Providerは異なった方法で生成されるが動作は一緒 一般的に以下のように、グローバル定数として宣言される

final myProvider = Provider((ref) {
  return MyValue();
});

Note

Providerをグローバル変数で宣言することを恐れる必要はない
Providerは完全に変更不可能だ。Providerを宣言することは関数を宣言することと変わらない、そしてテストやメンテナンスをしやすい

このスニペットは3要素から成り立っている - final myProviderという変数宣言 将来的にProviderから状態を読み込むための変数で、つねに変更不可能だ。

  • Provider、使用するProviderのp種類 Providerは一番基本的なProviderだ。二度と変更されないオブジェクトを公開する。状態をインタラクティブに変更するためにProviderStreamProviderStateNotifierProviderに変更することもできる。

  • 状態を共有する関数 関数は常にrefとよばれるオブジェクトを引数で受け取る。このオブジェクトはほかのProviderを読み取ることや、Providerが破棄されるときになんらかの処理を実行することを可能にする(フック的なやつ)。

関数によって作られたオブジェクトの型は、使用されるProviderに依存したProviderに渡される。 例えば、Providerの関数はなんらかのオブジェクトを生成できる。一方、StreamProviderのコールバックはStreamを返すだろう。

Info

宣言可能なProviderはどれも制限を必要としない。package:providerを使用した場合とは逆に、Riverpodでは同じ型のProviderを公開できる。 実際、ふたつのProviderはどちらもStringを生成しているが、問題はない。

Caution

Providerを使用する時、ProviderScopeをFlutterApplicationsのルートに追加してください。

Stateが破棄される前の処理の実行について

時には、状態についてのProviderは破棄・再生成される。一般的な使用例は、providerが破棄される前に状態を破棄する。たとえばStreamControllerをcloseするように。

これはref.onDispose()を使用することで実現する。

final example = StreamProvider.autoDispose((ref) {
  final streamController = StreamController<int>();
  ref.onDispose(() {
    streamController.close();
  });
  return streamController.stream;
}

Note

使用するProviderによっては、事前にProviderを破棄する方法が用意されているかもしれない。 たとえば、StateNotifierProviderStateNotifierdisposeメソッドを呼ぶ

ProviderのModifierについて

すべてのProviderには、さらなる機能をついかする方法が用意されている。 refに新しい特徴を追加するか、ProviderをConsumeする方法を少し変更している。

final myAutoDisposeProvider = StateProvider.autoDispose<String>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');

ときには、二種類のModifiersを使用することが可能だ

  • .autoDispose, 参照されなくなった時にProviderの状態を自動で破棄する
  • .family, 外部のパラメータからProviderの生成を可能にする。

Note

はじめに複数のmodifiersを使う時

final userProvider = FeatureProvider.autoDispose.family<User, int>(ref, userId) async {
  return fetchUser(userId);
});

Providerをreadする

このガイドを読む前にread about Providersを読んね

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

Dartのコンストラクタ紹介2

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

今回は、前回の記事で紹介しきれなかったファクトリコンストラクタについて紹介したいと思います。

ファクトリコンストラク

Dartにはgenerative constructor, constant constructorの他にfactory constructorというものがあります。

factoryコンストラクタはコンストラクタ内でインスタンスを生成しません。なので、自分でインスタンスを生成する必要があります。

コンストラクタ内でインスタンスを生成しない特性を生かし、シングルトンパターンで使われたりします。

以下のスニペットのように宣言します。

class Sample {
  factory Sample() {
    _instance ??= Sample._();
    return _instance;
  }

  Sample._();

  static Sample _instance;
}

このスニペットでは、_instancenullのとき、プライベートなネームドコンストラクタを呼び出して_instanceに代入することでシングルトンパターンを実現しています。

ですが、こんな書き方をせずとも、以下のように実装することもできそうです。

class Sample {
  Sample._();

  static Sample _intance;

  static Sample getInstance() {
    _instance ??= Sample._();
    return _instance;
  }
}

コードの振る舞いは変わりませんが、コードの印象は大きく変わります。

factoryを使った時

final sample = Sample();

従来のシングルトンパターン

final sample = Smaple.getInstance();

考えは様々ですが、使い手にシングルトンパターンを意識させたいなら従来のシングルトンパターンを使い、使い手にシングルトンであることを意識させたくないならfactoryを使うと混乱を避けらると思いました。


少し話は変わりますが、インスタンスの生成方法を隠蔽する実装もできそうです。(むしろこっちがファクトリコンストラクタの本命だとおもってる)

class Sample {
    factory Sample(SampleType type) {
      switch () {
        case SampleType.A:
          Sample._a();
          return;
        case SampleType.B:
          Sample._b();
          return;
      }
    }

    Sample._a() {
      /// todo somthing
    }

    Sample._b() {
      /// todo somthing
    }
}

enum SampleType {
  A,
  B,
}

おわりに

ファクトリコンストラクタを使わなくても実装できてしまうケースがほとんどだと思うので、ファクトリコンストラクタは使いどころが難しいと私は感じました。ですがこれを使いこなすことでよりDartライクなコードになるとおもうので、積極的に使っていきましょう。

Dartのコンストラクタ紹介

こんばんは。最近Flutterでアプリ開発をしているとりかつです。

Dartにはコンストラクタが数種類あるのですが、Flutterをはじめたてのころ、けっこうつまづきました。

そこで今回の記事ではDartのコンストラクタの種類と使い方について紹介したいと思います。

Generative Constructor

ここでは生成的コンストラクタ(通常のコンストラクタ)を紹介していきます。

通常のコンストラク

Dartのコンストラクタはクラス名で宣言します。Javaを触ったことのある人には馴染みがあるかと思います。

class Sample {
  Sample(String text) {
    someField = text;
  }
  
  String someField;
}

フィールド代入の省略

これで動作はするのですが、もうひと工夫加えたいと思います。

Dartにはコンストラクタでのフィールド代入を省略する機能があります。

以下のようにコンストラクタの引数でthis.hogehogeとしてやるだけです。さらに、コンストラクタの処理がない時は{・・・}を省略することができます。

class Sample {
  Sample(this.someField);
  
  String someField;
}

簡単ですね。 こうするとコードの行数が減るだけでなく、変数名を考える手間も省けるので生産性が上がりそうです。

積極的につかっていきましょう。

ネームドコンストラク

Dartではコンストラクタのオーバーロードがサポートされていません。 その代わり、ネームドコンストラクがあります。

以下は色を表すクラスです。

class Color {
  Color.fromRGB(this.r, this.g, this.b) {
    alpha = 1.0;
  }

  Color.fromRGBA(this.r, this.g, this.b, this.a);

  /// red
  int r;

  /// green
  int g;

  /// blue
  int b;

  /// alpha
  double alpha;
}

このようにクラス名.コンストラクタ名で宣言できます。 これも簡単ですね。

ネームドコンストラクタをつかうことで、どうやってインスタンスを生成しているかが明確になります。

ネームドコンストラクタの命名にはfromキーワードを使うとインスタンス生成時のコードが自然言語ライクになるので個人的にオススメです。

/// fromを使わない場合
final color = Color.RGB(100, 20, 49);

/// fromを使った場合
final color = Color.fromRGB(100, 20, 49);

オプショナルパラメータを使えばもっとスマートに実装できますが、それについてはここでは触れないどきます。

定数コンストラク

Dartには定数コンストラクというものがあります。 以下は定数コンストラクタの例です。

class Sample{
  const Sample(this.text);
  
  final String text;
}

コンストラクタの先頭にconstがついているのがわかると思います。 これが定数コンストラクタです。

定数コンストラクタを使うことで、インスタンスの生成時にconstキーワードをつけて、定数オブジェクトをインスタンス化することができます。

 const Sample('サンプル');

定数コンストラクタを宣言する条件は以下の2つです。

  • クラスのフィールドが全てfinal
  • コンストラクタのボディがない(InitializerListはOK)

少し難しく感じますが、慣れてくるとなんてことはなくなります。

ユースケースとしてはパフォーマンスの向上が考えられます。

FlutterのStreamなんかがわかりやすいと思います。

以下のコードはStereamBuilderの例です。

StreamBuilder<String>(
  stream: textStream,
  builder: (context, snapshot) => Column(
    children: <Widget>[
      /// text widget A
      Text(snapshot.data),
      
      /// text widget B
      Text('サンプル'),
    ],
  ),
);

この場合、steramに値が流れてくるたびにtext widget Atext widget Bが更新されます。

text widget Bのコンストラクタの引数は'サンプル'という定数の文字列のため、constキーワードをつけて定数化することができます。

StreamBuilder<String>(
  stream: textStream,
  builder: (context, snapshot) => Column(
    children: <Widget>[
      /// text widget A
      Text(snapshot.data),
      
      /// text widget B
      const Text('サンプル'),
    ],
  ),
);

text widget Bは定数のため、streamに値が流れてきても再描画されるのはtext widget Aのみで済みます。

今回の例では、パフォーマンスへの影響はほとんどありませんが、「チリも積もれば山となる」ので、積極的に使っていきましょう。

終わりに

本当はファクトリコンストラクタまで紹介しようと思ったのですが、寝るのが遅くなってしまいそうなので、次回の記事で紹介したいと思います。

Dartでコンストラクタをつかいこなすことは可読性とパフォーマンスの向上に大きく貢献すると思うので、ぜひみなさんもコンストラクタとお友達になりましょう。

WidgetやProviderのライフサイクルについて調べてみた

現在Flutterでアプリ開発をしているのですが、ウィジェットやProviderのライフサイクル周りで少しハマりました。

そこで今回の記事ではWidgetやProviderのライフサイクルについて紹介したいと思います。

ハマったケース1

問題

CustomScrollView内にProviderを複数配置した時にBlocA、BlocBは生成されますが、BlocCは生成されない

ウィジェットの構成
ウィジェットの構成

解決方法

この問題の原因はウィジェットのライフサイクルにあります。 スクロール可能な画面において、ウィジェットは画面外に出てしまうと自動で破棄され、再び画面内に入るときに生成されるようです。

今回のケースではBlocA、BlocBを注入するProvider<BlocA>Provider<BlocB>はすでに画面内に存在するため生成済みとなりますが、BlocCを注入するProvider<BlocC>は画面外のため未生成となります。

これより、画面の生成時にBlocA・locB・BlocCを注入するProviderをすべて生成するにはCustomScrollViewの位置でMultiProviderをつかうなどして各Providerを生成すれば良さそうです。

ただし、この方法でProviderを生成してしまうとCustomScrollView以下全てのウィジェットから全てのBlocにアクセスできてしまうので注意が必要です。

私はBlocの肥大化を防ぐためにBlocを分割したかったので、今回のケースではどこからでもBlocへアクセスできることには目を瞑ることにしました。

ハマったケース2

問題

先ほどのケース1を踏まえウィジェットの構成を修正してみました。

CustomScrollViewrより親でMultiProviderを使ってBlocを注入するよう変更しました。

しかし、ここでまた問題が発生しました。

Providerは全て生成済みですが、BlocCは未生成のままです。

修正したウィジェットの構成
修正したウィジェットの構成

解決方法

今回のケースの問題はProvider.of<Bloc>(context)のタイミングにあります。

Providercreateを呼ぶタイミングはProvider.of()が初めて呼ばれるタイミングであって、Providerを生成するタイミングではないようです。

今回のケースではBlocABlocBProvider.of()するウィジェットは画面内に存在していますが、BlocCProvider.of()するウィジェットは画面外にあります。

問題1よりウィジェットは画面内に入ってきて初めて生成されるようなので、BlocCProvider.of()するウィジェットは生成されておらず、そのためProvider.of<BlocC>(context)が呼ばれていないためBlocCが未生成となるようです。

この問題を解決する安直な方法として予めCustomScrollViewの外でProvider.of<BlocC>(context)をしておくことが考えられます。

ですが、Blocを必要としない場所でProvider.of()をすることはあまり良くないように思えます。

幸いProviderにはこの問題を解決してくれるlazyというパラメータが用意してありました。

lazyは注入したいインスタンスを遅延初期化するかどうかについてのパラメータで、何も指定しなければ遅延初期化するようです。

lazeにはboolを渡すことができ、laze = falseとすることで遅延初期化を無効化できます。

これにより全てのBlocを全て生成することができました。

おわりに

今回はウィジェットとProviderのライフサイクルについてまとめました。

今回のハマりを通し、ライフサイクルの理解度がフレームワークを使いこなす鍵になると改めて感じました

Blocパターンでのフォームバリデーション

最近、Flutterでアプリ開発をしているのですが、Blocパターンでフォームのバリデーションのスマートな実装方法についてまとめられた記事があまりなく、どハマりしました。

そこで今回の記事では、BlocパターンでRxDartを用いたスマートなフォームのバリデーションの実装方法をサインインページを例として紹介したいと思います。

記事の対象:

Rx・flutter初心者

得られる情報

Blocを使ったフォームの実装方法

サインインページの要件は以下の通りとします。

  • メールアドレス

    @を含む1文字以上の文字列

  • パスワード

    8文字以上の英数字からなる文字列

はてなシンタックスハイライトが弱いので、今回はあえてスクリーンショットを用いてコードを掲載します。

1. UIを用意する

まずUIを用意します。今回はパスワードとメールの入力欄+送信ボタンのシンプルな構成とします。

UI
用意したUI

2. Blocを用意する

今回は以下のようなBlocクラスを用意します。

ポイントはBehaviorSubjectを使うところです。 BehaviorSubjectはイベントをキャッシュしておいてくれるので、過去のイベントに対し、BehaviorSubject.valueでアクセスできます。 

そのため入力情報を保持しておくフィールドを用意しなくてすみます。

Blocクラス
Bloc

Blocでは全ての入出力をStreamにするべきだとありますが、ここではあえてそのルールを破りsubmitをパブリックなメソッドとして定義しています。

なぜならば、ストリームでsubmitを定義すると送信ボタンのonPressedが冗長になってしまうからです。

ストリームで実装すると以下のように冗長な書き方になりますが、

onPressed: () => bloc.submit(null)

メソッドで定義しておくと

onPressed: bloc.submit

と定義でき、UI側のコードがすっきりします。

UIにBlocを注入する

Providerを使ってUIにBlocを注入します。
ついでにTextFieldとRaisedButtonにコールバックを渡します。

BlocをUIに注入
BlocをUIに注入

Providerはウィジェットの生成と破棄に合わせた処理を渡しておけるので便利です。

ここでProviderの直下にBuilderを配置していますが、これはこのクラス内からProvider.of(context)Blocにアクセスするためです。

なぜBuilderを挟む必要があるのかについてはまたいずれ記事にしたいと思います。

バリデーションの実装

いよいよバリデーションを実装していきます。 UI側にバリデーションの結果を伝えるためにStream.addErrorRx.combineStreamを使います

以下はバリデーションを追加したBlocクラスです。

バリデーション追加後のBloc
バリデーション追加後のBloc

ポイントはRx.combineLatestを使用することです。 これを使わずに送信ボタンにバリデーションの結果を通知するストリームを用意してもいいのですが、新しくストリームを生成しなければならないうえ、closeもしなくてはなりません。

今回のケースでは気になりませんが、Streamの数が増えるほどStreamの管理がしんどくなってくるので、combineLatestを使った方がスマートだと思います。

UIでバリデーションの結果を受け取る

UIでバリデーションの結果を受け取るにはInputDecorationクラスのerrorTextを利用します。

StreamBuilderで入力情報のストリームをリッスンしておき、エラーが流れてきたらsnapshot.errorがエラーテキストとして表示されると言った感じです。

また送信ボタンもバリデーションの結果を受け取れるようにしておきます。

UIでバリデーションの結果を受け取る
UIでバリデーションの結果を受け取る

コード全体像

コードの全体像は以下の通りです。 UI

UI全体
UI全体
Bloc全体
UI全体

おわりに

今回フォームの実装方法を探る中で、Rxdartのコードを読みました。英語が苦手で過ごし面食らいましたが、じっくり読んでみるとかなり丁寧にコメントがされていて、ネットの情報見るよりわかりやすかったので、もし困ったらコードのコメントを読むのをお勧めします。

Flutterで菱形を描画する

こんばんは。最近はFlutterを使ってアプリ開発をしているのですが、Flutterで図形を描画する方法に少しハマりました。

そこで今回の記事では個人的に得られた知見を共有したいと思います。

まだFlutterの経験が浅いため、誤った解釈をしている部分があればご指摘いただければ幸いです。

手順

任意の図形を描画するときは以下の手順に従います。

Flutterで図形を描画する時のイメージは、図形を描くペンと、図形の頂点の座標一覧の二つを予め用意しておき、その二つを元にFlutterが図形を描画してくれる感じでいいと思います。

Flutterが図形を描画するイメージ
描画のイメージ

今回は例として菱形を描画してみたいと思います。

1.CustomPaintを継承したクラスを作成する

Flutterで図形の描画を担うのはCustomPainterクラスです。
CustomPainterクラスはFlutterの標準ライブラリに含まれているのでパッケージを追加する必要はありません。

図形を描画する際はCustomPainterクラスを継承したクラスを自作して、paint()shouldRepaint()をオーバーライドする必要があります。

それぞれの役割は次の通りです。

  • paint(Canvas canvas, Size size)

描画する図形を指定する

canvasは言葉の通り図形を描く座標平面みたいなもので、sizeはWidgetツリーの親要素のサイズと考えて良さそうです。

  • shouldRepaint(CustomPainter oldDelegate)

再描画に関する設定をする

以下のスニペットはCustomPainterクラスを継承したクラスです。 この状態ではまだなんの図形も描画しません。

class MyPainter extends CustomPainter {

  @override
  void paint(Canvas canvas, Size size) {
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
  }
}

2.Paintインスタンスを生成する

図形を描画するペンの役割を担うPaintインスタンスを生成します。

Paintインスタンスは様々な値を設定することができます。 詳細は以下のリファレンスが参考になります。

api.flutter.dev

ここでは黒色で太さが1のペンを指定します。

class MyPainter extends CustomPainter {

  @override
  void paint(Canvas canvas, Size size) {
    const paint = Paint();
    paint.color = Colors.black;
    paint.strokeWidth = 1.0;
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
  }
}

3.Pathインスタンスを生成し、図形の頂点の座標をせっていする

つぎにPathインスタンスを生成し、図形の頂点を与えます。 ここでは菱形を描くので(1, 0), (1, 2), (2, 1), (0, 1)の4つの点を与えます。

ここで気をつけなければならないのは図形を座標を負の数にしないことです。 もちろん負の数でも描画はできるのですが、負の数で座標を指定するとCenterなどでWrapした時に図形の中心がずれてしまいます。

CustomPainterは図形の描画のみに集中し、センタリングや大きさなどはContainer()Center()などでラップしてあげる方が図形に関する関心ごとを分離できて再利用性が高まるので、個人的には全て正の数で描画するのがおすすめです。

またpaint()の引数で渡ってくるSizeを利用して座標を指定するとより柔軟な図形を描くことができます。

Path().moveTo(x, y)でペンの位置を任意の場所に移動することができ、Path().lineTo(x, y)で現在のペンの位置から引数で指定した座標まで線を引くことができます。 線を引き終わったらPath().close()で描画を終了します。

ここではdartのカスケードが使えるので是非使いましょう。

class MyPainter extends CustomPainter {

  @override
  void paint(Canvas canvas, Size size) {
    const paint = Paint();
    paint.color = Colors.black;
    paint.strokeWidth = 1.0;
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    /// ペンの設定
    const paint = Paint();
    paint.color = Colors.black;
    paint.strokeWidth = 1.0;

    /// 座標の設定
    ..moveTo(size.width / 2, 0)
        ..lineTo(size.width, size.height / 2)
        ..lineTo(size.width / 2, size.height)
        ..lineTo(0, size.height / 2)
        ..lineTo(size.width / 2, 0)
        ..close();
  }
}

これでペンと座標の設定が終わりました。あと一息です。

4.canvasにPathインスタンスを渡す

さいごに今まで準備したPath()Paint()paintの第一引数に渡ってくるcanvasに渡して完成です。

class MyPainter extends CustomPainter {

  @override
  void paint(Canvas canvas, Size size) {
    const paint = Paint();
    paint.color = Colors.black;
    paint.strokeWidth = 1.0;
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    /// ペンの設定
    const paint = Paint();
    paint.color = Colors.black;
    paint.strokeWidth = 1.0;

    /// 座標の設定
    ..moveTo(size.width / 2, 0)
        ..lineTo(size.width, size.height / 2)
        ..lineTo(size.width / 2, size.height)
        ..lineTo(0, size.height / 2)
        ..lineTo(size.width / 2, 0)
        ..close();

    /// キャンバスにペンと座標を渡す
    canvas.drawPath(path, paint);
  }
}

以上で菱形を描画することができました!お疲れ様です!

おわりに

Flutterは公式パッケージに用意されているWidgetがとても充実しているのでいざオリジナルの図形を描画するとなると面食らってしまいますが、実際にコードを書いてみると意外と単純で、とっつきやすいと思います。

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

生産性爆上がり間違いなしのデスク周りのアイテム紹介!

腰が痛い...
目が疲れた...
手が痛い......

長時間PCで作業していると体の不調を感じることってかなりあると思います。

コンディションが整ってないと作業に集中できず、生産性が下がってしまいます。

そこで今回は私が実際に使ってみて「絶対に使ったほうがいい!」と思ったアイテムたちを紹介していきたいと思います!

全体像

こちらが今の私のデスク環境です。

デスク環境
デスク環境

なかなかシンプルでクリアな環境だと思います。

いますぐにでも座ってコードを描き始めたくなる環境です笑

デスク周りのアイテムを揃えるにあたって

自分の生産性に投資

というワードを強く意識しました。

生産性を少しでも上げるためにアイテム選びには一切の妥協をしていません。

以下では、生産性こだわりお化けな私が選び抜いたアイテムについて書いていきます。

PC

PCは絶対に必要ですよね笑

私はMacBook Proをチョイスしました。

www.amazon.co.jp

私は普段アプリ開発をしているので、メモリを16GBにしました。

おかげでビデオミーテイングをしながらでもキビキビと動いてくれています。

コード書いたり動画編集したりしなければMacBook Airでもスペックは十分だと思います。

いろいろな場所へ持ち運ぶことを想定し、あえて13インチをチョイスしました。

MacBookスタンド

MacBookの画面に合わせると首に負担がかかるのでスタンドを使います。

さまざまなスタンドがありますが個人的にMajextandが一番お勧めです。

www.amazon.co.jp

Majextandのオススメポイントは「とても薄い(1mm)のに安定感抜群で丈夫」です

はじめは薄さが1mmということで、

壊れやすそう... ぐらつきそう...

とネガティブな印象を持っていました。

しかし、使ってみると、「こいつ壊れることあんの?」ってぐらい丈夫で安定感抜群でした。

しかも付属の滑り止めのゴムのグリップ力が抜群で、安定感マシマシでした。

横からみるとこんな感じです。

MacBookスタンド
横から見たMajextand

この隙間がPCの排熱をよくしたり、収納スペースの役割もになってくれるので、一石三鳥なスタンドです。

モニター・アーム

モニターはHPのP224を購入しました

www.amazon.co.jp

そこまで大きなモニターを必要としていなかったのでこれをチョイスしました。

それでもMacBookよりは大きい画面で作業ができるので、生産性に貢献してる気がします。


モニターにはモニターアームが欠かせません

せっかく広い画面で作業ができても、首が痛くなってしまったら非常にもったいないです。

私はColebrookBossonSaunders CBS Flo Blackをチョイスしました。

item.rakuten.co.jp

このモニターアームは3次元で細かく調整できるので、
首が疲れないベストな位置にモニターを調整することができました。

値段は35000と少ししますが、
自分の首の疲れを軽減して生産性を向上させることを考えたら十分アリな値段だと思います

モニターアーム裏側
モニターアームの裏側 コードがすっきり収納できる

写真のようにこのモニターアームはコードをすっきり収納できる構造になっているので、 配線が視界にチラつくのも防いでくれます。

オフィスチェア

イスはとても大事です

1日8時間、、、つまり1日の1/3はイスに座ることになります。

こんなにも長い時間、コンスタントにお世話になるイスを中途半端に選んでしまうと、首、肩、腰が簡単に崩壊してしまいます。

コンディションの乱れは生産性が低下するサインです。

いいイスにすわりましょう!

私はアーロンチェアマスタードを選びました。

ZOZOでも導入されているなど、実績があるオフィスチェアです。

こいつに座って世界が変わりました笑

Flexispot E3
スタンドデスクとしての運用

そのため、普段は普通の机として使い、姿勢変えたい時はスタンドデスクとして運用することができます。

自分の最適な高さに調整して、体を守ることができ、スタンドデスクとしても運用可能と、いいことづくしの机です。

キーボード・パームレスト

イス、机、モニターアーム...で生産性を向上させてきました。

あとひとつ、生産性を向上させるのにマストなアイテムがあります。

そう、キーボードです!

私はHHKB TypeS HYBRIDをチョイスしました。

www.amazon.co.jp

キーボードとしては少し値段が張ってしまいますが、値段以上のベネフィットがあります。

HHKBはキー数が60程度と通常のキーボードに比べキー数が少ないのが特徴です。
そのため、ホームポジションから手を動かさなくていいため、高速なタイピングが可能です。

また、キー数の少なさのおかげで机の占有率も抑えられ、すっきりとしたデスクを実現できます。

HHKB
すっきりとした印象のHHKB
HHKBはしばしば「馬の鞍」に喩えられます。

以下はHHKB公式サイトからの引用です。

アメリカ西部のカウボーイたちは、馬が死ぬと馬はそこに残していくが、どんなに砂漠を歩こうとも、鞍は自分で担いで往く。 馬は消耗品であり、鞍は自分の体に馴染んだインタフェースだからだ。 いまやパソコンは消耗品であり、キーボードは大切な、生涯使えるインタフェースであることを忘れてはいけない。 東京大学 名誉教授 和田英一

私はこの生涯使えるインターフェースという言葉に惚れ込み、購入を決意しました。

実際にこのHHKBは静電容量無接点方式という仕組みを採用しており、理論上は無限に使えるため、まさに生涯使えるインターフェースです。

実用面についてもキートップの角度押下圧など何から何まで考え尽くされており、打鍵感はこの上ないぐらい気持ちがいいです。

このキーボードを使い始めてからレポートを各スピードが体感1.5倍上がりました。

しっかりと生産性の向上に貢献し、とにかく文字を打つことが楽しくなるキーボードです。

このキーボードの魅力をさらに引き出すのにパームレストがオススメです。 筆者がチョイスしたのはFilco ウッドパームレスト(小)です

このパームレストの小サイズはHHKBの幅とぴったりなので相性が抜群です。

パームレストが木製と聞いて、ウレタンのように柔らかい材質の方がいいんじゃないかと疑問を感じる人もいると思います。

私ははじめは木よりも柔らかい材質の方がいいと考えていました。しかし、長時間使う場合、柔らかい素材だと手首を安定させることができなかったり、蒸れて不快に感じることを考え、木製をチョイスしました。

購入してから半年ほどたちますが、木製を選んで正解だと感じています。 安定感や蒸れにくさはもちろんですが、木の温もりがどこか心地よく、快適に作業をすることができています。

www.amazon.co.jp

Filcoウッドパームレストはその高さもHHKBと相性がよく、写真のようにHHKBと机の間にできる段差をカバーすることができ、より快適にキーボードを使うことができます。

パームレスト
パームレストがHHKBと机の段差をカバーしている

このパームレストをよくみると真ん中ぐらいから手前に向かって傾斜があるのがわかるでしょうか。

この傾斜があることによって手首の角度が自然な形になり、より長く快適に作業をすることができます。 HHKB公式のウッドパームレストととても迷ったのですが、このわずかな傾斜が決めてとなってFilcoウッドパームレストを購入しました。

www.pfu.fujitsu.com

さいごに

ここまで私が生産性を向上させるためにチョイスしてきたアイテムについて紹介してきました。

それなりに値が張るアイテムが多いですが、日々の生産性の向上に投資すると考えれば実はそれほど高くなかったりするのかもしれません。

生産性の向上に貢献してくれるアイテムはほかにもたくさんあると思うので、アップグレードした際にまた紹介したいと考えています。

デスク周りのアイテムについてはそれなりに調べたつもりなので、もしかしたら何か有益なアドバイスをできるかもしれません。もし聞きたいことがあれば気軽に聞いてください!

それではいいデスクライフを!

ダックタイピングの紹介

はじめに

お久しぶりです。とりかつです。
最近は設計とかアーキテクチャとかOOPとかを学習しているのですが、そのなかで「ダックタイピング」というものが出てきました。今回の記事ではその紹介とそれについて思ったことを簡単に書いていこうと思います。
一応なんですけど、自分は静的型付け言語触ってきたこともあり、若干偏見が混じってるかもしれません。ご了承ください。

ダックタピングとは

うまく説明できる自信がないので具体例をあげながら説明します。

犬を表すDogクラスと、猫を表すCatクラスがあります。
犬と猫はそれぞれ吠えたり鳴いたりするメソッドを持っています。

class Cat
  def meow
    "にゃー"
  end
end

class Dog
  def bow
    "わん"
  end
end

class Main
  dog = Dog.new
  cat = Cat.new

  # なかせる

  # わん
  dog.bow

  #にゃー 
  cat.meow
end

で、ここからがダックタイピングなんですけども、犬も猫も鳴き声は違えど、とりあえず鳴きますよね。
犬も猫も鳴き声ごとに呼び出すメソッドを変えるんじゃなくて、どっちにも「鳴いて!」って言えば鳴いてくれた方がクラス間の依存を疎結合に保つことができそうですよね。 ということで上のコードをダックタイピングで書き換えてみました。

class Cat
  # パブリックインターフェース
  def cry
    meow
  end

  def meow
    "にゃー"
  end
end

class Dog
  def cry
    bow
  end

  def bow
    "わん"
  end
end

class Main
  dog = Dog.new
  cat = Cat.new

  # なかせる
  # わん
  dog.cry
  #にゃー 
  cat.cry
end

DogクラスもCatクラスも固有の鳴き声メソッドをcryメソッドでラップしてあげることでどちらも同じメソッドを呼び出すだけで鳴かせることができるようになりました。これがダックタイピングです。

ダックタイピングの利点

ダックタイピングの利点として以下が挙げられると思います。

  • 型を判別して呼び出すメソッドを分岐させる必要がないということは依存性を減らせる
  • 明示的にインターフェースを実装する必要がない

もちろん他にも利点はあると思いますが自分の認識では上の2つが大きい利点だと考えています。

静的型付け言語のinterfaceつかえばよくない?

javaとか静的型付け言語触ってきた人はここであれ?と思っていると思います。
わざわざ暗黙的にインターフェースを実装しなくてもinterface使えば良さそうですよね。インターフェースの実装が強制されていること知らないチームメンバーがいたら...なんてケースも考えられます。
もちろんその考え方も間違ってないと思います。でもそれは静的型付け言語に慣れてきたからこその感覚だと思います。

そもそも静的型付け言語は「こんなインターフェース用意してるから使って!」ってイメージで、動的型付け言語は「とりあえずこのインターフェース呼べば良さそう!なかったらその時考えよう」ってイメージです(偏見)。
なのでどっちが優れてるとかじゃなくてどっちにもメリットとデメリットはあるよってことだと思います。
毒だって使い方によっては薬になりますよね。今回紹介しているテクニックもそれと同じで適しているところに使えば大きなメリットを生みますし、変な使い方をすれば大きくコストをかけることになったりもします。
ちなみに先ほどの

インターフェースの実装が強制されていること知らないチームメンバーがいたら...

の解決策としてテストで実装を強制することが挙げられます。

まとめ

今回はダックタイピングについて紹介しました。個人的には動的型付け言語ならではの面白いテクニックだなと感じました。Railsとかで開発する際に積極的に使ってみたいです。

Roomとかデータレイヤー実装で詰まったこと ・便利なこと

はじめに

こんにちは。とりかつです。
今回も前回に引き続き冷蔵庫管理アプリ「RefMA」 の改修に関する記事です。
今日は前回の記事で予告していた通りRoomとかデータレイヤーの実装で詰まったこととか便利だったことを紹介します。以下は今回の記事のトピックです。

  • DB設計の日付問題
    • 日付はString型?
    • java8で出たLocalDateTimeとかのお話
    • Roomは任意の型を使えちゃう??
  • Roomの細かい設定について
    • 外部キーの設定
    • Entity,Daoの使ったほうが良さそうなやつ
  • Kotlinコルーチンについて
    • コルーチンって何?
    • コルーチンの実装方法

素人なりにいろいろ調べてみたのですが間違ってる部分があったら教えていただけると助かります。

DB設計の日付問題

 以前このアプリ を開発していた際に日付を扱っていたのですが型の変換とかオーバーフローとかの問題でかなり苦しめられました。なので今回は日付の型定義をしっかり詰めて行こうと思います。 特に知見もないので手探りにはなりますが自分なりに答えを見つけてみました。  以下は最終的に決まった食品のテーブル設計図です。(図の書き方あってるかな)

f:id:torikatsu923:20191113131125p:plain

日付はString型?

 データベースで日付って何で扱ったらいいんでしょうか。 なんとなくミリ取得してLong型に落とし込めばいいかなって思ったんですけども

  • オーバーフローが怖い
  • Long ↔️ 日付の型、パースした文字列の変換が面倒・キャストしまくるのが怖い

って理由からLong型は避ける方向にしました。
とりあえずいろいろな記事みてた結果、日付系のクラスをStringになんとかキャストして扱う方針で落ち着きました。

java8で出たLocalDateTimeとかのお話

日付系のクラスを使うことになったんですけどjavaって日付関係のクラスめっちゃややこしいんですよね。
日付の型はCalendarがあって、書式変更にはSimpleDateFormatつかって、他の型に直すときになぜかDateを経由させないといけなかったり…
はっきりいってめちゃめちゃややこしいです。
 滅入りながら調べ続けているとjava8で新しく追加された日付関係のクラスを見つけました!

  • java.time.LocalDateTime
  • java.time.ZonedDateTime
  • java.time.OffsetDateTime
    • オフセット付きの日時。
    • 例:2015-12-15T23:30:59.999+09:00

引用: Java8の日時APIはとりあえずこれだけ覚えとけ

ちなみに変換にはDateTimeFormatterを使っとけ場便利らしいです。
とにかく便利そうなのでここら辺を使うことにしました。
今回のアプリでは賞味期限を扱うので年月日だけで良さそうなのでLocalDateTimeが内包しているDateTimeを採用しました。

Roomは任意の型を使えちゃう??

 RoomのEntity作ってるときに思ったんですけども、Entityの宣言って普通に変数名と型宣言してるだけですよね。ならもしかしたらDateTimeとか任意の型を使えちゃうんじゃないかな?って思ったらそのまさかでした。
 厳密にはStringとかにキャストしているんですけども、そのキャストをそこまで意識しなくても実装できるって点がとても便利だと思います。  実装にはRoomのTypeCoverterってやつを使います。
実装手順は

  • とりあえずクラスを作成。名前はHogehogeConverterが良さそうです
  • 宣言したクラス内で先頭に@TypeConverterを宣言したメソッドを定義
  • このメソッドは引数と戻り値が1個づつのものでなければダメらしいです

以下は実装例です。自分はLocalDate↔️Stringで変更するので以下のように実装しました。

 internal class LocalDateConverter {
     /**
      * LocalDate → フォーマット文字列
      * フォーマットはyyyy-MM-dd(デフォルト)
      * */
     @TypeConverter
     fun fromLocalDate(localDate: LocalDate): String {
         return localDate.toString()
     }

     /**
      * フォーマット文字列 → LocalDate
      * フォーマットはyyyy-MM-dd(デフォルト)
      * */
     @TypeConverter
     fun toLocalDate(stringDate: String): LocalDate {
         return LocalDate.parse(stringDate)
     }
 }

まずこのConverterは外部から利用することがないのでinternalで宣言してあります。もしかしたらnullチェックいるかもしれませんが、UseCaseでバリデーションをしっかりするつもりなので、nullが渡ってくるケースが思い浮かばないため一旦保留にしてあります。
 これでConverterの実装は完了です。結構簡単ですね。ちゃんと変換されるか心配だったためテストをしておきました。

つぎにこのConverterを設定する必要があるので設定をします。 Roomでは実装する際に

  • Entity(テーブル的なやつ)
  • Dao(DataAccessObject)
  • Database

の3つのクラスを実装する必要がありました。詳しくは前回の記事をみてみてください。

この3つのうちDatabaseにConverterを設定します。  以下は実装例です。

@Database(entities = [Food::class, Category::class], version = 1)
@TypeConverters(LocalDateConverter::class)
abstract class MyDatabase: RoomDatabase() {

    ・・・・・・

}

2行目に注目してください。2行目で

@TypeConverters(Converterのクラス名::class)

でConverterを設定しています。これだけです。 ここでの注意ですがDatabaseに宣言するアノテーションTypeConverterではなくTypeConvertersです。最後にsがあるかどうかの違いですが、入力補完使ってるとうっかりTypeConverterを宣言してしまいエラーに気づくまで時間取られちゃいます。私はここで1時間ぐらいハマりました笑

 これで実装は完了です。結構簡単で便利そうですよね。

Roomの細かい設定について

前回と今回でRoomについてざっと触れてきましたが、紹介してきたもの以外にもいろんな設定とかあります。本項ではその中でもこれ使ったほうがいいんじゃない?みたいなものをざっくり紹介します。

外部キーの設定

 RoomはSQLiteのラッパーライブラリです。SQLiteRDBMSなので外部キーとかを設定できます。食品テーブルの外部キーがカテゴリーテーブルのIDになります。以下は二つのテーブルの関係のイメージです。

f:id:torikatsu923:20191114151830p:plain
テーブル関係イメージ

この関係に沿って外部キーを設定していきます。以下は実装例です。 CategoryクラスはEntityになります。

@Entity(tableName = "foods",
        foreignKeys = arrayOf(ForeignKey(
            entity = Category::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("categoryId")
        ))
)
data class Food (
    @PrimaryKey
    var id: Int,

    var categoryId: Int,

    ・・・・・・
)

実装手順としては@Entityの引数?内でforeignKey

  • Entity = エンティティクラス::class
  • parentColums = arrayOf(親テーブルでの外部キーの変数名)
  • childColumns = arrayOf(子テーブルの外部キーの変数名)

の3つの要素を定義してあげる感じです。おそらくこれで実装はOKです。

Entity,Daoの使ったほうが良さそうなやつ

 個人的にこれは使ったほうがいいって思ったものとして Entityでは

  • @Entity(tablename = hoge)
    • テーブル名は自分で決めたほうがどっかで変なバグとかを防げそう
  • @ColumnInfo(name = "任意のカラム名")
    • 変数名がスモールキャメルとかの時にカラム名を指定しておくことで思わぬところでカラム名の違いにハマらずに済みそう

Daoでは - @Insert(onConfloict = Roomで用意された定数) - コンフリのハンドリングがこれだけで済むから便利そう - 思わぬ挙動を防げそう

こんなものが挙げられます。公式とか見るともっとあるので必要に応じて積極的に使っていくのがいいかと思います。

Kotlinコルーチンについて

コルーチンって何?

Qiitaの記事によると以下のようにありました。

Coroutineとは「特定のスレッドに束縛されない、中断可能な計算インスタンス」です。 非同期処理で用いられますが、Threadよりも軽量で、実行途中で処理を中断・再開することができます。

引用: 【Kotlin】Coroutineを理解する

制御できる非同期処理みたいなやつですね。コルーチンって言葉は至る所で聞くので積極的に使って慣れていこうってことで、今回の改修では採用することにしました。

コルーチンの実装方法

実装はめちゃシンプルです。 Daoのメソッドの先頭にsuspendを宣言するだけです。
以下は実装例です。

@Dao
interface HogeDao {

    @Insert
    suspend fun insert(hoge: Hoge)

    @Update
    suspend fun update(hoge: Hoge)

    @Delete
    suspend fun delete(hoge: Hoge)
}

簡単ですね(仕組みは難しそう)後注意ですがsuspendを宣言したメソッドを呼び出す時、そのメソッドもsuspendでなければダメなようです。またコルーチンについて理解が深まったらまとめようと思います。

おわりに

 今日も結構ボリューミーな内容になってしまいました。次回ではRoomでのLiveDataの使い方についてまとめようと思います。 また何か間違っているところがあればコメントにて指摘していただければ幸いです。

参考文献

Room使ってデータレイヤーを実装してみた

はじめに

こんにちは!とりかつです。
前回に引き続き冷蔵庫管理アプリ「RefMA」 の改修に関する記事です。
今日はRoomを使ったデータレイヤー(緑色の部分)の実装をしていきます。

f:id:torikatsu923:20191113123158p:plain
設計

思ったより記事のボリュームがやばくなりそうなので何回かに分けていこうと思います。
今回紹介する内容は

  • Roomについて
    • Entity(テーブルっぽいもの)の作成
    • Dao(Data Access Object)の作成
    • RoomDatabaseを使って実装
  • Repositoryの実装

です。どれも自分なりに理解したものなので間違ってたらすみませんm( )m
LiveDataの実装はまた次回しようと思いますのでよろしくお願いします。 あと言い忘れてましたがKoitlinで開発していきます。

Roomについて

AndroidDevelopersみると

Room永続化ライブラリは、SQLiteののパワーをフルに活用しながら、より堅牢なデータベース・アクセスを可能にするためのSQLiteの上に抽象化レイヤーを提供します。

ってありました。とりあえず便利だからつかっとけばOKって認識でいいと思います。あとRoomはLiveData使えるらしいので使わない手はないですね!

f:id:torikatsu923:20191113142351p:plain

Roomのアーキテクチャです。RoomDatabaseを介してDaoを取得、DaoからEntityを取得、更新するって感じですね。

Roomの実装は大きく分けて以下の工程に分かれます。

  • build.gradleに依存関係の設定
  • Entity(テーブルっぽいもの)の作成
  • Dao(Data Access Object)の作成
  • RoomDatabaseを使って実装

では早速実装していきましょう!

build.gradleに依存関係の設定

build.gradleに依存関係を設定していきましょう

apply plugin: 'kotlin-kapt'

dependencies{
    def roomVersion = '2.2.1'
    implementation "android.arch.persistence.room:runtime:$roomVersion"
    kapt "android.arch.persistence.room:compiler:$roomVersion"
    androidTestImplementation "android.arch.persistence.room:testing:$roomVersion"
}

Roomの最新バージョンは公式に載ってるので確認しましょう。

Entity(テーブルっぽいもの)の作成

Entityはプロパティしか持たないのでdataクラスで宣言するといいと思います。以下のコードスニペットはEntityの実装例です。

@Entity
data class Hoge (
    @PrimaryKey
    var id: Int,

    var name: String
)

めっちゃシンプルですね。

  • クラスの先頭に@Entityを宣言します
  • 主キーにするものは@PrimaryKeyを変数の前に宣言します。

これだけです。

Dao(Data Access Object)の作成

Daoはinterfaceで宣言します。以下は実装例です。

@Dao
interface HogeDao {
    @Query( "select * from hoges")
    suspend fun getAllHoges() : List<hoge>

    @Query("select * from hoges where id = :id")
    suspend fun getHoge(id: Int) : Hoge

    @Insert
    suspend fun insert(hoge: Hoge)

    @Update
    suspend fun update(hoge: Hoge)

    @Delete
    suspend fun delete(hoge: Hoge)
}

実装の手順は@Daoを先頭に宣言してあとは必要なメソッドを書いてくだけです。メソッドの先頭には以下のアノテーションをつけなければならないです。

  • クエリ:@Query( / SQL/ )
  • 挿入:@Insert
  • 更新:@Update
  • 削除:@Delete

@Queryでメソッドの引数(fun getHoge(id: Int))を使いたい時は、SQL文でid = :idみたいに: を変数の前につけてあげればOKです。
@DeleteではHogeクラスを渡しているだけで特にSQL書いたりしてないですが、どうやらデフォルトで主キーで判別してくれるらしいです。
@Insertとかの設定?でコンフリクトのハンドリングできるらしいですがまたいつかまとめようと思います。

RoomDatabaseを使って実装

いよいよRoom実装も終わりが見えてきました。Databaseの実装は以下のコード通りやれば良さそうです。

@Database(entities = [HogeDao::class], version = 1)
abstract class MyDatabase: RoomDatabase() {

    abstract fun hogeDao(): HogeDao

    companion object{
        @Volatile
        private var instance: MyDatabase? = null
        private const val databaseName = "hoge.db"

        fun getInstance(context: Context): MyDatabase =
            instance ?: synchronized(this) {
                Room.databaseBuilder(context,
                    MyDatabase::class.java, databaseName)
                    .build()
            }
    }
}

Roomでは抽象クラスでデータベースクラスを定義しておけばBuilderが勝手にクラスを作成してくれるらしいです。さすがBuilder...
実装手順としては

  • クラスをabstractで宣言する
  • クラスの先頭に@Database(entities = [さっき作ったDao::class], version = 1)って宣言する(ここで宣言するversionはマイグレーションとかで使うらしいです)
  • クラス内にさっき作ったDaoを戻り値とする抽象メソッドを定義
  • companion objectを宣言してその中でインスタンスを生成する処理を書く

って感じです。どうやらインスタンスの生成はsynchronizedを宣言しておくといいらしいです。

とりあえずこれでRoomの実装は終わりました!

findAll()を呼び出したかったら以下のようにすれば呼び出せます!

MyDatabase.getInstance(context).hogeDao().findAll()

Repositoryの実装

 最後にRepositoryの実装を紹介します。まだ理解が完全ではないので間違ってたらすみません。

そもそもリポジトリってなんだ?

Qiitaの記事によると

Repositoryパターンとは永続化を隠蔽するためのデザインパターンで、DAO(DataAccessObject)パターンに似ていますが、より高い抽象度でエンティティの操作から現実の永続化ストレージを完全に隠蔽します 例えばDBコネクションやストレージのパス等はReposiotoryのインターフェースからは隠蔽され、Repositoryのユーザは永続化ストレージが何であるか(例えばMySQLやRedis等)を意識することなく保存や検索の操作を行うことができるようになります。

引用:やはりお前たちのRepositoryは間違っている

とありました。
 おそらく生のSQL文書いたりデータベースにアクセスするためのインスタンスを取得したり...とかの処理を隠蔽してfindAll()するだけで全件取得できちゃうぜみたいな設計のことだと思います。
 今回の改修では関心の分離が大きなテーマの一つだったので使わない手はないですね!

Repositoryの実装

 データベースアクセスは先に紹介したように

MyDatabase.getInstance(context).hogeDao().findAll()

こうすればいいわけですがこれってなんだか毎回書いてたらデータベースアクセスとビジネスロジックが混在してコードがぐちゃぐちゃになりそうですよね。ってことなのでデータベースアクセスの処理をRepositoryに書いて責務を分離しようと思います。

class HogeRepository(val context: Context) {
    private val hogeDao: HogeDao by lazy { MyDatabase.getInstance(context).hogeDao() }

    fun getAll(): List<Hoge> {
        MyDatabase.getInstance(context).hogeDao().findAll()
   
    ......
    }
}

こんな感じにしてみました!これならデータベースアクセスとビジネスロジックが綺麗に分けられてて良さそうですね!  Daoを取得する際にby lazy { ・・・ }ってしましたが、これは使うときになったら取得する方がなんとなく良さそう()って理由と出来るだけnullableな変数を宣言したくない理由からこうしました。lateinitも使えそうですがこっちの方がコードがスッキリしますよね。

おわりに

 今回はRoomの使い方とRepositoryの実装をざっくりまとめてみました。次回は実際に自分が実装している中で詰まったこととか、これ便利だなって感じたものを紹介したいと思います。

参考文献とか

アプリの改修方針

アプリの改修方針について

はじめに

こんにちは。管理人のとりかつです。
冷蔵庫管理アプリ「RefMA」
↑こいつを改修していくわけですが、アプリの現状と方針をざっくり紹介したいと思います。

現在のアプリの問題点

一応ソースコード をあげておきます。
正直、問題点しか見当たりませんがとりあえず

この辺について改修を進めてきたいと考えています。 いまのアプリは単一モジュールで、MVVMとかの存在は一切なく神Activityのみで構成されてる感じです。神Activityが集まってできているんで神話みたいなアプリですね()
おかげで保守性のかけらもなくバグもたくさんあります。 とりあえずこいつを堅牢なアプリにしていろんな人に使ってもらいたいってのが最終目標です。

設計とかアーキテクチャとか

まずはじめに設計から手をつけてきたいと思います。

f:id:torikatsu923:20191112175310p:plain
設計

まず手をつけるところ

公式にもあったんですけども各レイヤーは下の要素のみに依存するということだったのでなんとなくデータレイヤーから手をつけていきます!
今回はデータバインディング使いたいということなのでLiveDataを扱える(らしい)Roomをデータベースに採用したいと思います。

おわりに

今回はざっくりとした方針についてまとめました。
次回以降ではRoom使ったデータレイヤーの実装をゴリゴリ進めていきたいと思います!

はじめまして!

ブログを始めました!

はじめに

はじめまして!ブログ管理人のとりかつです。
私は大学生でITエンジニアを目指しているのですが改めてアウトプットが大事だと感じたのでブログを書くことにしました。
このブログでは以前私がリリースした冷蔵庫管理アプリ「RefMA」の大幅改修をメインテーマとして書いていくつもりです。
一応JSとかJavaとかKotlinとか触ったことありますがRxJavaとかMVVMとかさっぱりのプログラミング素人です。()
内容はかなり素人向けになるのでもしAndroid開発の勉強始めたよ!って人いたら一緒に勉強していきましょう!

追記

ツイッターやってるんでもしよければぜひ! twitter.com