ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Flutterに入門してみて理解したところやハマったところ

前説

こんにちは、tkyです。

2018/10/08 に開催された技術書典5で購入(知人経由で・・・)したFlutterの入門本でいよいよ私もFlutter入門者になりました。 その時学習した内容とハマりポイントなどを知見として残しておこうと思います。

今回学習に際し、mBaaSとしてFirebase Cloud Firestoreを使用しています。

  • Flutter
  • Dart
  • Firebase Cloud Firestore

なお、このときFlutter1.0がリリースしたてだったのでそれなりにハマるだろうな〜という想定はしていましたが、そこまでズボズボではなかったですw

本記事ではFlutterのインストール手順や各プラットフォームのセットアップ方法などは記載しません。

flutter doctor しまくってエラーに記載されているコマンドをひたすら実行していったら環境整えられたので調べる手間が省けて楽でした。

何作ったの

『Flutte x firebaseで始めるモバイルアプリ開発』という本で解説している貸し借りアプリを書籍ともに作りました。

booth.pm

f:id:ticktakclock:20181215192411p:plain
つくったもの

良かったところ

iOSにはもちろんXCodeは必要ですし、エミュレータも同様ですが、デプロイがAndroid Studio上からできて、hot reloadもちゃんと効くのはまじで便利なのでは?

f:id:ticktakclock:20181216171224p:plain

学習してみて

まだDartの言語仕様部分だったり、どう作る?みたいな深掘りはできていませんが、こんな感じでFlutterって動いてるんだ〜という入門程度には学習できたかと思われます。

今後はWidgetの学習とレイアウトの特訓、アーキテクチャBLoCを使ったアプリ開発あたりに触れて知識の深掘りをしていこうと思います。

今回の気付き

  • Widget充実しすぎて便利
  • publicとprivateの区分方法がアンダースコア(_)
  • 直感的にレイアウトを置ける、onPressedなどのイベントが書きやすい
  • 画面遷移がpush,pop形式でわかりやすい
  • 画面は基本的にStatefulで良さそう?

Widget充実しすぎて便利

FlutterはWidgetと呼ばれるUIオブジェクトを配置することで描画しますが、かなり種類が豊富なのでは?と感じています。

AndroidのUIっぽくMaterial Componentsが、iOSのUIっぽくCupertinoという種別のUIがありました。 書籍ではMaterial Componentsで実装し、Android, iOSどちらも同じUIとなっています。

flutter.io

publicとprivateの区分方法がアンダースコア(_)

Dartにはpublicやprivateのような修飾子はなく、基本publicの扱いとなります。 アンダースコア(_)を変数、関数、クラスにの先頭につけることで同じような働きができるようになります。

Dart公式ドキュメントにはアンスコつけたらそのライブラリだけで見れるようになるよ、と書いてあります。(この"ライブラリ"のスコープがわからん・・・) www.dartlang.org

start with an underscore (_) are visible only inside the library.

直感的にレイアウトを置ける、onPressedなどのイベントが書きやすい

例えばボタンを配置するときに、以下のように記述します。

ログアウトボタンを押したとき、Topページに戻る

              FlatButton(
                child: const Text("ログアウト"),
                onPressed: () {
                  _auth.signOut();
                  Navigator.pushNamedAndRemoveUntil(context, "/", (_) => false);
                },
              ),

FlatButtonのプロパティにイベントを記述する書き方は結構好きです。 またNavigatorを使用して画面遷移を実現しています。画面遷移はルーティングで行うため、画面設計は必要そうですね。

画面遷移がpush,pop形式でわかりやすい

先程の例で説明しましたがNavigator.push or popで画面遷移を実現しています。それなりにわかりやすいかなと思いました。

// Topページに遷移する

Navigator.pushNamedAndRemoveUntil(context, "/", (_) => false);

// /newというルートを作成して、InputForm画面に遷移する
Navigator.push(
                context,
                MaterialPageRoute(
                    settings: const RouteSettings(name: "/new"),
                    builder: (BuildContext context) => InputForm(null)));

// 1つ前の画面に遷移する
Navigator.pop(context);

画面は基本的にStatefulで良さそう?

Flutterにはスプラッシュ画面など動きがない固定画面で使用するStatelessと、

内部でデータを保持し動的に表示を行う画面などで使用するStatefulという考え方があるようです。

ん〜、基本内部で状態を保持して画面更新するパターンのほうが多い気がするのですがStafefulで良さそうな感じでしょうか?もう少し学びながら掴んでいきたいと思います。

ハマりポインツ

AndroidについてはAndroid Studioで開発できる点でほぼ何もハマることなく開発できました。

iOSがちょっとハマりました。

  • iOSCould not build Objective-C module 'Firebase 問題
  • iOScloud_firestore/CloudFirestorePlugin.h' file not found 問題

