- 投稿日:2020-07-29T22:10:28+09:00
*Android*ピンチイン・ピンチアウトを検知する
はじめに
こんにちは.今回は,Androidでピンチ操作を検知しようと思います.ピンチイン・ピンチアウトを判別する方法について解説します.
前提
開発環境は以下の通りです.
*Android Studio 4.0.1
*targetSdkVersion 28
*Google Nexus 5xピンチイン・ピンチアウトの判別
ピンチ操作の検知には,
ScaleGestureDetector
を使用します.第一引数にContext,第二引数には,OnScaleGestureListener
インタフェースを実装したクラスのオブジェクトを指定します.今回は,匿名クラスを使用して,インタフェースを実装します.this.scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.OnScaleGestureListener() { // ピンチ操作中に繰り返し呼ばれる @Override public boolean onScale(ScaleGestureDetector detector) { return true; } // ピンチ操作を開始したときに呼ばれる @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } // ピンチ操作を終了したときに呼ばれる @Override public void onScaleEnd(ScaleGestureDetector detector) { } });次に,
onTouchEvent
メソッドを用意し,onTouchEvent
メソッドが呼ばれた際に、ScaleGestureDetector.onTouchEvent
が呼ばれるようにすることで,ピンチ操作を検知することができます.@Override public boolean onTouchEvent(MotionEvent motionEvent){ this.scaleGestureDetector.onTouchEvent(motionEvent); return true;ピンチ操作を検知できたら,次にピンチイン・ピンチアウトを判別します.ピンチ操作では,画面に2本の指を触れています.指間の距離を測定するメソッドとして
getCurrentSpan()
が用意されているので,それを使用します.指間の距離の変化を利用することで,ピンチイン・ピンチアウトを判別することができます.ピンチイン・ピンチアウトの誤判定を防ぐために,閾値として指間の距離xを指定し,x以上変化した場合にピンチイン・ピンチアウトの判定を行います.@Override public boolean onScale(ScaleGestureDetector detector) { distance_current = detector.getCurrentSpan(); return true; }ピンチ操作中の指間の距離の正確性
ピンチ操作中の指間の距離を取得するには
getCurrentSpan()
を使用します.取得した指間の距離は,スマートフォンの画面の対角線の長さより小さいはずです.誤った距離を取得してしまった場合に取り除くために,スマートフォンの対角線の長さと比較して,それより小さいもののみを指間の距離とします.ここでは,Navigation bar を除いた画面の領域から,対角線の長さを求めます.画面の対角線の長さを取得するには以下のようにします.WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); Point size = new Point(); Display disp = wm.getDefaultDisplay(); disp.getSize(size); screen_width = this.size.x; screen_height = this.size.y; //対角線の長さを求める int screen_diagonal = (int) Math.sqrt((int)(Math.pow(screen_width, 2)) + (int)(Math.pow(screen_height, 2)));サンプルコード
AndroidManifest.xml?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapplication"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>MainActivity.javapackage com.example.myapplication3; import androidx.appcompat.app.AppCompatActivity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.WindowManager; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private ScaleGestureDetector scaleGestureDetector; private long time_elapsed; private long time_start; private long time_current; private float distance_current; private float distance_start; private Boolean flg_pinch_out; private Boolean flg_pinch_in; private WindowManager wm; private Display disp; private Point size; private int screen_width; private int screen_height; //画面の対角線の長さ private int screen_diagonal; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.wm = (WindowManager) getSystemService(WINDOW_SERVICE); this.size = new Point(); this.disp = wm.getDefaultDisplay(); this.disp.getSize(size); screen_width = this.size.x; screen_height = this.size.y; //対角線の長さを求める screen_diagonal = (int) Math.sqrt((int)(Math.pow(screen_width, 2)) + (int)(Math.pow(screen_height, 2))); this.scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.OnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { time_current = detector.getEventTime(); time_elapsed = time_current - time_start; if (time_elapsed >= 0.5){ distance_current = detector.getCurrentSpan(); if (distance_start == 0){ distance_start = distance_current; } flg_pinch_out = (distance_current - distance_start) > 300; flg_pinch_in = (distance_start - distance_current) > 300; if (flg_pinch_out){ Toast.makeText(getApplicationContext(), "Pinch out", Toast.LENGTH_LONG).show(); time_start = time_current; distance_start = distance_current; } else if (flg_pinch_in){ Toast.makeText(getApplicationContext(), "Pinch in", Toast.LENGTH_LONG).show(); time_start = time_current; distance_start = distance_current; } else { //pass } } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { distance_start = detector.getEventTime(); if (distance_start > screen_diagonal){ distance_start = 0; } time_start = detector.getEventTime(); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { } }); } @Override public boolean onTouchEvent(MotionEvent motionEvent){ this.scaleGestureDetector.onTouchEvent(motionEvent); return true; } }
- 投稿日:2020-07-29T14:23:12+09:00
android webviewをchromeみたいに使う
まずは全体のコードはこんなかんじです。
MainActivity.ktpackage com.sample.android.webview import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.KeyEvent import android.view.WindowManager import android.webkit.WebView import android.webkit.WebViewClient import com.sample.android.webview.R class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val myWebView: WebView = findViewById(R.id.webview) // ポイント1,JavaScriptを有効化 myWebView.getSettings().setJavaScriptEnabled(true); // Web Storage を有効化 myWebView.getSettings().setDomStorageEnabled(true); // HTML5 Video support のため // Hardware acceleration on getWindow().setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED ); // ポイント2,ページナビゲーションの処理 myWebView.webViewClient = WebViewClient() myWebView.loadUrl("https://yahoo.co.jp") } // ポイント3, ウェブページ履歴を操作する override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { val myWebView: WebView = findViewById(R.id.webview) if (keyCode == KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) { myWebView.goBack() return true } return super.onKeyDown(keyCode, event) } }ポイント1、JavaScriptを有効にする
myWebView.getSettings().setJavaScriptEnabled(true);ポイント2、ページ ナビゲーションの処理
リンクを開いたときにブラウザが開かないようにします。
myWebView.webViewClient = WebViewClient()ポイント、3ウェブページ履歴を操作する
WebView が URL の読み込みをオーバーライドすると、アクセスしたウェブページの履歴が自動的に蓄積されます。履歴を前後に移動するには、goBack() および goForward() を使用します。
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { // Check if the key event was the Back button and if there's history if (keyCode == KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) { myWebView.goBack() return true } // If it wasn't the Back key or there's no web page history, bubble up to the default // system behavior (probably exit the activity) return super.onKeyDown(keyCode, event) }以上です。
参考にさせて頂いたサイト
https://developer.android.com/guide/webapps/webview?hl=ja
- 投稿日:2020-07-29T13:25:19+09:00
GitHub Actions で Android アプリを手動ビルドできるようにする
下記のようなyamlファイルをAndroidプロジェクトのリポジトリに置きます。
.github/workflows/manual-build.ymlname: Manual build on: workflow_dispatch: inputs: variant: description: 'Build variant' required: true ref: description: 'branch name|tag name|commit SHA' required: true default: master jobs: build: name: Build runs-on: ubuntu-18.04 steps: - name: Check out uses: actions/checkout@v2 with: ref: ${{ github.event.inputs.ref }} - name: Set up JDK uses: actions/setup-java@v1 with: java-version: 1.8 - name: Restore gradle cache uses: actions/cache@v2 with: path: ~/.gradle key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}-${{ hashFiles('**/*.gradle.kts') }} - name: Run Android Lint run: ./gradlew lint${{ github.event.inputs.variant }} - name: Run unit test run: ./gradlew test${{ github.event.inputs.variant }}UnitTest - name: Build with Gradle run: | # 署名まわり。https://qiita.com/hkusu/items/cadb572c979c4d729567 を参照 if [[ -n '${{ secrets.KEYSTORE_BASE64 }}' ]]; then echo '${{ secrets.KEYSTORE_BASE64 }}' | base64 -d > release.keystore export KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' export KEY_ALIAS='${{ secrets.KEY_ALIAS }}' export KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' fi ./gradlew assemble${{ github.event.inputs.variant }} - name: Get apk path id: apk-path run: | path=$(find **/build/outputs/apk -name '*.apk' -type f | head -1) echo "::set-output name=path::$path" - name: Upload apk file uses: actions/upload-artifact@v2 with: name: apk path: ${{ steps.apk-path.outputs.path }}そうすると、下図のように GitHub の画面からワークフローを実行してアプリがビルドできるようになります。
apk ファイルは、下図のように結果画面からダウンロードできます。
- 投稿日:2020-07-29T12:23:16+09:00
【個人開発】FlutterとFirebaseでサボり防止アプリを作った
先日、個人開発アプリをリリースしました!やったね!!!
審査も通りまして、現在App StoreとPlay Storeで配信されております。
せっかくなので、利用した技術のメリットと注意点をまとめておこうと思います。
作ったアプリ
これです。
広報用ページも作ってみたのでリンクを貼っておきます。
広報用ページはスマホで見る用にレイアウトしてます。
英語名: Procrastinators Watch Mutually
解決したい課題
やらなきゃいけないことがあるのに、なかなか取り組めずにグダグダしてしまう経験がみなさんあると思います。
でも、会社や学校で「今はこれをやる時間です」と管理されたり、誰かと一緒に「今からやるぞ!」って決めると、取り組めますよね。
そういう「誰かと一緒」「時間を管理される」という状況をアプリ上で再現して、作業に取り組みやすくするためのアプリです。
解決方法
- 一緒に作業してるメンバーと作業内容が表示される
- タイマーで全員同時に作業時間と休憩時間を指定される(49分作業12分休憩)
- ちゃんと取り組めたかどうか自己申告し、それに対して褒めたり罰したりできる
- 褒められたいし、自分に嘘をつきたくないので、作業に取り組むモチベーションアップ
- 罰された人は社会貢献活動で償うまでアプリを利用できない
- 社会貢献活動とは動画広告視聴のことで、広告主・広告配信業者・アプリ制作者に貢献できる
- サボりに対し、有意義な軽い「罰」を用意することで、作業に取り組むモチベーションアップ
そんなアプリです。
ということは、広告の実装が必要ですね。
また、ユーザーの入力した内容が他者から見えるので、プライバシーポリシーや利用規約で不適切な入力を防止し、違反者にはミュートやBANをできるようにする必要があります。
そのためにはログイン機能でユーザーを識別する必要もありそうですね。
では技術方面を見ていきましょう。
Flutter
フロントエンドはFlutterを使います。
Flutterのメリット
最大のメリットはなんと言っても、AndroidとiOSの両アプリをそれぞれ作らなくて済むことです。
また、同じメリットを持つフレームワークは他にもありますが、Flutterが今最も勢いがあるフレームワークと言えると思います。コミュニティも活発で情報が得やすいです。
あのGoogleがかなり力を入れて作っているという安心感もありますね。
今後はWebや、Mac、Windows用のネイティブアプリも作れるようになるということで、スマホアプリに限らず広範囲にわたるクロスプラットフォーム開発が可能になっていくようです。
Webは2020年7月27日現在、既にかなりのレベルでアプリが作れます。
開発環境
Flutterの開発環境はAndroid Studioを使いました。
VSCodeでもいいと思います。
Flutter Inspectorの見やすさや新規プロジェクトの作りやすさでAndroid Studioを選びましたが、それも半年以上前のことなので、今のVSCodeと比較するとどうかわかりません。
静的解析
Flutter開発をする際は静的解析を有効にするようにしましょう。
これはほぼ必須だと思います。
行儀がよく見やすい、ルールの統一されたコードを書くことが出来ます。
詳しくはこちら。
https://dart.dev/guides/language/effective-dart
https://medium.com/flutter-jp/analysis-b8dbb19d3978
状態管理手法
Provider + ChangeNotifier パターンを採用しました。
2020年7月27日現在、Flutter公式が最もおすすめしている方法です。
Flutter開発を始めると状態管理手法に悩むことになると思いますが、まずはこれでいいと思います。
他の方法については、こちらの記事を参考にしてもらえると嬉しいです。
Flutterの状態管理いろいろ比較 〜グローバル変数StateからBLoCパターンまで〜
なお、リンク先記事を書いた後で登場した
- StateNotifier
- Riverpod
- Cubit
などの方法については記事内で触れていません。興味ある方はお調べください。
筆者自身の次のアプリはRiverpod+ChangeNotifierパターンで作ろうと思っています。
多言語対応
英語・日本語・中国語の3言語に対応させました。
多言語対応の方法はこちらの公式解説のAn alternative class for the app’s localized resourcesを参考にしつつ、オレオレ手法でやってます。
公式オススメの方法は、自動コード生成を含んでおり、それと静的解析ツールがバッティングして警告を吐きまくり、静的解析の例外設定をしてみたらそこにバグがあり例外設定が効かず…というトラブルに見舞われたので、それ以来オレオレでやってます。今はどうなんでしょう。
筆者のオレオレ手法は、自動コード生成がなく、コード補完も効いて
L10n.of(context).title
のように欲しい文言を得られるので、悪くないと思ってます。ここでは詳しく解説しませんが、そのうち記事にするかもしれません。
なお、もちろん、後述のプライバシーポリシーやスクリーンショット、ストア上のタイトルや説明文も全て3言語分用意してます。ちょっと大変。
Firebase
バックエンドにはFirebaseを使っています。
ログイン・データベース・サーバーサイドロジック・広告・プライバシーポリシーページの用意を全てFirebaseに頼っています。様様ですね。
下記に出てくるfirebase関係のパッケージを使うにはfirebase_coreパッケージが必要になるのでご注意ください。
Authentication
Google、Facebookアカウントを使ったログインを実装しました。
firebase_authパッケージを使います。
ただし、iOSアプリの場合はサードパーティログインにAppleを加えることが必須なので、iOS版に限りAppleログインも実装しました。
Appleに関してはこちらの記事が大変参考になりました。
[Flutter] Sign in with Apple を Cloud Functions 無しで実装する
Androidアプリの方は、Androidのapkファイルの署名に使った証明書のハッシュをFirebaseやFacebookに伝えておく必要があるのでご注意ください。
デバッグ時にデバッグ用証明書のハッシュを渡していても、リリース時にはリリースに使った鍵の証明書のハッシュを伝える必要があります。筆者はこれを忘れていて、リリース直後にAndroid版で一切ログインできない事故を起こしました。
Firestore
cloud_firestoreパッケージを使います。
Firestoreの使い方・考え方についてはこちらの動画シリーズがとてもわかりやすいので一通り見ておくといいと思います。
従来のRDBMSとは結構異なる考え方が推奨されてます。
「データの正規化を頑張るより、重複を許した方が良い」とか「リアルタイム接続をデフォルトにして一件ずつフェッチするのを例外的な場合に限定しよう」とか。
「クエリ一回でも、読み込んだドキュメントの件数に応じて料金がかかる」なんかは結構重要です。
セキュリティルールとテスト
Firestoreで重要になるのがセキュリティルールです。
アクセス権限の管理だけではなく、サーバーサイドバリデーションもセキュリティルールを使って行います。
筆者はセキュリティルールとそのテストはAndroid StudioではなくVSCodeで書いています。
テストコードをTypeScriptで書いているのですが、Android Studioではシンタックスハイライトや補完が効かないので。
セキュリティルールだけ書いていくとゴチャゴチャしてきて管理が大変なので、やはりテストとセットで書いていくのが良いと思います。
その際はこちらの記事を参考にしてもらえたらとても嬉しいです。
VSCodeデバッガを使ってCloud FunctionsとFirestoreの連携テスト(TypeScript)
2020年6月のバージョンで、セキュリティルール内で一時変数が使えるようになったりして、どんどん便利になっていますね。
New improvements to Firestore Security Rules
Functions
Firestoreの中身をどうしてもサーバーサイドから操作したい時は、Cloud Functionsを使います。
「どうしても」と書いたのは、Firestoreの操作をする時はまず第一にクライアントのSDKから操作することを考えるべきだからです。
Firestore SDKはオフラインサポートなどいろいろうまいことやってくれるので、積極的に利用しましょう。
先程の動画シリーズの一つですが、これが参考になります。
https://www.youtube.com/watch?v=oDvdAFP6OhQ
また、Functionsを使う必然性がある場合でも、呼び出し可能関数を作るのはできるだけ避けましょう。
理由はこちらの記事が参考になります。
CQRSを学ぶついでにCloud FunctionsとFirestoreを連動させる時の小技も学ぶ
Web APIの操作に慣れているとつい同様にAPIを作って叩きたくなりますが、耐えましょう。クライアントからの操作で完結するものは完結させます。
じゃあどうするかというと、Cloud Functionsには、さまざまな「トリガー」が用意されています。
- Firebase Analyticsに新しいイベントが送られた時
- Firebase Authentication で新しいユーザーが作られた時
- Firestore上でドキュメントが削除されたとき
などなど、たくさんあります。
これらに応答してFunctionsが起動するようにしましょう。
ただし、Functions上の各関数は一回のトリガーに対して複数回起動することがあるので、「冪等」にしておくといいです。「冪等」というのは、「2回以上実行しても結果が変わらない」ということです。
今回のアプリでは、ユーザーがきちんと退室せずに放置した場合に退室処理を走らせるためにFunctionsを利用しています。
hosting
サーバー上にファイルを置いておき、特定のURLでアクセスできるようになります。
一昔前だったらレンタルサーバーを有料で借りないとこういうことはできませんでしたが、便利な時代になりました。
今回は、プライバシーポリシーを掲載したページや、広報用ページを置くために利用しています。
開発環境
FunctionsもVSCodeで書いています。TypeScriptで書いているのですが、Android Studioだと補完やシンタックスハイライトが効かないからです。
Functionsのテスト
公式記事が大いに参考になります。
また、こちらは上に既に貼ったのと同じ私の記事ですが、再掲します。参考にしてもらえると嬉しいです。
VSCodeデバッガを使ってCloud FunctionsとFirestoreの連携テスト(TypeScript)
AdMob
このアプリは広告が出るので、firebase_admobパッケージを使って広告を出しています。
ただ、このパッケージは何かと使いにくいので、色々注意が必要です。
筆者がこのパッケージについてあれこれ嘆いている様子がわかるツイッター検索結果がこちらです。
ストア申請のために
アプリが動くようになったら、ストアで配信したいですよね。
ここでもいろいろ障壁があります。
ミュート機能・通報機能
このアプリはユーザーが入力した表示名やタスク内容が他人から見えるため、「ユーザー生成コンテンツ」に該当すると思われます。
この場合、少なくともApp Storeに掲載されるためには、不適切な内容を投稿するユーザーに対して対策を施しておく必要があります。Play Storeについては未調査ですが、おそらく同様だと思います。
具体的には、ユーザーをミュートや報告する機能が必要です。
詳しくはApple公式のこちらが参考になります。
App Store Reviewガイドライン - 1.2 ユーザー生成コンテンツ
プライバシーポリシー
個人情報をどう扱うか、というものです。
すべてのアプリで必須のはずです。一切情報収集しない場合でもその旨を記述したプライバシーポリシーが必要だと思われます。
Firebase Analyticsを使っている場合は情報収集していることになるので、その旨の記述が必要ですね。
AdMobを使っている場合はユーザーに合わせた広告を出すために情報収集してますので、それについても記述が必要です。
詳しくはこちらを御覧ください。
App Store Reviewガイドライン - 5.法的事項
とはいえプライバシーポリシーなんてどう書いたら良いかよくわからないものです。
検索すると書き方の解説や雛形がヒットしますので、参考にするといいと思います。
申請用スクショ撮影
ストアで申請するためにはアプリのスクリーンショットが必須です。
これが案外面倒です。スクショとして欲しい画面が、アプリを実際に操作して簡単に作れるとは限らないからです。
それが複数端末分必要になります。
また、多言語対応している場合はその数だけ必要になります。
4画面*4端末*3言語とすると48枚必要になりますね。手動で撮っては結構な手間です。
これについては私のこちらの記事を参考にしてもらえると嬉しいです。
Flutterアプリのスクショを(極力)自動で撮る on AndroidStudio
署名について
署名周りも、アプリ配信時の結構なハードルになると思います。
これも自分の記事で恐縮ですが、概要をまとめておりますので参考にしてもらえたら嬉しいです。
Android・iOSアプリを配信する時の署名について整理する
終わりに
技術周りをまとめておこうと思って書いてみたら、自分の過去記事へのリンクが多くなってしまいました。
まあ、難所を突破するたびにその結果を記事にまとめるようにしていたので、それらへのリンクが多くなるのは必然か……。。。。
ということで、皆様、アプリ開発ライフを楽しみましょう!
サボり防止アプリもよろしくおねがいします!これで時間管理しながらアプリ開発してくれたらメチャクチャ嬉しいです!
- 投稿日:2020-07-29T09:08:14+09:00
Codemagic でモバイルアプリの CI/CD を始めましょう
この記事は、Codemagic の運営元である Nevercode Ltd の依頼で作成しました。2020/7/6 に公開された オリジナルの英語記事の日本語訳に加え、冒頭に私のコメントを、末尾におすすめの Codemagic 関連の日本語記事へのリンクを列挙しています。
Codomagic は、今年の2月に発表された Flutter UX 調査チームによる Q4 2019 の調査結果において、開発者が CI/CD で採用しているサービスのトップ3に挙げられており、(Codemagic、 Github Actions、 GitLab)、その中でも満足度が最も高かったことが示されています。
Codomagic は、Google と提携して Flutter 専門の CI/CD サービスとして開始されただけあって、Flutter を手厚くサポートしています。また、以下の翻訳で示されているように、Flutter に加えて Android ネイティブ、 iOS ネイティブ、React Native のサポートが追加されましたので、既存のそれらのアプリの資産がある場合でも、Codomagic をすべてのアプリに対応する統一した選択肢として検討できるようになりました。
また、プライベートリポジトリでもクレジットカード登録なしで使える無料枠が用意されていますので、試用を開始する敷居がとても低いです。
公式のドキュメンテーションや公式ブログの内容も、英語ではありますが、まずまず充実していると言えるのではないでしょうか。
あらたに Flutter プロジェクトを始める際の CI/CD サービスの選定作業については、Codemagic の試用から始めるのがひとつの無難な選択肢でしょう。
以下が、オリジナルの英語記事の日本語訳です。
2018 年にロンドンで開催された Flutter Live おいて、 Nevercode は Google と提携し、Flutter アプリ専用の CI/CD ソリューションとして Codemagic を立ち上げました。Codemagic を使えば、Flutter アプリを辛い設定作業をせずにテストしてリリースできます。
Codemagic は Flutter の扱いに熟達していますが、今やそれだけにとどまりません。Flutter アプリでないネイティブアプリでも同等に扱えるように、
codemagic.yaml
ファイルを使用して、ネイティブの Android と iOS アプリ、および React Native アプリの設定とビルドができるようになりました。スクリプトをより詳細に制御したい場合は、カスタムスクリプトを実行したりカスタムワークフローを作成できます。高品質なアプリをさらに迅速に開発できるようになりました。ここでは、Codemagic を使えば、Flutter アプリ、Android ネイティブ アプリ、 iOS ネイティブ アプリ、React Native アプリの CI/CD をいかに簡単に設定できるのかを、その手順を説明を通じてご紹介いたします。
必要条件
Flutter アプリ、iOS ネイティブアプリ、Android ネイティブアプリ、React Native アプリを Codemagic 上でセットアップするには、いくつかのものが必要です。
- もちろん、Flutter アプリ、iOS ネイティブアプリ、Android ネイティブアプリ、React Native アプリとそのテストコードが必要です。そして、それらのアプリは GitHub、GitLab、または Bitbucket でホストされていることを念頭に置いています。
- App Store や Play Store に公開したい場合は、証明書や Provisioning Profile のようなコード署名を用意します。
- ビルドレポートやアーティファクトを送信する場合は、Slack のワークスペースを用意します。
以上です。これでアプリに CI/CD を追加する準備ができました。では、実際に設定することを通じて、基本的なことを説明していきましょう。なお、コード署名と公開作業についてはこのチュートリアルでは対象とせず、別途説明とさせていただきます。
Codemagic にアカウント登録
最初の一歩は、codemagic.ioウェブサイトにアクセスし、GitHub、GitLab、または Bitbucket のアカウントで Codemagic のアカウント登録をすることです。これで、そのサービスのすべてのリポジトリーを Codemagic アカウントに統合して扱えるようになります。
アプリケーション ダッシュボード ( Applications Dashboard )
サインインすると、Codemagic のダッシュボードにアプリのリポジトリーの一覧が表示されます。一覧をフィルターにかけたり検索したりして、ビルドしたいアプリを見つけることができます。初回のアクセスでは、以下のように表示されるでしょう。
ビルドのワークフローを設定するには、Set up buildボタンをクリックします。
設定がすでに済んで、まだビルドを開始していない場合は、このように表示されます。
Finish build setup をクリックして設定を終え、ビルドを開始できます。
ちなみに、以前に Codemagic 上でビルドしたアプリがある場合は、以下のような表示になります。
Start new build をクリックして以前の設定でビルドを開始したり、Settings(歯車のアイコン)をクリックして変更したりできます。
ビルドの設定
Set up buildボタンをクリックすると、このページが表示されます。ここでは、プロジェクトの種類を選択できます。
この一覧から適切なもの選択するだけです。選択すると、ワークフローの設定画面に移動します。
Flutter の設定
このページでは、以下のような UI で Flutter アプリのビルドワークフローを設定できます。
ビルドワークフローをより詳細に制御したい場合は、Download configuration ボタンをクリックして
codemagic.yaml
ファイルを使用できます。この
codemagic.yaml
ファイルを使用して、ワークフローを変更できます。プロジェクトのルートディレクトリに置いて、VCS にコミットするだけです。リポジトリーにこのファイルがあれば、Codemagic が自動的に検出します。Start your first build ボタンをクリックして、Flutter アプリのビルドをすぐに開始できます。ワークフローをまだ変更していない場合は、以下の一連のステップのデフォルトのワークフローが使用されます。
ビルドマシンの準備、ソースコードのチェックアウト、Flutter の依存関係のインストール、テストの実行、アプリのビルド、アーティファクトの公開といった一連のステップがあります。これが基本的な CI/CD のワークフローに必要なものです。各ステップをクリックして展開すると、Codemagic はそれぞれのステップのログを表示します。
その他の設定
Flutter アプリ以外のプロジェクト(Android ネイティブアプリ、iOS ネイティブアプリ、React Native アプリ、または Flutter Module)をビルドする場合は、
codemagic.yaml
ファイルを使用してワークフローを設定する必要があります。選択したプロジェクトの種類ごとにテンプレートが用意されています。ビルドレポート
ビルドが終了すると、Codemagic のダッシュボードに、ビルド時間、ビルドステータス、コミットなどの概要とそれらの詳細が以下のように表示されます。
これに加えて、デフォルトでは iOS と Android アプリの両方の、ビルドのステータスとアーティファクトがメールで送信されます。メールの内容は以下のようになります。
Slack の設定をすることで、Slack チャンネルにビルドレポートを投稿させることもできます。
以上です!なにも設定しなくても Flutter アプリに CI/CD サポートを追加できました。これが Codemagic のマジックです。
ビルドの失敗
ここまでは、すべてが完璧に動作しているハッピーパスのシナリオを説明してきましたが、現実の世界では必ずしもそうなるとは限りません。ビルドに失敗したり、テストに失敗したり、調査が必要な問題が発生することもあります。Codemagic は、各段階においてそれが成功したか失敗したかに関わらず、詳細なログを提供しています。これらのログは失敗したビルドをデバッグするのに非常に便利です。以下の画面は、ユニットテストが失敗していて、ビルドの失敗のすべての詳細がログに記録されているものです。
見ての通り、インテグレーションテストとウィジェットテストは成功していますが、ユニットテストが失敗しています。これを修正することでビルドを修正できました。テストが失敗すると、それを知らせるメールもビルドログへのリンク付きで届きます。
ビルドの失敗についてもっと詳しく知るには、モバイルアプリの CI ビルドが失敗してしまう、よくある 12 個の設定間違い をお読みください。
デフォルトワークフローとカスタムワークフローの比較
デフォルトのビルドワークフローには CI/CD ワークフローに必要となる基本的なものが含まれていますが、ほとんどの場合、ビルドプロセスの一部になんらかのカスタマイズを加える必要があります。例えば、ビルドステータスを Slack や他のコミュニケーションチャンネルに送信したり、アーティファクトを Fabric や HockeyApp などの他のサービスに発行したい場合があります。これらのシナリオを実現するためには、カスタムワークフローを作成し、カスタムスクリプトを実行します。それでは、プロジェクトの必要に応じてワークフローをカスタマイズする方法を見ていきましょう。
Build フェーズ
Codemagic における Flutter アプリの Build フェーズには、ビルドプロセスを定義するためのさまざまな手段があります。どのブランチをビルドするか、そして何のイベントをきっかけにしてビルドするかを選択することから始めます。
次に、Godemagic では、以下のようなもっと詳細な設定も可能です。
- Flutter のバージョン
- アプリをビルドする設定。debug や release など
- iOS や Android、またはその両方といったプラットフォームの選択
- Xcode のバージョン
- カスタムビルドの引数
デフォルトのワークフローでは、以下の設定が選択されています。
とは言え、ビルドプロセスはデフォルトの動作をプロジェクトの必要に応じて変更することで、いつでもカスタマイズできます。Xcode の異なるバージョンや異なるリリース設定でアプリのビルドが、上記の画面のオプションの変更、さらにカスタムワークフローによって可能です。
Codemagic のもう一つの大きな特徴として、すべてのフェーズの前にカスタムスクリプトを追加できます。これにより、開発者は各フェーズの前後で任意のコマンドを実行できます。カスタムスクリプトを設定するには、単純に Build、Test、Publish などの各フェーズの上部にある
+
ボタンをクリックします。ここでは、Build フェーズの前に配置するスクリプトの例を示します。Pre-build scriptに、プロジェクト固有の依存関係のインストールなど、任意のコマンドを追加できます。
Test フェーズ
Codemagic の Test フェーズでは、開発者はテストの有効化と無効化を切り替えられます。プロジェクトで実行する Flutter テストの種類を選択できますし、テスト中に使用する追加の引数の指定もできます。プロジェクトで Flutter Driver tests を使用している場合は、使用するエミュレータの種類も指定できますし、AWS Device Farm の恩恵も受けられます。
こちらで、Flutter のテストに関する詳細をご覧いただけます。
とは言え、デフォルトの動作を無効にして、カスタマイズしたテストコマンドを使用することも可能です。例えば、コマンドラインから UI テスト(訳注: Flutter のインテグレーションテスト) だけを実行したい場合は、次のコマンドを pre-test フェーズに追加します。
flutter drive --target=test_driver/main.dartこれにより、デフォルトのテストワークフローを使用する代わりに、このコマンドが実行されます。
Publish フェーズ
Codemagic の Publish フェーズでは、ビルドレポートやアーティファクトの公開のための様々な設定が必要です。デフォルトでは、あなたが GitHub/Bitbucket/GitLab にメールアドレスを提供している場合は、メールアドレスだけが設定されています。
このフェーズでは、その他にも以下のものを明示的に設定する必要があります。
- iOS と Android の両方のコード署名に必要な情報をアップロード
- Slack の webhook を設定
- 公開のための、Google Play や App Store Connect の詳細を入力
これらに加えて、カスタムスクリプトを書いて、Fabric や HockeyApp などのサードパーティサービスにアーティファクトをデプロイすることもできます。
スクリプトの大きな力を利用して Codemagic でカスタムワークフローを設定し、アプリのビルドプロセスをより強固なものにできます。
多数のワークフロー
Codemagic では、異なる設定やバージョンのソフトウェアの要件を満たすために、多数のワークフローを作成できます。既存のワークフローを複製して名前を変更することで、新しいワークフローを作成できます。以下の手順です。
- App settingsに移動します。(訳注: https://codemagic.io/apps ページで、それぞれのプロジェクトの歯車アイコンをクリックして移動できます)
- Workflow settings セクションの、Duplicate workflow をクリックします。複製されたワークフローは、アプリ名の下の一覧に "Default Workflow (Copy)" として表示されます。ワークフローを選択し、名前を変更します。
上の画面では、デフォルトのワークフローの名前を "Custom Workflow" に変更しています。異なる Xcode のバージョンやデバッグ用設定やリリース用設定での Flutter アプリのテストなど、多くのワークフローを作成できます。
まとめ
Codemagic は、Flutter アプリ専用の公式の CI/CD ソリューションとして始まりましたが、今ではすべてのモバイルプロジェクトをこの最速の CI/CD に迎え入れています。Codemagic のマジックを使えば、設定なしで Flutter アプリのビルド、テスト、公開ができますし、カスタムワークフローを使えば環境を制御してビルドを実行できます。Android ネイティブアプリ、iOS ネイティブアプリ、React Native アプリをお持ちの場合は、
codemagic.yaml
ファイルを使用するだけで、発進の準備が完了です!ハッピー ビルディング!
お役立ちリンクと参考文献
- Codemagic の公式ドキュメンテーション
- Flutter Module をネイティブ Android アプリプロジェクトに追加して、 Codemagic でテストする方法
- Flutter Module をネイティブ iOS アプリプロジェクトに追加して、Codemagic でテストする方法
- Flutter Module が追加されたネイティブ Android アプリを、 Codemagic を使用して配布する方法
- iOS のコード署名と公開を、 codemagic.yaml を使用して行う方法
以上が翻訳です。
お役立ち日本語記事へのリンク
- 投稿日:2020-07-29T07:45:03+09:00
FlutterFragmentとViewPagerの組み合わせはonResumeのタイミングで難しかった
ViewPager2を使って2ページ目以降にFlutterFragmentを設定すると、完全にページ遷移するまではFlutterFragmentが描画されず真っ白になります。一度表示すると真っ白になりません。
このように遷移途中でもFlutterFragmentが描画されて欲しいです。
なぜそうなるか
作り方
ViewPager2のAdapterはこのように作りました。
MyFragmentStateAdapter.ktclass MyFragmentStateAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { override fun getItemCount() = 2 override fun createFragment(position: Int): Fragment { return if (position == 0) // AndroidネイティブなFragment MainFragment() else // FlutterFragment FlutterFragment.withNewEngine() .shouldAttachEngineToActivity(false) .build() } }ViewPager2はこのように設定しました。
PagerActivity.ktviewPager.adapter = MyFragmentStateAdapter(this) // こちらを設定しないとスワイプ開始時に一瞬ひっかかる viewPager.offscreenPageLimit = 1Fragmentのライフサイクルを確認する
2ページ目をFlutterFragmentから空のフラグメントにします。
MyFragmentStateAdapter.ktclass MyFragmentStateAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { override fun getItemCount() = 2 override fun createFragment(position: Int): Fragment { return if (position == 0) MainFragment() else EmptyFragment() } }空のフラグメントのライフサイクルを確認します。
EmptyFragment.ktclass EmptyFragment : Fragment() { override fun onStart() { super.onStart() Log.d("EmptyFragment", "onStart") } override fun onResume() { super.onResume() Log.d("EmptyFragment", "onResume") } override fun onPause() { super.onPause() Log.d("EmptyFragment", "onPause") } override fun onStop() { super.onStop() Log.d("EmptyFragment", "onStop") } }logcatを確認した所、onStartは画面を開いたときに呼ばれますが、onResumeは2ページ目に完全遷移するまで呼ばれませんでした。
# 画面を開いた D EmptyFragment: onStart # 2ページ目に完全に遷移 D EmptyFragment: onResumeどうやらFlutterFragmentは初回のonResumeで描画されるようです。この挙動を変更する方法は見つけられませんでした。
ViewPager2ではなくViewPagerを使った場合
コンストラクタの
behavior
フィールドにBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
定数を設定した場合はViewPager2と同じくonResumeは2ページ目に完全遷移するまで呼ばれずFlutterFragmentの描画も完全遷移まで開始されません。MyFragmentStateAdapter.ktclass MyFragmentStateAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getCount() = 2 override fun getItem(position: Int): Fragment { return if (position == 0) MainFragment() else FlutterFragment.withNewEngine() .shouldAttachEngineToActivity(false) .build() } }
behavior
フィールドをBEHAVIOR_SET_USER_VISIBLE_HINT
定数にすれば、画面が開かれると同時にonResumeが呼ばれるので、FlutterFragmentの描画もスワイプ中に行われます。しかしその定数は非推奨扱いなので、後ほど技術的負債になる危険性を感じました。onResumeが完全遷移まで呼ばれない理由
非推奨になったBEHAVIOR_SET_USER_VISIBLE_HINT定数について調べてみたところ、setMaxLifecycleメソッドが紹介されていて、それについて調べてみたところ、こちらの記事に行き着きました。
setUserVisibleHintのdeprecatedとsetMaxLifecycle
この記事の情報を持ってViewPager, ViewPager2のアダプターのソースコードを見てみたところsetMaxLifecycleメソッドの呼び出しで、完全遷移するまではonStartまでしかライフサイクルを遷移させない設定にしていたようです。どうしてもViewPagerを使いたい場合は、ライブラリをフォークしてsetMaxLifecycle呼び出し部分を書き換える必要がありそうです。
最終的にはUIデザインを変更しました
結局、横スワイプによるページ切り替えは諦めて、ボタンによる画面呼び出しにしました。
今回に関してはそちらの方がUIとしては分かりやすいと思いました。
- 投稿日:2020-07-29T01:29:01+09:00
【Android/Kotlin】ViewModel・LiveData基本
ViewModelとは?
ViewModelとは、UI関連のデータを保存し管理してくれるクラスです。分かりやすい活用方法でいうと、画面のローテーションをしてもUIを保持してくれたりします。ローテーションに対してUIを保存する方法として、
onSaveInstanceState()
メソッドをアクティビティのライフサイクルに組み込むという手がありますが、複雑なデータは保持できない+ライフサイクルを考えなければならないという点で、リスクがあります。そこで、ViewModelが大活躍します。以下の図を見てください。
この図は、ViewModelとActicvityのライフサイクルを表したものです。左がActivity、右がViewModelとなっています。ぱっと見で分かるように、ViewModelにはほぼライフサイクルにおける過程が存在しません。Activityが完全に破棄される(別のアプリに切り替えたり)ときにonClearedが呼ばれるだけです。したがって、いくら画面を回転させてActivityが再生成されようが、ViewModelはそのデータを保持し続けてくれるのです。
もう一つのViewModelのメリットとして、アクティビティやフラグメントが行っていた非同期呼び出しにおけるメンテナンスを容易にしてくれる事が挙げられます。先述したように、ViewModelのライフサイクルは非常にシンプルなので、onDestroy
でいちいち通信をキャンセルさせるなどといったリソースの無駄を排除できます。
以上二つの点がViewModelの主なメリットと役割です。UIに関わるロジックをまとめて管理して複雑さの軽減と効率性の向上を図りましょう。LiveDataとは?
ライフサイクルに応じて、データを監視することが出来るクラスです。ActivityやFragmentがアクティブであるときに更新があったデータに対して、自動的に更新を行ってくれます。LiveDataの説明としては以上の通りですが、これではあまり理解が出来ないと思うのでいくつかあるメリットを紹介していきます。
まず、先ほど示唆したようにUI をデータの状態と一致させてくれます。これによって、監視対象にしたデータはアプリのデータが変更されるたびに UI を更新するのではなく、変化が生じるたびに UI を更新できます。言い換えると、能動的にUIを更新していたところを、受動的に更新してくれるということです。また、それに伴ってデータを常に最新にしてくれます。
次に、手動でライフサイクルを処理する必要を省いてくれます。これは、LiveDataが自動的に関連するライフサイクルの状態を認識してくれるためです。ViewModelで例に挙げたように、ローテーションでもデータを保持してくれたりします。それと同時に、メモリリークが発生しません。関連付けているライフサイクルが破壊されたら、それに連動してデータを自動的にクリーンアップしてくれます。
このように、LiveDataも煩わしい処理を自動でこなしてくれます。ViewModelとLiveData
ViewModelとLiveDataのそれぞれについて、↑のように説明したわけですが、どちらも同じようなメリットを持っていて、いまいち違いが判らないと思います。利点だけを見れば確かに似通うんですが、この二つはそもそも使われ方が異なります。基本的に、この二つはセットで使われますが、ViewModelはUIに関するロジックを入れる箱で、LiveDataはその中のデータ一つ一つの監視を担います。
この二つを使うのは非常に簡単です。以下にその例を示します。class Hoge : ViewModel() { private var fuga: LiveData<String> }このように、ViewModelクラスを拡張した箱を作って、LiveDataをインスタンス化して各データの監視を行う場所を作ります。今回はHogeクラスにはfugaしか格納されていませんが、ViewModelを拡張するクラスは基本的にUIに関わるロジックしか入れないことになっています。これは、ViewModelでクラスごとに機能の住み分けを徹底するためです。具体的にはMVVMという設計方針が関係します。
次に、MainActivityでfugaを監視対象にします。private val hoge : Hoge by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) hoge.fuga.observe(this, Observer { it -> foo = it }) }hogeというViewModelを拡張したHogeクラスのインスタンスを作り、onCreate内でfugaの具体的な内容(foo)を指定してデータをセットします。observeメソッドはその名の通り、引数を監視対象にします。
これで、fugaは先述したようなViewModelとLiveDataの恩恵を受けれるようなプロパティになりました。これだけで様々な手間が省けるのは非常に楽です。ViewModelとLiveDataの実装
最後に、この二つを使うためにbuild.gradle(app)ファイルで必要な実装をしましょう。ライフサイクル対応コンポートの中から、ViewModelとLiveDataだけを実装します。現在の最新版は2.2.0です。
dependencies { def lifecycle_version = "2.2.0" // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" }おわり
今回は備忘録的な意味もかねて記事を書きました。まだ不透明な部分もあるので、多少誤りがあると思われますがご了承ください。今回は公式リファレンスのViewModelとLiveDataのページを主に参照しました。