20200729のAndroidに関する記事は7件です。

*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.java
package 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;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

android webviewをchromeみたいに使う

まずは全体のコードはこんなかんじです。

MainActivity.kt
package 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub Actions で Android アプリを手動ビルドできるようにする

下記のようなyamlファイルをAndroidプロジェクトのリポジトリに置きます。

.github/workflows/manual-build.yml
name: 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 の画面からワークフローを実行してアプリがビルドできるようになります。

スクリーンショット_2020-07-29_13_03_35.png

apk ファイルは、下図のように結果画面からダウンロードできます。

スクリーンショット_2020-07-29_13_06_24.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】FlutterとFirebaseでサボり防止アプリを作った

先日、個人開発アプリをリリースしました!やったね!!!

審査も通りまして、現在App StoreとPlay Storeで配信されております。

App Store

Play Store

せっかくなので、利用した技術のメリットと注意点をまとめておこうと思います。

作ったアプリ

これです。

広報用ページも作ってみたのでリンクを貼っておきます。

広報用ページはスマホで見る用にレイアウトしてます。

監視し合う会 〜お互いサボらないようにみんなで作業しよう〜

英語名: Procrastinators Watch Mutually

kanshi_feature_1024_500.png

解決したい課題

やらなきゃいけないことがあるのに、なかなか取り組めずにグダグダしてしまう経験がみなさんあると思います。

でも、会社や学校で「今はこれをやる時間です」と管理されたり、誰かと一緒に「今からやるぞ!」って決めると、取り組めますよね。

そういう「誰かと一緒」「時間を管理される」という状況をアプリ上で再現して、作業に取り組みやすくするためのアプリです。

解決方法

  • 一緒に作業してるメンバーと作業内容が表示される
  • タイマーで全員同時に作業時間と休憩時間を指定される(49分作業12分休憩)
  • ちゃんと取り組めたかどうか自己申告し、それに対して褒めたり罰したりできる
    • 褒められたいし、自分に嘘をつきたくないので、作業に取り組むモチベーションアップ
  • 罰された人は社会貢献活動で償うまでアプリを利用できない
    • 社会貢献活動とは動画広告視聴のことで、広告主・広告配信業者・アプリ制作者に貢献できる
    • サボりに対し、有意義な軽い「罰」を用意することで、作業に取り組むモチベーションアップ

そんなアプリです。

監視し合う会 〜お互いサボらないようにみんなで作業しよう〜

ということは、広告の実装が必要ですね。

また、ユーザーの入力した内容が他者から見えるので、プライバシーポリシーや利用規約で不適切な入力を防止し、違反者にはミュートやBANをできるようにする必要があります。

そのためにはログイン機能でユーザーを識別する必要もありそうですね。

では技術方面を見ていきましょう。

Flutter

フロントエンドはFlutterを使います。

https://flutter.dev/

Flutterのメリット

最大のメリットはなんと言っても、AndroidとiOSの両アプリをそれぞれ作らなくて済むことです。

また、同じメリットを持つフレームワークは他にもありますが、Flutterが今最も勢いがあるフレームワークと言えると思います。コミュニティも活発で情報が得やすいです。

スクリーンショット 2020-07-27 15.01.59.png

あの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の使い方・考え方についてはこちらの動画シリーズがとてもわかりやすいので一通り見ておくといいと思います。

Get to know Cloud 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のテスト

公式記事が大いに参考になります。

Cloud 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アプリを配信する時の署名について整理する

終わりに

技術周りをまとめておこうと思って書いてみたら、自分の過去記事へのリンクが多くなってしまいました。

まあ、難所を突破するたびにその結果を記事にまとめるようにしていたので、それらへのリンクが多くなるのは必然か……。。。。

ということで、皆様、アプリ開発ライフを楽しみましょう!

サボり防止アプリもよろしくおねがいします!これで時間管理しながらアプリ開発してくれたらメチャクチャ嬉しいです!

監視し合う会 〜お互いサボらないようにみんなで作業しよう〜

App Store

Play Store

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 の試用から始めるのがひとつの無難な選択肢でしょう。

以下が、オリジナルの英語記事の日本語訳です。


codemagic-blog-getting-started-with-codemagic.ecdfe0547c7678263bedb56f1533acb13784617e42a96812891fe87bd3b2e03a.png

