decochのブログ

フリーランスのiOSエンジニア decoch のブログです

アプリ起動をショートカットする

仕事をしていると色々なアプリを起動する。アプリの切り替え(⌘ + Tab)だと次に作業したいアプリが何個前に開いていたか覚えておらず、切り替えに手間取りストレスを感じるため解決策を考えた。

f:id:decoch:20210419164801p:plain

BetterTouchToolというアプリを使うとショートカットキーを定義して指定したアプリを起動することができる。

f:id:decoch:20210419164513p:plain

こうすることで

  • カレンダーで予定を確認したい -> option + C
  • Android Studio でコードを書きたい -> option + A

のように流れるように作業ができる。

新しいキーボードショートカットを覚えるのは大変なのでなるべく option + アプリの頭文字にしている。

仮想デスクトップを使うことも考えたが、新しく開いたアプリが現在のデスクトップに表示されてデスクトップを整理するのが手間だったので断念した。

ヘッドホン Bose Noise Cancelling Headphones 700

f:id:decoch:20210419162917j:plain

f:id:decoch:20210419162803j:plain

これまでイヤホンを使っており、よく内耳炎になっていたので思い切って高級ヘッドホンを購入した。
デザインがよく、ノイズキャンセリングの性能もよくほとんど雑音が聞こえない。

集中したいときやZoomでの会議に使っていて非常に満足している。
頭の形がおかしいのか、スポンジが硬いのか3時間ほど使うと頭頂部が痛くなる。

Firestore × Flutterでリアルタイムにデータ変更を検知する

Flutter と Firebase を使ってリアルタイムのアプリを開発する方法を説明していきます。

firebase.flutter.dev

ライブラリの追加

pubspec.yaml を変更して追加します。

dependencies:
  flutter:
    sdk: flutter
  firebase_core: "^0.7.0"
  cloud_firestore: "^0.16.0"

ドキュメントの変更イベントを検知する

snapshots() を使うことで変更の検知できます。

1つのドキュメントの場合

class Example1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final example = FirebaseFirestore.instance.collection('examples').doc('id');

    return StreamBuilder<DocumentSnapshot>(
      stream: example.snapshots(),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return const Text('Something went wrong');
        }

        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Text('Loading');
        }

        return Text(snapshot.data['title'] as String);
      },
    );
  }
}

複数ドキュメントの場合

class Example2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final examples = FirebaseFirestore.instance.collection('examples');

    return StreamBuilder<QuerySnapshot>(
      stream: examples.snapshots(),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return const Text('Something went wrong');
        }

        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Text('Loading');
        }

        return ListView(
          children: snapshot.data.docs.map((document) {
            return ListTile(
              title: Text(document.data()['title'] as String),
              subtitle: Text(document.data()['subtitle'] as String),
            );
          }).toList(),
        );
      },
    );
  }
}

doc メソッドでドキュメントを指定するかどうかの違いだけで基本的には同じように書くことができます。

スナップショット間の変更の表示をする

新しいデータだけでなく現在表示している項目と比較したい場合、docChanges を利用すると実現することができます。

class Example3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final examples = FirebaseFirestore.instance.collection('examples');

    return StreamBuilder<QuerySnapshot>(
      stream: examples.snapshots(),
      builder: (context, snapshots) {
        if (snapshots.hasError) {
          return const Text('Something went wrong');
        }

        if (snapshots.connectionState == ConnectionState.waiting) {
          return const Text('Loading');
        }

        return ListView(
          children: snapshots.data.docChanges.map((change) {
            return ListTile(
              title: Text(change.doc.data()['title'] as String),
              subtitle: Text(change.doc.data()['subtitle'] as String),
            );
          }).toList(),
        );
      },
    );
  }
}

この場合、追加、変更、削除のいずれかの変更がFirestoreに行われています。 変更した種類に合わせて List のデータの更新処理を記述する必要があります。

/// An enumeration of document change types.
enum DocumentChangeType {
  /// Indicates a new document was added to the set of documents matching the
  /// query.
  added,

  /// Indicates a document within the query was modified.
  modified,

  /// Indicates a document within the query was removed (either deleted or no
  /// longer matches the query.
  removed,
}

Flutter開発を始めるときに最初にやっておくこと

