Using GraphQL with Ruby on Rails: 作業ログ

https://www.apollographql.com/blog/community/backend/using-graphql-with-ruby-on-rails/

apollo blogが一番わかりやすかったので見ながら作成

まずはpostgresqlをインストール。

sudo port install postgresql14

  postgresql14 has the following notes:
    To use the postgresql server, install the postgresql14-server port

sudo port install postgresql14-server

--->  Some of the ports you installed have notes:
  postgresql14-server has the following notes:
    To create a database instance, after install do
     sudo mkdir -p /opt/local/var/db/postgresql14/defaultdb
     sudo chown postgres:postgres /opt/local/var/db/postgresql14/defaultdb
     sudo su postgres -c 'cd /opt/local/var/db/postgresql14 && /opt/local/lib/postgresql14/bin/initdb -D /opt/local/var/db/postgresql14/defaultdb'

    A startup item has been generated that will aid in starting postgresql14-server with launchd. It is disabled by default. Execute the following
    command to start it, and to cause it to launch at startup:
    
        sudo port load postgresql14-server

上記のコマンドを上から流す。そうすると最後のコマンドのログはこんな感じ

$ sudo su postgres -c 'cd /opt/local/var/db/postgresql14 && /opt/local/lib/postgresql14/bin/initdb -D /opt/local/var/db/postgresql14/defaultdb'
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "ja_JP.UTF-8".
The default database encoding has accordingly been set to "UTF8".
initdb: could not find suitable text search configuration for locale "ja_JP.UTF-8"
The default text search configuration will be set to "simple".

Data page checksums are disabled.

fixing permissions on existing directory /opt/local/var/db/postgresql14/defaultdb ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Asia/Tokyo
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    /opt/local/lib/postgresql14/bin/pg_ctl -D /opt/local/var/db/postgresql14/defaultdb -l logfile start

最後のコマンドを実行してみる

$ /opt/local/lib/postgresql14/bin/pg_ctl -D /opt/local/var/db/postgresql14/defaultdb -l logfile start
pg_ctl: could not open PID file "/opt/local/var/db/postgresql14/defaultdb/postmaster.pid": Permission denied
$ sudo /opt/local/lib/postgresql14/bin/pg_ctl -D /opt/local/var/db/postgresql14/defaultdb -l logfile start
Password:
pg_ctl: cannot be run as root
Please log in (using, e.g., "su") as the (unprivileged) user that will
own the server process.

Permission deniedなのにsudoをつけるとエラーになる。

さっきやってなかったコマンド

~ takayuki$ sudo port load postgresql14-server
Password:
--->  Loading startupitem 'postgresql14-server' for postgresql14-server
$ sudo ls -l /opt/local/var/db/postgresql14/defaultdb/postmaster.pid
-rw-------  1 postgres  postgres  107 11 30 22:19 /opt/local/var/db/postgresql14/defaultdb/postmaster.pid

pidファイルはpostgresユーザなのは当たり前

~ takayuki$ /opt/local/lib/postgresql14/bin/psql -U postgres
psql (14.0)
Type "help" for help.

postgres=# 

接続はできるので、とりあえず、先に進む。

やっとここでrails new。とりあえず今回はかなり単純に実行 rails new rails_graphql_practice -d postgresql

こんなエラーが出て失敗

checking for pg_config... no
No pg_config... trying anyway. If building fails, please try again with
 --with-pg-config=/path/to/pg_config
checking for libpq-fe.h... no
Can't find the 'libpq-fe.h header

https://stackoverflow.com/questions/6040583/cant-find-the-libpq-fe-h-header-when-trying-to-install-pg-gem

古い記事ですが、これのMacPortsを参考にするとこんな感じ

$ sudo gem install pg -- --with-pg-config=/opt/local/lib/postgresql14/bin/pg_config
Password:
Building native extensions with: '--with-pg-config=/opt/local/lib/postgresql14/bin/pg_config'
This could take a while...
Successfully installed pg-1.2.3
Parsing documentation for pg-1.2.3
Installing ri documentation for pg-1.2.3
Done installing documentation for pg after 0 seconds
1 gem installed

成功

bundle installも成功

ここから先は先ほどの記事の内容を上からなぞっていきます。

models作成

rails g model Artist first_name last_name email
rails g model Item title description:text image_url artist:references

