eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

AndroidアプリプロジェクトのJDK11対応

AndroidJDK

  • Android Studioに付属しているJDKはJava8相当
  • なので、公式にはJava8でビルドするのが良いと思われる

なぜJava11対応が必要になったのか

  • CircleCIが用意しているAndroidビルド用Docker Imageが突然Java11になった
  • ああもうしゃーないなぁとか思いながらも、変更することにした
    • RobolectricがAPI29以降Java9以上必須となっていたので、まあいい機会だと

JDKインストール

  • 好きな方法でインストールする
  • 以下、Windows環境
  • sdkman でinstallし、currentを切り替えた
  • JAVA_HOMEは変更なし

Android Studioの設定変更

  • Project Structureから使用するJDKを切り替える
  • デフォルトはAndroid Studio付属になっている
  • build.gradleの構成?によってはProject Structureが開かない不具合があるので、その場合はDefault Project Structureで切り替え、.ideaディレクトリを削除して再度プロジェクトファイルを生成させれば良い

_JAVA_OPTIONSの設定

  • 初めて知ったが、 JAVA_OPTIONS_JAVA_OPTIONS は明示的に異なり、通常ユーザー向けのRuntimeは前者を、開発者向けのJDKは後者をロードする
  • _JAVA_OPTIONSに -Djavax.net.ssl.trustStoreType=JKS を追加する

robolectricの設定

  • sdk=28 とかで固定を入れている場合、sdk=29 に変更して実行環境を調整

CircleCI設定

  • circleci/android:api-30 とか切り替えちゃっていいんじゃないかな

dokka設定

  • ドキュメントをdokka出力しているなら、バージョンを変更する必要がある
  • JDKのClass名が変わったりとかしたので
// /build.gradle
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.4.0-rc"
dokkaHtml {
    outputFormat = 'javadoc'
    outputDirectory = "$buildDir/dokka"
    noStdlibLink = true
    dokkaSourceSets {
        noAndroidSdkLink = true
        configureEach {
            noStdlibLink = true
            noJdkLink = true
            noAndroidSdkLink = true
        }
    }
}
}

メイン開発PCのパーツ交換をした

メイン開発マシンのスペックアップのため、一部のパーツを入れ替えたので今後のためにメモ。

メイン開発マシンのスペック(2017~)

基本スペック

表OS / Windows 10 / ストレージ512GB割当

  • Android(Kotlin) / Flutter / Go Serverあたりの開発
  • Adobe製品
  • Steamとかのゲーム(リモートプレイの母艦)

裏OS / Ubuntu 20.04 / ストレージ512GB割当

  • Android(Kotlin) / Flutter / Go Serverあたりの開発に使用
  • 特にAndroid Studioで顕著だが、Windowsでの動作が緩慢なため切り替える場合が多い

不満だった点

アップグレード計画

  • シングルスレッド性能の向上と、スペックダウンしないという要件を満たすため、Ryzen 3950X以上のスペックが必要
  • 第3世代Ryzen TRは非常に高額になっている

乗り換え先選定

  • 2017年購入時と同じく、サイコム の水冷BTOが第一候補
  • 予算的にはまあギリいけるか、的な範囲
  • 筐体がかっこ悪くなっている
    • 追加購入して、デスクトップ2台分の接地面積を取ることも難しい
    • 中古で売るにしても面倒だけが多い
  • 筐体を変えない(Fractal DesignDefine R5)ため、パーツ交換を行うことにする

交換内容

  • Ryzen 3950Xを導入することにした
    • TRを使うと、「長く使わないともったいない」ってなってしまう
    • Ryzen進化速度早いので、まあ2~3年もしたらRyzen9系に追い抜かれるだろう
    • シングルスレッド性能も3950Xと3960Xはほとんど変わらない
  • ソケットが異なるため、自動的にマザーボードも交換することになる
  • 元々の水冷クーラーがAM4非対応(オプション別売り、終売)のため、水冷クーラーも交換することになる
  • 最大メモリスロットが8スロット -> 4スロットに半減するため、メモリも交換することになる
  • PCIe 4が使えることと、Windowsの動作速度向上のためストレージも買い換えることになる
  • 後々グラボを載せ替えるのも面倒なので、グラフィックボードも交換しておく
    • Lightroomのプレビューやスクロールの遅さも気になってたしね
    • せっかくならレイトレーシング対応がいいね
    • せっかくならゲームももう少し品質向上できるやつがいいね
  • という理由でドミノ倒しのように殆どの機材が入れ替えとなる