2018 年にロンドンで開催された Flutter Live おいて、 Nevercode は Google と提携し、Flutter アプリ専用の CI/CD ソリューションとして Codemagic を立ち上げました。Codemagic を使えば、Flutter アプリを辛い設定作業をせずにテストしてリリースできます。

Codemagic は Flutter の扱いに熟達していますが、今やそれだけにとどまりません。Flutter アプリでないネイティブアプリでも同等に扱えるように、codemagic.yamlファイルを使用して、ネイティブの AndroidiOS アプリ、および React Native アプリの設定とビルドができるようになりました。スクリプトをより詳細に制御したい場合は、カスタムスクリプトを実行したりカスタムワークフローを作成できます。高品質なアプリをさらに迅速に開発できるようになりました。

ここでは、Codemagic を使えば、Flutter アプリ、Android ネイティブ アプリ、 iOS ネイティブ アプリ、React Native アプリの CI/CD をいかに簡単に設定できるのかを、その手順を説明を通じてご紹介いたします。

必要条件

Flutter アプリ、iOS ネイティブアプリ、Android ネイティブアプリ、React Native アプリを Codemagic 上でセットアップするには、いくつかのものが必要です。

  1. もちろん、Flutter アプリ、iOS ネイティブアプリ、Android ネイティブアプリ、React Native アプリとそのテストコードが必要です。そして、それらのアプリは GitHub、GitLab、または Bitbucket でホストされていることを念頭に置いています。
  2. App Store や Play Store に公開したい場合は、証明書や Provisioning Profile のようなコード署名を用意します。
  3. ビルドレポートやアーティファクトを送信する場合は、Slack のワークスペースを用意します。

以上です。これでアプリに CI/CD を追加する準備ができました。では、実際に設定することを通じて、基本的なことを説明していきましょう。なお、コード署名と公開作業についてはこのチュートリアルでは対象とせず、別途説明とさせていただきます。

Codemagic にアカウント登録

最初の一歩は、codemagic.ioウェブサイトにアクセスし、GitHub、GitLab、または Bitbucket のアカウントで Codemagic のアカウント登録をすることです。これで、そのサービスのすべてのリポジトリーを Codemagic アカウントに統合して扱えるようになります。

image2.png

アプリケーション ダッシュボード ( Applications Dashboard )

サインインすると、Codemagic のダッシュボードにアプリのリポジトリーの一覧が表示されます。一覧をフィルターにかけたり検索したりして、ビルドしたいアプリを見つけることができます。初回のアクセスでは、以下のように表示されるでしょう。

app_not_configured.png

ビルドのワークフローを設定するには、Set up buildボタンをクリックします。

設定がすでに済んで、まだビルドを開始していない場合は、このように表示されます。

app_configured.png

Finish build setup をクリックして設定を終え、ビルドを開始できます。


ちなみに、以前に Codemagic 上でビルドしたアプリがある場合は、以下のような表示になります。

app_built.png

Start new build をクリックして以前の設定でビルドを開始したり、Settings(歯車のアイコン)をクリックして変更したりできます。


ビルドの設定

Set up buildボタンをクリックすると、このページが表示されます。ここでは、プロジェクトの種類を選択できます。

viimane.png

この一覧から適切なもの選択するだけです。選択すると、ワークフローの設定画面に移動します。

Flutter の設定

このページでは、以下のような UI で Flutter アプリのビルドワークフローを設定できます。

workflow_config_flutter.png

ビルドワークフローをより詳細に制御したい場合は、Download configuration ボタンをクリックして codemagic.yamlファイルを使用できます。

yaml_config.png

このcodemagic.yamlファイルを使用して、ワークフローを変更できます。プロジェクトのルートディレクトリに置いて、VCS にコミットするだけです。リポジトリーにこのファイルがあれば、Codemagic が自動的に検出します。

Start your first build ボタンをクリックして、Flutter アプリのビルドをすぐに開始できます。ワークフローをまだ変更していない場合は、以下の一連のステップのデフォルトのワークフローが使用されます。

default_workflow.png

ビルドマシンの準備、ソースコードのチェックアウト、Flutter の依存関係のインストール、テストの実行、アプリのビルド、アーティファクトの公開といった一連のステップがあります。これが基本的な CI/CD のワークフローに必要なものです。各ステップをクリックして展開すると、Codemagic はそれぞれのステップのログを表示します。

