Edy's Hub

プログラミング・シェアハウス・ライフスタイルについて書き綴っています

個人開発しているRailsプロジェクトにCircleCIを導入する

はじめに

職場でCircleCIによるRspec及びrubocopの自動実行をしているので個人開発しているサービスにも導入してみようと思いました。

CircleCIのファイル編集とかは職場でもやったことが無かったので調査から始めてみます。

CircleCIの本家を当たってみる

作っているサービスは Ruby on Rails で運用しているため、CircleCiのRubyのガイドを読み進めてみます。

言語ガイド:Ruby - CircleCI

version: 2 # CircleCI 2.0 を使用します
jobs: # ステップの集合
  build: # Workflows を使用しない実行では、エントリポイントとして `build` ジョブが必要
    parallelism: 3 # このジョブのインスタンスを 3つ並列実行します
    docker: # Docker でステップを実行します
      - image: circleci/ruby:2.4.2-jessie-node # このイメージをすべての `steps` が実行されるプライマリコンテナとして使用します
        environment: # プライマリコンテナの環境変数
          BUNDLE_JOBS: 3
          BUNDLE_RETRY: 3
          BUNDLE_PATH: vendor/bundle
          PGHOST: 127.0.0.1
          PGUSER: circleci-demo-ruby
          RAILS_ENV: test
      - image: circleci/postgres:9.5-alpine # データベースイメージ
        environment: # データベースの環境変数
          POSTGRES_USER: circleci-demo-ruby
          POSTGRES_DB: rails_blog
          POSTGRES_PASSWORD: ""
    steps: # 実行可能コマンドの集合
      - checkout # ソースコードを作業ディレクトリにチェックアウトする特別なステップ

      # Bundler のバージョンを指定します

      - run:
          name: Bundler を指定
          command: bundle -v

      # バンドルキャッシュを復元します
      # 依存関係キャッシュについては https://circleci.com/docs/ja/2.0/caching/ をお読みください

      - restore_cache:
          keys:
            - rails-demo-bundle-v2-{{ checksum "Gemfile.lock" }}
            - rails-demo-bundle-v2-

      - run: # Ruby の依存関係をインストールします
          name: バンドルインストール
          command: bundle check || bundle install

      # Ruby の依存関係のバンドルキャッシュを保存します

      - save_cache:
          key: rails-demo-bundle-v2-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle

      # アプリケーションで Webpacker または Yarn を他の何らかの方法で使用する場合にのみ必要です

      - restore_cache:
          keys:
            - rails-demo-yarn-{{ checksum "yarn.lock" }}
            - rails-demo-yarn-

      - run:
          name: Yarn をインストール
          command: yarn install --cache-folder ~/.cache/yarn

      # Yarn または Webpacker のキャッシュを保存します

      - save_cache:
          key: rails-demo-yarn-{{ checksum "yarn.lock" }}
          paths:
            - ~/.cache/yarn

      - run:
          name: DB を待機
          command: dockerize -wait tcp://localhost:5432 -timeout 1m

      - run:
          name: データベースをセットアップ
          command: bin/rails db:schema:load --trace

      - run:
          name: RSpec を並列実行
          command: |
            bundle exec rspec --profile 10 \
                              --format RspecJunitFormatter \
                              --out test_results/rspec.xml \
                              --format progress \
                              $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)

      # タイミング解析のテスト結果を保存します

      - store_test_results: # テストサマリー (https://circleci.com/docs/ja/2.0/collect-test-data/) に表示するテスト結果をアップロードします
          path: test_results
      # デプロイコンフィグの例については https://circleci.com/docs/ja/2.0/deployment-integrations/ を参照してください

現状、開発しているプロダクトの構成は下記のとおりです。 - Dockerでweb, webpacker, dbコンテナをビルドする仕組み - ruby 2.6.6 - rails 6.0.0 - mysql 5.7

それぞれ対応するように修正していきます。

実際にファイルを作成してみた