Flutterのプロジェクトを立ち上げる機会があり、プロジェクト開始時にやっておいた方が良いと感じたものを列挙しました

目次

  • Widgetの学習
  • 静的解析
  • CI/CDの自動化
  • 状態管理方法の選定

Widgetの学習

Flutter開発をするにあたって、画面は提供されているWidgetを使って作っていきます。
iOS開発におけるUIKit(UILabelやTableView)のようなものです。
ある程度知らないと画面を組み立てられないですし、公式ドキュメントやYouTubeにまとまっているので一度目を通しておいた方が良いと思います。

flutter.dev

www.youtube.com

YouTubeは英語ですが、字幕をつけれますし動画があってわかりやすいのでオススメです。

静的解析

改行されていないことや、無駄なスペースがあるなどの本質的でないコードレビューをしなくて済むように最初に静的解析を入れておくと良いです。

Flutterではプロジェクト直下に analysis_options.yml を作成し、 flutter analyze コマンドを実行すると静的解析できま、細かい指摘だけでなくdart Flutter のより良い書き方も指摘してくれます。

また自動フォーマットもやりたい場合は flutter format -n ./lib を実行すると自動フォーマットできます。

dart.dev

開発途中から静的解析を入れて直して行くのは、いろんな箇所に影響が出てしまい導入するのが大変なので、経験上プロジェクトを始めるタイミングで入れておいた方のが良いと思います。

CI/CDの自動化

静的解析で触れましたが、途中で入れづらいのがCI/CDの自動化です。
プロジェクト開始時にワークフローを定義しておいた方がいいです。

Flutter 開発では以下のようなワークフローがあると便利です。

  • 静的解析の実行 (flutter analyze, flutter format -n ./lib --set-exit-if-changed)
  • 単体テストWidgetテスト実行 (flutter test)
  • デプロイ

iOS開発の場合は、dSYMのアップロードも自動化しておきましょう。

状態管理方法の選定

まだ新しい手法が出てきているタイミングで、完全にこのパターンが良いというのは現状ありません。そのため、アプリの特性に合わせて選定する必要があります。 管理方法のパターンが公式ドキュメントにまとまっているのでこちらを参考に考えると良いと思います。

flutter.dev

Web support for Flutter のプロジェクトを GitHub Action で Firestore Hosting にビルド&デプロイする

年末休みの間に、Flutter Webを使ったサービスを作りFirebase Hosting で公開したので公開手順をまとめました。

Flutter Webでアプリ開発する

2020年12月時点だと、Flutter でWeb開発をするには beta 版を使う必要があります。

セットアップはFlutterの環境構築後に以下コマンドを実行することでが完了します。

flutter channel beta
flutter upgrade
flutter config --enable-web

flutter.dev

セットアップ後に flutter create myapp を実行すると以下のようなディレクトリ構造でアプリが作成されます。

> myapp tree -L 1
.
├── README.md
├── android
├── build
├── integration_test
├── ios
├── lib
├── myapp.iml
├── pubspec.lock
├── pubspec.yaml
├── test
└── web // これが作成されていること

Firebase Hostingの設定を行う

Firebase Hosting にデプロイを行うのでその設定を行います。

GitHub Actionsの設定もfirebase cliがやってくれるので先にリポジトリを作り以下コマンドを実行します。
※ あとで細かい設定は手動で直せるので一旦デフォルトの設定で作ってしまっても大丈夫です。

firebase init

生成された firebase.json を以下のように修正します。

{
  "hosting": {
    "public": "build/web", // Futterが書き出すディレクトリに変更
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

ここまでできれば、以下コマンドを実行することでデプロイできます。

flutter build web
firebase deploy

GitHub Actions でデプロイを自動化する

先程のコマンドを実行時にデプロイの設定をしておけばある程度自動生成してくれていると思います。 自動生成された yml ファイルは nodejs 用になっていると思うので以下のように修正をします。

name: Deploy to Firebase Hosting

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v1
        with:
          java-version: '12.x'
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '1.25.0-8.1.pre'
          channel: beta
      - run: flutter config --enable-web
      - run: flutter pub get
      - run: flutter build web
      # 以下は自動生成されたものをそのまま使う
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_XXXX }}' // Firebase init で生成された環境変数
          channelId: live
          projectId: xxxx // FirebaseのプロジェクトID
        env:
          FIREBASE_CLI_PREVIEWS: hostingchannels