そして、seedsの追記やdb:migrateなど、なんやかんやありまして、 rails g graphql:object itemするところでエラー。

bundle add graphqlでGemfileにgraphiql-railsも追加されるが、別途bundle installしないとgemが追加されてなかったです。

また、rails g graphql:object -hすると最後の行に

Create a GraphQL::ObjectType with the given name and fields.If the given type name matches an existing ActiveRecord model, the generated type will automatically include fields for the models database columns.

と表示されるので、databaseの内容を直接読み出してくれてるみたいです。

item_type.rbをgenerateした後でartist fieldを追加する

item_type.rb: add field :artist · na8esin/rails_graphql_practice@843f9fa · GitHub

こんな感じです。ここは流石に手動で追加しないといけないみたいです。

rails webpacker:installはやっぱり必要

artist_type.rbも生成した後で、rails sを実行したところエラー。

rails new の時に--apiつければいらないのだろうか?

何はともあれ、この時点でrails sで起動して、 http://localhost:3000/graphiql にアクセスするとクエリを実行することができます。

f:id:ta_watanabe:20211223104439p:plain

ログはこんな感じで出力されます。

この時点だと、全てのItemのArtistがtaylorなので同じArtistがn回呼び出されてます。 キャッシュが使われているので要件によってはこれでいいのかもしれないですが、 できれば

https://graphql-ruby.org/dataloader/overview.html

のあたりが使いたいです。

自分の描いたソース

GitHub - na8esin/rails_graphql_practice: railsでgraphqlやってみる

m1 mac: rails6.1でbin/rails server が成功するまでの作業ログ

公式を見ながらインストールしていきます。

https://guides.rubyonrails.org/getting_started.html#installing-ruby

rubyのバージョン確認

~ takayuki$ ruby -v
ruby 2.6.8p205 (2021-07-07 revision 67951) [universal.x86_64-darwin21]

下記をみるとバージョンは大丈夫
https://guides.rubyonrails.org/getting_started.html#installing-ruby

念の為gemのバージョン

~ takayuki$ gem --version
Ignoring ffi-1.15.0 because its extensions are not built. Try: gem pristine ffi --version 1.15.0
3.0.3.1

flutterというか、podsでも登場したffi。指示通りのコマンドを打ってみる。

~ takayuki$ gem pristine ffi --version 1.15.0
Ignoring ffi-1.15.0 because its extensions are not built. Try: gem pristine ffi --version 1.15.0
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory.

sudoつけ忘れ

~ takayuki$ sudo gem pristine ffi --version 1.15.0
Password:
Ignoring ffi-1.15.0 because its extensions are not built. Try: gem pristine ffi --version 1.15.0
Restoring gems to pristine condition...
Building native extensions. This could take a while...
Restored ffi-1.15.0
~ takayuki$ gem --version                    
3.0.3.1

今度は成功。ちなみにターミナルはrossetaで動かしてます。 ffiのissuesを見ても、まだrossetaがないと動かなそう

https://github.com/ffi/ffi/issues?q=is%3Aissue+is%3Aopen+m1

bundler

~ takayuki$ sudo gem install bundler
Password:
Fetching bundler-2.2.31.gem
Successfully installed bundler-2.2.31
Parsing documentation for bundler-2.2.31
Installing ri documentation for bundler-2.2.31
Done installing documentation for bundler after 2 seconds
1 gem installed

リファレンスはここ

https://bundler.io/v2.2/guides/rails.html

そして、rails newでエラー