嘆き

  • タイムセールで買っておきゃよかったぁ!!!!

最終的な構成変更

  • Ryzen 1950X -> Ryzen 3950X
  • 水冷クーラー Corsairの昔の -> Corsair h100i Pro RGB
  • メモリ 2400MHz 8GB×8 -> 3600MHz 16×4
  • マザーボード X399 Taichi -> X570 Taichi
  • ストレージ NVMe 512GB×2(PCIe 3.0) -> NVMe 512×2(PCIe 4.0)
  • グラボ GTX 1060 -> RTX 2080 Super

交換後の感触

  • ゲームのフレームレート向上した!!
    • BIOHAZARD RE2のオプションもりもりがだいぶできるようになってきた
    • ライザのアトリエ 4Kで起動できるようになった
    • オプションもりもりでもFF15がそこそこ動くようになった
  • Windows10 2004がインストールできるようになった
    • Ryzen 1950X+X399Taichiにはインストール不可能だった
    • WSL2サポートされた
    • 超快適
  • Cinebenchスコアが6000 -> 9000に向上
  • Androidプロジェクトはまだ触ってない。快適だと嬉しい。

Mac版IntellijでGradle Syncが出来なくなった場合の対処

発生した問題

  • 最近購入したMac Book Pro 13inch / 2020 / RAM32GBでIntellijをインストール
  • gradle syncが行えない
    • 必ず失敗し、ログも表示されない
  • IntelliJ自体のログを見ると、 Java home is different と言われてDaemon接続に失敗しているらしい

解決

  • Androidアプリビルドのために、 Java Zulu 1.8.0_252 をsdkmanを通して利用している
  • 最近のバージョンから、JAVA_HOMEが変更になった
    • 以前はインストール直下がJAVA_HOMEとして機能した
    • 新しいバージョンでは、インストール直下に zulu-8.jdk というディレクトリが使われて、そっちがJAVA_HOMEとして機能している
  • 間違ったJAVA_HOMEを指定していたので、正常にgradle syncできなかった
    • zulu-8.jdkIntellijに読ませることで解決

まじかよ

  • 何じゃこりゃ
  • 半日潰してしまった

Intellij Idea 日本語EAP版が配信されているので試した結果、アンインストールした

日本語化の利点

  • 日本語話者なので、自然に使えるでしょう
  • 初学者に勧めやすい
    • これは重要。超重要
    • 日本語Visual Studioから、英語Eclipseに移った当時(10年以上前)、必死に日本語パックの初期設定とか探した

EAP版の問題点

  • フィードバック済み
  • Run... とか、たまに使う(けどショートカットを覚えるほど使ってない)アクションの検索(Ctrl+Shift+Aに割り当ててる)も日本語化されてしまう
    • Run... ではヒットせず、 実行... と検索しなければならない
    • 翻訳されたキーワードを知らなければ使えないし、日本語・英語を検索時に切り替えるのはちょっと面倒
  • 日本語化が完全でないため、英語メニューと日本語メニューが混在
    • Build はビルドではなくBuildである
    • が、メニューには ビルド もあるので、どっちのBuildかビルドか選ぼう
  • せめて英語メニューと日本語メニューを同時に検索してほしい

結論

  • まだIntellijを触ったことがない人で、英語だとちょっと。。。みたいな感触であれば入れてあげるがよし

ChildFragmentのViewが一部条件で表示されない場合のワークアラウンド

問題点

  • MotionLayoutの中にChildFragmentでConstraintLayoutの中にFragmentの中にカメラプレビューを置いた
    • ユーザーの操作にあわせてアニメーションしたり、Viewを入替えたりする
  • サムスンHuaweiの端末(もしくはAndroid 8以上?)でChildFragment.replace()すると、カメラが表示されない 場合がある