以上でデプロイのワークフローを設定することができました。
この設定だと main ブランチにプッシュするたびに自動デプロイされるのでお好みのワークフローに改善して使ってください。

終わりに

多くの記事では firebase ci:login で生成した token を使ってデプロイする方法が出回っています。
これだと生成したユーザーとしてFirebaseのコマンドが実行されるため、生成した人がやめたときのことや、トークンの管理を考えるとプロジェクトに紐づく Service Account で行ったほうが良いので firebase init 時に生成された Service Accountを使うのが良いと思います。

2020年 買って良かったもの

ディスプレイとゲーミングチェア

decoch.hatenablog.com

decoch.hatenablog.com

リモートワークが増えたのですぐに購入しました。最初は知らないメーカーで不安もありましたが非常に費用対効果が高かったです。
ノートパソコンで作業をすると姿勢が悪くなりすぐに肩が痛くなるので助かっています。

Mac mini

www.apple.com

もともとMacBook Proを使っていたのですが、Xcodeでビルドすると時間が非常にかかり、発熱もすごかったのでMac miniを購入しました。 フルカスタマイズしたおかけでビルド時間が短縮でき、ビルド中に他の作業もできるようになり満足しています。
ただ購入直後にm1のMacが発売されたのでもう少し待ってもよかったのかなと少しだけ後悔しています。

USBスイッチ

https://www.amazon.co.jp/gp/product/B072R16Z6S/

一社はPCを支給していただく形で働いており、Mac miniと支給されたPCを切り替えることが多かったので、 USBスイッチを購入しました。 購入前はコードの抜き差しが必要だったのですが、ボタン一つでキーボードとマウスを切り替えられるようになりました。

ディスプレイの切り替えだけまだコードの抜き差しが必要なのでHDMIのスイッチも購入しようかと思っています。

Amazon Alexa

https://www.amazon.co.jp/gp/product/B07PHPYPYK/

Amazonのセールで購入したので¥3,500ぐらいで購入できました。
手が離せない時にスマホを出さずに料理のタイマーや買い物リストへの追加をすることができ便利です。 年末には Nature Remo を購入したのでエアコンやテレビの操作もAlexaでできるようになりました。

2020年の振り返り

仕事納めしたので1年を振り返ってみたいと思います。(今年から毎年振り返りをちゃんとしていきたい)

フリーランス

今年の2月末に体調を崩してしまい会社を退職後したのですが、すぐに新型コロナウイルスの影響がありました。
この影響で面接予定の企業との連絡が取りづらくなったり、案件がなくなったりとタイミングがよくなかったので、知り合いに相談したところ2件ほどお仕事を紹介していただきフリーランスとして働くことになりました。

今は iOS アプリの開発と Flutter でのアプリ開発をメインでしています。

この大変な時期に参画させてくださりありがとうございました。

仕事関係

上述の退職前は、バックエンド開発とiOSの開発を半々ぐらいで行ってきたキャリアだったのですが、 今年は、バックエンドはFirebaseメイン、アプリはiOS, Flutter をやっておりアプリ開発に特化した1年でした。

CI/CD構築や、技術選定など責任ある仕事も任せていただきました。

今後の主軸をアプリにしていこうと思っていたので、良い案件に参加させていただけたと思っています。

リモートワーク

5月以降はコロナウイルスの影響で完全リモートになりました。一人で集中できる環境が手に入ったのはよかったのですが、外に出れずストレスがたまることは多かったり時間を同期して働きづらいので仕事の効率はあまり変わらなかったと思います。

自宅で働く時間が増えたので、ゲーミングチェアやディスプレイを購入して働く環境の改善もすることができました。

HKCの34インチのウルトラワイドディスプレイを買った - decochのブログ

作業用にゲーミングチェアを買った話 - decochのブログ

また、出社しないので郊外に引っ越したのですが、部屋が広くなったり、大きなスーパーが近くにあったりと非常にQOLが上がりました。

来年の抱負

今年は前述のように体調を崩したので、のんびりと過ごす時間を多く作ることができたと思います。

来年は引き続きモバイルアプリ開発を中心に働いて行こうと考えていますが 今年も外出はあまりできそうにないので、体調を崩さないよう気をつけていきたいと考えています。