/Library/Ruby/Gems/2.6.0/gems/activesupport-6.1.4.1/lib/active_support/message_encryptor.rb:170:in `auth_data=': couldn't set additional authenticated data (OpenSSL::Cipher::CipherError)
    from /Library/Ruby/Gems/2.6.0/gems/activesupport-6.1.4.1/lib/active_support/message_encryptor.rb:170:in `_encrypt'
    from /Library/Ruby/Gems/2.6.0/gems/activesupport-6.1.4.1/lib/active_support/message_encryptor.rb:148:in `encrypt_and_sign'
    from /Library/Ruby/Gems/2.6.0/gems/activesupport-6.1.4.1/lib/active_support/encrypted_file.rb:88:in `encrypt'
    from /Library/Ruby/Gems/2.6.0/gems/activesupport-6.1.4.1/lib/active_support/encrypted_file.rb:61:in `write'
    from /Library/Ruby/Gems/2.6.0/gems/activesupport-6.1.4.1/lib/active_support/encrypted_configuration.rb:29:in `write'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/generators/rails/credentials/credentials_generator.rb:30:in `add_credentials_file_silently'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/generators/rails/app/app_generator.rb:194:in `credentials'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/generators/app_base.rb:165:in `public_send'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/generators/app_base.rb:165:in `build'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/generators/rails/app/app_generator.rb:386:in `create_credentials'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/command.rb:27:in `run'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/invocation.rb:127:in `invoke_command'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/invocation.rb:134:in `block in invoke_all'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/invocation.rb:134:in `each'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/invocation.rb:134:in `map'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/invocation.rb:134:in `invoke_all'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/group.rb:232:in `dispatch'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/base.rb:485:in `start'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/commands/application/application_command.rb:26:in `perform'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/command.rb:27:in `run'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor/invocation.rb:127:in `invoke_command'
    from /Library/Ruby/Gems/2.6.0/gems/thor-1.1.0/lib/thor.rb:392:in `dispatch'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/command/base.rb:69:in `perform'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/command.rb:48:in `invoke'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/lib/rails/cli.rb:18:in `<top (required)>'
    from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    from /Library/Ruby/Gems/2.6.0/gems/railties-6.1.4.1/exe/rails:10:in `<top (required)>'
    from /usr/bin/rails:22:in `load'
    from /usr/bin/rails:22:in `<main>'

https://github.com/ruby/openssl/blob/5c85b4385f114400d901ed7dd89ce43489b9bceb/ext/openssl/ossl_cipher.c#L577

https://github.com/openssl/openssl/blob/0f70d6013435308ada5d0eb662b31f370b07ebd7/crypto/evp/evp_enc.c#L401

ここまで深く読んでもよくわからないので、railsのバージョンを下げることに。

sudo gem uninstall rails
sudo gem uninstall railties -v 6.1.4.1

sudo gem install rails -v 6.0.4.1

でも6.0.4.1でもだめ。

rubyのバージョンを上げる。ためrvmをインストール。

https://rvm.io/

takayuki$ sudo port install gnupg2
Password:
Error: Current platform "darwin 21" does not match expected platform "darwin 20"
Error: If you upgraded your OS, please follow the migration instructions: https://trac.macports.org/wiki/Migration
OS platform mismatch
    while executing
"mportinit ui_options global_options global_variations"
Error: /opt/local/bin/port: Failed to initialize MacPorts, OS platform mismatch

MacPorts Migration

https://trac.macports.org/wiki/Migration

  • xcodeのバージョンはApp Storeで確認したので最新のはず
  • MacPortsのMonterey v12用のpkgをダウンロード。

Reinstall your ports

~ takayuki$ port -qv installed > myports.txt
~ takayuki$ port echo requested | cut -d ' ' -f 1 | uniq > requested.txt
~ takayuki$ sudo port -f uninstall installed
Password:
--->  Deactivating openjdk8 @8u292_0
--->  Cleaning openjdk8
--->  Uninstalling openjdk8 @8u292_0
--->  Cleaning openjdk8
~ takayuki$ sudo port reclaim
--->  Checking for unnecessary unrequested ports
Found no unrequested ports without requested dependents.
--->  Checking for inactive ports
Found no inactive ports.
--->  Building list of distfiles still in use
--->  Searching for unused distfiles
Found 1 files (total 98.98 MiB) that are no longer needed and can be deleted.
[l]ist/[d]elete/[K]eep: d
Deleting...
--->  Build location: /opt/local/var/macports/build
This appears to be the first time you have run 'port reclaim'. Would you like to be reminded to run it every two weeks? [Y/n]: n
Reminders disabled. Run 'port reclaim --enable-reminders' to enable.

openjdk8しかなかった。

ファイル移動

~ takayuki$ mkdir MigratingMacPorts 
~ takayuki$ mv myports.txt MigratingMacPorts 
~ takayuki$ mv requested.txt MigratingMacPorts 

restore