iOSCould not build Objective-C module 'Firebase 問題

最初の問題でした。エラーの内容通り、Ob-cのFirebaseモジュールがSwiftではビルドできない、と言われていますが、なぜ???という感じですね。

Ob-cのモジュールをinstallしてしまったのだろう、という感覚レベルで、対応法がぐぐったらすぐに出てきたので解決でした。

簡単に言うと、「Podfile 系でinstallしたやつ全部消してもっかいinstallし直す」です。

pippi-pro.com

iOScloud_firestore/CloudFirestorePlugin.h' file not found 問題

こいつにずっと悩まされました。。

f:id:ticktakclock:20181215190413p:plain
cloudfirestoreplugin file not found

試行錯誤した内容は以下です。まぁ解決できませんでした。

  • pod install しなおす
  • flutter clean してみる
  • コマンドでビルドしてみる「flutter build ios
  • flutter packages getしてみる

また、「flutter firestore」のチュートリアル等を確認して自分の手順と違うところを確認したり。 Firebase for Flutter

Podfileはこんな感じで書いてました。

target 'Runner' do
  use_frameworks!

  pod 'Firebase/Core'
  pod 'Firebase/Firestore'
end

この問題2つの共通点

よくよくハマった2点を考えてみるとどちらもPodでした。実はFlutterをやるまでcocoapodsについては未導入だったので今回初めてインストールしたのですが、 setupコマンドを打っていないことに気が付きました。

$pod setup

本にも書いてあるので、ちゃんと本に書いてあることは読みましょう、というn度目の教訓を得ました・・・

setupしたあとにxcode起動したらエラーは消えて無事に動作確認できました。

podfileに書かれていることはまだ理解できていないので、「なぜエラーが消えたのか?」は微妙にわからないままなので要勉強です。

僕はそもそもiOSをやったことなかった

Cordovaを使ったアプリ開発の経験はあるので、XCodeiOSプロジェクトをビルドするとかはできるのですが、 iOSネイティブをやったことなかったことで基礎的な問題の解決に少し時間がかかりました。

しかしながら、開発の効率が非常に高そうなFlutterは今後とも触れて行きたいと思います。

付録(バージョン情報など)

開発時点でのFlutter doctor内容を載せておきます。

$ flutter doctor -v
[✓] Flutter (Channel beta, v1.0.0, on Mac OS X 10.13.6 17G2307, locale ja-JP)
    • Flutter version 1.0.0 at /Users/ticktakclock/flutter
    • Framework revision 5391447fae (2 weeks ago), 2018-11-29 19:41:26 -0800
    • Engine revision 7375a0f414
    • Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)

[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
    • Android SDK at /Users/ticktakclock/Library/Android/sdk
    • Android NDK at /Users/ticktakclock/Library/Android/sdk/ndk-bundle
    • Platform android-28, build-tools 28.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 9.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 9.2, Build version 9C40b
    • ios-deploy 1.9.4
    • CocoaPods version 1.5.3

[✓] Android Studio (version 3.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 31.1.1
    • Dart plugin version 181.5656
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06)

[!] VS Code (version 1.29.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[!] Connected device
    ! No devices available

pod setupしたときのPodfileの内容も載せておきます。

# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def parse_KV_file(file, separator='=')
  file_abs_path = File.expand_path(file)
  if !File.exists? file_abs_path
    return [];
  end
  pods_ary = []
  skip_line_start_symbols = ["#", "/"]
  File.foreach(file_abs_path) { |line|
      next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
      plugin = line.split(pattern=separator)
      if plugin.length == 2
        podname = plugin[0].strip()
        path = plugin[1].strip()
        podpath = File.expand_path("#{path}", file_abs_path)
        pods_ary.push({:name => podname, :path => podpath});
      else
        puts "Invalid plugin specification: #{line}"
      end
  }
  return pods_ary
end

target 'Runner' do
  use_frameworks!

  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
  # referring to absolute paths on developers' machines.
  system('rm -rf .symlinks')
  system('mkdir -p .symlinks/plugins')

  # Flutter Pods
  generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
  if generated_xcode_build_settings.empty?
    puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first."
  end
  generated_xcode_build_settings.map { |p|
    if p[:name] == 'FLUTTER_FRAMEWORK_DIR'
      symlink = File.join('.symlinks', 'flutter')
      File.symlink(File.dirname(p[:path]), symlink)
      pod 'Flutter', :path => File.join(symlink, File.basename(p[:path]))
    end
  }

  # Plugin Pods
  plugin_pods = parse_KV_file('../.flutter-plugins')
  plugin_pods.map { |p|
    symlink = File.join('.symlinks', 'plugins', p[:name])
    File.symlink(p[:path], symlink)
    pod p[:name], :path => File.join(symlink, 'ios')
  }
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['ENABLE_BITCODE'] = 'NO'
    end
  end
end