eaglesakuraの技術ブログ

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

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 ライブラリが同じ機能をサポートしたらコイツは役目を終える
  • 公式、ガンバ・・・

秘伝のタレを受け取ったときにまずやること

秘伝のタレとは

  • 年単位で職人によって継ぎ足され、熟成されたソースである
  • 衛生環境が良い秘伝のタレは素晴らしい味だが、常に良い衛生環境の中で育つわけではない

深呼吸する

  • 何事も、一歩ずつ解決しなければならない

コードフォーマッタを適用する

  • 最初にやる
  • フォーマッタが適用されてないことは多い
  • これを最初にやらないと、Intellij等の自動フォーマッタが意図しないタイミングで走ったときにマージ地獄に陥る
    • 手癖でFormatコマンドを実行する人は要注意だ
    • 普通はそんな注意しなくていいんだけど

sdkVersionを更新する

  • 様々な事情により(大抵はAndroid OSの破壊的変更の影響を回避するため)、compileSdkVersionが古い場合がある
  • しかしGoogle Playは無慈悲で、新しいバージョンでcompileしないとアップロードすら行えない場合がある
  • 更新してあげよう

dependenciesを更新する

JavaをKotlinに変換する

  • 我慢しよう
  • 一気にやるとどこかでミスる上に差分管理が断絶するので、ここは慎重に行おう
  • 開発範囲内で、ちょっとずつやれるところからやっていく
  • 戒めである

CIを組む

  • やれたらやったほうが良い
  • ソースコードは引き継いてでも、CIは引き継げないことも多い(例えば元がオンプレJenkinsとか、無理でしょ)

覚悟を決めて、 abortOnError false を外す

  • abortOnError false のコードは、すべてレガシーコードである
  • lint解決に時間をかけなかった、かける価値を感じなかった、やり方を知らなかった、そういった積み重ねの産物である
  • これの有無を見るだけで、秘伝のタレの深みがわかるであろう
  • 覚悟です。 覚悟ですよ、ナナチ

Android Studio 3.6.1で稀にReleaseビルドが失敗するのを回避する

失敗時のログ

Execution failed for task ':app:minifyReleaseWithR8'.
> Multiple entries with same key: Method $$ServiceLoaderMethods.$load0 Proto L java.util.Iterator =Encoded method Method $$ServiceLoaderMethods.$load0 Proto L java.util.Iterator  and Method $$ServiceLoaderMethods.$load0 Proto L java.util.Iterator =Encoded method Method $$ServiceLoaderMethods.$load0 Proto L java.util.Iterator 

おそらくの理由

何度か実行してみた結果、 --parallel オプションを付けて minifyを有効化する と4〜5回に1回位の割合でビルドに失敗する。 --parallel オプションを消せば再現しなくんった(多分)

CIのリリースビルドの時しか影響がないので、素直に --parallel を外してビルドする。

Issue Tracker

https://issuetracker.google.com/issues/148929520

ワークアラウンドが載っているが、R8バージョンをデフォルトから変更するので選択は慎重に。