~ takayuki$ cd MigratingMacPorts 
~/MigratingMacPorts takayuki$ curl --location --remote-name https://github.com/macports/macports-contrib/raw/master/restore_ports/restore_ports.tcl
chmod +x restore_ports.tcl
xattr -d com.apple.quarantine restore_ports.tcl
sudo ./restore_ports.tcl myports.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   164  100   164    0     0    518      0 --:--:-- --:--:-- --:--:--   532
100  9934  100  9934    0     0  11422      0 --:--:-- --:--:-- --:--:--     0
xattr: restore_ports.tcl: No such xattr: com.apple.quarantine
Password:
--->  Computing dependencies for openjdk8
--->  Dependencies to be installed: openjdk8-zulu
--->  Fetching archive for openjdk8-zulu
--->  Attempting to fetch openjdk8-zulu-8.58.0.13_0.darwin_21.arm64.tbz2 from https://packages.macports.org/openjdk8-zulu
--->  Attempting to fetch openjdk8-zulu-8.58.0.13_0.darwin_21.arm64.tbz2 from https://nue.de.packages.macports.org/openjdk8-zulu
--->  Attempting to fetch openjdk8-zulu-8.58.0.13_0.darwin_21.arm64.tbz2 from http://atl.us.packages.macports.org/openjdk8-zulu
--->  Fetching distfiles for openjdk8-zulu
--->  Attempting to fetch zulu8.58.0.13-ca-jdk8.0.312-macosx_aarch64.tar.gz from https://cdn.azul.com/zulu/bin/
--->  Verifying checksums for openjdk8-zulu
--->  Extracting openjdk8-zulu
--->  Configuring openjdk8-zulu
--->  Building openjdk8-zulu
--->  Staging openjdk8-zulu into destroot
Warning: openjdk8-zulu installs files outside the common directory structure.
--->  Installing openjdk8-zulu @8.58.0.13_0
--->  Activating openjdk8-zulu @8.58.0.13_0
--->  Cleaning openjdk8-zulu
--->  Fetching archive for openjdk8
--->  Attempting to fetch openjdk8-8u302_0.darwin_21.arm64.tbz2 from https://packages.macports.org/openjdk8
--->  Attempting to fetch openjdk8-8u302_0.darwin_21.arm64.tbz2 from https://nue.de.packages.macports.org/openjdk8
--->  Attempting to fetch openjdk8-8u302_0.darwin_21.arm64.tbz2 from http://atl.us.packages.macports.org/openjdk8
--->  Fetching distfiles for openjdk8
--->  Verifying checksums for openjdk8
--->  Extracting openjdk8
--->  Configuring openjdk8
--->  Building openjdk8
--->  Staging openjdk8 into destroot
--->  Installing openjdk8 @8u302_0
--->  Activating openjdk8 @8u302_0
--->  Cleaning openjdk8
sudo port unsetrequested installed
xargs sudo port setrequested < requested.txt

sudo port install gnupg2ログが長いので割愛

下記の手順に戻って、上から実行。bashのままでも実行できる
https://rvm.io/

インストール成功

~ takayuki$ rvm -v
rvm 1.29.12 (latest) by Michal Papis, Piotr Kuczynski, Wayne E. Seguin [https://rvm.io]
~ takayuki$ rvm install 2.7.4
Searching for binary rubies, this might take some time.
No binary rubies available for: osx/12.0/x86_64/ruby-2.7.4.
Continuing with compilation. Please read 'rvm help mount' to get more information on binary rubies.
Checking requirements for osx.
Installing requirements for osx.
Updating system - please wait
takayuki password required for 'port -dv selfupdate': 
Installing required packages: autoconf, automake, gdbm, libtool, libyaml, pkgconfig, openssl - please wait
There were package installation errors, make sure to read the log.
Error running 'requirements_osx_port_libs_install autoconf automake gdbm libtool libyaml pkgconfig openssl',
please read /Users/takayuki/.rvm/log/1637541408_ruby-2.7.4/package_install_autoconf_automake_gdbm_libtool_libyaml_pkgconfig_openssl.log
Requirements installation failed with status: 1.
Error: Failed to build openssl3: command execution failed
148 Error: See /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_devel_openssl3/openssl3/main.log for details.
149 Error: rev-upgrade failed: Error rebuilding openssl3
150 Error: Follow https://guide.macports.org/#project.tickets if you believe there is a bug.
5188 :error:build See /opt/local/var/macports/logs/_opt_local_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_devel_openssl3/openssl3/main.log for details.

ログを見てもよくわからないので、今度はportでruby27をインストール

sudo port select --set ruby ruby27

ターミナル再起動

~ takayuki$ ruby -v
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-darwin21]

