- 投稿日:2020-08-08T21:33:00+09:00
実践・新規モバイルアプリ開発の手順(iOS)
久しぶりに新規アプリ開発をしていて、改めて手順を確認しました。
前提
実際の開発では多くの場合、要件定義、設計、API開発、デザインがある程度並行で走るため、仕様がフワフワした状態で着手することが多いです。
その際にどのように立ち回るのがよいか。また、複数人で開発する場合にどのように作業分担するか書いてきます。
なお、設計方針はオーソドックスなMVCで、アプリの構成は一般的なAPI通信をするサービス系アプリと想定します。
設計方針やアプリの構成が異なる場合は読み替えてください。このフローは何案件かで新規開発を行った人であれば頭の中で構築しているのでしょうけど、言語化されてるのはあまり見かけません。
利用方法
- どういうタスクが存在するか確認
- 今の状態でできることは何かを見つける
- 今やってるタスクの意味を理解する
- 今やってるタスクが他のタスクとどう繋がっているか確認する
- 残タスクに対して何の資料や仕様か、何がブロック要因なのか確認する
- 進捗の目処を立てる
iOSアプリの新規開発フロー
既に仕様が全て揃っている状態からスタートする場合はこの様になると思います。
手順は必ずしも順番に行わなければならないわけではありませんが、上のタスクが終わらないと下のタスクができないことがあります。手順の確認
これらは必ずしも順番にやる必要はありません。
(順番にやらなくても良いように手順を構築しています)
仕様が固まったところから順に着手していっていいと思います。1. Codableの作成
必要なドキュメント:
API仕様書私はCodableに関してはAPI仕様書に完全に準拠させて、アプリ側で使い回すためのModelは別途作成しています。
これはCodableとModel(とEntity)を同一化すると、一部が仕様変更になった際に影響範囲が非常に広く重くなるためです。2. Sample JSONを作成し、Codableにdecodeする
必要なドキュメント:
API仕様書確認すること:
Sample JSONがちゃんとdecodeできること。WebAPIがまだ動いていない場合でもこの作業はできます。
WebAPIが既に動いている場合でも、この作業はやることをオススメします。
もしdecode周りでエラーが有った際に、WebAPIに不備があるのか、アプリ側に不備があるのかの判別が簡単にできます。
アプリ作成後は、この部分はテストコードに書き換えても良いと思います。もちろん最初からテストコードでも構いません。3. 画面が使用するModelの作成
必要なドキュメント:
仕様書全般
ER図
ワイヤー など私はCodableとModelは分ける主義です。
Modelの設計は要件定義やインターフェースの相談、デザイン面での相談も必要になります。
よく変更が入るのでコメントをちゃんと残しましょう。4. CodableからModelへの変換
これもテストコード化が可能です。
5. Modelのスタブを作成
5〜7はワンセットです。
6. 画面遷移を設計し、大まかなレイアウトを作成する
必要なドキュメント:
ワイヤー各viewに値を埋め込める状態にしておくあたりまで。
これはワイヤーや未完成のデザインの状態でも着手可能ですが、後で変更になることも想定しておいてください。7. Modelのスタブの内容を画面に表示する
画面やviewが何のModelをどのように持つか検討し、データが与えられたらそれを表示できるようにします。
8. 細かなデザインの反映
必要なドキュメント:
UI仕様書デザイン仕様ができたらそれを反映します。
NoImageやデフォルトテキスト、テキストが長い場合、端末差による画面崩れ、アニメーションなど、ここらへんはちゃんとやると結構重いです。9. API反映
APIが動いたらそれが反映されることを確認します。
10. 異常系、その他のタスク
9まで出来たら半分以上終わったイメージです。
そこから異常系とか、特殊ケースの対応をしたり、ページング処理や細かな調整をしていきます。おわりに
かなりざっくりしていますし、タスクはこれ以外にもたくさんありますが。
骨子はこんな感じで作るとスムーズかと思います。何か改善点があればご指摘ください。
- 投稿日:2020-08-08T21:31:20+09:00
【Android/Kotlin】RecyclerViewでオーバースクロールした際のエフェクトを消す
RecyclerViewやListView、ScrollViewなどで端までスクロールするとエフェクトが出る。これを消すには
overScrollMode
プロパティにView.OVER_SCROLL_NEVER
を指定する。指定できる値
OVER_SCROLL_ALWAYS
(デフォルト)OVER_SCROLL_IF_CONTENT_SCROLLS
OVER_SCROLL_NEVER
val recyclerView = findViewById(R.id.recyclerView) recyclerView.overScrollMode = View.OVER_SCROLL_NEVER
- 投稿日:2020-08-08T21:09:45+09:00
Koinを使っているマルチモジュールプロジェクトをHiltに移行してみた
はじめに
会社のアプリも個人的なアプリもマルチモジュールプロジェクトでDIフレームワークにはKoinを使っています。Googleさんお勧めのDaggerは難解でメンバーへの展開が難しかっため、直感的に理解しやすいKoinの方を採用していました。
しかし、Android Studio 4.1(執筆時点ではBeta)ではDaggerに対応したソースコードのナビゲーション機能が追加されたらしく、Koinではこのようなことはできないので、Koinを採用したことを軽く後悔したできことがありました。
また先日のAndroid 11 Meetupsで、Jetpackの一部としてDaggerベースのHiltがリリースされてDaggerよりも簡単に使えるという内容のセッションがありました。
それらを受けて、Koinを使っている個人的なアプリについて、Hiltに移行してみました。
KoinよりHiltを使うメリット
Koin採用済みのある程度規模のある実プロダクトで、わざわざHiltに移行しなくても良いと思います。私の今回の移行作業は学習のためというモチベーションが大きいです。しかし新規プロダクトやこれからDIを採用するプロダクトにおいてはKoinではなくHiltを使うと以下のメリットがあると思います。
- 依存性注入のModuleを探しやすくなる(HiltはAndroid Studio 4.2からの対応)
- 依存性を解決できないことで発生する実行時エラーは起こらない
- KotlinとJavaで依存するインスタンスの取得方法が同じになる
Hiltに移行するアプリの解説
今回、KoinからHiltに移行するアプリはシンプルな音声の録音再生アプリです。
画面回転や画面分割に対応しています。クラスの紹介
今回解説する範囲における、クラスの依存関係はこのようになっています。
MainFragment
Fragmentです。画面回転や画面分割で再生成されます。
RecordViewHelper
録音のために録音デバイス制御などを行います。ViewModelを継承しているので画面回転や画面分割が起きても同じインスタンスが保持されます。
PlayViewHelper
再生のために音声デバイス制御などを行います。RecordViewHelperと同様にViewModelを継承していて、画面回転や画面分割が起きても同じインスタンスが保持されます。
SoundRepository
録音音声の保持と取り出しを担当します。シングルインスタンスです。音声がメモリー、ローカルファイル、クラウド等どこにあるか呼び出し元は関与しません。
SoundMemoryLocalDataStore
録音音声をメモリーに保持する担当です。シングルインスタンスです。
SoundFileLocalDataStore
録音音声をAACにエンコードしてローカルファイルファイルに保持します。シングルインスタンスです。プロセスキルからの復帰ではこちらが使われます。
モジュールの紹介
レイヤードアーキテクチャへの強制力を持たせるために、レイヤー別マルチモジュールプロジェクトになっています。下図のように役割に応じてクラスをモジュールに配置しています。
移行の手順
ライブラリ追加
ほぼ公式情報の手順です。
プロジェクトルートの
build.gradle
ファイルにhilt-android-gradle-plugin
プラグインを追加します。build.gradlebuildscript { // 略 dependencies { // 略 classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' } }私のプロジェクトでは
Minimum supported Gradle version is 5.6.4. Current version is 5.4.1.
エラーが表示されたので、Gradleバージョンを上げました。gradle/wrapper/gradle-wrapper.propertiesdistributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zipマルチモジュールプロジェクトなので、各プロジェクトへのライブラリの追加のために共通のgradleファイルの方を修正しました。
gradle/common.gradleapply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' // 略 dependencies { // 略 implementation "com.google.dagger:hilt-android:2.28-alpha" kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" }
gradle/common.gradle
ファイルは以前からこのように取り込んでいます。localDataStore/build.gradleapply from: rootProject.file('gradle/common.gradle')ViewModelも注入したいため、公式のこちらの文献に沿って、さらにライブラリを追加します。
gradle/view-common.gradleapply from: rootProject.file('gradle/common.gradle') // 略 dependencies { // 略 implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02' kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02' }ViewModelを含むmainモジュールではこのように取り込んでいます。
main/build.gradleapply from: rootProject.file('gradle/view-common.gradle')モジュール依存関係変更
Hiltはアプリケーションモジュールから直接参照しているモジュールにあるクラスしか注入できないので、このようにモジュールの依存関係を変更しました。
app/build.gradledependencies { // 略 implementation project(':repository') implementation project(':localDataStore') }Koinでは直接参照していないモジュールにあるクラスを注入できるのですが、その件は補足で説明します。
Applicationクラスにアノテーション追加
すでにKoinのためにApplicationを継承したクラスを定義していました。
そのクラスに@HiltAndroidApp
アノテーションを追加します。QuickEchoApplication.kt@HiltAndroidApp class QuickEchoApplication : Application() { override fun onCreate() { super.onCreate() // 略 } }モジュールを作成する
SoundMemoryLocalDataStore、SoundFileLocalDataStore、SoundRepositoryを注入できるように、Hiltのモジュールを作成します。
MockKでモックを作って単体テストできるように、インターフェースと実装を分けています。HiltLocalDataStoreModule.kt// appモジュールまたはlocalDataStoreモジュールに置く @Module @InstallIn(ApplicationComponent::class) abstract class HiltLocalDataStoreModule { @Binds @Singleton abstract fun bindSoundMemoryLocalDataStore( soundMemoryLocalDataStore: SoundMemoryLocalDataStoreImpl ): SoundMemoryLocalDataStore @Binds @Singleton abstract fun bindSoundFileLocalDataStore( soundFileLocalDataStore: SoundFileLocalDataStoreImpl ): SoundFileLocalDataStore }HiltRepositoryModuke.kt// appモジュールまたはrepositoryモジュールに置く @Module @InstallIn(ApplicationComponent::class) abstract class HiltRepositoryModule { @Binds @Singleton abstract fun bindSoundRepository( soundRepository: SoundRepositoryImpl ): SoundRepository }注入されるクラスのコンストラクタには
@Inject
アノテーションを付けます。
アプリケーションコンテキストは@ApplicationContext
アノテーションで注入できます。SoundMemoryLocalDataStore.ktclass SoundMemoryLocalDataStoreImpl @Inject constructor(@ApplicationContext private val context: Context) : SoundMemoryLocalDataStore { // 略 }SoundFileLocalDataStore.ktclass SoundFileLocalDataStoreImpl @Inject constructor(@ApplicationContext private val context: Context) : SoundFileLocalDataStore { // 略 }SoundRepository.ktclass SoundRepositoryImpl @Inject constructor(private val soundMemoryLocalDataStore: SoundMemoryLocalDataStore, private val soundFileLocalDataStore: SoundFileLocalDataStore) : SoundRepository { }ViewModelを注入する
ViewModelのコンストラクタに
@ViewModelInject
アノテーションを付けます。RecordViewHelper.ktclass RecordViewHelper @ViewModelInject constructor( private val repository: SoundRepository) : ViewModel() {注入されるActivityとFragmentに
@AndroidEntryPoint
アノテーションを付けます。Activityでは注入を行ってなくても、Activityが持つFragmentに対して注入を行っている場合はActivityにもアノテーションを付けないと実行時エラーになります。MainActivity.kt@AndroidEntryPoint class MainActivity : AppCompatActivity() { // 略 }MainFragment.kt@AndroidEntryPoint class MainFragment : Fragment() { // 略 }ここまで定義すると、このようにするだけでViewModelが依存するインスタンスとともにViewModelが生成されます。
MainFragment.kt@AndroidEntryPoint class MainFragment : Fragment() { private val recordViewHelper: RecordViewHelper by viewModels() private val playViewHelper: PlayViewHelper by viewModels() // 略 }補足
Koinでアプリケーションモジュールから直接参照していないモジュールにあるクラスを注入できるようにする
一番下のレイヤーのモジュールです。
LocalDataStoreModule.ktval localDataStoreModule = module { single { SoundMemoryLocalDataStoreImpl(androidContext()) as SoundMemoryLocalDataStore } single { SoundFileLocalDataStoreImpl(androidContext()) as SoundFileLocalDataStore } }その上のレイヤーのモジュールでは、さらに上のレイヤーのモジュールから下のレイヤーのモジュールを参照できるようにします。
RepositoryModule.ktval localDataStoreModuleInRepositoryModule = localDataStoreModule val repositoryModule = module { factory { SoundRepositoryImpl(get(), get()) as SoundRepository } }その上のレイヤーでも同様にします。
MainModule.ktval localDataStoreModuleInMainModule = localDataStoreModuleInRepositoryModule val repositoryModuleInMainModule = repositoryModule val mainModule = module { viewModel { RecordViewHelper(get()) } viewModel { PlayViewHelper(get()) } }アプリケーションモジュールにある、startKoinブロックではすべてのモジュールを指定します。
KoinSetting.ktobject KoinSetting { fun start(application: Application) { startKoin { androidContext(application.applicationContext) modules(listOf(localDataStoreModuleInMainModule, repositoryModuleInMainModule, mainModule)) } } }全体ソースコード
プルリクはこちらです。
この記事では@Binds
を使用したインターフェースからのインスタンス注入しか説明してないですが、@Provides
を使用したインスタンス注入もこちらにあります。
- 投稿日:2020-08-08T20:44:31+09:00
【Flutter】Pigeon を使ってネイティブコードをスマートに呼ぶ
はじめに
先日発表された、 Flutter 1.20 のリリース記事 で、 Pigeon というパッケージが紹介されていました。
このパッケージを使うことで、
- ネイティブ側と型安全に通信
- 自動生成よって手書きコード量を削減
することができます。
今回は、このパッケージを使って、swift / Kotlin で実装した単純な add メソッドを Flutter 側 から呼ぶ方法を見ていきます。サンプルプロジェクトはこちらで公開しています。
Pigeon が行うこと
Dart ファイルで定義した、引数や戻り値の情報を元に、Java / Objective-C のインターフェースやプロトコルを自動生成します。
ネイティブ側ではこれらを元にした実装を行うことで、Flutter 側と型安全に通信することができるようになります。インストール
pubspec.yamldev_dependencies: pigeon: ^0.1.4Dart 側
まずは、ネイティブと通信するスキーマを定義した dart ファイルを作ります。
自分の場合はプロジェクトルートにpigeon/
というフォルダを作ってその中に置きました。schema.dartimport 'package:pigeon/pigeon.dart'; // 引数の定義 class AddRequest { int n1; int n2; } // 戻り値の定義 class AddReply { int result; } @HostApi() abstract class Api { AddReply add(AddRequest req); } // 生成されるファイルの出力先などの設定 void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/api_generated.dart'; opts.javaOut = 'android/app/src/main/java/io/flutter/plugins/Pigeon.java'; opts.javaOptions.package = "io.flutter.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'FLT'; }次に、pigeon コマンドを実行して、必要な Java ファイルなどを生成します。
flutter pub run pigeon --input pigeon/schema.dart
あとは、ネイティブ関数を呼びたい箇所で、Pigeon によって生成された dart ファイルをインポートして使うだけです。
home_page.dartimport './api_generated.dart'; void callNativeAdd () async { final api = Api(); final req = AddRequest() ..n1 = 10 ..n2 = 20; final reply = await api.add(req); print(reply.result); // prints 30 }Kotlin 側
先ほどの pigeon コマンドによって、Api のインターフェースが書かれた Java ファイルが生成されているので、これを実装したクラスを作って、setup メソッドに渡します。
MainActivity.ktclass MainActivity: FlutterActivity() { // 1. 自動生成されたApiインターフェースを実装したクラスを作る private class MyApi: Pigeon.Api { override fun add(arg: Pigeon.AddRequest): Pigeon.AddReply { val reply = Pigeon.AddReply() reply.result = arg.n1 + arg.n2 return reply } } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // 2. setup()を呼ぶ Pigeon.Api.setup(flutterEngine.dartExecutor.binaryMessenger, MyApi()) } }自分の場合は kotlin から Java 側の static なメソッドは呼べないよというエラーが出ましたが、エラーメッセージにしたがって build.gradle に下記を追加することで動きました。
app/build.gradleandroid { ... kotlinOptions { jvmTarget = '1.8' } }Swift 側
Pigeon によって生成された Objective-C ファイルを Swift 側から参照できるようにするため、
ios/Runner
内にあるRunner-Bridging-Header.h
にインポート文を追加します。(参考)ios/Runner/Runner-Bridging-Header.h#import "Pigeon.h"
次に、生成されたプロトコルを実装したクラスを作ります。
専用のファイルを作っても良いですし、AppDelegate.swift
内にベタ書きでも OK です。class MyApi: FLTApi { func add(_ input: FLTAddRequest, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FLTAddReply? { let reply = FLTAddReply() let result = input.n1!.intValue + input.n2!.intValue reply.result = NSNumber.init(value: result) return reply } }あとは、
AppDelegate
内で Api をインスタンス化して setup に渡すだけです。AppDelegate.swift@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // Setupを呼ぶ FLTApiSetup(controller.binaryMessenger, MyApi()) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }まとめ
今まで MethodChannel を使って書いていたネイティブとの接続部分を自動生成してくれる。( 厳密にはPigeon では BasicMessageChannel が使われてる)
事前にスキーマを定義するので、ネイティブと型安全に通信できる
生成されるコードは、Java / Objective-Cだが、Kotlin からは Java が、Swift からは Objective-C が呼べるので、普通に使える。
Dart 側からネイティブのコードを意識しなくて良い (自動生成されたAPIを呼ぶだけ)
ネイティブ側から Dart のコードを意識しなくて良い (自動生成されたインターフェースを実装するだけ)
公式の vider_player プラグインで実際に使われていたり、 Flutter 1.20 のリリース記事 で紹介されていたりするので、今後広まっていくかも。
- 投稿日:2020-08-08T20:44:31+09:00
【Flutter】Pigeon を使ってネイティブコードを型安全に呼ぶ
はじめに
先日の、 Flutter 1.20 のリリース記事 で、 Pigeon というパッケージの紹介がありました。
本来 Flutter 側からネイティブコードを呼ぶには、関数名や引数などを文字列ベースで合わせる必要があるなど、少し大変ですが、このパッケージを使うことで、
- ネイティブ側と型安全に通信
- 自動生成よる手書きコード量の削減
が可能になります。
本投稿では、このパッケージを使って、swift / Kotlin で実装した単純な add メソッドを Flutter 側 から呼ぶ方法を見ていきます。サンプルプロジェクトはこちらで公開しています。
Pigeon が行うこと
Dart 側で定義した、引数や戻り値の情報を元に、Java / Objective-C のインターフェースやプロトコルを自動生成します。
ネイティブ側ではこれらを元に実装を行うことで、Flutter 側と型安全に通信することができるようになります。
イメージとしては、 TypeScript で型定義ファイルを作るのに近いかもしれません。インストール
pubspec.yamldev_dependencies: pigeon: ^0.1.4Dart 側
まずは、ネイティブと通信するスキーマを定義した dart ファイルを作ります。
自分の場合はプロジェクトルートにpigeon/
というフォルダを作ってその中に置きました。schema.dartimport 'package:pigeon/pigeon.dart'; // 引数の定義 class AddRequest { int n1; int n2; } // 戻り値の定義 class AddReply { int result; } @HostApi() abstract class Api { AddReply add(AddRequest req); } // 生成されるファイルの出力先などの設定 void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/api_generated.dart'; opts.javaOut = 'android/app/src/main/java/io/flutter/plugins/Pigeon.java'; opts.javaOptions.package = "io.flutter.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'FLT'; }次に、pigeon コマンドを実行して、必要な Java ファイルなどを生成します。
flutter pub run pigeon --input pigeon/schema.dart
あとは、ネイティブ関数を呼びたい箇所で、Pigeon によって生成された dart ファイルをインポートして使うだけです。
home_page.dartimport './api_generated.dart'; void callNativeAdd () async { final api = Api(); final req = AddRequest() ..n1 = 10 ..n2 = 20; final reply = await api.add(req); print(reply.result); // prints 30 }Kotlin 側
Pigeon によって生成された Java ファイルの中に Api のインターフェースが書かれているので、これを実装したクラスを作って、setup メソッドに渡します。
MainActivity.ktclass MainActivity: FlutterActivity() { // 1. 自動生成されたApiインターフェースを実装したクラスを作る private class MyApi: Pigeon.Api { override fun add(arg: Pigeon.AddRequest): Pigeon.AddReply { val reply = Pigeon.AddReply() reply.result = arg.n1 + arg.n2 return reply } } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // 2. setup()を呼ぶ Pigeon.Api.setup(flutterEngine.dartExecutor.binaryMessenger, MyApi()) } }自分の場合は kotlin から Java 側の static なメソッドは呼べないよというエラーが出ましたが、エラーメッセージにしたがって build.gradle に下記を追加することで動きました。
app/build.gradleandroid { ... kotlinOptions { jvmTarget = '1.8' } }Swift 側
Pigeon によって生成された Objective-C ファイルを Swift 側から参照できるようにするため、
ios/Runner
内にあるRunner-Bridging-Header.h
にインポート文を追加します。(参考)ios/Runner/Runner-Bridging-Header.h#import "Pigeon.h"
生成されたファイルには、 Api のプロトコルが書かれているので、これを実装したクラスを作ります。
専用のファイルを作っても良いですし、AppDelegate.swift
内にベタ書きでも OK です。class MyApi: FLTApi { func add(_ input: FLTAddRequest, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FLTAddReply? { let reply = FLTAddReply() let result = input.n1!.intValue + input.n2!.intValue reply.result = NSNumber.init(value: result) return reply } }あとは、
AppDelegate
内で Api をインスタンス化して setup に渡すだけです。AppDelegate.swift@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // Setupを呼ぶ FLTApiSetup(controller.binaryMessenger, MyApi()) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }まとめ
今まで MethodChannel を使って書いていたネイティブとの接続部分を自動生成してくれる。( 厳密にはPigeon では BasicMessageChannel が使われてる)
事前にスキーマを定義するので、ネイティブと型安全に通信できる
生成されるコードは、Java / Objective-Cだが、Kotlin からは Java が、Swift からは Objective-C が呼べるので、普通に使える。
Dart 側からネイティブのコードを意識しなくて良い (自動生成されたAPIを呼ぶだけ)
ネイティブ側から Dart のコードを意識しなくて良い (自動生成されたインターフェースを実装するだけ)
公式の vider_player プラグインで実際に使われていたり、 Flutter 1.20 のリリース記事 で紹介されていたりするので、今後広まっていくかも。
- 投稿日:2020-08-08T19:59:10+09:00
Android向けにstraceをクロスコンパイルする方法
Android向けにstraceをクロスコンパイルする方法に苦戦したので自分用メモ。
ビルド環境
Ubuntu 20.04 LTS
arm対応gcc等のインストール
sudo apt install g++-arm-linux-gnueabihf g++-aarch64-linux-gnu autoconfソースのダウンロード
git clone https://github.com/strace/strace.gitコンパイラ設定
#armの場合 export CC=/usr/bin/arm-linux-gnueabihf-gcc export CFLAGS="-O2 -static" export LDFLAGS="-static -pthread" #aarch64の場合 export CC=/usr/bin/aarch64-linux-gnu-gcc export CFLAGS="-O2 -static" export LDFLAGS="-static -pthread"コンパイル
cd strace autoconf ./bootstrap #armの場合 ./configure --host=arm-linux #aarch64の場合 ./configure --host=aarch64-linux --enable-mpers=no makestraceフォルダ直下にstraceという実行ファイルが出来上がります♪
- 投稿日:2020-08-08T17:09:25+09:00
【Android/Kotlin】SharedPreferencesでデータを永続化する
やりたいこと
ユーザーの入力(EditText)を永続化し、アプリ起動のたびに初期化されないようにしたい。
開発環境
- macOS 10.15.5
- Android Studio 4.0
- Kotlin 1.3.72
SharedPreferencesとは?
SharedPreferences とは、boolean, float, int, long, string などのデータ型について、キーと値のペアを読み書きできるAPI。キーと値のペアは内部ストレージにxmlファイルとして保存される。初回アクセス以降はメモリ上に展開されたキャッシュからデータを取得するため、高速にアクセスできる。
API名「SharedPreferences」から誤解されることもあるが、厳密には「ユーザー設定」を保存するためのものではなく、ユーザーのハイスコアなどの単純なデータを保存する際に使われる。↓ デベロッパーガイドより
注: SharedPreferences API は Key-Value ペアの読み書き用であり、アプリ設定向けのユーザー インターフェースを作成するための Preference API と混同しないように注意してください(ただし、後者もユーザー設定の保存には SharedPreferences を使用します)。Preference API の詳細については、設定デベロッパー ガイドをご覧ください。
使い方
まず、次のいずれかのメソッドを呼び出し、
SharedPreferences
オブジェクトを取得する。
getSharedPreferences()
アプリの任意のContextから呼び出せる。
getPreferences()
一つのActivityに対してプリファレンスファイルを一つだけ使用する必要がある場合に、Activityからこのメソッドを使用する。val sharedPref = getSharedPreferences("プリファレンスファイル名", Context.MODE_PRIVATE)*第二引数はファイルの共有モードで
Context MODE_PRIVATE
は他のアプリとデータを共有しない設定。↓他のモードについては非推奨になっているそうです。(デベロッパーガイドより)注: MODE_WORLD_READABLE モードと MODE_WORLD_WRITEABLE モードは、API レベル 17 でサポートが終了しています。Android 7.0(API レベル 24)以降は、サポートが終了したモードを使用すると SecurityException がスローされます。アプリ専用のファイルを他のアプリと共有する必要がある場合は、FileProvider を使用して FLAG_GRANT_READ_URI_PERMISSION を指定してください。 詳細については、ファイルを共有するをご覧ください。
ファイルへの書き込み
SharedPreferences.edit()
メソッドでEditorを作成putInt()
やputString()
を呼び出し、Key-Valueを渡すapply()
またはcommit()
を呼び出して、変更内容を保存sharedPref.edit().putString("key名", inputText).apply()*
apply()
は非同期、commit()
は同期的にディスクに書き込む。ファイルから読み取る
getInt()
やgetString()
を呼び出し、値に対応するキーを指定する- 必要に応じて、キーが存在しない場合に返すデフォルト値を指定する
val savedText = sharedPref.getString("key名", "デフォルト文字列")サンプルコード
レポジトリはこちら。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // getSharedPreferencesメソッドでSharedPreferencesオブジェクトを取得 val sharedPref = getSharedPreferences("プリファレンスファイル名", Context.MODE_PRIVATE) // getString()を呼び出して保存されている文字列を読み込む // まだ保存されていない場合はデフォルトの文字列を返す val savedText = sharedPref.getString("key名", "デフォルト文字列") val editText = findViewById<EditText>(R.id.editText) editText.setText(savedText) val button = findViewById<Button>(R.id.saveButton) button.setOnClickListener { // テキストボックスに入力されている文字列を取得 val inputText = editText.text.toString() // プリファレンスに書き込む sharedPref.edit().putString("key名", inputText).apply() } }xmlファイルが実際に保存されている場所の確認
AndroidStudio の Device File Explorerから確認できます。(詳しくはこちら)
参考
- デベロッパーガイド
- みんなのプログラミング
- 書籍(Amazonのリンク)
- 投稿日:2020-08-08T16:31:11+09:00
Android 端末内にデータを保存する際の指針
概要
Android 端末内にデータを保存する際の指針をまとめました。
包括的情報
Data and file storage overview 参照のこと。
ストレージ選択の指針
アプリ内のみで利用されアンインストール時に削除されるかどうか?
- 他のアプリとデータを共有する場合は no, それ以外は yes。1
内部ストレージが好ましいかどうか?
メディアファイルかどうか?
- Android はメディア情報を統合的に扱う Media Store Framework を提供しているため、これを利用することが好ましい。
構造化されたデータの扱い
◆ 一般論
構造化されたデータについては、key-value pair の場合は App preferences, それ以外の構造の場合は Database という指針が一般的なようです。
◆ 個人的見解
SharedPreferences のデフォルトの DataStore を利用するのは避けたいところです。理由は、SQLite のような汎用的なマイグレーションパスが存在しないためです。
ここら辺は、時が来たら追記しようと思います。
App Specific Files
Access app-specific files 参照のこと。
MediaStore API
Access media files from shared storage 参照のこと。
Storage Access Framework
- 投稿日:2020-08-08T13:38:24+09:00
kotlinでJSONObjectを簡単作成
kotlinで、変数から
JSONObject
を作成したかったのですが、 まずはオブジェクト作って〜とかputして〜とか面倒だったのでパーサーを作りました。
自分用ですが、忘れないように残しておきます。パーサー作成
JsonObjectBuilder.ktinternal fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { return JsonObjectBuilder().json(build) } internal class JsonObjectBuilder { private val deque: Deque<JSONObject> = ArrayDeque() fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { deque.push(JSONObject()) this.build() return deque.pop() } @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") infix fun <T> String.to(value: T) { deque.peek().put(this, value ?: JSONObject.NULL) } }使用例
fun parseSample() { // こんな感じでJSONObjectを作成できます val jsonObject = json { "str" to "hoge" "int" to 123 "null" to null } // 文字列にしたい場合は、普通にtoString() val jsonString = jsonObject.toString(); // ネストしたJSONObjectはこんな感じ val nestedJsonObject = json { "hoge" to "hoge" "nestObj" to json { "fuga" to "fuga" } } // JSONArrayが作りたい場合はputしていってください val jsonArray = JSONArray() val list = listOf(1, 2, 3) list.forEach { val jsonObject = json { "num" to it } jsonArray.put(jsonObject) } }
- 投稿日:2020-08-08T13:05:21+09:00
HttpURLConnectionでPATCHリクエストを投げる
Androidアプリ作成時にAPI通信を行いたいとき、訳合ってOSSライブラリを使用せずに実装する必要がありました。
そのため、HttpURLConnection
を使って独自に通信部分を作成しました。しかし、PATCHリクエストを送りたいときにエラー発生。
なんと、HttpURLConnection
はPATCHリクエストを投げられないらしいです。
うそ…だろ…。
X-HTTP-Method-Override
ヘッダーを使って偽装する方法もありますが、サーバー側の依存もあり確実では無いようです。
https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patchOSSライブラリはどうやってるんだ?
ということで、 Jersey というHTTPクライアントライブラリの公開されているコードを参考にして、実装しました。
少し古いライブラリですが、動作確認できました。実装例
HttpClient
クラスを、下記のように実装します。
今回はBearer認証を想定しています。HttpClient.ktinternal class HttpClient { companion object { fun patch(url: String, accessToken: String? = null, body: String? = null): Pair<Int, String?> { var responseJsonString: String? = null val urlTmp = URL(url) val con = urlTmp.openConnection() as HttpURLConnection try { setRequestMethodViaJreBugWorkaround( con, "PATCH") con.instanceFollowRedirects = false con.doOutput = true con.connectTimeout = 60000 con.readTimeout = 60000 con.setRequestProperty("Content-Type", "application/json") con.setRequestProperty("Accept", "application/json") if(accessToken != null) con.setRequestProperty("Authorization", "Bearer $accessToken") if(body != null) { val os: OutputStream = con.outputStream val ps = PrintStream(os) ps.print(body) ps.close() } val reader: BufferedReader reader = if (con.responseCode == HttpURLConnection.HTTP_OK) { BufferedReader(InputStreamReader(con.inputStream, "UTF-8")) } else { BufferedReader(InputStreamReader(con.errorStream, "UTF-8")) } responseJsonString = reader.readLine() con.disconnect() } catch (e: Exception) { e.printStackTrace() Log.e("ApiClientException", e.toString()) } return con.responseCode to responseJsonString } private fun setRequestMethodViaJreBugWorkaround(httpURLConnection: HttpURLConnection, method: String) { try { httpURLConnection.requestMethod = method // Check whether we are running on a buggy JRE } catch (pe: ProtocolException) { try { AccessController.doPrivileged(PrivilegedExceptionAction<Any?> { try { httpURLConnection.requestMethod = method // Check whether we are running on a buggy // JRE } catch (pe: ProtocolException) { var connectionClass: Class<*>? = httpURLConnection.javaClass val delegateField: Field? try { delegateField = connectionClass!!.getDeclaredField("delegate") delegateField.isAccessible = true val delegateConnection = delegateField[httpURLConnection] as HttpURLConnection setRequestMethodViaJreBugWorkaround(delegateConnection, method) } catch (e: NoSuchFieldException) { // Ignore for now, keep going } catch (e: IllegalArgumentException) { throw RuntimeException(e) } catch (e: IllegalAccessException) { throw RuntimeException(e) } try { var methodField: Field while (connectionClass != null) { try { methodField = connectionClass.getDeclaredField("method") } catch (e: NoSuchFieldException) { connectionClass = connectionClass.superclass continue } methodField.isAccessible = true methodField[httpURLConnection] = method break } } catch (e: java.lang.Exception) { throw RuntimeException(e) } } null }) } catch (e: PrivilegedActionException) { val cause: Throwable? = e.cause if (cause is RuntimeException) { throw cause } else { throw RuntimeException(cause) } } } } } }使用例
上記
HttpClient
をimportして、下記のように使用します。fun patchRequestSample() { val (statusCode, responseJsonString) = HttpClient.patch("https://hoge/users/10", accessToken ="xxxx", body = sampleJson.toString()) }
statusCode
にステータスコード、responseJsonString
にレスポンスが返ってきます。調べてもヘッダーのオーバーライドの方法ばかり出てきたので、備忘として残しておきます。
- 投稿日:2020-08-08T12:42:00+09:00
Flutterで作成したアプリをAndroid用でリリースビルドする手順を解説してみる
本記事の内容
- Flutterで作成したアプリをAndroid用でリリースビルドする手順を解説
前提
- Flutterでアプリが作れていて、シミュレータで動作確認が済んでいること
- GoogleDeveloper登録が済んでいること(リリースするときは結局必須なので・・・)
目次
keystoreファイル(署名するときの設定ファイル)を作成
①Macのターミナルで以下のコマンドを叩く。
alias
に指定した文字はあとから使うので覚えておく(以下の場合、最後の「key」の部分)keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key②以下の入力を済ませる。
キーストアのパスワードを入力してください:`パスワードを入力する` 新規パスワードを再入力してください:`パスワードを入力する` 姓名は何ですか。 [Unknown]: namae myouji 組織単位名は何ですか。 [Unknown]: app team 組織名は何ですか。 [Unknown]: arg myouji 都市名または地域名は何ですか。 [Unknown]: kabukityo 都道府県名または州名は何ですか。 [Unknown]: tokyo この単位に該当する2文字の国コードは何ですか。 [Unknown]: JP CN=namae myouji, OU=app team, O=arg myouji, L=kabukityo, ST=tokyo, C=JPでよろしいですか。 [いいえ]: y 10,000日間有効な2,048ビットのRSAの鍵ペアと自己署名型証明書(SHA256withRSA)を生成しています ディレクトリ名: CN=namae myouji, OU=app team, O=arg myouji, L=kabukityo, ST=tokyo, C=JP <test>の鍵パスワードを入力してください (キーストアのパスワードと同じ場合はRETURNを押してください): `RETURN押せばいい` [key.jksを格納中]③作成されたkey.jksファイルを「android\app\」配下に格納する。
android/app/build.gradleを編集
以下の点に注意しながら、編集する
- defaultConfig部分(コメント見てほしい)
- buildTypesを signingConfig signingConfigs.release にする ← 重要!
- 「ここから追加」と書いてある部分を追加する
項目 書くべきこと storePassword ここで設定したStoreパスワード keyAlias ここで設定したエイリアス名 keyPassword ここで設定したキーパスワード(Storeパスワードと大抵同じ) android/app/build.gradledef localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 28 sourceSets { main.java.srcDirs += 'src/main/kotlin' } lintOptions { disable 'InvalidPackage' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "XXX.YYY.ZZZ" // App Bundle のパッケージ名(アプリごとに変更) minSdkVersion 16 targetSdkVersion 29 // 記事執筆時点で、29以上にしておかなくてはいけなかった versionCode 1 // アプリを更新するたびに数値を上げる必要がある versionName "1.0.0" // アプリのバージョン testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } // ここから追加 signingConfigs { release { storeFile file("key.jks") storePassword "password" keyAlias "alias-name" keyPassword "password" } } // ここまで追加 buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release // ←コレ大事! } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }AndroidManifest.xmlを編集
packageが指定されていることを確認する。(おそらく最初からなっている)
android\app\src\main\AndroidManifest.xml<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xxx.yyy.zzz">ターミナルでコマンドを叩く
以下のコマンドを
AndroidStudioのターミナルで叩く
(Macのターミナルでもいいけど、カレントディレクトリをちゃんとする)「build\app\outputs\apk\release\ 」に「app-release.apk」ができる
flutter build apk --releaseAndroid App Bundle でリリースしたい場合は以下のコマンド
「build\app\outputs\bundle\release\ 」に「app-release.aab」ができる
flutter build appbundle --release参考
- https://qiita.com/kasa_le/items/d23075d817f42e869778
- https://algorithm.joho.info/flutter/flutter-android-apply-google-play/
その他
不備がありましたら、コメント願います。
- 投稿日:2020-08-08T05:47:12+09:00
DialogFragmentの大きさを自由に変えたい
背景
ダイアログは基本小さめの画面なことが多いが、カスタムUIでフルスクリーンで表示したり画面比率何%で表示したり自在に操りたいなと思ったので実装してみた備忘録です
・androidX環境
・色々なサイズで使いたい
・xmlはConstraintLayoutで中身を書きたいstackoverflowはじめ古い記事を見ていると、ParentをRelativeLayoutやLineaLayoutにしようとか、Styleを定義しようと書かれていましたがこちらが簡単かな?と思ったので書くことにします
フルスクリーン
実装
FullScreenDialogFragment.ktpackage com.example.coroutines import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.DialogFragment import kotlinx.android.synthetic.main.view_full_screen_dialog.view.* class FullScreenDialogFragment : DialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.view_full_screen_dialog, container, false).apply { text.setOnClickListener { Toast.makeText(it.context, text.text, Toast.LENGTH_SHORT).show() } close.setOnClickListener { dismiss() } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) dialog?.setCancelable(true) } override fun onStart() { super.onStart() dialog?.window?.setBackgroundDrawable(null) dialog?.window?.setLayout( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) } companion object { fun newInstance() = FullScreenDialogFragment() } }view_full_screen_dialog.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" android:background="@android:color/white"> <TextView android:id="@+id/close" android:layout_width="24dp" android:layout_height="24dp" android:layout_marginTop="24dp" android:layout_marginEnd="24dp" android:drawableLeft="@drawable/ic_close_24" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="RtlHardcoded" /> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="TEXT" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>肝なのは
Activity
に関連するonStart
でsetBackGroundDrawable/setLayout
でwindow
に対してサイズを決めてしまうということなぜxmlでmatch_parentとか適当にXXXdpとか設定してもうまくいかないのは、あくまでDialogFragmentはDialogのwrapperにすぎないので、サイズ調整するのはcontainerではなくwindowで行う必要があるということですね。
DialogFragmentのスタイルを設定する
DialogFragment#setTyle
でもこのような記述があります。The same effect can be achieve by manually setting Dialog and Window attributes yourself. Calling this after the fragment's Dialog is created will have no effect.
DialogFragment.java/** * Call to customize the basic appearance and behavior of the * fragment's dialog. This can be used for some common dialog behaviors, * taking care of selecting flags, theme, and other options for you. The * same effect can be achieve by manually setting Dialog and Window * attributes yourself. Calling this after the fragment's Dialog is * created will have no effect. * * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or * {@link #STYLE_NO_INPUT}. * @param theme Optional custom theme. If 0, an appropriate theme (based * on the style) will be selected for you. */ public void setStyle(@DialogStyle int style, @StyleRes int theme) { mStyle = style; if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { mTheme = android.R.style.Theme_Panel; } if (theme != 0) { mTheme = theme; } }サイズ指定
上記も踏まえてとあるサイズを指定したい場合は以下のようになります
snippet.ktoverride fun onStart() { super.onStart() dialog?.window?.setBackgroundDrawable(null) val width = (resources.displayMetrics.widthPixels * 0.95).toInt() val height = (resources.displayMetrics.heightPixels * 0.95).toInt() dialog?.window?.setLayout(width, height) }
setLayout
にはwidth
,height
で指定できるので各々好きなサイズで実装ができますまとめ
・onStartにサイズの記載をしよう
・xmlでParentViewのサイズをコントロールしてもうまくはいかない間違い等ありましたら遠慮なく指摘していただけると幸いです
- 投稿日:2020-08-08T03:09:54+09:00
Andorid Native + Flutterのビルドに便利なDocker Imageを作る
Androidアプリのビルドによう使うツールをインストールしたDocker Imageの作り方を説明する(Android SDK, Flutter SDK, GCloud, Bundler, Firebase tools)。
Dockerを使ったことはあるが、実用的なDocker Imageを作ったことはない人向けの説明。Docker Imageを作る手順
1. Dockerfileを記述する
# BaseはCircle CIのものにする。android sdk, gcloudが既に入っているので楽。 FROM circleci/android:api-29-node # Install firebase tools RUN sudo curl -sL https://firebase.tools | bash # Install tools for easylauncher RUN sudo apt-get update && sudo apt-get install -y fontconfig ttf-dejavu # Install Flutter SDK # ほぼ以下のファイルのコピペ。flutter_verisonの初期化とprecacheの部分は独自。 # https://github.com/cirruslabs/docker-images-flutter/blob/master/sdk/Dockerfile ARG flutter_version=1.20.1 ENV FLUTTER_HOME=${HOME}/sdks/flutter \ FLUTTER_VERSION=$flutter_version ENV FLUTTER_ROOT=$FLUTTER_HOME ENV PATH ${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin RUN git clone --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} RUN flutter precache RUN yes | flutter doctor --android-licenses && flutter doctor2. Docker Imageをビルドする
上記のDockerfileと同じディレクトリで以下のコマンドを実行する。
docker build -t kamikazezirou/android:1.0 .「kamikazezirou/android」の部分はImage名。
「:1.0」は、ImageのTag。
適宜変更すること。3. Docker ImageをDocker Hubにpushする
docker push kamikazezirou/android:1.0※docker loginでDocker Hubにログインしておくこと。アカウントも必要。
4. CIから使う(Cirlce CIでの例)
jobs: build: working_directory: ~/code docker: - image: kamikazezirou/android:1.0 ...背景
仕事で開発しているアプリで、Android Native onlyのアプリにFlutterモジュールを組み込むことになった。
仕事のAndroidアプリ開発ではCirlce CIを使っているが、その設定をしていて、ビルドツール全部入りのDocker Imageが必要になった。CIのワークフローはは、デバッグapkのビルド -> Firebase Test Lab実行 -> リリースビルドというような流れにしている。最初は、ビルドのDocker Imageには「cirrusci/flutter:1.17.5」を使い、Firebase Test Labの実行のDocker Imageには「circleci/android:api-28-node」を使う、といった形にしようとした(前者はFlutter SDK有り かつ GCloudなし。後者はFlutter SDKなし かつ GCloudあり)。
この方式だと2つ問題があった。
- 依存ライブラリのキャッシュの復元時にエラーが発生した(2つのDocker Imageのhomeディレクトリが違うことが原因)
- この2つのDocker Imageに含まれないビルドツールがあり、それらを毎回インストールする必要があった(→ビルド時間が遅くなる、ビルドスクリプトが雑然とする)
この2つの問題を解決するために、ビルドツール全部入りのDocker Imageを作ろうと思い立った。
1つのDocker Imageであればhomeディレクトリは常に同じであり、ビルドツールのインストール処理も不要。参考:作ったDocker Image
https://hub.docker.com/repository/docker/kamikazezirou/android
- 投稿日:2020-08-08T00:57:28+09:00
[Android]エミュレータで Screen Pinning (画面の固定) を解除する方法
問題
Screen Pinning(画面の固定)の解除は 戻るボタンとタスクボタンを長押しすることで解除できるが、エミュレータではマルチタップが上手くできなくて解除できない。ちなみに Screen Pinning (画面の固定)とは次のように戻るボタンやタスクボタンなどを効かなくして他のアプリを起動できなくするモードのことです。
画面固定設定を変更する アプリを画面固定する ボタンが効かなくなる 解決方法
ADB コマンドで解除する
次の ADB コマンドを実行すれば Screen Pinning(画面の固定)は解除できる。
adb shell am task lock stopCommand + 右クリック + 左クリックで解除する
エミュレータ種類によってはできないこともあったのですが Macだと Command + 右クリック + 左クリックで解除できます。(Linuxだと Ctrl Left + 右クリック + 左クリックで解除できます。)