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

github actionsでunityビルドしてiOSとAndroidのストアに自動的に提出する

こんにちは。virapture株式会社もぐめっとです。

mogmet.jpg

この記事を書いてるときは冬なんですけど、もう自分の写真がなさすぎて夏までさかのぼってしまいました。

本日はCI/CD with Unity, GitHub Actions, and Fastlaneという記事を参考にUnityでの自動ビルドとストア提出を作ってみたのでそのメモ書きを残しておきます。

概要

UnityにはUnity Cloud Buildという公式のCIがあるのですが、いかんせんfastlaneとの連携ができないので、証明書を引っ張ってきたり、ストアにアップロードというのができません。

そこで探してみたところ、GameCIが出しているunity-builderというgithub actionsを使ってビルドを行い、デプロイにはfastlaneを使用することでうまくできました。

このunity-builderが結構優秀で、ライセンスの認証とかもやってくれる。そして、ビルドが終わった後はライセンスをリターンしてくれるという優れもの。
もはやUnity Cloud Build使わなくてもCIができちゃいます。

構築手順

github actions workflowの設置

記事の人のリポジトリのファイルをベースにしながら作りました。

ios,android以外にもmac, windowsなどもあったのですが、邪魔だったのでそのへんは抹消しました。

こんなワークフローになってます。

image.png

iOSのビルド長すぎぃぃ。。
お待ちかね実体ファイルはこんな感じになってます。

name: Test, Build, and Release CGS

on:
  # push: { branches: [master] } #masterにpushされたときにやりたい人はこちらをお使いください
  release: { types: [published] }

env:
  UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
  UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
  UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
  BUILD_NUMBER: ${{ github.run_number }}