その他の設定

Flutter アプリ以外のプロジェクト(Android ネイティブアプリ、iOS ネイティブアプリ、React Native アプリ、または Flutter Module)をビルドする場合は、codemagic.yamlファイルを使用してワークフローを設定する必要があります。選択したプロジェクトの種類ごとにテンプレートが用意されています。

Android ネイティブ用の設定

ビルドレポート

ビルドが終了すると、Codemagic のダッシュボードに、ビルド時間、ビルドステータス、コミットなどの概要とそれらの詳細が以下のように表示されます。

build_report.png

これに加えて、デフォルトでは iOS と Android アプリの両方の、ビルドのステータスとアーティファクトがメールで送信されます。メールの内容は以下のようになります。

email.jpg

Slack の設定をすることで、Slack チャンネルにビルドレポートを投稿させることもできます。

以上です!なにも設定しなくても Flutter アプリに CI/CD サポートを追加できました。これが Codemagic のマジックです。

ビルドの失敗

ここまでは、すべてが完璧に動作しているハッピーパスのシナリオを説明してきましたが、現実の世界では必ずしもそうなるとは限りません。ビルドに失敗したり、テストに失敗したり、調査が必要な問題が発生することもあります。Codemagic は、各段階においてそれが成功したか失敗したかに関わらず、詳細なログを提供しています。これらのログは失敗したビルドをデバッグするのに非常に便利です。以下の画面は、ユニットテストが失敗していて、ビルドの失敗のすべての詳細がログに記録されているものです。

image3.png

見ての通り、インテグレーションテストとウィジェットテストは成功していますが、ユニットテストが失敗しています。これを修正することでビルドを修正できました。テストが失敗すると、それを知らせるメールもビルドログへのリンク付きで届きます。

ビルドの失敗についてもっと詳しく知るには、モバイルアプリの CI ビルドが失敗してしまう、よくある 12 個の設定間違い をお読みください。

デフォルトワークフローとカスタムワークフローの比較

デフォルトのビルドワークフローには CI/CD ワークフローに必要となる基本的なものが含まれていますが、ほとんどの場合、ビルドプロセスの一部になんらかのカスタマイズを加える必要があります。例えば、ビルドステータスを Slack や他のコミュニケーションチャンネルに送信したり、アーティファクトを Fabric や HockeyApp などの他のサービスに発行したい場合があります。これらのシナリオを実現するためには、カスタムワークフローを作成し、カスタムスクリプトを実行します。それでは、プロジェクトの必要に応じてワークフローをカスタマイズする方法を見ていきましょう。

Build フェーズ

Codemagic における Flutter アプリの Build フェーズには、ビルドプロセスを定義するためのさまざまな手段があります。どのブランチをビルドするか、そして何のイベントをきっかけにしてビルドするかを選択することから始めます。

build_triggers.png

次に、Godemagic では、以下のようなもっと詳細な設定も可能です。

  1. Flutter のバージョン
  2. アプリをビルドする設定。debug や release など
  3. iOS や Android、またはその両方といったプラットフォームの選択
  4. Xcode のバージョン
  5. カスタムビルドの引数

デフォルトのワークフローでは、以下の設定が選択されています。

build_default.png

とは言え、ビルドプロセスはデフォルトの動作をプロジェクトの必要に応じて変更することで、いつでもカスタマイズできます。Xcode の異なるバージョンや異なるリリース設定でアプリのビルドが、上記の画面のオプションの変更、さらにカスタムワークフローによって可能です。

Codemagic のもう一つの大きな特徴として、すべてのフェーズの前にカスタムスクリプトを追加できます。これにより、開発者は各フェーズの前後で任意のコマンドを実行できます。カスタムスクリプトを設定するには、単純に Build、Test、Publish などの各フェーズの上部にある + ボタンをクリックします。ここでは、Build フェーズの前に配置するスクリプトの例を示します。

カスタムスクリプトを追加

Pre-build scriptに、プロジェクト固有の依存関係のインストールなど、任意のコマンドを追加できます。

Test フェーズ

Codemagic の Test フェーズでは、開発者はテストの有効化と無効化を切り替えられます。プロジェクトで実行する Flutter テストの種類を選択できますし、テスト中に使用する追加の引数の指定もできます。プロジェクトで Flutter Driver tests を使用している場合は、使用するエミュレータの種類も指定できますし、AWS Device Farm の恩恵も受けられます。