改めてrailsインストール。古いバージョンがある場合は、ターミナルを再起動

~ takayuki$ rails -v
Rails 6.1.4.1

sqlite3関連でエラーが出たのでsudo port install sqlite3でインストールしたけど、 develも足りない。

portでrb-sqlite3というのがあるのでインストールしてみることに。 ruby18もインストールされたけど、それは無視して、再度bundle installするとsqlite3のgemのインストールに 成功した。

なので、bin/rails serverを実行すると...

/opt/local/lib/ruby2.7/gems/2.7.0/gems/webpacker-5.4.3/lib/webpacker/configuration.rb:103:in `rescue in load': Webpacker configuration file not found /Users/takayuki/github/rails6_1-practice/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_check_realpath_internal - /Users/takayuki/github/rails6_1-practice/config/webpacker.yml (RuntimeError)
~/github/rails6_1-practice takayuki$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/
Exiting!
~/github/rails6_1-practice takayuki$ npm install --global yarn

さらにrails webpacker:install。これでようやくbin/rails serverが成功しました。

flutter: artemisをpokemon apiで動かしてみる

artemisの現在の最新版6.18.4をReadmeの通り入れてbuild_runnerしてみると

$ flutter pub run build_runner build
Failed to build build_runner:build_runner:
../../development/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-0.41.2/lib/src/error/best_practices_verifier.dart:258:50: Error: The property 'displayString' is defined in multiple extensions for 'TargetKind' and neither is more specific.
 - 'TargetKind' is from 'package:meta/meta_meta.dart' ('../../development/flutter/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/meta_meta.dart').
Try using an explicit extension application of the wanted extension or hiding unwanted extensions from scope.
        var kindNames = kinds.map((kind) => kind.displayString).toList()
                                                 ^^^^^^^^^^^^^
../../development/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-0.41.2/lib/src/error/best_practices_verifier.dart:1950:14: Context: This is one of the extension members.
  String get displayString {
             ^^^^^^^^^^^^^
../../development/flutter/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/meta_meta.dart:91:14: Context: This is one of the extension members.
  String get displayString {
             ^^^^^^^^^^^^^
../../development/flutter/.pub-cache/hosted/pub.dartlang.org/analyzer-0.41.2/lib/src/error/best_practices_verifier.dart:260:36: Error: The getter 'commaSeparatedWithOr' isn't defined for the class 'List<dynamic>'.
 - 'List' is from 'dart:core'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'commaSeparatedWithOr'.
        var validKinds = kindNames.commaSeparatedWithOr;
                                   ^^^^^^^^^^^^^^^^^^^^
pub finished with exit code 1

https://github.com/dart-lang/sdk/issues/46687

7系に上げてみることに

そうするとbuild時に下記のエラー

: Error: Required named parameter 'response' must be provided.
../…/src/link.dart:128
    yield Response(
                  ^
: Context: Found this candidate, but the arguments don't match.
../…/src/response.dart:23
  const Response({
        ^^^^^^^^

: Error: Required named parameter 'response' must be provided.
../…/src/response_parser.dart:10
  Response parseResponse(Map<String, dynamic> body) => Response(
                                                               ^
: Context: Found this candidate, but the arguments don't match.
../…/src/response.dart:23
  const Response({
        ^^^^^^^^

このエラーはどういうことかというと

https://github.com/gql-dart/gql/blob/master/links/gql_exec/lib/src/response.dart#L27

上のパラメータの部分。

一番上のエラーはgql_http_linkのmasterだと直ってる。

https://github.com/gql-dart/gql/blob/master/links/gql_http_link/lib/src/link.dart#L131

ということで、 gql_http_link: ^0.4.2-alpha を追加

これでやっとbuild_runnerが成功

そして実行してみるとpokemon apiのURLが変わってるようなので、betaブランチのexampleのmain.dartを 持ってくるとやっと動きました。

https://github.com/comigor/artemis/blob/beta/example/pokemon/lib/main.dart

そして、ポケモンの画像をGridViewで表示してみました

f:id:ta_watanabe:20211118215651p:plain

書いたソースはこちら

https://github.com/na8esin/flutter_artemis_practice/tree/main/lib

flutter: riverpod setState() or markNeedsBuild() called during build.

エラーの内容はもうちょっと長いバージョンだとこうなります。

setState() or markNeedsBuild() called during build.
This UncontrolledProviderScope widget cannot be marked as needing to build because the framework is already in the process of building widgets.  A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.

エラーを引き起こしたコードはこんな感じです。 debugコンソールに永遠とログが出力されるので実行しないでください。

void main() {
  runApp(ProviderScope(
      child: MaterialApp(
    home: Scaffold(
      body: MyBody(),
    ),
  )));
}

class CheckController extends StateNotifier<bool> {
  CheckController(state) : super(state);

  toggle() {
    state = !state;
  }
}

final checkProvider =
    StateNotifierProvider<CheckController, bool>((_) => CheckController(true));

class MyBody extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, ref) {
    final controller = ref.watch(checkProvider.notifier);
    final check = ref.watch(checkProvider);
    return Center(
      child: ElevatedButton(
        child: Text('$check'),
        // 本当は ()=> _onPressed(controller)
        onPressed: _onPressed(controller),
      ),
    );
  }

  _onPressed(CheckController controller) {
    controller.toggle();
  }
}

onPressedに引数ありでfunctionを渡すときにやりがちです。

引数で、controller(StateNotifier)を渡すのもどうかと思いますが。

MyBodyのbuildの最中に_onPressed(controller)が実行されてしまい、 中の処理でstateが変わるので、hookが検知して、またbuildが走り、無限ループです。

flutter: 角丸ボーダー付きのボックスの右上にチェックマークを入れる

f:id:ta_watanabe:20211107170140p:plain

Container + BoxDecorationだと実現できなかったのでpaintしました。

本体

import 'package:flutter/material.dart';

class CheckableBox extends CustomPainter {
  CheckableBox({required this.isChecked});
  final bool isChecked;

  @override
  void paint(Canvas canvas, Size size) {
    double w = size.width;
    double h = size.height;
    double r = 15; //<-- corner radius

    // ボーダー部分
    Paint borderPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3;

    // 内側は白く塗る
    Paint innerPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    // 右上の三角でチェックが入ってる部分を塗る
    Paint topRightArcPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill
      ..strokeWidth = 5;

    RRect fullRect = RRect.fromRectAndRadius(
      Rect.fromCenter(center: Offset(w / 2, h / 2), width: w, height: h),
      Radius.circular(r),
    );

    Path topRightArc = Path()
      // 右上が開始なので、まず左に移動
      ..moveTo(w - 6 * r, 0)
      // arc始まりの部分まで線を引く
      ..relativeLineTo(5 * r, 0)
      // 右上のarcの部分を描くというかなぞる
      ..arcToPoint(Offset(w, r), radius: Radius.circular(r))
      // arcの終わりから下に線を引く
      ..relativeLineTo(0, 3 * r)
      // 最後は斜辺を描く
      ..relativeLineTo(-6 * r, -4 * r);

    canvas.drawRRect(fullRect, innerPaint);
    canvas.drawRRect(fullRect, borderPaint);

    if (isChecked) {
      canvas.drawPath(topRightArc, topRightArcPaint);

      final icon = Icons.check;
      TextPainter textPainter = TextPainter(textDirection: TextDirection.rtl);
      textPainter.text = TextSpan(
          text: String.fromCharCode(icon.codePoint),
          style: TextStyle(
              fontSize: 30.0,
              fontFamily: icon.fontFamily,
              color: Colors.white));
      textPainter.layout();
      textPainter.paint(canvas, Offset(w - 2.5 * r, 0));
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

実行する側のコード

import 'package:flutter/material.dart';

import 'checkable_box.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.light(),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(body: SafeArea(child: MyScreen())),
    );
  }
}

class MyScreen extends StatelessWidget {
  const MyScreen({Key? key}) : super(key: key);

  static final checks = [true, false];

  @override
  Widget build(BuildContext context) {
    return Center(
        child: ListView.builder(
            itemCount: 2,
            itemBuilder: (context, index) {
              return Padding(
                padding: const EdgeInsets.all(8.0),
                child: MyWidget(
                  isChecked: checks.elementAt(index),
                ),
              );
            }));
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key, required this.isChecked}) : super(key: key);

  final bool isChecked;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width - 32,
      height: 200,
      child: LayoutBuilder(builder: (context, constraints) {
        return CustomPaint(
            painter: CheckableBox(isChecked: isChecked),
            child: const Padding(
              padding: EdgeInsets.only(left: 8.0),
              child: Center(child: Text('hello')),
            ));
      }),
    );
  }
}

https://github.com/na8esin/flutter2_practice/tree/main/lib/src/custom_paint

Dart: The Iterable.firstWhere method no longer accepts orElse: () => null.

https://dart.dev/null-safety/faq#the-iterablefirstwhere-method-no-longer-accepts-orelse---null

と、FAQにも書いてありますが、

こっちはもちろんエラーになる

void main(List<String> args) {
  final list = [1, 2, 3];
  // The return type 'Null' isn't a 'int', as required by the closure's context.
  final x = list.firstWhere((element) => element > 3, orElse: () => null);

  if (x == null) {
    // do stuff...
  }
}

こっちは正常に実行できる

void main(List<String> args) {
  final List<int?> list = [1, 2, 3];
  // 本当にlistにnullが混ざるなら、element!だとだめですが。。。
  final x = list.firstWhere((element) => element! > 3, orElse: () => null);

  assert(x == null);
}

後者のパターンも疑問が残る使い方ではあるので、 大人しくpackage:collectionを使うのがいいとは思います。

もしくは

void main(List<String> args) {
  final List<int> list = [1, 2, 3];
  final found = list.where((element) => element > 3);
  final x = found.isEmpty ? null : found.first;

  assert(x == null);
}

こんな感じかなと思います。

Flutter fabの使い方を説明するために上に吹き出しをつける

f:id:ta_watanabe:20211022163002p:plain

言葉で説明しなくてもいいのがマテリアルデザインですが、 こういう依頼が来ることもたまにありますよね。

Bubbleもパッケージがあるのですが、矢印の部分(三角のところ)が自由に移動できなかったので、 自作しました。

import 'package:flutter/material.dart';

class Bubble extends StatelessWidget {
  Bubble({Key? key, required this.text, required this.textStyle})
      : super(key: key);

  final String text;
  final TextStyle textStyle;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      padding: const EdgeInsets.only(left: 32, top: 16, right: 32, bottom: 8),
      // ここにconstつけるとhot reloadで変わらない
      decoration: ShapeDecoration(
        color: Colors.blue,
        shadows: [
          BoxShadow(
            color: Color(0x80000000),
            offset: Offset(0, 2),
            blurRadius: 2,
          )
        ],
        shape: BubbleBorder(),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            text,
            style: textStyle,
          ),
        ],
      ),
    );
  }
}

class BubbleBorder extends ShapeBorder {
  final bool usePadding;

  const BubbleBorder({this.usePadding = true});

  @override
  EdgeInsetsGeometry get dimensions => const EdgeInsets.all(0);

  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) => Path();

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    return Path()
      // xは少なくなると左、多くなると右
      // yは少なくなると上に移動する
      ..moveTo(rect.bottomCenter.dx + 28, rect.bottomCenter.dy)
      ..relativeLineTo(45, 16)
      ..relativeLineTo(6, -16)
      ..addRRect(RRect.fromRectAndRadius(rect, const Radius.circular(8)))
      ..close();
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}

  @override
  ShapeBorder scale(double t) => this;
}

上のコードを呼び出すmain

import 'package:flutter/material.dart';

import 'bubble.dart';

void main() {
  runApp(MaterialApp(
    home: MySca(),
  ));
}

class MySca extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('fabの上に吹き出し'),
        ),
        floatingActionButton: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          mainAxisSize: MainAxisSize.min,
          children: [
            Padding(
              padding: const EdgeInsets.only(right: 0),
              child: Bubble(
                  text: 'こちらを押すと新規登録です',
                  textStyle: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                      fontSize: 18)),
            ),
            Padding(
                padding: const EdgeInsets.only(top: 16),
                child: FloatingActionButton(
                  child: Icon(Icons.add),
                  onPressed: () {},
                )),
          ],
        ),
        body: SizedBox.shrink());
  }
}

https://github.com/na8esin/flutter2_practice/tree/main/lib/src/bubble