原因

  • onCreateView後、なぜかViewのmeasure, width, heightが全部0になっている
    • Viewがレイアウトされず放置されるらしい
    • Activity.onPause/onResumeを経由(Fragment開いているときに他のActivityからonActivityResultで結果を受け取ったり)すると100%再現
  • どこが原因か、正直不明だった

ワークアラウンド

  • 自動でViewの配置やViewレイアウトの構築をトリガーしてくれないなら、手動トリガーすればいいじゃない
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 親FragmentのViewを経由して再度レイアウトさせる. this.viewをやっても無駄
        requireParentFragment().requireView().requestLayout()
    }

どこが原因だ?

  • MotionLayoutか?
  • ChildFragment 2段階入れ子だからか?
  • ConstraintLayout in MotionLayoutだからか?
  • わからないが、コレで逃げられることはわかった

仕事用モバイルノートの要件 2020

新型PCが発売される季節なので、選定の際の要件を定義しておく。 状況を整理するのは良いことだ。

利用シーン

  • 会社が運営している施設で作業するのに使う
    • 往復で徒歩40〜50分くらいになる
  • 基本的に自宅作業なので、気分転換にカフェとかで仕事する
    • 平時であれば子供を保育園に送ったあと、眠気覚ましも兼ねてカフェで仕事する
    • 30分くらい歩く
  • 子供の習い事の待ち時間に、カフェとかで仕事する
  • 最近は子供の自宅保育になってしまったので、近所の嫁さん実家で仕事するときに使う
  • 映画を見るとき、時間調整でカフェで仕事するときに使う
    • 往復で40分くらい歩く

現状のマシンの不満点

  • 2019年に購入した Thinkpad X1 Carbon 2018
  • メモリが足りない
    • 16GBじゃAndroid Studioだけでカツカツになる
    • moduleを細分化して影響範囲を最小化するアーキテクチャを採用してるので、とにかくメモリを喰う
  • HDMIで4K/60Hz出力できない
    • たまに作業のために4Kモニタに接続するとカクカクして気持ち悪い

必須機能

  • ストレージ512GB / OS
    • Macなら最低512GB、Windows/Ubuntu機なら合計1TBが最低ライン
    • それ以下だとすぐにストレージが枯渇して死ぬ
    • 写真とかは母艦マシンで扱うから、基本的に仕事のみ
  • RAM 32GB
    • できれば64GB
  • Thunderbolt3端子
    • ほしい
    • HDMIで4K/60Hz出力できるとなお良い
  • Type-C PD充電対応
    • 必須
    • 専用の充電スポットを設けるとか、2020年には考えられない
    • Mac Book Pro 15inch 2013を使わなくなった理由が、充電スポット問題
  • GPU
    • 基本的にiGPUで十分
    • 4K出力だけ必須
  • Wi-Fi6
    • できればほしい
  • 重さ
    • スペック次第で、1.4〜.5kgくらいまでは許容
    • 2kgは腰に辛い

いつ買い換えるか

  • すぐにでもほしいが
    • 現状不満があるので、決まればすぐにでも買いたい
  • すぐじゃなくてもいい
    • できるだけ家で仕事するように調整は、まあできる
    • 昨今の社会情勢のため、カフェでの作業は最近してない

候補

  • XPS 13 32GBモデル
  • Mac Book Pro 13inch 2020?
    • 期待してる
  • Mac Book Pro 16inch
    • 腰に悪そう
    • 同様の重さのMBP15inchを持ってみたが、重い
    • Flutter用に購入したMac miniが無駄になりそう
      • mini買う前に発売してくれればなぁ・・・
      • メルカリしてしまうか・・・
    • Flutter作業するとき、クラムシェルだと爆熱ゴッドフィンガーしそう
      • MBP15-2013は爆熱ゴッドフィンガーでずっとふおーんしてた
    • スペック的に3〜4年は戦えそう
      • 3〜4年後の腰に悪影響ありそう
  • ASUS ROG Zephyrus G14
    • Ryzenすき
    • 発売日未定
    • おま国になるかも未定
  • ThinkPad E495
    • くっそ安い
    • 1.7kg
    • RAM 2スロットあるので自前で交換して64GBまで拡張できる