version: 2
jobs:
  build:
    working_directory: ~/Hookah
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          RAILS_ENV: test
          DATABASE_HOST: 127.0.0.1
      - image: mysql:5.7
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes

    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-

      - run:
          name: Install bundle dependencies
          command: bundle install --path=vendor/bundle --jobs 4 --retry 3

      - run:
          name: Install yarn
          command: |
            yarn install

      - save_cache:
          paths:
            - ./vendor/bundle
            - ./node_modules
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      - run:
          name: Database Setup
          command: |
            bundle exec rake db:create db:schema:load

これでリモートリポジトリにプッシュしてみると、、 bundle exec rake db:create db:schema:load

の段階でエラーに。

bundle exec rake db:create db:schema:load
2020-05-23 16:22:30 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.
DEPRECATION WARNING: Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
  >> Coffee::Rails::TemplateHandler.call(template)
To:
  >> Coffee::Rails::TemplateHandler.call(template, source)
 (called from <top (required)> at /home/circleci/Hookah/Rakefile:6)
Unknown MySQL server host 'db' (-2)
Couldn't create 'hookah_test' database. Please check your configuration.
rake aborted!
Mysql2::Error::ConnectionError: Unknown MySQL server host 'db' (-2)

bundle update slim を実行

エラーメッセージでググってみると、どうやら gem 'slim' が原因になっていそうな。

qiita.com

ただ、bundle update をしても既に最新バージョンになっていた。

別のクエリでググってみると、 coffee-rails が怪しそうに。

github.com

coffee-rails のバージョンを 5.0.0 に上げるのが良さそうだったので変更。

すると、エラーが一つ消えたがさらにMySQL周りでエラーに...(面倒くさそうw)

2020-05-23 16:36:15 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead.
Unknown MySQL server host 'db' (-2)
Couldn't create 'hookah_test' database. Please check your configuration.
rake aborted!
Mysql2::Error::ConnectionError: Unknown MySQL server host 'db' (-2)

省略

この記事を見て、

CircleCIのmysqlイメージを用いるように変更を加えました。

teratail.com

しかし、結果は変わらず。

データベース名を削除

Unknown MySQL server host 'db' (-2)
Couldn't create 'hookah_test' database. Please check your configuration.