jobs:
  tests:
    name: Test Code Quality
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Cache Library
        uses: actions/cache@v2
        with:
          path: Library
          key: Library
      - name: Run EditMode and PlayMode Tests
        uses: game-ci/unity-test-runner@main
      - name: Publish Test Results
        if: ${{ always() }} # Avoid skipping on failed tests
        uses: davidmfinol/unity-test-publisher@main
        with:
          githubToken: ${{ secrets.GITHUB_TOKEN }}

  buildWithLinux:
    name: Build for ${{ matrix.targetPlatform }} by Unity
    runs-on: ubuntu-latest
    timeout-minutes: 90
    needs: tests
    strategy:
      fail-fast: false
      matrix:
        targetPlatform:
          - Android
          - iOS
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
          lfs: true
      - name: Cache Library
        uses: actions/cache@v2
        with:
          path: |
            Library
            build/${{ matrix.targetPlatform }}
          key: Library-${{ matrix.targetPlatform }}-
          restore-keys: Library-
      - name: Free Disk Space for Android
        if: matrix.targetPlatform == 'Android'
        run: |
          sudo swapoff -a
          sudo rm -f /swapfile
          sudo apt clean
          docker rmi $(docker image ls -aq)
          df -h
      - name: Build Unity Project
        uses: game-ci/unity-builder@main
        with:
          customParameters: -buildNumber ${{ github.run_number }}
          targetPlatform: ${{ matrix.targetPlatform }}
          buildMethod: Cgs.Editor.BuildCgs.BuildOptions
          androidAppBundle: true
          androidKeystoreName: keystore.keystore
          androidKeystoreBase64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
          androidKeystorePass: ${{ secrets.ANDROID_KEYSTORE_PASS }}
          androidKeyaliasName: ${{ secrets.ANDROID_KEYALIAS_NAME }}
          androidKeyaliasPass: ${{ secrets.ANDROID_KEYALIAS_PASS }}
      - name: Upload Build
        uses: actions/upload-artifact@v2
        if: github.event.ref != 'refs/heads/develop'
        with:
          name: cgs-${{ matrix.targetPlatform }}
          path: build/${{ matrix.targetPlatform }}

  releaseToGooglePlay:
    name: Release to the Google Play Store
    runs-on: ubuntu-latest
    timeout-minutes: 60
    needs: buildWithLinux
    if: github.event.action == 'published'
    env:
      GOOGLE_PLAY_KEY_FILE: ${{ secrets.GOOGLE_PLAY_KEY_FILE }}
      GOOGLE_PLAY_KEY_FILE_PATH: ${{ format('{0}/fastlane/api-finoldigital.json', github.workspace) }}
      ANDROID_BUILD_FILE_PATH: ${{ format('{0}/build/Android/Android.aab', github.workspace) }}
      ANDROID_PACKAGE_NAME: com.sample.app # 自分のアプリのパッケージ名に合わせよう!
      RELEASE_NOTES: ${{ github.event.release.body }}
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Download Android Artifact
        uses: actions/download-artifact@v2
        with:
          name: cgs-Android
          path: build/Android
      - name: Prepare for Upload
        run: |
          echo "$GOOGLE_PLAY_KEY_FILE" > $GOOGLE_PLAY_KEY_FILE_PATH
          echo "$RELEASE_NOTES" > fastlane/metadata/android/en-US/changelogs/default.txt
      - name: Upload to Google Play
        uses: maierj/fastlane-action@v1.4.0
        with:
          lane: 'android playstore'

  buildIOS:
    name: Build Archive for iOS
    runs-on: macos-latest
    timeout-minutes: 60
    needs: buildWithLinux
    if: github.event.action == 'published'
    env:
      APPLE_CONNECT_EMAIL: ${{ secrets.APPLE_CONNECT_EMAIL }}
      APPLE_DEVELOPER_EMAIL: ${{ secrets.APPLE_DEVELOPER_EMAIL }}
      APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
      APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
      APPLE_ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
      MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
      MATCH_PERSONAL_ACCESS_TOKEN: ${{ secrets.MATCH_PERSONAL_ACCESS_TOKEN }}
      IOS_APP_ID: com.sample.app # bundle idを指定します
      IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
      PROJECT_NAME: onenitejinro
      RELEASE_NOTES: ${{ github.event.release.body }}
      MATCH_REPOSITORY_ACCOUNT: ${{ secrets.MATCH_REPOSITORY_ACCOUNT }}
      USYM_UPLOAD_AUTH_TOKEN: 'fake' # ビルドが途中でこけるのでfake用に環境変数を追加。
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Cache restore for debug
        uses: actions/cache@v2
        with:
          path: |
            Library
            build/iOS
          key: Library-iOS-
          restore-keys: Library-
      - name: Download iOS Artifact
        uses: actions/download-artifact@v2
        with:
          name: cgs-iOS
          path: build/iOS
      - name: Cache restore cocoapods # firebase使ってるとcocoapodsを使うのですが、cocoapodsのキャッシュとらないと毎回時間かかるのでキャッシュしておきます。
        uses: actions/cache@v2
        if: ${{ always() }}
        with:
          path: |
            build/iOS/iOS/Pods
            ~/.cocoapods/repos
          key: Pods-${{ hashFiles('**/Podfile') }}
          restore-keys: Pods-
      - uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.7.2'
      - name: Prepare for fastlane # GateKeeper対策
        run: |
          sudo spctl --master-disable
      - name: Archive iOS
        uses: maierj/fastlane-action@v2.0.1
        with:
          lane: 'ios build'
      - name: run if fail_step failed # ビルドがコケた原因がわかるようにcatしておきます
        if: failure()
        run: cat /Users/runner/Library/Logs/gym/*Unity-iPhone.log
      - name: Upload Build
        uses: actions/upload-artifact@v2
        if: github.event.ref != 'refs/heads/develop'
        with:
          name: ipa
          path: |
            ${{ github.workspace }}/*.ipa
            ${{ github.workspace }}/*.dSYM.zip

  releaseToAppStore:
    name: Release to the App Store
    runs-on: macos-latest
    timeout-minutes: 60
    needs: buildIOS
    if: github.event.action == 'published'
    env:
      APPLE_CONNECT_EMAIL: ${{ secrets.APPLE_CONNECT_EMAIL }}
      APPLE_DEVELOPER_EMAIL: ${{ secrets.APPLE_DEVELOPER_EMAIL }}
      APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
      APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
      APPLE_ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
      FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
      FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
      MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
      MATCH_PERSONAL_ACCESS_TOKEN: ${{ secrets.MATCH_PERSONAL_ACCESS_TOKEN }}
      ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
      ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
      ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
      IOS_APP_ID: com.sample.app # bundle idを指定します
      IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
      PROJECT_NAME: onenitejinro
      RELEASE_NOTES: ${{ github.event.release.body }}
      MATCH_REPOSITORY_ACCOUNT: ${{ secrets.MATCH_REPOSITORY_ACCOUNT }}
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Download iOS Artifact
        uses: actions/download-artifact@v2
        with:
          name: ipa
          path: |
            ${{ github.workspace }}/*.ipa
            ${{ github.workspace }}/*.dSYM.zip
      - name: Upload to the App Store
        uses: maierj/fastlane-action@v1.4.0
        with:
          lane: 'ios release'

このファイルのポイントを話しておくと、linuxでビルドが終わった後、fastlaneを使ってandroid/iosともにビルドもしくはアップロードを行っています。
iosは結構曲者で、UnityでCLIビルドが結構大変なので随所随所にビルドできるような仕組みを置いてます。

fastlaneの設置

fastlaneで使うファイルを参考ファイルをベースにカスタマイズした下記を設置しています。

keychain_name = "temporary_keychain"
keychain_password = SecureRandom.base64

platform :android do

  desc "Upload a new Android version to the Google Play Store"
  lane :playstore do
    upload_to_play_store(
      aab: "#{ENV['ANDROID_BUILD_FILE_PATH']}",
      track: 'internal',
      skip_upload_screenshots: true,
      skip_upload_images: true
    )
  end

end

platform :ios do

  desc "Push a new release build to the App Store"
  lane :release do
    api_key = app_store_connect_api_key(
      key_id: ENV['ASC_KEY_ID'], # your key id
      issuer_id: ENV['ASC_ISSUER_ID'], # your issuer id
      key_content: ENV['ASC_KEY_CONTENT'], # your secret key body
      # ex) key_content: '-----BEGIN PRIVATE KEY-----\nfoobar\n-----END PRIVATE KEY-----'
    )
    upload_to_app_store(
      api_key: api_key, # pass api_key
      force: true,
      skip_screenshots: true,
      skip_metadata: true
    )
  end

  desc "Submit a new Beta Build to Apple TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV['ASC_KEY_ID'], # your key id
      issuer_id: ENV['ASC_ISSUER_ID'], # your issuer id
      key_content: ENV['ASC_KEY_CONTENT'], # your secret key body
      # ex) key_content: '-----BEGIN PRIVATE KEY-----\nfoobar\n-----END PRIVATE KEY-----'
    )
    upload_to_testflight(
      api_key: api_key, # pass api_key
      skip_waiting_for_build_processing: true
    )
  end

  desc "Create .ipa"
  lane :build do
    cocoapods(podfile: "#{ENV['IOS_BUILD_PATH']}/iOS/Podfile")
    disable_automatic_code_signing(path: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj")
    certificates
    update_project_provisioning(
      xcodeproj: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj",
      target_filter: "Unity-iPhone",
      profile: ENV["sigh_#{ENV['IOS_APP_ID']}_appstore_profile-path"], # より動的にみるようにしています
      code_signing_identity: "Apple Distribution: #{ENV['APPLE_TEAM_NAME']} (#{ENV['APPLE_TEAM_ID']})"
    )
    gym(
      workspace: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcworkspace",
      scheme: "Unity-iPhone",
      clean: true,
      #clean: false,
      skip_profile_detection: true,
      codesigning_identity: "Apple Distribution: #{ENV['APPLE_TEAM_NAME']} (#{ENV['APPLE_TEAM_ID']})",
      export_method: "app-store",
      export_options: {
        method: "app-store",
        provisioningProfiles: {
          ENV["IOS_APP_ID"] => "match AppStore #{ENV['IOS_APP_ID']}"
        }
      }
    )
  end

  desc "Synchronize certificates"
  lane :certificates do
    cleanup_keychain
    create_keychain(
      name: keychain_name,
      password: keychain_password,
      default_keychain: true,
      lock_when_sleeps: true,
      timeout: 3600,
      unlock: true
    )
    match(
      type: "appstore",
      readonly: true,
      keychain_name: keychain_name,
      keychain_password: keychain_password
    )
  end

  lane :cleanup_keychain do
    if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
      delete_keychain(name: keychain_name)
    end
  end

  after_all do
    if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
      delete_keychain(name: keychain_name)
    end
  end
end

元ファイルからcocoapodsのインストールや、api keyまわりの設定、provisioning profileの指定方法などが若干違います。

また、問題を切り分けやすくするためにビルドとapp storeにリリースする処理はわけたりしています。

参考元ファイルでは現在、apikey周りは証明書をおいていい感じにやるようにしているみたいです。

他にfastlaneに付随するファイルも置いていきます

  • Appfile
for_platform :android do
  package_name(ENV["ANDROID_PACKAGE_NAME"])
  json_key_file(ENV["GOOGLE_PLAY_KEY_FILE_PATH"])
end

for_platform :ios do
  app_identifier(ENV["IOS_APP_ID"])

  apple_dev_portal_id(ENV["APPLE_DEVELOPER_EMAIL"])  # Apple Developer Account
  itunes_connect_id(ENV["APPLE_CONNECT_EMAIL"])     # App Store Connect Account

  team_id(ENV["APPLE_TEAM_ID"]) # Developer Portal Team ID
  itc_team_id(ENV["APPLE_ITC_TEAM_ID"]) # App Store Connect Team ID
end
  • Deliverfile
submit_for_review false
automatic_release true
force true
skip_screenshots true
release_notes({
  'default' => ENV["RELEASE_NOTES"],
  'en-US' => ENV["RELEASE_NOTES"]
})
run_precheck_before_submit false

submission_information({
  add_id_info_uses_idfa: false,
  export_compliance_compliance_required: false,
  export_compliance_encryption_updated: false,
  export_compliance_app_type: nil,
  export_compliance_uses_encryption: false,
  export_compliance_is_exempt: false,
  export_compliance_contains_third_party_cryptography: false,
  export_compliance_contains_proprietary_cryptography: false,
  export_compliance_available_on_french_store: false
});
  • Matchfile
git_url("https://github.com/iosのcertificateおいてるgitリポジトリ") # sshのurlではなく、httpsで指定してます。
git_basic_authorization(Base64.strict_encode64("#{ENV['MATCH_REPOSITORY_ACCOUNT']}:#{ENV['MATCH_PERSONAL_ACCESS_TOKEN']}"))
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development

app_identifier(["com.sample.app"]) # bundleIDを指定しよう
username("apple@example.com") # Your Apple Developer Portal username

projectの準備

BuildCgs.csをAssets/Scripts/Cgs/Editorディレクトリに置いておく。
このファイルがビルド番号の設定などをしてくれている。

もぐめっとの場合、CIのビルド番号とアプリのビルド番号を紐付けしたかったので冒頭部分だけbuildNumberを参照するように少し修正しました。

修正後BuildCgs.cs
            PlayerSettings.macOS.buildNumber = options["buildNumber"];
            PlayerSettings.iOS.buildNumber = options["buildNumber"];
            PlayerSettings.Android.bundleVersionCode = int.Parse(options["buildNumber"]);
            PlayerSettings.WSA.packageVersion = new Version(options["buildVersion"]);

metadataの準備

metadataがないとfastlane途中でこけるため準備しておく。

androidの準備

fastlane run download_from_play_store json_key:<json path>

jsonファイルはストアにアクセスするために必要なものになるが、こちらの記事に解説は委ねます。

iosの準備

fastlane deliver download_metadata

上記で生成されたファイルをコミットしておく

githubのsecrets設定

先術したymlやfastfileなどを見てもらったとおり、環境変数をふんだんに使うので、その準備をしていきます。

game-ci/unity-builder周りで使う環境変数

細かいことはGame CIのドキュメントをご参照ください

Unityのアカウント周りの情報は下記の変数になります

  • UNITY_EMAIL
  • UNITY_PASSWORD
  • UNITY_SERIAL

unityのserialについてはunityのwebサイトにログインして確認します。
スクリーンショット 2021-03-21 15.40.06.png

Androidの公開設定周りはこちらの変数になります

  • ANDROID_KEYALIAS_NAME
  • ANDROID_KEYALIAS_PASS
  • ANDROID_KEYSTORE_BASE64
  • ANDROID_KEYSTORE_PASS

上記四点はandroidの公開設定でも使うこの辺の情報ですね。
image.png

keystoreの作り方は公式に委ねます

fastlaneのアップロード周りで使ってる環境変数

アップロードするときなどに使われるfastlaneのDeliverで使われるAppfileで定義して使ってる一覧です。

  • APPLE_CONNECT_EMAIL

App Store Connect Accountにアクセスできるアカウントを指定します

  • APPLE_DEVELOPER_EMAIL

Apple Developer Accountにアクセスできるアカウントを指定します。

  • APPLE_TEAM_ID
  • APPLE_TEAM_NAME

上記2点はDeveloper Portalのサイトでアクセスできる項目を確認します

スクリーンショット 2021-03-21 16.00.14.png

  • APPLE_ITC_TEAM_ID

APPLE_TEAM_IDではなくて、app store connectで使うアカウントのほうのIDになるらしい。とりあえずfastlane使えるならspaceship使えば取得できます。

  • ASC_ISSUER_ID
  • ASC_KEY_CONTENT
  • ASC_KEY_ID

Appleの2段階認証を突破してアップロードできるようにする用の変数です。解説は下記記事に委ねます。

  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
  • FASTLANE_PASSWORD

先程のAppStoreConnect API Keyを使った方法でできると思うので、おそらくこの2点は設定しなくてもいいと思いますが、念の為設定しておきます。

設定方法はこちらに。

  • GOOGLE_PLAY_KEY_FILE

metadataをダウンロードする時お話した、jsonファイルの中身をはっつけます。

再掲)

fastlane match周りの環境変数

fastlane matchについてはこちらをご参照ください。証明書を管理するやつです。

  • MATCH_PASSWORD

matchに設定してるパスワードを指定します。

  • MATCH_PERSONAL_ACCESS_TOKEN
  • MATCH_REPOSITORY_ACCOUNT

上記2つはMatchfileで指定したgit_basic_authorizationに使うものなのですが、repositoryのcloneがgithub action上でできるようにするために使っています。
MATCH_PERSONAL_ACCESS_TOKENはgithubのaccess tokenを指定します。

MATCH_REPOSITORY_ACCOUNTはMatchのリポジトリにアクセスできるアカウントを指定します。(access tokenを発行したアカウント名ですね)

使い方

ようやく設定がおわりました。

このworkflowはreleaseを切ったときに初めて発火されます。

リポジトリの右側にあるreleasesから
image.png

Draft a new releaseボタンからリリースを作りましょう!
image.png

成功すればreleaseで書いた内容でappstoreとplay storeにリリースされるはずです!

まとめ

というわけでgame ciさんのunity-builderを使うことでgithub actionsでunityのCIを回すことができるようになりました!
unityはビルドに時間かかるので結構めんどくさかったのですがこの辺が省略できるようになるのはとても楽になりました。

みなさんの開発にあてる時間がこれで増えたら幸いです!

最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!
他にもcameconoffchaといったサービスも作ってるのでよかったら使ってね!

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

VLC for Android:楽曲管理画面の解析

はじめに

VLC for Androidの楽曲管理画面を解析する。ゴールは登場するクラスの洗い出しと関連性のざっくりとした把握。

各種リンク

アプリ
GitHub
公式サイト

楽曲管理画面のスクリーンショット

WIP

楽曲管理画面のオブジェクト図

こんな感じ。赤いオブジェクト図は楽曲管理部分
ざっくりいうとリポジトリクラスの状態を監視して、各モデルクラスのインスタンスを取り出してUIに反映している。

Class Diagram0.png

楽曲管理画面を構成する主なクラス

UI部分

  • AudioBrowserFragment:楽曲管理を表現するフラグメント
  • AudioPagerAdapter:ジャンル、楽曲、アルバム、アーティスト用画面を切り替えるViewPager用のアダプター
  • AudioBrowserAdapter:ビューページャが管理するRecyclerViewに紐づくアダプター。ジャンル、楽曲、アルバム、アーティスト用の4つある。
  • AudioBrowserViewModel:〜Providerクラスのインスタンスを管理するViewModel
  • ArtistsProvider:MediaLibraryを監視してArtistを供給する。
  • AlbumsProvider:MediaLibraryを監視してAlbumを供給する。
  • TracksProvider:MediaLibraryを監視してTrackを供給する。
  • GenresProvider:MediaLibraryを監視してGenreを供給する。

モデル部分

  • MediaWrapper:音楽ファイルを表現する。
  • Album:アルバムを表す。
  • Arist:アーティストを表す。
  • Genre:ジャンル
  • MediaLibrary:上記4つのインスタンスを供給する。所謂リポジトリクラス。シングルトン。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android] Jetpack DataBinding

Android Studio 4.1.3(windows版) での流れとなります

Jetpack DataBindingでボタンイベントを設定する方法を例にして説明します

Android DeveloperのJetpack DataBindingの説明は以下です

準備

以下を参考にJetpack Navigation + ViewModelを作成してください

DataBindingを使用するための設定

build.gradleに以下を追加します
DataBindingを有効にするとBindingクラスが自動生成されます

build.gradle
android {
    dataBinding {
        enabled = true
    }
}

ViewModelでの準備

ボタンイベントを受けるメソッドを用意します

MainViewModel.java
public void onClickButton() {
}

FragmentのLayoutの準備

ここではFrameLayoutをConstraintLayoutにしてid:buttonのボタンを追加します

image.png
image.png
※ Convert FraneLayout to ConstraintLayoutでも変更できます

Codeを開きConstraintLayoutタグの箇所でwindowsの場合はAlt+Enterなどで
Convert to data bindinglayout を選択します
image.png

追加されたdataタグにViewModelを追加します

main_fragment.xml
<data>
    <variable name="viewModel" type="com.xxx.sample.MainViewModel" />
</data>

ボタンのonClickに以下を追加します
※DesignまたはCodeで直接追加してください

@{() -> viewModel.onClickButton()}

image.png

Fragmentでの準備

DataBindingの準備とDataBindingにViewModelをセットします

MainFragment.java
// ViewModelはFragment作成時のテンプレートで自動生成されたものです
private MainViewModel mViewModel;
// 自動生成されます(main_fragment.xml)
private MainFragmentBinding binding;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
    binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false);
    final View view = binding.getRoot();
    return view;
}

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mViewModel = new ViewModelProvider(this).get(MainViewModel.class);
    // TODO: Use the ViewModel
    binding.setViewModel(mViewModel);
}

この記事は以下の記事の補足です

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

[Android/Java] Jetpack DataBinding

Android Studio 4.1.3(windows版) での流れとなります

Jetpack DataBindingでボタンイベントを設定する方法を例にして説明します

Android DeveloperのJetpack DataBindingの説明は以下です

準備

以下を参考にJetpack Navigation + ViewModelを作成してください

DataBindingを使用するための設定

build.gradleに以下を追加します
DataBindingを有効にするとBindingクラスが自動生成されます

build.gradle
android {
    dataBinding {
        enabled = true
    }
}

ViewModelでの準備

ボタンイベントを受けるメソッドを用意します

MainViewModel.java
public void onClickButton() {
}

FragmentのLayoutの準備

ここではFrameLayoutをConstraintLayoutにしてid:buttonのボタンを追加します

image.png
image.png
※ Convert FraneLayout to ConstraintLayoutでも変更できます

Codeを開きConstraintLayoutタグの箇所でwindowsの場合はAlt+Enterなどで
Convert to data bindinglayout を選択します
image.png

追加されたdataタグにViewModelを追加します

main_fragment.xml
<data>
    <variable name="viewModel" type="com.xxx.sample.MainViewModel" />
</data>

ボタンのonClickに以下を追加します
※DesignまたはCodeで直接追加してください

@{() -> viewModel.onClickButton()}

image.png

Fragmentでの準備

DataBindingの準備とDataBindingにViewModelをセットします

MainFragment.java
// ViewModelはFragment作成時のテンプレートで自動生成されたものです
private MainViewModel mViewModel;
// 自動生成されます(main_fragment.xml)
private MainFragmentBinding binding;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
    binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false);
    final View view = binding.getRoot();
    return view;
}

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mViewModel = new ViewModelProvider(this).get(MainViewModel.class);
    // TODO: Use the ViewModel
    binding.setViewModel(mViewModel);
}

この記事は以下の記事の補足です

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

Firebaseの関連するユーザデータ削除を簡単に実現してみる【Authentication,Firestore,Functions】

前置き

Firebase Authenticationで、クライアント側から簡単にユーザ登録できるんだから、削除も簡単でしょうと思ってた愚か者のお話です。

ログイン中のユーザインスタンスからdeleteメソッドを叩けばユーザ削除も簡単だよね!やってみよう!

mAuth.getCurrentUser().delete().addOnCompleteListener(task -> {
    if (task.isSuccessful()) {
        Log.d("DEBUG","Successful to delete user.");
    } else {
        Log.e("DEBUG", "Failed to delete user.", task.getException());
    }
});
2021-03-13 11:04:38.569 6841-6841/com.crabsan.anshare.debug E/DEBUG: Failed to delete user.
    com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException: This operation is sensitive and requires recent authentication. Log in again before retrying this request.
        at com.google.android.gms.internal.firebase-auth-api.zztt.zza(com.google.firebase:firebase-auth@@20.0.2:22)
        at com.google.android.gms.internal.firebase-auth-api.zzvb.zza(com.google.firebase:firebase-auth@@20.0.2:9)
        at com.google.android.gms.internal.firebase-auth-api.zzvc.zzk(com.google.firebase:firebase-auth@@20.0.2:1)

上記コードだとエラーに。ログを読むと、ユーザ削除等のセキュリティ上重要な操作には再認証が必要とのこと。
再度ドキュメントを読み直してみると、がっつり書いてましたw
ユーザを削除するページの抜粋PNG.PNG

ログ通りの実装に変更するにはSSO毎に別途トークンの取得処理が必要のため、複雑なロジックとなりそうです。
また、その他下記理由から別の方法を取りました。

  • ユーザの再認証(トークン取得等)をすれば実現可能だが、例外処理のパターンが増えクライアント側の処理が複雑になる
  • Authenticationで管理しているユーザ削除と同時に、Firestore(DB)に保存しているユーザデータも削除したい
  • ユーザ削除した履歴も同時に残したい

この記事では、これらを実現するためにしたことをまとめています。

結論

Firebase AuthenticationやFirestoreに登録されたユーザデータを一括で削除するため、下記構成にしてみました。

Firebaseユーザ削除機能の全体フロー図.PNG

こちらの構成にすることで以下メリットがありました。

  • クライアント側でFirebase Authenticationの再認証が不要
  • クライアント側の例外処理が簡易(ここが1番のメリット:thumbsup:
  • Backgroundで全ユーザ情報を削除できるため、クライアント側の負担減
  • ユーザ削除履歴も処理の流れで生成できる

① 削除するUIDをFirestoreに登録

ユーザ削除機能の処理1.PNG
クライアント側はFirestoreに削除するユーザIDと削除日を書き込む。
これだけです!!
関連データの削除等もFirebase Cloud Functionsに委譲しており、簡単に実装可能です。

// 削除履歴データ生成
final Map<String, Object> deleteData = new HashMap<>();
deleteData.put("uid", uid);
deleteData.put("ctAt", FieldValue.serverTimestamp());

// Firestoreにデータ登録
mFirestore
        .collection('deleted_users')
        .add(deleteData)
        .addOnCompleteListener(mThreadExecutor, task -> {
            if (task.isSuccessful()) {
                // ユーザ削除完了後の処理
            } else {
                // ユーザ削除失敗のため、UIへ失敗通知
            }
        });
{
    uid: "xxx"
    createdAt: 1615600018000
}

注意点としては、Firestoreのセキュリティールールをしっかり導入しておくこと。
Firestoreの特定コレクションにデータ追加さえすればユーザ削除ができてしまうので、悪用される懸念は当然あります。
自分は以下ルールを設定し、リクエストしたユーザしか自身のデータを追加できない制約を与えています。

  • 登録するドキュメントのuidとリクエストのuidが同じであること
  • ドキュメント内のデータ数が同じであること
  • 登録日時がサーバ時刻と極端に離れていないこと

②③ ドキュメント登録をトリガーに、Function経由でAuthenticationからユーザ削除

ユーザ削除_parts2.PNG

Firestore Functionsに関数を登録します。
①のドキュメント追加(onCreate)をトリガーに、Authenticationのユーザ情報を削除する流れです。
とても簡単ですね。

exports.deleteUser = functions
    .region('asia-northeast1')
    .firestore
    .document('deleted_users/{docId}')
    .onCreate(async (snap, context) => {
      const deleteDocument = snap.data();
      const uid = deleteDocument.uid;

      await auth.deleteUser(uid);
})

④⑤ Authenticationのユーザ削除をトリガーに、Function経由でユーザ情報削除

ユーザ削除_parts3.PNG
②③でAuthenticationのユーザが削除された事をトリガーに、Firestore(DB)に保存されているユーザ情報をFunctionsで全削除します。
こちらは、Firebase側が提供している Delete User Data Extensionsを利用します。

Delete User Data Extensionsを利用することで、Authenticationのユーザが削除されたをトリガーに、UIDに紐づく指定したサブコレクションをキレイに削除可能です。
また、下図のようにUI上で削除対象を登録することができます。
②③のような、Functionsのコードを自前で管理する必要もないので、オススメです!

delete-user-data.PNG

まとめ

Firebase Authentication、Firestoreのユーザデータ削除方法をまとめてみました。
個人Androidアプリ開発で困った箇所なので、同じ悩みを持たれている方の参考になれば嬉しいです!

※ 内容に誤りやこの構成マズイんじゃない?というご意見あれば、コメントやTwitterでご連絡いただけるととても助かります:pray:

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

Android ナビゲーションバー透過

app/res/values/themers/themes.xml
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        window.setFlags(
            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
            WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
        )
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidStudioでメモリ容量が足りないときの対処法

AndroidStudioでメモリ容量が足りないときの対処法

筆者がAndroidStudioでアプリを作成しているときに、AndroidStudio側から「メモリの容量が少なくなっている」との指摘がありました。

そのときに作っていたアプリですが、変数の数が非常に多いアプリだったので、作業中に打ち込んだコードの読み込み時間が長くなってしまって、効率よく作業を行うことができなくなってしまいました。
原因を調べてみると、改善策がいくつか見つかりましたが、その中で実際に行ったら読み込み時間が改善されて、最も簡単だった方法を載せます。

AndroidStudio起動時の「Welcome to Android Studio」ダイアログ右下の「Configure」をクリックして、「Preferences」を選択する。
2021-03-21_03.11.14_Image.png
2021-03-21_03.17.10_Image.png

「Appearance & Behavior」内の「System Settings」から、「Memory Settings」を選択する。
2021-03-21_03.14.07_Image.png

「IDE max heap size:」から、AndroidStudio使用時に使えるメモリの最大容量を変更できるので、現在よりも大きい値を選ぶ。(デフォルト値は1280だったので、2048を選択してみました)
2021-03-21_03.14.51_Image.png

変更するまでコードを記述するごとに読み込みが発生してしまって、作業が遅くなっていましたが、上記を変更することで読み込み時間が短くなりました。
参考になれば幸いです。

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