こちらで、Flutter のテストに関する詳細をご覧いただけます。

Codemagic の Test セクション

とは言え、デフォルトの動作を無効にして、カスタマイズしたテストコマンドを使用することも可能です。例えば、コマンドラインから UI テスト(訳注: Flutter のインテグレーションテスト) だけを実行したい場合は、次のコマンドを pre-test フェーズに追加します。

flutter drive --target=test_driver/main.dart

これにより、デフォルトのテストワークフローを使用する代わりに、このコマンドが実行されます。

Publish フェーズ

Codemagic の Publish フェーズでは、ビルドレポートやアーティファクトの公開のための様々な設定が必要です。デフォルトでは、あなたが GitHub/Bitbucket/GitLab にメールアドレスを提供している場合は、メールアドレスだけが設定されています。

このフェーズでは、その他にも以下のものを明示的に設定する必要があります。

  1. iOS と Android の両方のコード署名に必要な情報をアップロード
  2. Slack の webhook を設定
  3. 公開のための、Google Play や App Store Connect の詳細を入力

Codemagic の Publish セクション

これらに加えて、カスタムスクリプトを書いて、FabricHockeyApp などのサードパーティサービスにアーティファクトをデプロイすることもできます。

スクリプトの大きな力を利用して Codemagic でカスタムワークフローを設定し、アプリのビルドプロセスをより強固なものにできます。

多数のワークフロー

Codemagic では、異なる設定やバージョンのソフトウェアの要件を満たすために、多数のワークフローを作成できます。既存のワークフローを複製して名前を変更することで、新しいワークフローを作成できます。以下の手順です。

  1. App settingsに移動します。(訳注: https://codemagic.io/apps ページで、それぞれのプロジェクトの歯車アイコンをクリックして移動できます)
  2. 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 ファイルを使用するだけで、発進の準備が完了です!

ハッピー ビルディング!

お役立ちリンクと参考文献

以上が翻訳です。

お役立ち日本語記事へのリンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterFragmentとViewPagerの組み合わせはonResumeのタイミングで難しかった

ViewPager2を使って2ページ目以降FlutterFragmentを設定すると、完全にページ遷移するまではFlutterFragmentが描画されず真っ白になります。一度表示すると真っ白になりません。

device-2020-07-28-223018.gif

このように遷移途中でもFlutterFragmentが描画されて欲しいです。

device-2020-07-29-065552.gif

なぜそうなるか

作り方

ViewPager2のAdapterはこのように作りました。

MyFragmentStateAdapter.kt
class 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.kt
viewPager.adapter = MyFragmentStateAdapter(this)
// こちらを設定しないとスワイプ開始時に一瞬ひっかかる
viewPager.offscreenPageLimit = 1

Fragmentのライフサイクルを確認する

2ページ目をFlutterFragmentから空のフラグメントにします。

MyFragmentStateAdapter.kt
class MyFragmentStateAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    override fun getItemCount() = 2

    override fun createFragment(position: Int): Fragment {
        return if (position == 0)
            MainFragment()
        else
            EmptyFragment()
    }
}

空のフラグメントのライフサイクルを確認します。

EmptyFragment.kt
class 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.kt
class 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デザインを変更しました

結局、横スワイプによるページ切り替えは諦めて、ボタンによる画面呼び出しにしました。

device-2020-07-29-070749.gif

今回に関してはそちらの方がUIとしては分かりやすいと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android/Kotlin】ViewModel・LiveData基本

ViewModelとは?

 ViewModelとは、UI関連のデータを保存し管理してくれるクラスです。分かりやすい活用方法でいうと、画面のローテーションをしてもUIを保持してくれたりします。ローテーションに対してUIを保存する方法として、onSaveInstanceState()メソッドをアクティビティのライフサイクルに組み込むという手がありますが、複雑なデータは保持できない+ライフサイクルを考えなければならないという点で、リスクがあります。そこで、ViewModelが大活躍します。以下の図を見てください。
https___qiita-image-store.s3.amazonaws.com_0_88858_68e99b0c-215d-325e-1f75-6875fa580031.png
この図は、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のページを主に参照しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む