20200808のAndroidに関する記事は14件です。

実践・新規モバイルアプリ開発の手順(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まで出来たら半分以上終わったイメージです。
そこから異常系とか、特殊ケースの対応をしたり、ページング処理や細かな調整をしていきます。

おわりに

かなりざっくりしていますし、タスクはこれ以外にもたくさんありますが。
骨子はこんな感じで作るとスムーズかと思います。

何か改善点があればご指摘ください。

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

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

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に移行するアプリはシンプルな音声の録音再生アプリです。
画面回転や画面分割に対応しています。

クラスの紹介

今回解説する範囲における、クラスの依存関係はこのようになっています。

スクリーンショット 2020-08-08 18.12.39.png

MainFragment

Fragmentです。画面回転や画面分割で再生成されます。

RecordViewHelper

録音のために録音デバイス制御などを行います。ViewModelを継承しているので画面回転や画面分割が起きても同じインスタンスが保持されます。

PlayViewHelper

再生のために音声デバイス制御などを行います。RecordViewHelperと同様にViewModelを継承していて、画面回転や画面分割が起きても同じインスタンスが保持されます。

SoundRepository

録音音声の保持と取り出しを担当します。シングルインスタンスです。音声がメモリー、ローカルファイル、クラウド等どこにあるか呼び出し元は関与しません。

SoundMemoryLocalDataStore

録音音声をメモリーに保持する担当です。シングルインスタンスです。

SoundFileLocalDataStore

録音音声をAACにエンコードしてローカルファイルファイルに保持します。シングルインスタンスです。プロセスキルからの復帰ではこちらが使われます。

モジュールの紹介

レイヤードアーキテクチャへの強制力を持たせるために、レイヤー別マルチモジュールプロジェクトになっています。下図のように役割に応じてクラスをモジュールに配置しています。

QiitaのHiltの記事.png

移行の手順

ライブラリ追加

ほぼ公式情報の手順です。

プロジェクトルートの build.gradle ファイルに hilt-android-gradle-plugin プラグインを追加します。

build.gradle
buildscript {
    // 略
    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.properties
distributionBase=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.gradle
apply 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.gradle
apply from: rootProject.file('gradle/common.gradle')

ViewModelも注入したいため、公式のこちらの文献に沿って、さらにライブラリを追加します。

gradle/view-common.gradle
apply 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.gradle
apply from: rootProject.file('gradle/view-common.gradle')

モジュール依存関係変更

Hiltはアプリケーションモジュールから直接参照しているモジュールにあるクラスしか注入できないので、このようにモジュールの依存関係を変更しました。

スクリーンショット 2020-08-08 20.17.35.png

app/build.gradle
dependencies {
    // 略
    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.kt
class SoundMemoryLocalDataStoreImpl
@Inject constructor(@ApplicationContext private val context: Context) : SoundMemoryLocalDataStore {
    // 略
}
SoundFileLocalDataStore.kt
class SoundFileLocalDataStoreImpl
@Inject constructor(@ApplicationContext private val context: Context) : SoundFileLocalDataStore {
    // 略
}
SoundRepository.kt
class SoundRepositoryImpl
@Inject constructor(private val soundMemoryLocalDataStore: SoundMemoryLocalDataStore,
private val soundFileLocalDataStore: SoundFileLocalDataStore) : SoundRepository {
}

ViewModelを注入する

ViewModelのコンストラクタに @ViewModelInject アノテーションを付けます。

RecordViewHelper.kt
class 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.kt
val localDataStoreModule = module {
    single { SoundMemoryLocalDataStoreImpl(androidContext()) as SoundMemoryLocalDataStore }
    single { SoundFileLocalDataStoreImpl(androidContext()) as SoundFileLocalDataStore }
}

その上のレイヤーのモジュールでは、さらに上のレイヤーのモジュールから下のレイヤーのモジュールを参照できるようにします。

RepositoryModule.kt
val localDataStoreModuleInRepositoryModule = localDataStoreModule
val repositoryModule = module {
    factory { SoundRepositoryImpl(get(), get()) as SoundRepository }
}

その上のレイヤーでも同様にします。

MainModule.kt
val localDataStoreModuleInMainModule = localDataStoreModuleInRepositoryModule
val repositoryModuleInMainModule = repositoryModule
val mainModule = module {
    viewModel { RecordViewHelper(get()) }
    viewModel { PlayViewHelper(get()) }
}

アプリケーションモジュールにある、startKoinブロックではすべてのモジュールを指定します。

KoinSetting.kt
object KoinSetting {
    fun start(application: Application) {
        startKoin {
            androidContext(application.applicationContext)
            modules(listOf(localDataStoreModuleInMainModule,
                    repositoryModuleInMainModule,
                    mainModule))
        }
    }
}

全体ソースコード

プルリクはこちらです。
この記事では @Bindsを使用したインターフェースからのインスタンス注入しか説明してないですが、 @Provides を使用したインスタンス注入もこちらにあります。

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

【Flutter】Pigeon を使ってネイティブコードをスマートに呼ぶ

はじめに

先日発表された、 Flutter 1.20 のリリース記事 で、 Pigeon というパッケージが紹介されていました。
このパッケージを使うことで、

  • ネイティブ側と型安全に通信
  • 自動生成よって手書きコード量を削減

することができます。
今回は、このパッケージを使って、swift / Kotlin で実装した単純な add メソッドを Flutter 側 から呼ぶ方法を見ていきます。

サンプルプロジェクトはこちらで公開しています。

Pigeon が行うこと

Dart ファイルで定義した、引数や戻り値の情報を元に、Java / Objective-C のインターフェースやプロトコルを自動生成します。
ネイティブ側ではこれらを元にした実装を行うことで、Flutter 側と型安全に通信することができるようになります。

インストール

pubspec.yaml
dev_dependencies:
  pigeon: ^0.1.4

Dart 側

まずは、ネイティブと通信するスキーマを定義した dart ファイルを作ります。
自分の場合はプロジェクトルートにpigeon/というフォルダを作ってその中に置きました。

schema.dart
import '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.dart
import './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.kt
class 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.gradle
android {
  ...
  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 のリリース記事 で紹介されていたりするので、今後広まっていくかも。

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

【Flutter】Pigeon を使ってネイティブコードを型安全に呼ぶ

はじめに

先日の、 Flutter 1.20 のリリース記事 で、 Pigeon というパッケージの紹介がありました。
本来 Flutter 側からネイティブコードを呼ぶには、関数名や引数などを文字列ベースで合わせる必要があるなど、少し大変ですが、このパッケージを使うことで、

  • ネイティブ側と型安全に通信
  • 自動生成よる手書きコード量の削減

が可能になります。

本投稿では、このパッケージを使って、swift / Kotlin で実装した単純な add メソッドを Flutter 側 から呼ぶ方法を見ていきます。サンプルプロジェクトはこちらで公開しています。

Pigeon が行うこと

Dart 側で定義した、引数や戻り値の情報を元に、Java / Objective-C のインターフェースやプロトコルを自動生成します。
ネイティブ側ではこれらを元に実装を行うことで、Flutter 側と型安全に通信することができるようになります。
イメージとしては、 TypeScript で型定義ファイルを作るのに近いかもしれません。

インストール

pubspec.yaml
dev_dependencies:
  pigeon: ^0.1.4

Dart 側

まずは、ネイティブと通信するスキーマを定義した dart ファイルを作ります。
自分の場合はプロジェクトルートにpigeon/というフォルダを作ってその中に置きました。

schema.dart
import '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.dart
import './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.kt
class 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.gradle
android {
  ...
  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 のリリース記事 で紹介されていたりするので、今後広まっていくかも。

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

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

make

straceフォルダ直下にstraceという実行ファイルが出来上がります♪

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

【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.kt
class 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から確認できます。(詳しくはこちら)

参考

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

Android 端末内にデータを保存する際の指針

概要

Android 端末内にデータを保存する際の指針をまとめました。

包括的情報

Data and file storage overview 参照のこと。

ストレージ選択の指針

Android Storage Selection Chart.png

アプリ内のみで利用されアンインストール時に削除されるかどうか?

  • 他のアプリとデータを共有する場合は no, それ以外は yes1

内部ストレージが好ましいかどうか?

  • 存在の保証2が必要なデータは内部ストレージにすることが必須。
  • アプリの基本的な機能3から必要とされるデータは内部ストレージが好ましい。
  • 大量のデータを保存する場合は外部ストレージが好ましい。

メディアファイルかどうか?

  • 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

Access documents and other files from shared storage 参照のこと。


  1. アプリ内のみで利用されるがアンインストール時に削除しないという選択肢は、アンインストール後にゴミを残してしまうため、選択肢としてあり得ない。 

  2. 外部ストレージは取り外される可能性を常に考慮する必要がある。 

  3. アプリの起動など。 

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

kotlinでJSONObjectを簡単作成

kotlinで、変数から JSONObject を作成したかったのですが、 まずはオブジェクト作って〜とかputして〜とか面倒だったのでパーサーを作りました。
自分用ですが、忘れないように残しておきます。

パーサー作成

JsonObjectBuilder.kt
internal 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)
    }
}

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

HttpURLConnectionでPATCHリクエストを投げる

Androidアプリ作成時にAPI通信を行いたいとき、訳合ってOSSライブラリを使用せずに実装する必要がありました。
そのため、 HttpURLConnection を使って独自に通信部分を作成しました。

しかし、PATCHリクエストを送りたいときにエラー発生。
なんと、 HttpURLConnection はPATCHリクエストを投げられないらしいです。
うそ…だろ…。

X-HTTP-Method-Override ヘッダーを使って偽装する方法もありますが、サーバー側の依存もあり確実では無いようです。
https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch

OSSライブラリはどうやってるんだ?
ということで、 Jersey というHTTPクライアントライブラリの公開されているコードを参考にして、実装しました。
少し古いライブラリですが、動作確認できました。

実装例

HttpClient クラスを、下記のように実装します。
今回はBearer認証を想定しています。

HttpClient.kt
internal 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 にレスポンスが返ってきます。

調べてもヘッダーのオーバーライドの方法ばかり出てきたので、備忘として残しておきます。

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

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.gradle
def 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 --release 

Android App Bundle でリリースしたい場合は以下のコマンド

「build\app\outputs\bundle\release\ 」に「app-release.aab」ができる

flutter build appbundle --release 

参考

その他

不備がありましたら、コメント願います。

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

DialogFragmentの大きさを自由に変えたい

背景

ダイアログは基本小さめの画面なことが多いが、カスタムUIでフルスクリーンで表示したり画面比率何%で表示したり自在に操りたいなと思ったので実装してみた備忘録です

・androidX環境
・色々なサイズで使いたい
・xmlはConstraintLayoutで中身を書きたい

stackoverflowはじめ古い記事を見ていると、ParentをRelativeLayoutやLineaLayoutにしようとか、Styleを定義しようと書かれていましたがこちらが簡単かな?と思ったので書くことにします

フルスクリーン

実装

FullScreenDialogFragment.kt
package 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に関連するonStartsetBackGroundDrawable/setLayoutwindowに対してサイズを決めてしまうということ

なぜ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.kt
override 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のサイズをコントロールしてもうまくはいかない

間違い等ありましたら遠慮なく指摘していただけると幸いです

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

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 doctor

2. 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つ問題があった。

  1. 依存ライブラリのキャッシュの復元時にエラーが発生した(2つのDocker Imageのhomeディレクトリが違うことが原因)
  2. この2つのDocker Imageに含まれないビルドツールがあり、それらを毎回インストールする必要があった(→ビルド時間が遅くなる、ビルドスクリプトが雑然とする)

この2つの問題を解決するために、ビルドツール全部入りのDocker Imageを作ろうと思い立った。
1つのDocker Imageであればhomeディレクトリは常に同じであり、ビルドツールのインストール処理も不要。

参考:作ったDocker Image

https://hub.docker.com/repository/docker/kamikazezirou/android

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

[Android]エミュレータで Screen Pinning (画面の固定) を解除する方法

問題

Screen Pinning(画面の固定)の解除は 戻るボタンとタスクボタンを長押しすることで解除できるが、エミュレータではマルチタップが上手くできなくて解除できない。ちなみに Screen Pinning (画面の固定)とは次のように戻るボタンやタスクボタンなどを効かなくして他のアプリを起動できなくするモードのことです。

画面固定設定を変更する アプリを画面固定する ボタンが効かなくなる
Image from Gyazo Image from Gyazo Image from Gyazo

解決方法

ADB コマンドで解除する

次の ADB コマンドを実行すれば Screen Pinning(画面の固定)は解除できる。

adb shell am task lock stop

Image from Gyazo

Command + 右クリック + 左クリックで解除する

エミュレータ種類によってはできないこともあったのですが Macだと Command + 右クリック + 左クリックで解除できます。(Linuxだと Ctrl Left + 右クリック + 左クリックで解除できます。)

Image from Gyazo

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