- 投稿日:2020-10-19T23:39:41+09:00
システム情報ダイアログをUIテストする
はじめに
業務でAndroidを初めて触っていて、テストコードを書くことになったのでコードを書く上で困った点の備忘録および日本語の記事が一切なかったので情報共有の記事になります。
対象読者
Androidエンジョイ勢
テストコードを書くことの素晴らしさを感じ始めた人
検索: Espresso Permission dialog とかしてる人環境
- minSdkVersion 26
- targetSdkVersion 29
- gradle 4.0.0
- kotlin 1.3.30
- espresso 3.2.0
- uiautomator 2.2.0
AndroidのUIテストについて
Androidのテストとしては、主にUI絡みのテスト(AndroidTest)と単体テスト(test)があります。
その中で、AndroidTestのやり方として2つあって
- Espresso 単一アプリのUIテストをする
- Uiautomator 複数アプリのUIテストをする
と別れていることが公式のドキュメントを見れば分かると思います。
この文面だけを見ると、他のアプリと関連なければEspressoで事足りるやん。と思いますが、公式のドキュメントには複数のアプリにまたがるユーザー インタラクションを対象とするユーザー インターフェース(UI)テストでは、ユーザーフローに他のアプリやシステム UI が関与する場合にアプリが正しく動作することを検証できます。
との記載があります。そのため、正確には以下の理解をしなければ永遠にハマることになります。
- Espresso 単一アプリのUIテストをする
- Uiautomator 複数アプリのUIテストおよびシステムUIのテスト
僕はドキュメントを読み込んでおらず、ながら見をして理解していたのでこの問題が解決するまで1日潰れました
社内ではテストコードに落とすことができなさそうだから手動で~
となっていたので、結果的には時間をかけて調べて良かったとは思っています。システム情報ダイアログの取得方法
御宅はいいから早くやり方教えろ。という人もいるとは思うので、解決方法を記載します。
やり方としては、Uiautomatorの方を使えばシステム情報ダイアログ(認可ダイアログ)のUIテストが可能になります。
また、システム情報ダイアログをUIテストの際に考慮する必要がなければGrantPermissionRuleでRuleを定義すれば解決します。
ここでのやりたいこととしては、例えば認可を否定した場合に独自処理を実装していて、それをテストコードで確認を行いたい場合の話になります。
以下、ソースコードの修正箇所になります。gradleの設定
UiAutomatorがプロジェクト内に入っていない方は、依存関係の参照を設定しましょう。
build.gradleandroidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"テストコードの作成
サンプルとしてテストコードには、システム情報ダイアログが表示されている状態で許可を押下してActivityの起動を待つという内容になっています
HogeTest.ktpublic class HogeTest { private lateinit var device: UiDevice fun pushAllowDialog() { // UiDeviceクラスのインスタンス化 device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) // 認可ダイアログから許可という文字が入っているオブジェクトを取得 val allowPermission = device.findObject(UiSelector().text("許可")) // 取得したオブジェクトが画面上に存在すれば押下 if (allowPermission.exists()) { allowPermission.click() } // Activityの表示まで待つ等あれば待機する device.wait(Until.hasObject(By.pkg("テストクラスのパッケージ名") .depth(0)), 3000) // Activity起動後のテストコード } } ※ 例外処理等は一切やっていないので、適宜修正してください ※ import文とアノテーションは省略また、認可をしない場合はtext("許可しない")でオブジェクトが取得できます。
やったこと
ここまで辿り着くのに意外と苦労したので、まとめました。
RecordEspressoTestで自動生成してみる
結果
自動生成したコードが動かなかった。。人生そんなに甘くないってことね。また、生成されたコードを自分が理解できなかったのでメンテナンスを考慮すると全くの無意味に近かった。
押したいボタンの名前がpermission_allow_buttonという事が分かったので調べる方向性を変えるくらいしか役に立たなかった。Espressoで本当にテストコードが書けないかめちゃくちゃ調べる
結果
できない。ただ、stackoverflowでこうすれば出来るよ。と回答している外国の技術者の方法を実際にやってみてもできなかったので昔はできたのか??と自分の中で解釈した。
出来るかもしれないという情報がめちゃくちゃミスリードになった。おわりに
findObject()の指定方法がいろいろあり過ぎて、どう書けば現在表示されているUIオブジェクトを取得できるのかがちょっとわからなかった。
というのも、システム情報ダイアログの取得をindex指定で出来るよ~と言っている人もいて、いろんなやり方があるのかorどちらかが間違えているかという
判断ができなかったので辛かった。
なので、今後は自分なりのやりかたの確立をしていきたいなぁ。
あと、uiautomatorviewerをMacで起動するとエラーになるのどうにかしてくれ。。参考リンク
https://medium.com/exploring-android/handling-android-runtime-permissions-in-ui-tests-981f9dc11a4e
https://developer.android.com/training/testing/ui-testing/uiautomator-testing?hl=ja
- 投稿日:2020-10-19T21:06:58+09:00
#11 Kotlin Koans Introduction/SAM conversions 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans/Object expressionsの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
2 インターフェース/SAM conversions
インターフェース:抽象関数のみ(プロパティは定義されていない)で構成されるクラス。
SAM (Single Abstract Method)インターフェース:抽象関数がただ1つで構成されるインターフェース。
SAM coversionsとは、SAMインターフェース(のオブジェクト)を引数として要求する関数には、ラムダ式が渡せるといったルールです。
3 Introduction/SAM conversionsの解説
Kotlin Koans Introduction/SAM conversionsの解説です。
随時本サイトの内容を引用させていただきます。右側の本文を見てみましょう。
When an object implements a SAM interface (one with a Single Abstract Method), you can pass a lambda instead. Read more about SAM-conversions.
In the previous example change an object expression to a lambda.
Introduction/Object expressionsのコードを書き換えるので、こちらのコードと本章のKotlinのコードを見てみましょう。
//Introduction/Object expressionsのコード import java.util.* fun getList(): List<Int> { val arrayList = arrayListOf(1, 5, 2) Collections.sort(arrayList, object : Comparator<Int>{ override fun compare(x : Int,y : Int) = y - x }) return arrayList } //Introduction/Object expressionsのコード import java.util.* fun getList(): List<Int> { val arrayList = arrayListOf(1, 5, 2) Collections.sort(arrayList, { x, y -> TODO() }) return arrayList }Introduction/Object expressionsのコードについて考えましょう。
sort()関数は、第1引数にListを第2引数にobject式を受け取っています。
このとき、object式ではComparatorクラスを継承しています。
Comparatorクラスは、インターフェースで、かつ、関数が1つしか定義されていません。
つまり、ComparatorクラスはSAMインターフェースです。
なので、sort()関数は第2引数にSAMインターフェースを継承したオブジェクトを要求していることになるので、SAM conversionsを利用することができます。
Introdcution/SAM converionsのコードを見てみましょう。
sort()関数の第2引数にxとyという2つの引数を(compare()関数に)渡してTODO()を返すという
ラムダ式を渡しています。Intrduction/Object expressionsのcompare()関数では戻り値が
y - x
ですので、Collections.sort(arrayList, { x, y -> y - x })このように書き換えればいいですね。
4 最後に
次回はKotlin Koans Introducion/Extensions on collectionsの解説をします
- 投稿日:2020-10-19T19:53:31+09:00
#10 Kotlin Koans Introduction/Object expressions 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans/Object expressionsの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
2 オブジェクト式(Object Expressions)
オブジェクト式とは、無名クラスのオブジェクトを生成するための
object
という表現のことです。(※無名クラスとは、特定のクラスを継承したクラスであり、かつクラス名が無く宣言と利用を同時に行うクラスのことです。)
具体例を見てみましょう。
window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { /*...*/ } override fun mouseEntered(e: MouseEvent) { /*...*/ } })上の例ではMousuAdapterクラスを継承したオブジェクトを生成しています。
生成したオブジェクトはMouseAdapterクラスのmouseClicked()関数とmouseEntered()関数をオーバーライドしています。
3 Introduction/Object expressionsの解説
Kotlin Koans Introduction/Object expressionsの解説です。
随時本サイトの内容を引用させていただきます。右側の本文を見てみましょう。
Read about object expressions that play the same role in Kotlin as anonymous classes in Java.
Add an object expression that provides a comparator to sort a list in a descending order using java.util.Collections class. In Kotlin you use Kotlin library extensions instead of java.util.Collections, but this example is still a good demonstration of mixing Kotlin and Java code.
左側のKotlinのコード(Javaのライブラリをimportしています。)を見てみましょう。
import java.util.* fun getList(): List<Int> { val arrayList = arrayListOf(1, 5, 2) Collections.sort(arrayList, object {}) return arrayList }Collectionsクラスのsort関数では、第1引数でListを渡します。Listの順番を第2引数で渡すComparatorオブジェクトがcompare()関数を呼び出すことで決定します。
Comparatorオブジェクトは、
compare()関数
を呼び出し第1引数、第2引数それぞれに要素を渡していき大きさを比較します。デフォルトで
第1引数-第2引数
の値が
- 正のとき:1を返す
- 0のとき:0を返す
- 負のとき:-1を返す
というように設計されおり、3種の値-1/0/1に応じて第1引数と第2引数の大小を判断しています。
Listすべての要素に対してこの比較を行います。
(今回の場合、初めに第1引数に
1
を第2引数に5
を渡し-1
が返ってきます。次に第1引数に5
を第2引数に'2'を渡し1
が返ってきます。)こうすることで、List中のすべての要素の大小関係を判定し、大きい順に要素を並び替えます。
ただ、今回は降順に並び替えたいのでデフォルトで設定されている
第1引数-第2引数
の値に応じて戻り値を確定させるところを、第2引数-第1引数
の値に応じるようにすれば良いです。なので、
Collections.sort(arrayList, object : Comparator<Int>{ override fun compare(x:Int,y:Int) = y - x })のように実装することになります。
(第1引数と第2引数のxとyは任意の変数名でOKです。)
Collectionsクラスについて
Comparatorクラスについて4 最後に
次回はKotlin Koans Introducion/SAM conversionsの解説をします
- 投稿日:2020-10-19T18:39:56+09:00
新google play consoleでのAndroidアプリ時間指定リリースの設定方法
- 投稿日:2020-10-19T18:39:04+09:00
【Android】Bitmapのピクセル操作
Javaのコードはあったが、Kotlinはあまり無いようだったので、メモ
fun Bitmap.filter(): Bitmap { val pixels = IntArray(width * height) getPixels(pixels, 0, width, 0, 0, width, height) for (y in 0 until height) { for (x in 0 until width) { //ここでピクセル操作(この場合は色反転を行っている) pixels[x + y * width] = pixels[x + y * width] xor 0x00ffffff } } return copy(Bitmap.Config.ARGB_8888, true).apply { setPixels(pixels, 0, width, 0, 0, width, height) } }
- 投稿日:2020-10-19T18:27:31+09:00
Androidカメラのプレビュー表示(Camera API + TextureView)
前書き
前編ではAndroidカメラのプレビュー表示(Camera API + SurfaceView)について述べました。本編ではそれを踏まえて、Camera API + TextureViewの実装方法を紹介します。注意点は前編と同じで、TextureViewについての注意点は特にありません。
実装方法はCamera API + SurfaceViewのとかなり被る部分があるので、ここではあえて実装の差分のみ説明します。Androidカメラのプレビュー表示(Camera API + SurfaceView)と合わせてお読みいただければ幸いです。
実装
レイアウト
SurfaceViewをTextureViewに置き換えます。
main_fragment.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main_container" android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> <!-- SurfaceViewをTextureViewに置き換える --> <TextureView android:id="@+id/texture_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>カメラ起動
camera.setPreviewDisplay(surfaceHolder)をcamera.setPreviewTexture(surfaceTexture)に置き換えます。
surfaceViewをtextureViewに置き換えます。MyActivity.ktprivate fun openCamera() { val context = context ?: return val windowManager = activity?.windowManager ?: return // カメラIDを特定 val cameraId = getCameraId(cameraType) ?: return // カメラを取得 camera = Camera.open(cameraId) camera?.let { camera -> try { // camera.setPreviewDisplay(surfaceHolder) // カメラ出力先を指定 // TextureViewに出力するので、setPreviewTexture()を使って、SurfaceTextureを設定 camera.setPreviewTexture(surfaceTexture) } catch (ioException: IOException) { ioException.printStackTrace() } // [注意点]:端末の回転角度に合わせて、カメラの回転角度を設定 camera.setDisplayOrientation( when (windowManager.defaultDisplay.rotation) { Surface.ROTATION_0 -> 90 Surface.ROTATION_90 -> 0 Surface.ROTATION_180 -> 270 Surface.ROTATION_270 -> 180 else -> 0 } ) // カメラのパラメータを設定 cameraParam = camera.parameters.apply { // プレビュー可能サイズを取得 val size = supportedPreviewSizes.firstOrNull() size?.let { size -> // プレビューサイズを設定 setPreviewSize(size.width, size.height) } } // カメラのパラメータを設定 camera.parameters = camera.parameters.apply { val size = supportedPreviewSizes.firstOrNull() size?.let { size -> setPreviewSize(size.width, size.height) } } // カメラプレビューを開始 camera.startPreview() // [注意点]:プレビューサイズに合わせて、TextureViewのサイズを調整する。 // この処理が抜けると、プレビューのアスペクト比がおかしくなる可能性がある // surfaceViewをtextureViewに置き換える updateSurfaceSize( // binding.surfaceView, binding.textureView, camera.parameters.previewSize.width, camera.parameters.previewSize.height. surfaceWidth, surfaceHeight ) } }TextureView
SurfaceViewをTextureViewに置き換えます。
MyActivity.kt// SurfaceHolderをSurfaceTextureに置き換える // private var surfaceHolder: SurfaceHolder? = null private var surfaceTexture: SurfaceTexture? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = DataBindingUtil.inflate<MainFragmentBinding>( inflater, R.layout.main_fragment, container, false ) // SurfaceViewをTextureViewに置き換える binding.textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { surfaceTexture = surface surfaceWidth = width surfaceHeight = height checkAndAskPermission() } override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { surfaceWidth = width surfaceHeight = height } override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { } override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { return false } } return binding.root }まとめ
ご覧になればおわかりだと思いますが、Camera API + SurfaceViewとCamera API + TextureViewの差分がかなり少ないです。しかもカメラ制御方法もほぼ一緒です。コツさえつかめば、SurfaceViewとTextureViewの双方向変換は簡単にできてしまいます。
参考
- 投稿日:2020-10-19T16:56:37+09:00
Androidカメラのプレビュー表示(Camera API + SurfaceView)
Androidのカメラプレビュー
Androidでカメラのプレビューを表示するのに、大きく分けて2ステップがあります。
- カメラを開く
- カメラ画像をディスプレイに表示する
カメラを開く方法は2つあります。
- Camera API(android.hardware.Camera)
- Camera2 API(android.hardware.camera2)
カメラ画像をディスプレイに表示する方法も2つあります。
- SurfaceView
- TextureView
つまり、Androidのカメラプレビューを実現するのに、合計4つの方法があるというわけです。ネットでは4つの方法をまとめて紹介する記事がなかなか見つからないので、ここで紹介させていただきたいと思います。
本稿ではCamera API + SurfaceViewの実装方法と注意点を重点的に紹介します。
実装
レイアウト
main_fragment.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main_container" android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> <SurfaceView android:id="@+id/surface_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>カメラのパーミッション
AndroidManifest.xml
カメラを使うのに、カメラのパーミッションが必要です。AndroidManifest.xmlにandroid.permission.CAMERAを追加します。
AndroidManifest.xml<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xxx"> ... <uses-permission android:name="android.permission.CAMERA" /> ... </manifest>パーミッション要請
ユーザーにカメラ権限を要請します。
MyActivity.kt// カメラ権限があるかどうかを確認し、ある場合はカメラを起動し、ない場合はユーザーに要請します。 private fun checkAndAskCameraPermission(context: Context) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { // ユーザーにカメラ権限を要請 requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE) } else { // カメラ権限があるので、カメラを起動 openCamera() } } // カメラ権限要請のコールバック override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { when (requestCode) { PERMISSION_REQUEST_CODE -> { if (permissions.isNotEmpty() && grantResults.isNotEmpty()) { val resultMap: MutableMap<String, Int> = mutableMapOf() for (i in 0..min(permissions.size - 1, grantResults.size - 1)) { resultMap[permissions[i]] = grantResults[i] } when { resultMap[Manifest.permission.CAMERA] == PackageManager.PERMISSION_GRANTED -> { // ユーザーがカメラ権限を許可したので、カメラを起動 openCamera() } } } } } }カメラ起動
- カメラIDを特定します。
- カメラ出力先をSurfaceViewのsurfaceHolderに指定します。
- [注意点]:端末の回転角度に合わせて、カメラの回転角度を設定します。
- カメラのパラメータを設定します。
- カメラのプレビューを開始します。
- [注意点]:プレビューサイズに合わせて、SurfaceViewのサイズを調整します。
MyActivity.ktprivate var camera: Camera? = null private var cameraParam: Camera.Parameters? = null // カメラIDを特定 private fun getCameraId(): Int? { val cameraInfo = CameraInfo() for (i in 0 until Camera.getNumberOfCameras()) { Camera.getCameraInfo(i, cameraInfo) // 背面カメラ:Camera.CameraInfo.CAMERA_FACING_BACK // 前面カメラ:Camera.CameraInfo.CAMERA_FACING_FRONT if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { return i } } return null } // カメラを開く private fun openCamera() { val context = context ?: return val windowManager = activity?.windowManager ?: return // カメラIDを特定 val cameraId = getCameraId() ?: return // カメラを取得 camera = Camera.open(cameraId) camera?.let { camera -> try { // カメラ出力先を指定(ここではSurfaceViewのsurfaceHolderにする) camera.setPreviewDisplay(surfaceHolder) } catch (ioException: IOException) { ioException.printStackTrace() } // [注意点]:端末の回転角度に合わせて、カメラの回転角度を設定 camera.setDisplayOrientation( when (windowManager.defaultDisplay.rotation) { Surface.ROTATION_0 -> 90 Surface.ROTATION_90 -> 0 Surface.ROTATION_180 -> 270 Surface.ROTATION_270 -> 180 else -> 0 } ) // カメラのパラメータを設定 cameraParam = camera.parameters.apply { // プレビュー可能サイズを取得 val size = supportedPreviewSizes.firstOrNull() size?.let { size -> // プレビューサイズを設定 setPreviewSize(size.width, size.height) } } // カメラのパラメータを設定 camera.parameters = camera.parameters.apply { val size = supportedPreviewSizes.firstOrNull() size?.let { size -> setPreviewSize(size.width, size.height) } } // カメラプレビューを開始 camera.startPreview() // [注意点]:プレビューサイズに合わせて、SurfaceViewのサイズを調整する。 // この処理が抜けると、プレビューのアスペクト比がおかしくなる可能性がある updateSurfaceSize( binding.surfaceView, camera.parameters.previewSize.width, camera.parameters.previewSize.height. surfaceWidth, surfaceHeight ) } }SurfaceView
SurfaceViewにコールバックを設定します。
Surfaceの生成が終わってから、カメラ権限を確認し、カメラを起動します。MyActivity.ktprivate lateinit var binding: MainFragmentBinding private var surfaceWidth = 1 private var surfaceHeight = 1 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // データバインディングを使ったほうが楽 binding = DataBindingUtil.inflate<MainFragmentBinding>( inflater, R.layout.main_fragment, container, false ) // SurfaceViewにコールバックを設定 binding.surfaceView.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { surfaceHolder = holder // Surfaceの生成が終わってから、カメラ権限を確認し、カメラを起動します。 checkAndAskPermission() } override fun surfaceChanged( holder: SurfaceHolder, format: Int, width: Int, height: Int ) { surfaceWidth = width surfaceHeight = height camera?.let { camera -> updateSurfaceSize( binding.surfaceView, camera.parameters.previewSize.width, camera.parameters.previewSize.height. surfaceWidth, surfaceHeight ) } } override fun surfaceDestroyed(holder: SurfaceHolder) { } }) return binding.root }[注意点]:Surfaceサイズの調整
表示のアスペクト比を調整し、画面いっぱいに表示します。
MyActivity.ktprivate fun updateSurfaceSize(view: View, width: Int, height: Int, surfaceWidth: Int, surfaceHeight: Int) { val param = view.layoutParams var newWidth = width var newHeight = height // [注意点]:端末が縦の場合、設定する横幅と縦幅は逆になる if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) { newWidth = height newHeight = width } val scale1 = surfaceWidth / newWidth.toFloat() val scale2 = surfaceHeight / newHeight.toFloat() val scale = if (newWidth < surfaceWidth || newHeight < surfaceHeight) { max(scale1, scale2) } else if (newWidth < surfaceWidth) { scale1 } else if (newHeight < surfaceHeight) { scale2 } else { 1.0f } // [注意点]:入力された横幅と縦幅を使う(SurfaceViewの幅と高さだと、アスペクト比が合わないことがある) param.width = (newWidth.toFloat() * scale).toInt() param.height = (newHeight.toFloat() * scale).toInt() view.layoutParams = param }リソース解放
アプリから離れたら時にカメラを解放しましょう。
MyActivity.ktoverride fun onStop() { super.onStop() camera?.let { it.stopPreview() it?.release() } }参考
- 投稿日:2020-10-19T14:33:18+09:00
GDG DevFest 2020 勉強会に参加した
開催日程
- 2020年10月17日(土曜日)
場所
- オンライン
主催
- GDG Tokyo
申し込み
費用
- 無料
DevFestとは
- Google Developer Group (GDG) コミュニティによって世界各地で開かれるデベロッパー向けイベント
セッション - Flutterはプロダクション開発に耐えうるのか
- モバイルアプリの開発の現状と課題
- Androidアプリ開発の黎明期
- MVCフレームワーク
- Java
- eclipse, Maven
- Androidアプリ開発の成熟期
- MVVM, FLUX
- Kotlin, Java
- Dagger2, Gradle, Android Studio
- 課題
- iOS/Androidの機能仕様ずれ(SDKやライブラリの差、認識ずれなど)
- 開発期間の長期化
- クロスプラットフォームの期待と課題
- 課題
- アプリ動作の安定性
- 将来に先行き不安
- 共通のUI
- 新機能追加の遅れ
- FlutterとKotlin MPPのどちらを採用
- 事例
- スマホアプリ - Idle Fish
- カメラ機能で商品検索
- アプリ評価 ★4.6 DAU 2000万
- Flutter事例でユーザ数が世界最大級
- アプリ開発で必要な要素
- アーキテクチャ
- MVVM, Bloc, Redux, ELM/MVU
- MVVM + Repository
- Androidエンジニアはよく使っている
セッション - Angularの静的サイトジェネレーター「Scully」の最新情報
- Scullyとは
- Angular用のSSG(静的サイトジェネレータ)
- 2020.09にv1.0.0リリース
- https://scully.io
- 仕組み
- AngularアプリケーションをPuppeteerで実行
- 実行時にプリレングされたHTML -> 読み込み完了でAngularアプリケーションに置き換わる
- 簡単な静的サイト作る方法
- scullyのインストール
- scullyの設定ファイル
- ファイル生成
- ビルド
- PWA化
- @angular/service-workerとの組み合わせに課題
- Workboxなどの利用を推奨
- Scullyの今後
- ドキュメント拡充
- プラグイン追加
- 投稿日:2020-10-19T08:17:47+09:00
Android StudioでXML要素が勝手に並び替えられてしまう問題
最初は以下のように記載していた
<LinearLayout> <TextView/> <Button/> </LinearLayout>自動コード整形を適用すると、以下のように順番が変更になった
<LinearLayout> <Button/> <TextView/> </LinearLayout>これを防ぐために以下を実行した。
Preference → Code Style → XML → Set form → Android → Applyこれで、勝手に並び替えられることが無くなりました。
- 投稿日:2020-10-19T08:17:47+09:00
Android StudioでXML属性が勝手に並び替えられてしまう問題
最初は以下のように記載していた
<LinearLayout> <TextView/> <Button/> </LinearLayout>自動コード整形を適用すると、以下のように順番が変更になった
<LinearLayout> <Button/> <TextView/> </LinearLayout>これを防ぐために以下を実行した。
Preference → Code Style → XML → Set form → Android → Applyこれで、勝手に並び替えられることが無くなりました。
- 投稿日:2020-10-19T01:15:24+09:00
Flutter build error が起きたときにするべきこと
はじめに
Flutterとはネイティブアプリを開発するためのフレームワークとして2018年にGoogleが発表しました。今回はFlutterでbuildしているときにエラーが発生したときにするべきことを書こうと思います。
最初に、、 【エラー状況を知る】
実際にどういうエラーなのかなどの詳細を知るためには以下のコマンドを入力してください。これでflutter runコマンド実行中の詳細ログを表示します。
$ flutter run --verbose手段1 【flutter clean】
Flutter build errorが発生したときに一番最初にやるべきことはflutter cleanです。
$ flutter clean30%のエラーはこの方法で解決します。手段2以降はこの方法でもうまくいかなかった人向けです。
手段2 【Pod関連を再インストール】
$ flutter clean $ rm -Rf ios/Pods $ rm -Rf ios/.symlinks $ rm -Rf ios/Flutter/Flutter.framework $ rm -Rf ios/Flutter/Flutter.podspec $ rm ios/Podfile手段3 【Flutterのバージョンを更新】
次はFlutterのバージョンを更新します。
# flutter upgrade $ flutter clean手段4 【Pod update】
次はFlutterのバージョンを更新します。
# flutter upgrade $ flutter clean手段5 【FlutterのChannelを変更】
次はFlutterのChannelを変更します。FlutterのChannelはmaster、beta、stableなどがありますが基本的にstableにしていた方がバグやエラーが少ないです。
# flutter channel $ flutter channel stable $ flutter clean手段6 【Xcodeのキャッシュ削除】
Tip: Xcodeがフリーズする場合などに解決策として使われるコマンドのようです。
$ rm -rf ~/Library/Caches/com.apple.dt.Xcode手段7 【DerivedData(中間生成ファイル)の削除】
DerivedDataとはXcodeで生成される中間生成ファイルが保存されるディレクトリの名前です。
$ rm -rf ~/Library/Developer/Xcode/DerivedData/手段8 【Xcode Toolsによるキャッシュ削除】
DerivedDataとはXcodeで生成される中間生成ファイルが保存されるディレクトリの名前です。
$ xcrun --kill-cache手段9 【シミュレータにインストールしたアプリ、設定を削除】
$ xxcrun simctl erase all手段10 【CocoaPodsのキャッシュ削除】
$ pod cache clean --all
- 投稿日:2020-10-19T00:52:30+09:00
Android NDKのCMakeを使用したProductFlavorごとにネイティブコードを分ける
TL;DR
build.gradleのproductFlavorsでFlavorごとに引数を与えてCMakeListsで分岐させる。
Android NDKとは
Android NDK は、C / C++(ネイティブ コード)を Android アプリに埋め込むことができるツールです。
重い演算処理、ネイティブコードに秘匿情報を記述しデコンパイルしても見えないようにする、等に使用されます。CMakeとは
コンパイラに依存しないビルド自動化のためのツールです。androidで言うところのgradleですかね・・・・(レイヤーが違う気もする(anroidはjava自体はコンパイラに依存しないようになってますので)がビルドツールという点では一緒)
ProductFlavorとは
ProductFlavor
環境依存のコード(API endpointやその他環境変数)を難しいことをせずにソースファイルごとまるっと差し替えるときに使用します。通常のjavaコードまたはkotlinコードを差し替える場合
パッケージを変えることによって実現することができます。
例えば下記のようにproductFlavorsが定義されてるとします。
例 app/build.gradleandroid { ... defaultConfig {...} buildTypes { debug{...} release{...} } // ココでProductFlavorの種類を定義する flavorDimensions "flavor" productFlavors { "flavor1" { } "flavor2" { } } }上記の様に定義すると
src/flavor1/
src/flavor2/
配下にjava, kotlinコードを配置するとbuildする際に指定したproductFlavorによって自動でコードを差し替えてくれます。Android NDKのコードを差し替える場合はどうするのか
上記の様にソースファイルを差し替えれるのかなと思ったらどうやらできないご様子・・・・。
Cmakeを使用する場合はapp/build.gradleに下記のようにCmakeLists.txtへのパスを記載するのですが、CmakeはproductFlavorを感知しないので先述のやり方でソースコードを差し替えることはできませんでした(やり方あるのかもしれませんが・・・)。
android { externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.10.2" } } }じゃあCmakeする際に引数を与えて、CmakeLists.txt内で分岐させよう!となりました。
各ProductFlavor内のCmakeを定義する部分に独自引数を追加します。android { ... flavorDimensions "flavor" productFlavors { "flavor1" { externalNativeBuild { cmake { // ココ! arguments += "-DFLAVOR=FLAVOR1" } } } "flavor2" { externalNativeBuild { cmake { // ココ! arguments += "-DFLAVOR=FLAVOR2" } } } } }ここでは
-DFLAVOR
という引数を追加してあげています。
そして、CmakeLists.txt内で読み込むcppファイルを差し替える分岐を作ってあげます。if (${FLAVOR} STREQUAL "FLAVOR1") message("adding the source code for Flavor1...") add_library( # Sets the name of the library. hello-lib SHARED // 差し込んでるcppファイルが違う! ${CMAKE_CURRENT_SOURCE_DIR}/flavor1/hello-lib.cpp) elseif (${FLAVOR} STREQUAL "FLAVOR2") message("adding the source code for Flavor2...") add_library( # Sets the name of the library. hello-lib SHARED // 差し込んでるcppファイルが違う! ${CMAKE_CURRENT_SOURCE_DIR}/flavor2/hello-lib.cpp) endif ()こうすることでndkで使用するcppファイルをProductFlavorごとに差し替えることができました!
サンプルプロジェクトをGitHubに置いておくので参考にしてください。
NdkFlavor
- 投稿日:2020-10-19T00:52:30+09:00
Android NDKのCMakeを使用したProductFlavorごとにネイティブコードを分ける方法
TL;DR
build.gradleのproductFlavorsでFlavorごとに引数を与えてCMakeListsで分岐させる。
Android NDKとは
Android NDK は、C / C++(ネイティブ コード)を Android アプリに埋め込むことができるツールです。
重い演算処理、ネイティブコードに秘匿情報を記述しデコンパイルしても見えないようにする、等に使用されます。CMakeとは
コンパイラに依存しないビルド自動化のためのツールです。androidで言うところのgradleですかね・・・・(レイヤーが違う気もする(anroidはjava自体はコンパイラに依存しないようになってますので)がビルドツールという点では一緒)
ProductFlavorとは
ProductFlavor
環境依存のコード(API endpointやその他環境変数)を難しいことをせずにソースファイルごとまるっと差し替えるときに使用します。通常のjavaコードまたはkotlinコードを差し替える場合
パッケージを変えることによって実現することができます。
例えば下記のようにproductFlavorsが定義されてるとします。
例 app/build.gradleandroid { ... defaultConfig {...} buildTypes { debug{...} release{...} } // ココでProductFlavorの種類を定義する flavorDimensions "flavor" productFlavors { "flavor1" { } "flavor2" { } } }上記の様に定義すると
src/flavor1/
src/flavor2/
配下にjava, kotlinコードを配置するとbuildする際に指定したproductFlavorによって自動でコードを差し替えてくれます。Android NDKのコードを差し替える場合はどうするのか
上記の様にソースファイルを差し替えれるのかなと思ったらどうやらできないご様子・・・・。
Cmakeを使用する場合はapp/build.gradleに下記のようにCmakeLists.txtへのパスを記載するのですが、CmakeはproductFlavorを感知しないので先述のやり方でソースコードを差し替えることはできませんでした(やり方あるのかもしれませんが・・・)。
android { externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.10.2" } } }じゃあCmakeする際に引数を与えて、CmakeLists.txt内で分岐させよう!となりました。
各ProductFlavor内のCmakeを定義する部分に独自引数を追加します。android { ... flavorDimensions "flavor" productFlavors { "flavor1" { externalNativeBuild { cmake { // ココ! arguments += "-DFLAVOR=FLAVOR1" } } } "flavor2" { externalNativeBuild { cmake { // ココ! arguments += "-DFLAVOR=FLAVOR2" } } } } }ここでは
-DFLAVOR
という引数を追加してあげています。
cppファイル自体は
src/main/cpp/flavor1
src/main/cpp/flavor2
に配置しました。
そして、CmakeLists.txt内で読み込むcppファイルを差し替える分岐を作ってあげます。if (${FLAVOR} STREQUAL "FLAVOR1") message("adding the source code for Flavor1...") add_library( # Sets the name of the library. hello-lib SHARED // 差し込んでるcppファイルが違う! ${CMAKE_CURRENT_SOURCE_DIR}/flavor1/hello-lib.cpp) elseif (${FLAVOR} STREQUAL "FLAVOR2") message("adding the source code for Flavor2...") add_library( # Sets the name of the library. hello-lib SHARED // 差し込んでるcppファイルが違う! ${CMAKE_CURRENT_SOURCE_DIR}/flavor2/hello-lib.cpp) endif ()こうすることでndkで使用するcppファイルをProductFlavorごとに差し替えることができました!
サンプルプロジェクトをGitHubに置いておくので参考にしてください。
NdkFlavor