- 投稿日:2020-09-08T21:39:37+09:00
TestFlight「"app name"をインストールできません あとでやり直してください」が起きて原因がキレそうだった件
はじめに
タイトル通り、TestFlightにて急に昨日まで問題なかったインストールができなくなり、原因が謎過ぎて、今日7時間ぐらい使ってしまったのでキレそうになり、そのノリで書いてます。
原因
接続しているノード(WiFi)が問題でした、、
自宅のものでは問題なかったのですが、良くあるセキュアなWiFiではTestFlightからのインストールが出来ませんでした。
4Gに変えた瞬間解決しました(キレそう)ただ、恐らくWiFiが問題と言うより、MacBook Proの方で先にWiFiにつないで認証した後だと、同じapple idを使っているiPhoneでは認証を求められなかった為だと思われます。
原因が分からなすぎて、冗談抜きで証明書50回ぐらい作り直して、試したので証明書周りはもう脳死で出来る様になりました?
後、試せる人が他にいればその人に試してもらうのが一番早いです!
その人が問題無ければ環境に違いって事に目付けれるのでowari
何が言いたいかって、こんなものに時間を割いてるは本当にもったいないです、非合理的過ぎます。
と言う事でこう言うヤバ目の記事はなかったので誰かの為に置いておきます。
- 投稿日:2020-09-08T21:35:57+09:00
設計パターンMVCを学んだ
設計について
まだまだ初学者なので、いろいろと学習の基礎が疎かです。特に今まで設計の学習全くしてこなかったので、MENTAで課題をだしていただき、ハンズオンで取り組みました。そこで学んだことを簡単にメモ書きします。
なお、学習したのはMVCのみです。MVC: Modelにビジネスロジックを書く
MVCという設計について学習しました。
MVCそれぞれの頭文字は以下の略称です。
M:Model
V:View
C:Controller今まで参考にしてきた学習教材が良くなかったのか、Modelの役割をちゃんと理解していませんでした。Modelには主にビジネスロジックを記述すると言われています。ビジネスロジックとは簡単に言えば、アプリやシステムで必要不可欠な処理(例えば、Firebaseのログインの処理やDBからの読み込み書き込み、API通信処理など)を指しています。
参考
Viewは見た目の規定であり、ControllerはViewとModelの橋渡し的な役割です。iOS開発ではViewとControllerが分かれているか、いないかが重要なポイントだそうです。分かれない場合、ViewControllerのようなフォルダ構成をとったりします。今回はViewControllerのパターンで学習しました。本来、わかれている方が良いとされてるみたいです。
加えて、ModelからViewControllerへの橋渡しをプロトコルで行うのかKVOで行うのかが重要だそうです。今回はプロトコルのパターンを使って学習しました。
以下、コード例
protcolとDelegate// Model protocol SampleModelDelegate: AnyObject { func sampleAction(with someData: Data) } final class SampleModel { // Model で delegate の通知先を弱参照で保持 weak var delegate: SampleModelDelegate? func sampleAction() { let urlString = "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" let url = URL(string: urlString) AF.request(url!, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON { response in switch response.result { case .success: // JSON からの変換 = デコード do { guard let data = response.data else { return } let items = try JSONDecoder().decode([Items].self, from: data) print(items) self.delegate?.getQiitaData(items: items) } catch { // デコードのエラー print(error) } case .failure: print("failure") } } } } // ViewController final class SampleViewController: UIViewController { private let model = SampleModel() override func viewDidLoad() { super.viewDidLoad() // model の delegate の通知先に自身を設定 model.delegate = self } }※なおKVOとはKey Value Observeの略みたいです。
実装したことがないのでまだよくわかりません。要するにRuby on RailsのMVCとほぼ同じ?
iOS開発では、ViewとControllerが分かれない場合、ViewControllerのようにすると記述しました。これ以外を除けば、Modelの役割ってRailsチュートリアルで学んだModelとほぼ同じでは!?と学習途中で気付きました。泣
これから
MVCは現場ではもう使われないなどの記事を読みましたが、とりあえずはMVCをきっちり理解して使えるようになってから異なる設計パターンを学習していきたいと思います。MVVM、MVP、クリーンアーキテクチャ、VIPER(バイパー)などなど。
学んだら上記にまとめていこうと思います。備考: こちらのGitHubのソースはMVCの良い例になるらしいので読んでおきたい。
また、KickstarterのOSSはMVVMの学習時に良いらしい。
- 投稿日:2020-09-08T18:12:22+09:00
[初心者向け]DKimagePickerControllerのススメ
みなさんImagePickerController使ってますかー??
ライブラリから写真選択出来るヤツですね!!でもImagePickerって写真一枚しか選択できないんですよね、、
オリジナルアプリ作りたかった僕は、なんとか複数選択出来るの探しました!はい、これです!!
僕は実際に触らないとわかんないのでこちらを参考に作ってみました。
https://github.com/metasmile/DKImagePickerController
https://teratail.com/questions/189732
ビルドして改めてコードみてみるとふむふむ、、
アセッツに入れてcollectionViewで表示するのか!外部のライブラリを使うのは初めてでメンターに聞きまくってわかりました!
fetchFullScreenImage この中でループ回すのか、、、
DKImagePickerController これをインポートするのか、、
KDAssetsってのがあるのか、、分かったようでわかりませんでした
Swift深い!!!僕が作ったサンプルもどうぞ!
https://github.com/rentamaeda/DKImagePickerTest参考にしたサイト等
https://github.com/metasmile/DKImagePickerController
https://teratail.com/questions/189732
- 投稿日:2020-09-08T18:12:15+09:00
【Flutter×Firebse】非エンジニアがパワーリフター向けアプリをリリースした話【個人開発】
はじめに
最近、パワーリフター向けBIG3記録アプリ「LifterLog」をリリースしました。
以前、Swiftで開発したアプリをFlutterで完全リニューアルしたのですが、
今回の開発→リリースまでの過程や得られた知見を共有したいと思います。特にこれからアプリ開発やFlutterを始めようとしている人の参考になれば幸いです。
※因みにパワリフターとは
パワーリフティング(バーベルを持ち上げ、その重さを競うスポーツ by Wikipedia)を行う選手のこと。自己紹介
ITベンチャーに勤務する社会人2年目の非エンジニア。
筋トレと椎名林檎/東京事変とアメフトが好き。プログラミング経験は3年ほど。
独学を中心にSwiftやReactを触っていて、今年の5月頃からFlutterを始めました。開発背景
完全に自分が使いたかったからです笑。
既存のトレーニング記録系のアプリに自分が必要としている機能を満たしたものがなく、「無いなら自分で作るか〜」と勢いで始めました。当初はSwiftで開発し、昨年リリースもしたのですが
- androidでもリリースしたい。
- Flutterに興味があった。
以上の理由でFlutterで完全リニューアルを始めました。
自分が作りたいものを自分好みに作れることって、個人開発の素晴らしい美徳だと思います。
使用ツール・技術
- Flutter
- Firebase(Firestore / Firebase Authentication / Cloud Functions)
- Git / Github(バージョン管理, issue管理)
- Adobe XD(モック作成)
- Adobe Photoshop(モック作成)
- Adobe Illustrator(スクショ、アイコン作成)
- Googleスプレッドシート(DB設計など)
- Googleフォーム(アンケートや問い合わせフォームとして利用)
- Slack(メモ、issue管理、アンケート確認等)
リリースまでの流れ
- 必要機能洗い出し
- UIUX設計
- DB設計
- スケジュール作成
- 開発
- リリース申請
ざっとこんな流れで仕事のない土日祝日を中心にコツコツと進めていきました。
以下、各過程でやったことを説明したいと思います。1. 必要機能の洗い出し
まずアプリに必要となる機能を思いつく限り書き出し、その後本当に必要な機能のみをピックアップしてアプリ機能を固めました。(スプレッドシートにまとめた。)
機能をピックアップする際に意識したのは
「このアプリを一言で表すと?」
です。今回なら、「BIG3のトレーニングボリュームが記録できるアプリ」。
こちらを念頭に機能を選定しました。あれもこれも機能をつけようとすると結局何のためのアプリかわからなくなったり、開発がままならくなることも。。。
特に個人開発では必要最低限の機能で開発・リリースし、徐々にアップデートしていくことが大事だと思っています。2. UIUX設計
自分がトレーニングしている状況を想定し、画面遷移等を決めAdobe XDにてモックを作成しました。
やはり、モックがあると開発がグッと楽になりますね。3. DB設計
設計にあたっては主にこちらの記事を参考に致しました。
Cloud Firestoreの勘所 パート2 — データ設計
Firestore Database Design必要な機能や拡張性を考慮しつつ慎重に。。。
ここが一番気を使ったところでしたが、同時に楽しいものでもありました。4. スケジュール作成
スプレッドシートに開発する機能をまとめ、優先順位をつけざっくりとした開発日程を作成。
個人開発といえど、今回のように限られた開発時間でリリースまで最速でまでもっていくには特に必須な工程かと思っています。
地図のない旅行は無駄な回り道が増えますからね。(まぁ、往々にして予定通りにはいかなかったのですが。)また、開発する機能ごとにGithubのissueを立て、完了次第closeするという流れで進めました。
関連する記事などを逐次コメントでき、後々振り返る時にも便利です。
また、バグ等が発生した場合も同様にissueを立て管理しています。5. 開発
いよいよ開発スタートです。
Flutterの状態管理はprovider + ChangeNotifierを利用。
とにかくリリースすることを最優先に掲げて開発しました。ここで大事なのは以下の3点かな、と思います。
1. なんか動くぞ!を許容する。
→最初から100%理解することは不可能だ割り切ってとりあえず動く状態を作り先に進まないと時間がいくらあっても足りません。
モチベが下がってしまう危険性もありますし、そのうち理解できる時は必ずくるのでとりあえず進むことを優先しましょう。2. 妥協と追求の繰り返す。
→実装していく中でどうしても上手くいかない・思った通りに動かないことがでてくるかと思います。
そんな時は期限を決め、期限内に解決しなければ機能を簡潔にする・削る・代替するなどどうにかして前に進むことが大事!(もちろん、全力で調べることが前提ですが)
個人開発の良さはステークホルダーが存在せず、自分の思いのままに開発ができること。
リリースするために柔軟に対応してくことをお勧めします。3. 公式ドキュメントを読もう。
→原理原則は全てここにある、といっても過言でないでしょう。とっつきにくいですがこちらも繰り返し読んでいればそのうちなんとなーくわかってきます笑。6. リリース
ストアのスクショやアイコン等はIllustratorで作成し、公式ドキュメントにそってリリース作業を行いました。
iOSリリース
androidリリース
審査に提出後、iOS・android共に約二日後に審査結果がでて、晴れてリリース!?
開発を決めてから約4ヶ月程度。
やっぱり、リリースされる瞬間は嬉しいです。?7. 開発ではまった点
7_1. 本番/開発環境の切り替え
多分一番苦労した。。。
そもそもflavorってなんぞや?ってところからスタートし、あれこれ記事を読んで何とかできました。
特にiOS側の設定が複雑かつ、自分にほとんど知見のない領域だったのが要因。
こちらの記事を主に参考にいたしました。Flutterで環境ごとにビルド設定を切り替える — iOS編
flutterで本番/ステージング/開発を切り替える
【Flutter】Flavorの設定~buildまで(Android編)
Creating flavors for Flutter7_2. スクロールして画面外となったListView内のTextFieldの値がリセットされる。
トレーニングを記録する画面で入力した値が消える現状に遭遇。
最初はロジックエラーかと思いましたが、思い当たる節があり調べてみるとこちらがヒット。[Flutter]スクロール可能なFormFieldの取り扱い
非表示にされたり、ListViewの要素が画面外にスクロールされたりした時、つまりInvisibleな状態になると、そのwidgetはdisposeされます。再び表示される際には初期化されるため、それまでの値が書き換えられてしまいます
あーあるある、こういうの。
SwiftでいうTableViewCellの再利用的な話かなぁ、と目星がついていたので検索もスムーズにできました。
このように、わからないことを他の言語に置き換えて考えてみる、というのはとても有効な考え方だなと改めて感じました。7_4. 今後の展望
今回のリリースでは切り捨てた機能の実装、UIの見直しを直近の動きと捉えています。
- 今回切り捨てた機能
- ・スプラッシュ画面
- ・強制アップデート機能(Firebase Remote Config)
- ・ダークモード
- ・androidのGoogleログイン
ざっとこんなもんでしょうか。
リリースを最優先に考えた結果、実装が後回しになりましたが確実に今後必要となってくる機能ばかりなので確実に実装していきたい。
UIの見直し
ここもしっかり基礎から学んで反映させたい、切実に!
今は完全にノリと勢いに任せてます。苦笑。
学ぶにあたっては、ドキュメント等だけでなく、いろいろなアプリを触っていくことも大事にしたい。8. 未解決問題(どなたかお力添えを。。。?)
今回、どうしても解決できなかった問題。
リリース環境のandroidでGoogleログインができない!ログインしようとするとこんなエラーが。。。
(PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 12500: , null))
調べてみるといろいろ記事はでてきますがどれもすでに対処済み&効果なし。。。
サポートメールの設定、Firebaseにrelease用のSHA1のキーの入力、Google API ConsoleのOAuth同意画面の設定も全部やってるけど効果なし。
開発環境だと問題なく作動するのに。。。参考記事
https://stackoverflow.com/questions/56188338/platformexception-platformexceptionsign-in-failed-com-google-android-gms-comm
https://github.com/flutter/flutter/issues/25640
https://qiita.com/hiraski/items/c5fb20da4a8862ec72ea本当にここは妥協したくなかったのですがとりあえずメールとパスワード認証に代替して一時撤退。。。
同じような現象に遭遇して解決した方いたらどんな方法とったか教えてい頂けると嬉しいです...!全くわからん!笑。
9. まとめ
以上、リリースまでの流れをまとめてみました。
やはり、個人開発で大切なのは
・必要最低限の機能に絞る。
・妥協と追求を繰り返しとにかく前進する。ことかと思います。
まだまだ、できないこと・知らないことがたくさんありますが、やはり自分のアプリをリリースできるのは本当に楽しい。
これからも少しずつ問題を解消していき、学んでいきたいと思います。にしても、Flutter、楽しいなぁ。
- 投稿日:2020-09-08T17:58:44+09:00
RxSwift で非同期処理を合成しよう
今回は同じ型の非同期処理を RxSwift でまとめる際に使用する
concat
とmerge
の使い方と挙動を簡単にまとめとこうと思います。concat
concat()
は複数の非同期処理を渡された順番で順次処理を行っていきます。サンプルは次のようになります。let ob1 = Observable<String>.create { observer -> Disposable in // 3秒後にイベントを流す DispatchQueue.main.asyncAfter(deadline: .now() + 3) { observer.onNext("ob1") observer.onCompleted() } return Disposables.create() } let ob2 = Observable<String>.create { observer -> Disposable in // 2秒後にイベントを流す DispatchQueue.main.asyncAfter(deadline: .now() + 2) { observer.onNext("ob2") observer.onCompleted() } return Disposables.create() } Observable.of(ob1, ob2) .concat() .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) // 出力: // // ob1 // ob2 //また、
Observable
の合成は下記のように書くこともできます。Observable.concat(ob1, ob2)merge
merge()
は複数の非同期処理を並列に実行することができます。つまり、Observable の渡される順番などが関係なく処理が早く終わった順にストリームに流れます。下記がサンプルコードになります。let ob1 = Observable<String>.create { observer -> Disposable in // 3秒後にイベントを流す DispatchQueue.main.asyncAfter(deadline: .now() + 3) { observer.onNext("ob1") observer.onCompleted() } return Disposables.create() } let ob2 = Observable<String>.create { observer -> Disposable in // 2秒後にイベントを流す DispatchQueue.main.asyncAfter(deadline: .now() + 2) { observer.onNext("ob2") observer.onCompleted() } return Disposables.create() } Observable.of(ob1, ob2) .merge() .subscribe(onNext: { str in print(str) }) .disposed(by: disposeBag) // 出力: // // ob2 // ob1 //じゃあ異なる型の Observable はどうなるの?
基本的には、ストリームのイベントを逐次検知する必要がある場合は Observable の型を統一して、concat なり merge なりを使用する必要があります。並列で処理を実行して全ての処理が完了したタイミングで値を参照する場合は、
zip
という関数が用意されていますが、直列で実行が完了した値を参照したい場合はflatMap
なり、concat
なりを使って実装する感じでしょうか?(こんな方法があるよってやつがあれば教えてください?)直列で逐次イベントを検知
enum Container { case string(String) case int(Int) } let ob1 = Observable<String>.create { observer -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { observer.onNext("ob1") observer.onCompleted() } return Disposables.create() }.map { Container.string($0) } let ob2 = Observable<Int>.create { observer -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { observer.onNext(2) observer.onCompleted() } return Disposables.create() }.map { Container.int($0) } Observable.of(ob1, ob2) .concat() .subscribe(onNext: { c in switch c { case .string(let str): print("string: \(str)") case .int(let num): print("int: \(num)") } }) .disposed(by: disposeBag) // 出力: // // string: ob1 // int: 2 //並列で逐次イベントを検知
enum Container { case string(String) case int(Int) } let ob1 = Observable<String>.create { observer -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { observer.onNext("ob1") observer.onCompleted() } return Disposables.create() }.map { Container.string($0) } let ob2 = Observable<Int>.create { observer -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { observer.onNext(2) observer.onCompleted() } return Disposables.create() }.map { Container.int($0) } Observable.of(ob1, ob2) .merge() .subscribe(onNext: { c in switch c { case .string(let str): print("string: \(str)") case .int(let num): print("int: \(num)") } }) .disposed(by: disposeBag) // 出力: // // int: 2 // string: ob1 //並列で完了イベントを検知
zip
を使うと上記2つの方法とは違い型を統一する必要がないので、よりシンプルに実装することができます。let ob1 = Observable<String>.create { observer -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { observer.onNext("ob1") observer.onCompleted() } return Disposables.create() } let ob2 = Observable<Int>.create { observer -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { observer.onNext(2) observer.onCompleted() } return Disposables.create() } Observable.zip(ob1, ob2) .subscribe(onNext: { str, num in print("string: \(str), int: \(num)") }) .disposed(by: disposeBag) // 出力: // // string: ob1, int: 2 //参考
- 投稿日:2020-09-08T14:11:31+09:00
No suitable application records were foundの解決策
はじめに
AppStoreへipaファイルをアップロードする際に普段はエクスポートして、
AppStoreConnectの申請ページでアップしていましたが、今回はXcodeのOrganizerでそのままUploadすることにしました。
しかしエラーが...
しかし以下のようなエラーが出ました。
No suitable application records were found. Verify your bundle identifier ‘xxxx’ is correct.
そんなこんなで調べると解決策の載ったページがありました。
原因と解決策
原因はAppStoreConnectでの登録が必要だから。
基本的にはAppStoreConnectで登録をしてからXcodeでArchive→Validateするようにした方が良いようです。ということで、AppStoreConnectで新規アプリを作成し、該当のバンドルIDを設定して保存すれば、再度Validateすることで通りました。
- 投稿日:2020-09-08T10:43:23+09:00
【Flutter/コード自動生成】FlutterGenを使ってみる。
こんにちは。最近はニホンザルにハマっているminako-phです。
※おすすめの猿山があったら教えてください昨日、@wasabeef_jp さんが このようなTweetをされてらっしゃいました。
書いた / コード自動生成の FlutterGen を作りました。 https://t.co/nNOMDG7q1c #flutter #flutterdev
— ᴅᴀɪᴄʜɪ ғᴜʀɪʏᴀ / ᴡᴀsᴀʙᴇᴇғ (@wasabeef_jp) September 7, 2020Flutter向けに画像リソースなどのコードを自動生成するためのツールです。
ちょうど iOS開発の時に使っていた R.swift みたいなツール無いかなーと探していたタイミングだったので、
おおお.....!!これは!!? となり早速試してみました。何ができるのか
- 自動生成により画像などのリソースファイルへアクセスできるようになる
- 実装する時にコード補完が効くのでタイポしなくなる
- 静的解析ができるので存在しないファイルの指定などによる実行時のエラーを防ぐ
この記事の概要
FlutterGenについては作者のわさびーふさんが日本語で詳しく書いてくださっているので
この記事ではシンプルにFlutterGenを導入するとこから、
コード自動生成によるコード補完を実際に体験するまでを試してみました。FlutterGenのインストール
今回は
Homebrew
でインストールしました。$ brew install FlutterGen/tap/fluttergenインストールが終わったら使用できる事を確認してみましょう
$ fluttergen -h今回はHomebrewを使用しましたが 他にも、
Dart Command-line
やbuild_runner
が用意されています✔️実際に使ってみる
コードの自動生成を実行する
今回は3種類の画像とフォントを用意しました。それぞれを assets/ 配下に配置します。
pubspec.yaml に追加できたら、次にFlutterGenによるコードの自動生成を実行します。
パスはプロジェクトのpubspec.yaml
へのパスを指定します。$ fluttergen -c ./pubspec.yaml実際にエディタで使用してみる
assets/images/
配下の画像を指定したいので、Assets.images.
と入力してみます。すると以下のように、コード補完機能が使用できるようになっている事が確認できます?!
これでタイポを気にせずスピーディーに実装できるようになりました!
とっても快適 ....... ><✨公式でも解説されていますが、対象ファイルがFlutterで対応してる画像フォーマットの場合、
Assets.images.warauEbifried
で AssetImage classとして使え、実際に描画する際は
Assets.images.warauEbifried.image(...)
とする事で Image class として
Assets.images.warauEbifried.path
で String として使えます。
- 普通に書くと ↓
// 文字列指定する為 タイポしやすい... Image.asset( 'assets/images/warau_ebifried.png', width: 150.0, ),
- FlutterGenを使用して書くと ↓
// Image classとして使用する. Image classが持つパラメーターを使用できます Assets.images.warauEbifried.image( width: 150.0, ), // Stringとして使用する Image.asset( Assets.images.warauEbifried.path, width: 150.0, ),今回のようにディレクトリ名が
assets/
もしくはasset/
の場合は冗長となる為 省略されますが、
Assets.directoryName.directoryName.fileName
のように階層をそのまま使用できるようになっています。フォントでも使ってみましょう。
以下のようにStringとしてコード補完ができることが確認できます?!
フォントの場合FontFamily.notoSansJP
のように指定が出来ます。感想
ファイル名やディレクトリを追ってコピペして貼り付けて や 定数宣言して... みたいな
地味にコストのかかる作業が解消されとても快適になりました ><✨ 有り難いです ..他にも出来ることなど詳しくは公式も是非ご覧ください!
参考
コード自動生成の FlutterGen を作りました。
Github|FlutterGen/flutter_gen
pub.dev|flutter_gen
- 投稿日:2020-09-08T10:31:04+09:00
【Swift】Alamofire 5.x~のタイムアウト値(timeoutInterval)設定方法
調べてもあまり参考になるサイトが出てこなかったので、備忘録
環境
- Xcode: 11.3.1
- Swift5
- Alamofire 5.2.1
・requestModifierを指定
AF.request("https://xxxxxxxxxxxx", method: .get, parameters: parameters, requestModifier: { $0.timeoutInterval = 5.0 }).responseJSON { response in }AF.requestに「requestModifier: { $0.timeoutInterval = 5.0 }」を追加することで、
タイムアウト値をセットすることが可能となる。
※上記コードでは5.0秒にセットしています。こちらは任意の秒数に変更して下さい。備考
アプリ公開しました!よろしければインストールお願いします。
とらんぽTwitter始めました!よろしければフォローお願いします。
@yajima_tohshu
- 投稿日:2020-09-08T03:06:27+09:00
[Flutter]AnimatedBuilderで実用的なアニメーション
記念すべき初投稿。
おなじみの枕詞になるが、Flutterは公式のドキュメントが大変充実している。
したがって、我ながら「この記事いる?」という感じではあるが、学習発表会の感覚で書き残しておく。概要
Flutterアプリケーション上でアニメーションを書く方法は色々あるが、本記事ではAnimatedBuilderを使った最も単純なアニメーションレシピをまとめる。
本質的な理解よりも「早速Flutterアニメーションを体験できる」ことに重きをおく。Flutter初学者の方は、あらかじめ公式の「Write your first Flutter app, part 1」あたりは(できればpart2も)修了しておくことをおすすめする。
「実用的」とは言いつつも、サンプルはContainerのwidthが広がったりもとに戻ったりするだけのシンプルなもの。
ある程度複雑なアニメーションも、基本的にはこうしたシンプルなアニメーションの組み合わせで実現できる。animateボタン押下でContainerのwidthが変化
完成サンプル+αはコチラ。対象読者
- Flutter初学者(?)
- StatelessWidgetとStatefulWidgetの違いが分かっている
AnimatiedBuilderとは?
アニメーションを実装する際に繰り返し記述することになるお約束コードを端折るために用意された、StatefulWidgetのラッパー。シンプルな単一アニメーションから複数のシーケンシャルなアニメーションにまで幅広く使える実用性の高いアニメーション系Widgetである。
Flutterでアニメーションを描く際に最も使用頻度が高い、という方も少なくないのでは無いだろうか。しらんけど。ざっくりレシピ
- TickerProviderMixinを組み込んだStatefulWidgetを用意する。
- 1.で用意したStatefulWidgetにAnimationControllerのインスタンスを持たせる。
- 2.で用意したAnimationControllerとTweenを用いてAnimationのインスタンスを生成する。
- WidgetツリーにAnimatedBuilderを組み込む。その際、3.で用意したAnimationインスタンスを渡す。
- 任意のタイミングでAnimationControllerを操作する。
ざっくり用語解説
TickerProviderMixin
AnimationControllerにvsync(≒画面のリフレッシュイベント)を提供するためのMixinオブジェクト。
実装上はほとんどお作法的に出てくるだけなので、ここでは本質的なことは扱わない。Mixin
クラスの多重継承を避けつつ任意のクラスに汎用的な拡張を組み込むための仕組み。
ここでは詳しく扱わない。AnimationController
アニメーションの開始、停止、リセットなどの操作や、アニメーション状態の管理、参照を媒介するオブジェクト。
「value」というプロパティを持っており、アニメーションの始点を0.0、終点を1.0とした場合の現在値を参照できる。Tween
Animationインスタンスを生成するためのオブジェクト。
ちなみに、Tweenはin-betweeningの略とのこと。Animation
実際にWidgetに動きを与えるための実数値を生成、返却するオブジェクト。
FlutterのAnimationクラスには、始点〜終点の値を指定したDurationとCurveに従ってなめらかに遷移するTween animationと、実際の物理現象をモデリングしたPhysics-based animationの二種類が存在するが、実際に使うことになるのはほぼほぼTweenの方になると思われる。AnimationControllerも「value」というプロパティを持っているが(上記参照)、Animationインスタンスのvalueは、Tweenに設定した始点と終点の間で現在値を参照できる、というもの。
解説
レシピの内容を順を追って詳しく見ていこう。
1. TickerProviderMixinを組み込んだStatefulWidgetを用意する
AnimationControllerはStatefulWidgetのState内で宣言しライフサイクルを管理してやる必要があるので、そのためのStatefulWidgetを用意する。
width.dartclass WidthAnimationPage extends StatefulWidget { WidthAnimationPage({Key key}) : super(key: key); static const kRouteName = '/width'; @override _WidthAnimationPageState createState() => _WidthAnimationPageState(); } class _WidthAnimationPageState extends State<WidthAnimationPage> { AnimationController _controller; // AnimationControllerを宣言 }Controllerにvsyncを提供するため、StatefulWidgetにTickerProviderStateMixinを適用する。
単一のAnimationを組み込む場合はSingleTickerProviderStateMixin、複数のAnimationを組み込む場合はTickerProviderStateMixinを使用する。width.dartclass _WidthAnimationPageState extends State<WidthAnimationPage> with SingleTickerProviderStateMixin {2. StatefulWidgetにAnimationControllerのインスタンスを持たせる
initState()でControllerの生成、dispose()でControllerの破棄を行う。
AnimationControllerによるアニメーションのスタート・ストップ等の操作も、このStatefulWidgetで行うことになる。width.dartclass _WidthAnimationPageState extends State<WidthAnimationPage> with SingleTickerProviderStateMixin { AnimationController _controller; // AnimationControllerを宣言 @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 500), // アニメーションにかける時間の指定 vsync: this, // [SingleTickerProviderStateMixin]を組み込んだStatefulWidget自身を指定。 ); } @override void dispose() { _controller.dispose(); // Controllerの破棄を忘れずに super.dispose(); } }AnimationControllerの生成・破棄まででStatefulWidget全体は以下のような感じになる。(一部省略)
width.dartclass WidthAnimationPage extends StatefulWidget { ... @override _WidthAnimationPageState createState() => _WidthAnimationPageState(); } class _WidthAnimationPageState extends State<WidthAnimationPage> with SingleTickerProviderStateMixin { // StateにTicerの組み込み // AnimationControllerの宣言 AnimationController _controller; @override void initState() { super.initState(); // AnimationControllerのインスタンス生成 _controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); } @override void dispose() { // AnimationControllerの破棄 _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( ... ); } }3. AnimationControllerとTweenを用いてAnimationインスタンスを生成する
AnimationControllerの下準備が終わったら、実際に時間ごとに変化する値を返すAnimationオブジェクトを生成する。
アニメーションパーツは再利用性などを鑑みて予め別Widgetに切り出しておくといいだろう。
イニシャライザでAnimationインスタンスを生成しておけばスッキリ書ける。
今回はContainerのwidthに設定するdouble値を100⇔300で行ったり来たりさせたいので、以下のようにTweenのbeginには100、endには300を設定しておく。width.dartclass WidthAnimation extends StatelessWidget { WidthAnimation({ Key key, this.controller, }) : _width = Tween<double>(begin: 100.0, end: 300.0).animate(controller), super(key: key); final AnimationController controller; final Animation<double> _width; ... }Tweenのanimate()メソッドに任意のAnimationControllerを渡しておくと、戻り値としてAnimationが返される。
そして、そのアニメーションの開始、終了、停止等は引数に渡したController経由で操作できるというスンポーである。Animationインスタンス生成のタイミングだが、今回のように単一のアニメーションパーツを単一のAnimationControllerで操作するだけのシンプルな構成ならば、予め親のStatefulWidgetで生成して渡すだけでもいい。
width.dartclass WidthAnimation extends StatelessWidget { WidthAnimation({ Key key, this.width, // 今回のケースでは生成済みAnimationインスタンスをもらうのでも良い }) : super(key: key); final Animation<double> width; ... }実運用上は、Animationの仕様をどのレイヤーで確定させたいかによって使い分けるといいだろう。
4. WidgetツリーにAnimatedBuilderを組み込む
アニメーションパーツとして切り出したWidthAnimationにAnimatedBuilderを組み込む。
本来、アニメーションを含む全ての画面更新にはStatefulWidgetのSetState()ないしはそれに相当するトリガーが必要だが、そのあたりのことはAnimatedBuilderがよしなにやってくれるので、気にしなくて良い。そしてそれがまさにFlutterアニメーションにAnimatedBuilderを利用する主要な利点の一つである。
直接アニメーションするWidgetの生成関数をbuilderに渡し、その子Widget(アニメーションしない)をchildに渡す。
WidthAnimationの全体は以下のようになる。width.dartclass WidthAnimation extends StatelessWidget { WidthAnimation({ Key key, this.controller, }) : _width = Tween<double>(begin: 100.0, end: 300.0).animate(controller), super(key: key); final AnimationController controller; final Animation<double> _width; Widget _animationBuilder(BuildContext context, Widget child) { return Container( width: _width.value, // valueが変化する度にbuilderが実行され、アニメーションが実現する height: 100.0, alignment: Alignment.center, color: Colors.red, child: child, // 静的な子Widget(Text)はBuilderの親Widget(WidthAnimation)でキャッシュする ); } @override Widget build(BuildContext context) { return AnimatedBuilder( builder: _animationBuilder, animation: controller, // AnimationControllerでもAnimationでもよい child: Text('width', style: Theme.of(context).primaryTextTheme.headline6, ), ); } }Flutterにおけるbuilder型Widgetの定形的な書き方で、簡単に言えば「動的なWidgetにぶら下がる静的な子WidgetをBuilderの親Widgetでキャッシュする」というテクニックなのだが、具体的にどのような効果があるのかついての説明を始めてしまうと壮大な脱線になってしまうので本記事では扱わない。気が向いたらそのうち別記事で扱いたい。
AnimatedBuilderのanimationにはAnimationControllerもしくはAnimationのインスタンスを渡す。
どちらを渡すべきかの判断基準だが、AnimationControllerが複数のAnimationをハンドリングする場合にはAnimatedBuilderにはAnimationを直接渡す方が間違いが少ないはず。それ以外ならばどちらでも良いだろう。
サンプルではAnimationControllerとAnimatedBuilderが1対1のシンプルな構成なのでどちらを渡しても問題ない。これでWidthAnimationの準備ができたので、先に用意したWidthAnimationPageに組み込んでやればよい。
width.dartclass _WidthAnimationPageState extends State<WidthAnimationPage> with SingleTickerProviderStateMixin { AnimationController _controller; ... // 略 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( ... // 略 ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ SizedBox(height: 100.0), // topマージン用SizedBox() WidthAnimation(controller: _controller), // アニメーションパーツ ], ), ); } }5. 任意のタイミングでAnimationControllerを操作する
今回はボタンを押す度にWidthAnimationのwidthが100⇔300を行ったり来たりする仕様なので、そのためのボタンを用意する。
シンプルにこんな感じでいいだろう。widh.dartCupertinoButton( onPressed: () { if (_controller.status == AnimationStatus.dismissed) { _controller.forward(); // width: 100 -> 300 } else if (_controller.status == AnimationStatus.completed) { _controller.reverse(); // width: 300 -> 100 } }, color: Colors.green, child: Text('animate', style: Theme.of(context).primaryTextTheme.headline6, ), ),こいつをWidthAnimationPageに組み込んでやる。
widh.dartbody: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( children: <Widget>[ Spacer(), CupertinoButton( onPressed: () { if (_controller.status == AnimationStatus.dismissed) { _controller.forward(); } else if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } }, color: Colors.green, child: Text('animate', style: Theme.of(context).primaryTextTheme.headline6, ), ), Spacer(), ], ), SizedBox(height: 100.0), WidthAnimation(controller: _controller), ], ),おわりに
できるだけ、「今すぐわからなくてもできる」というような部分には言及しないように努めたつもりだが、それなりに散らかってしまっている気がしてならない。初投稿なので大目に見てほしい。というか、ご質問やご指摘を頂けたら嬉しいです。
継続的に記事を投稿しながら本記事もブラッシュアップしていく所存。そのうち、「AnimatedBuilderとは何か」という根本の部分をより深く掘り下げた記事なども書いてみたい。
以上。
参考文献:
Flutter公式『Animations tutorial』