Android定型コードをスッキリさせるための Wofkflow Dispatcher ライブラリ 1.0.0を公開

Wofkflow Dispatcherとは

GitHub - eaglesakura/workflow-dispatcher

  • Androidアプリ開発でよくある 画面やActivityやプロセスを跨ぐ可能性のある非同期処理 をなるべく簡単に扱うためのライブラリ
  • Annotation Processorを使って、定形処理を出力する
    • startActivityForResult/onActivityResultの分岐を書く必要がなくなる
    • Runtime Permissionの処理を書く必要が無くなる
    • DialogFragmentのコールバックに気を使う必要が無くなる
    • 一時的な情報をメンバ変数ではなく引数として扱うことができる
      • 個人的にコレが重要
    • Activity再生成(回転とかLowMemoryとか)しても大丈夫
  • 初めてAnnotation Processorを実装した
    • 以前は否定的だったけど、十分にマシンスペックが上がったので心の中で解禁

似たライブラリ

使うとどう書けるのか

  • 普通に書けるライブラリはいろいろあるが、例えば onActivityResult に戻ってきたときに一時変数の値を使いたい場合は結構気を使う
  • このサンプルでは、ブラウザを開いた時刻を保存しておき、onActivityResultでToast表示に使っている
  • AndroidではActivityやFragmentが再生成される(新しいインスタンスが作られる)場合があるため、save/resotreには気を使う
    • 別Activityで開く -> Runtime Permissionで権限を得る -> ダイアログでYES/NOを質問するとかを順番にやると一時変数が結構増えていく
  • ライブラリが自動的に一時情報を保存・レストアして、コールバック時に渡してくれるのでActivityとかFragmentをクリーンに保てる

Before

class ExampleBeforeActivity : AppCompatActivity() {

    private var tempDialogStartDate: Date? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if(savedInstanceState == null) {
            tempDialogStartDate = startDate
            startActivityForResult(
                Intent(
                    Intent.ACTION_VIEW,
                    Uri.parse("https://google.com")
                ), REQUEST_SHOW_BROWSER
            )
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putSerializable("startDate", tempDialogStartDate)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        tempDialogStartDate = savedInstanceState.getSerializable("startDate") as? Date
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            REQUEST_SHOW_BROWSER -> {
                // read temporary data, clear temporary
                val startDate = tempDialogStartDate!!
                tempDialogStartDate = null

                // show toast
                Toast.makeText(this, "done workflow, startDate='$startDate'", Toast.LENGTH_SHORT)
                    .show()
            }
            else -> super.onActivityResult(requestCode, resultCode, data)
        }
    }

    companion object {
        const val REQUEST_SHOW_BROWSER = 0x0011
    }
}

After

class ExampleAfterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if(savedInstanceState == null) {
            // show browser
            // ExampleAfterActivity.showWebsite() is generated by kapt(workflow-dispatcher-processor).
            // auto save/restore/clear `startDate` state.
            showWebsite(
                Intent(Intent.ACTION_VIEW,Uri.parse("https://google.com")),
                startDate = Date()
            )
        }
    }

    @OnActivityResultFlow("showWebsite")
    fun onShowWebsiteResult(result: ActivityResult, startDate: Date) {
        // show toast
        Toast.makeText(this, "startDate='$startDate'", Toast.LENGTH_SHORT).show()
    }
}

どうやって実現しているのか

  • startActivityとかするタイミングで、ライブラリが WorkflowProviderFragment をchildFragmentManagerに差し込む
  • WorkflowProviderFragmentの中でstartActivityとかpermissionとか処理をして、結果をコールバックしている
  • 一時データの保存もWorkflowProviderFragmentの中に持っているViewModelに持たせているので、呼び出し側を汚染しない

保存できるステート

  • 生成されるコードを見るとわかるが、内部的にはBundleに突っ込んでいるので、Bundleに保存可能な値であれば @OnActivityResultFlow の引数として受け取ることができる
  • ステートが不要な場合は result: ActivityResult とか第1引数だけでメソッドを宣言すれば良い

使い続けられるのか?

  • Jetpack Navigation ライブラリが同じ機能をサポートしたらコイツは役目を終える
  • 公式、ガンバ・・・