ここにある Unknown MySQL server host 'db' (-2) というのが気になったので調べてみると、config/database.ymlhost: db と定義していることを確認しました。 (1年以上前に記述していたので忘れていた・・・)

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: <%= ENV['DATABASE_NAME'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>

development:
  <<: *default
  database: hookah_development
  host: db

test:
  <<: *default
  database: hookah_test
  host: db

production:
  <<: *default
  database: hookah_production
  username: root
  socket: /var/lib/mysql/mysql.sock

そこで、こちらの記事を参考に、hostには localhost を割り当てるように変更しました。 こちらは環境変数DATABASE_HOST として 127.0.0.1 になるようにしました。

qiita.com

変更後

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: <%= ENV['DATABASE_NAME'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development:
  <<: *default
  database: hookah_development

test:
  <<: *default
  database: hookah_test

production:
  <<: *default
  database: hookah_production
  username: root
  socket: /var/lib/mysql/mysql.sock

完成版

こちらが初期段階として導入に成功したCircleCIのファイルです。 Ruby on Rails による開発を行うので、rubocopやRspecのjobを今後追加していこうと思います。

version: 2
jobs:
  build:
    working_directory: ~/Hookah
    docker:
      - image: circleci/ruby:2.6.6-node-browsers
        environment:
          RAILS_ENV: test
          MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
          DATABASE_HOST: 127.0.0.1
          DATABASE_USERNAME: root
      - image: circleci/mysql:5.7

    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-

      - run:
          name: Install bundle dependencies
          command: bundle install --path=vendor/bundle --jobs 4 --retry 3

      - run:
          name: Install yarn
          command: |
            yarn install

      - save_cache:
          paths:
            - ./vendor/bundle
            - ./node_modules
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      - run:
          name: Database Setup
          command: |
            bundle exec rake db:create db:schema:load

以上で導入を終了します。 当初予定していたrubocopやRspecは未対応ですが、基盤ができたので一旦これにて完了とします。

Reactを動かしてみる【準備編】

概要

社内でReactを用いた新規事業がスタートしたので、これを機に今までちゃんと勉強してなかったツケを回収する。

学習教材

Boothで購入したPDFをもとにサクッと進めていく。 すべて含めて200ページ弱なので週末で終わるかな。

booth.pm

作業手順

  • node, npm, yarnのインストール
  • React Appの作成

node, npm, yarnのインストール

npm と node をインストールしておきます。 筆者はすでに導入済みでした。

Nodeのバージョン管理に anyenv を用いるのでインストールを推奨します。

qiita.com

シェルは bash を使用しています。

$ brew install anyenv
$ echo 'eval "$(anyenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l
$ anyenv install --init
...
Completed!
$ node -v
v13.12.0
$ npm -v
6.14.4

この後は、yarn をインストールします

$brew install yarn
$yarn -v
1.22.4

React Appの作成

ここからは実際にアプリケーションの作成に移ります。 適当なワーキングディレクトリで下記コマンドを打ち込みましょう。

$ npx create-react-app hello-world --typescript

npx は npm のパッケージをいちいちインストールしなくてもそのバイナリが実行できるコマンドです。

完了したら作成したプロジェクトに移動して yarn start と入力します。

$ cd hello-world
$ yarn start

Chromeのブラウザ上にアプリが表示されれば成功です。

f:id:yuki-eda0629:20200415224131p:plain
ブラウザに表示されたアプリケーション

今回は一旦ここまでにしておきます。

続きは別記事で書いていこうと思います。

コーディングトレースにチャレンジ

背景

普段は業務でサーバサイドエンジニアとして従事しているため、マークアップを0から行うことがほとんどありません。

ただ、Web開発に関わる人間としてフロントエンドの知見も多少なりとも保持しておきたいと思い、HTML/CSSを復習してみようと思いました。

前提として、HTMLのタグ、CSSのプロパティについての知識はあります。

何をやるか?

世に公開されているWebサイトをトレース(= 模写)しようと思います。

そして、ただデザインをトレースするのではなく、HTML/CSSを書いてマークアップをしてみます。

コーディングトレースを経て、そのサイトがどんなデザイン・構造のもと作成されているのかを理解していきます。

下準備

事前に過去にコーディングトレースをしてきた方々がどんな方法を用いて、コーディングをされてきたのかを調べておきます

※ 本記事は実際にコーディングはせず、調査のみになります。

やること

必要なファイルを用意します
  • index.html
  • main.css
フォントがわかる拡張機能を追加

フォントもこだわるべきではあるのですが、文字を睨めっこしてフォントを探り当てるのは本質的ではないので、一旦今回はGoogle Chrome拡張機能「WhatFont」に頼ることにします。

WhatFont - Chrome ウェブストア

画像の一括ダウンロードツールを追加

フォントに加え、画像のダウンロードも時間短縮したいものです。 今回は、サイト上の画像を一括保存できるGoogle Chrome拡張機能「Image Downloader」で画像を一気にダウンロードします。

ただし、この拡張機能は背景として用いられている画像については保存が出来ないようになっています。

その際は、デベロッパーツールなどでコードを確認して保存する必要があります。

Image Downloader - Chrome ウェブストア

色の抽出ツールを追加

さらに色の判別もGoogle Chrome拡張機能におまかせします。

用いるのは、「ColorPick Eyedropper」です。

ColorPick Eyedropper - Chrome ウェブストア

参考サイト

shibaken.org

haniwaman.com

新卒2年目エンジニア記事Pick【4月上旬編】

働き方

転職エントリ①

dev.classmethod.jp

  • リリースサイクルが速い
    • 1週間に1度のアプリリリース...
    • 開発速度の視座が上がる
  • 在籍中は約640本の記事を書きました、というコメントがさらっと書かれてある
  • 総じてアウトプット量によって社内外でのポジションを確立した感じがある

転職エントリ②

yoshiyuki-kato.hatenablog.com

  • 4年働いたリクルートを退職した振り返り
  • 入社早々、アサインされるはずだった開発プロジェクトが吹き飛ぶ
    • 入社早々社内ニートになるというスタート
  • 2年目まではフロントエンド→バックエンド→パフォーマンス・チューニング
  • その後、組織と技術の関係性に興味を持つ
  • 最後に社内の新規事業にアサインされる
  • しかし、ご自身も含め、チームの「熱量」の不足がネックになる
  • 2020年4月からリクルート同期が立ち上げた会社に転職した
  • 人間の営みに即した仕組みを伴う良いシステムを今後も追求していく予定

技術

フロントエンド①

note.com

  • jQueryやDOMを知っていればいいという世界観ではなくなっている
    • それは自分も感じる(肌感覚で2019年はVueがプロジェクトに採用されることに驚きを感じなくなった)
    • モダンウェブ技術を知っていれば、静的ページの生成にも応用が効く
  • Reactが提供した最大の功績は概念でありソフトウェア世界観
  • 文字数多めだったのでちゃんと読むと15分くらい掛かる
  • フロントエンドに関わる人もソフトウェア設計論をガッツリ理解して運用できることが求められる世界が到来している
    • フロントエンドでは The Clean Architecture よりも派生形である VIPER の方が馴染みやすいかも
  • 記事末尾のおすすめ記事が良質

記事中で登場したprismaはこちら

www.prisma.io

フロントエンド②

blog.cybozu.io

  • レガシなフロント技術をモダンな技術に移行していくチャレンジのお話
  • すべてを一気に最新技術に置き換えるのは現実的ではない
  • デグレを起こすことなく、問題を解決して開発体験の向上が図れるかを重要視

ブログ運用

ブログに対してのスタンス

www.specializedblog.com

  • ブログだけに依存するのは危険
    • プラットフォームに関係なくファンがついている状態を作ろう
  • ブログを収入媒体と捉えるのではなく、集客媒体とみなす
  • フロントエンド(集客媒体)とバックエンド(収入媒体)は別々で用意する

RailsでZendeskAPIを使ってチケット情報を取得する

準備

API TOKENの発行をしてください

ご自身の管理画面の /agent/admin/api/settings から発行可能です。

https://<your site>.zendesk.com/agent/admin/api/settings

手順

Gemの導入

gem "zendesk_api"

bundle install

configファイルを用意

class Zendesk
  def self.client
    @client ||= ZendeskAPI::Client.new do |config|
      # Mandatory:

      config.url = ENV['ZENDESK_URL']

      # Basic / Token Authentication
      config.username = ENV['ZENDESK_USERNAME']

      # authentication
      config.token = ENV['ZENDESK_TOKEN']

      # Optional:

      # Retry uses middleware to notify the user
      # when hitting the rate limit, sleep automatically,
      # then retry the request.
      config.retry = true

      # Raise error when hitting the rate limit.
      # This is ignored and always set to false when `retry` is enabled.
      # Disabled by default.
      # config.raise_error_when_rate_limited = false

      # Logger prints to STDERR by default, to e.g. print to stdout:
      require 'logger'
      config.logger = Logger.new(STDOUT)

      # Changes Faraday adapter
      # config.adapter = :patron

      # Merged with the default client options hash
      # config.client_options = {:ssl => {:verify => false}, :request => {:timeout => 30}}

      # When getting the error 'hostname does not match the server certificate'
      # use the API at https://yoursubdomain.zendesk.com/api/v2
    end
  end
end

環境変数の設定

.env で管理しています。

ZENDESK_URL=https://sample.zendesk.com/api/v2
ZENDESK_USERNAME=login.email@zendesk.com
ZENDESK_TOKEN=TOKEN

ZENDESK_TOKEN は冒頭で取得したトークンを割り当ててください。

コンソールで疏通確認

> client  = Zendesk.client
> client.tickets
# => チケット一覧を返します

FlutterのサンプルAppのコードを読んでみる

はじめに

Flutterのアプリのセットアップが完了したら、AndroidStudioからアプリケーションを新規作成します。

File > New > New Flutter Project...を選びます。

一番左にある「」を選択すると自動でmain.dartを伴ったサンプルアプリが誕生しました。

f:id:yuki-eda0629:20191202003124p:plain
New Flutter Project

元々のコード

こちらが自動生成されたコードになります。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'ボタンを押した回数が記録されます',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

RunしてみるとAndroidバイスエミュレーターが立ち上がります。

f:id:yuki-eda0629:20191202003317p:plain
ボタン押下数をカウントするシンプルなアプリ

コードの読み解き

さてアプリケーションをさくさく作り進めていきたいところですが、Dartについては全くの未経験者なので一度立ち止まってコードの理解に勤しむことにします。

import

import 'package:flutter/material.dart';

冒頭にあるimport文は外部ライブラリを呼び出しを意味しています。

ライブラリファイルをインポートするときには、上記のようにファイルの URI を指定するために package: ディレクティブを使うことができます。

packages:パッケージ名/公開ライブラリファイル の形式でimportすると、外部ライブラリを読み込む事ができます。

メイン関数

void main() => runApp(MyApp());

voidは、「何も返さない」という意味で戻り値がない場合に用います。

main.dart ファイルを実行すると呼ばれるいわゆるエントリーポイントとなる関数が main 関数です。

アプリを起動するための処理を書くことになります。

MyAppクラス

次にクラスが登場します。

コメントアウトは英語を和訳して下部に日本語を書いていきます。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  // このウィジェットはアプリケーションのルート(大元)です。
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        // これがあなたのアプリケーションのテーマとなります。
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        // "flutter run"というコマンドを実行してみてください。
        // すると青いツールバーが目に入るでしょう。
        // アプリケーションを停止せずに、下のprimarySwatch(メインカラーを司るプロパティ?)を
        // Colors.greenに変更してみてください。
        // そして"ホットリロード"を起動してください。
        // カウンターがゼロにならないことに気づくでしょう。
        // アプリケーションは再起動していないのです。
        
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

コメントアウトにあったようテーマカラーを変更してみます。

primarySwatch: Colors.green

見事全体のカラーが変更されました。

f:id:yuki-eda0629:20191202011546p:plain
primarySwatch: Colors.green

class MyApp extends StatelessWidget { ... }

次に、クラスに記載のあるextendsStatelessWidgetについてです。

MyAppクラスはStatelessWidgetextendsしています。

(これだけだとさっぱり分からない...)

簡単に説明すると、Flutterが用意しているWidgetには2種類あります。

それがStatelessWidgetStatefulWidgetです。

アプリの状態を表すStateを扱う場合はStatefulWidget、そうでない場合はStatelessWidgetを利用します。

StatelessWidgetの特徴

- StatelessWidgetを継承した1つのクラスで構成されている
- StatelessWidgetはbuildメソッドを持ち、Widgetもしくはテキストを返す
- 返すWidgetはStatefulWidget(MaterialAppとか)でもOK
- 「state(状態)」の概念がないため、動的に変化しないWidget

StatefulWidgetの特徴

- Widgetにstateの概念をいれて拡張したもの
- stateが変化したときに再描画を指示するメソッドが組み込まれている
- StateクラスとWidgetクラスの2つのクラスで構成されている
- StatefulWidgetはbuildメソッドを持たず、createStateメソッドを持ち、これがStateクラスを返す
- StatefulWidget自体はシンプルな構造で複雑な処理等はStateクラスで持つ
- Stateクラスで機能的なメソッドを実装する
- Stateクラスはbuildメソッドをもち、Widgetを返すときに定義したメソッドを
- アクションとして組み込める
- returnできるWIdgetはいくつも種類があり、TextやImage、TextInput、Scaffold等
- ScaffoldはルートページとなるデザインWidgetで基本的なレイアウト構造

参考:Flutterの基礎 - Qiita

ActiveRecordのenumで定義した要素を元にセレクトボックスを作成する

はじめに

ActiveRecordenumで定義した要素をセレクトボックスを作りたいときの用法用量です。 ※gemを追加しても同等のことができますがコード量も少ないので自作で進めます。

i18n(Internationalization)について

モデルのattributeのi18nは通常、'activerecord.attributes.#{model_name}.#{attribute}'に定義します。 例えば、Userモデルのnameに適用する場合には、config/locales/ja/activerecord/user.ymlのようなファイルを作成します。

ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前

取得時は User.human_attribute_name(:name) という形でアクセスします。

enumを管理する場合

本題に移ります。

今回は例として、Userがgenderという属性を定義しているとします。 genderにはfemaleとmaleというを値を持たせます。 - female(女性) - male(男性)

class User < ApplicationRecord
  enum gender: {
    female: 0,
    male: 1
  }
end

この後にしがちな(想定しそうな)yamlの定義の仕方としては下記のような記述でしょうか。

ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        gender: 
          female: 女性
          male: 男性

このようにuesr > gender > attributeの順にネストして直接定義することは出来ません。

この場合、以下のような形でモデルと同じ階層に定義してあげる必要があります。(http://guides.rubyonrails.org/i18n.html)

ja:
  activerecord:
    models:
      user: ユーザー
    attributes:
      user:
        name: 名前
        gender: 性別
      user/gender:  # model/attribute
        female: 女性
        male: 男性

定義方法に注目してください。

これにより下記のようなアクセスが可能になります。

User.human_attribute_name('gender.female')
# => 女性

User.human_attribute_name('gender')
# => 性別
# こちらも問題なく呼べる

Modelに便利メソッドを定義

基底モデルにenum専用のメソッドを追加していきます。 変更するファイルはapp/models/application_record.rbです。

class ApplicationRecord < ActiveRecord::Base

  self.abstract_class = true

  def self.human_attribute_enum_value(attr_name, value)
    human_attribute_name("#{attr_name}.#{value}")
  end

  def human_attribute_enum(attr_name)
    class.human_attribute_enum_value(attr_name, [attr_name])
  end

end

これで、以下のようにアクセスすることが出来ます。

user = User.new(gender: :female)
user.human_attribute_enum(:gender)
#=> 女性

User.human_attribute_enum_value(:gender, :male)
#=> 男性

form用のメソッドも追加

最後にselectに値を渡していきます。 特にメソッドを使わなくともこのように書くことも出来ます。

app/views/users/_form.html.slim

# テンプレートはslimを使用
# app/views/users/_form.html.slimの呼び出し元でUserオブジェクトのインスタンスをローカル変数で定義してあります

= form_for user do |f|
  = f.select :gender, User.genders.map {|k, _| [User.human_attribute_enum_value(:gender, k), k] }.to_h #=> {"女性"=>"female","男性"=>"male"}

メソッドを追加して記述するとすると下記のようになります。

app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base

  # ...

  def self.enum_options_for_select(attr_name)
    send(attr_name.to_s.pluralize).map {|k, _| [human_attribute_enum_value(attr_name, k), k] }
  end

end

※ このように使います。

User.enum_options_for_select(:gender) #=> {"女性"=>"female","男性"=>"male"}

最後にformに追加します。

app/views/users/_form.html.slim

= form_for @user do |f|
  = f.select :gender, User.enum_options_for_select(:gender)

これで [["女性", "female"], ["男性", "male"]]の組み合わせのセレクトボックスを生成することができました!