ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

macの時刻がずれて治らない時にtimedプロセスkillしたら直った

こんにちは、tkyです。

伝えたいことはタイトルにすべて込めました!

なぜか突然Macの時刻がめちゃめちゃずれていて、試行錯誤の末直ったのでメモしておきます。

それはある日のこと

現在時刻2021年9月18日13時頃・・・

「あれ?なんで16時になってるの?てか8/31になってる!」

突然タイムリープしてしまったかと思いました。

f:id:ticktakclock:20210918132836p:plain
何故か時刻は8月31日16時36分・・・

再起動しても、シャットダウンしても直らない。どういうことなの・・・

時計の設定はサーバーと同期してある

何故かチェックをつけると8/31に戻されてしまって、正しい時刻が取得できない状態になってしまいました。

画像はすでに時刻が直っているものです。

f:id:ticktakclock:20210918133125p:plain
日付と時刻を自動的に設定

日本標準時を取得してみる

こちらのNTPサーバーから時刻を取得してみます

jjy.nict.go.jp

ticktakclockMacBook-Pro:~ ticktakclock$ date
2021831日 火曜日 164741秒 JST
ticktakclockMacBook-Pro:~ ticktakclock$ sudo sntp -S ntp.nict.jp
+1542602.443936 +/- 0.000000 ntp.nict.jp 133.243.238.243
// 自端末の時刻とどれだけ秒数乖離があるか返ってきます

めっちゃずれてる・・・w

timedプロセスをkillしたら直った

timedという時刻のプロセスをkillしたら直りました。というオチなのですが、なんでこうなってたのかはよくわかりませんでした・・・

ticktakclockMacBook-Pro:~ ticktakclock$ date
2021831日 火曜日 164741秒 JST
// めっちゃずれてる
ticktakclockMacBook-Pro:~ ticktakclock$ sudo sntp -S ntp.nict.jp
+1542602.443936 +/- 0.000000 ntp.nict.jp 133.243.238.243
// 自端末の時刻とどれだけ秒数乖離があるか返ってきます
ticktakclockMacBook-Pro:~ ticktakclock$ ps -ef |grep timed
  266   107     1   0  4:33PM ??         0:00.38 /usr/libexec/timed
  501  1170  1103   0  4:51PM ttys000    0:00.00 grep timed
ticktakclocknoMacBook-Pro:~ ticktakclock$ sudo kill 107
ticktakclocknoMacBook-Pro:~ ticktakclock$ sudo sntp -S ntp.nict.jp
+0.092306 +/- 0.000000 ntp.nict.jp 133.243.238.243
// 時刻差分が少なくなった!
ticktakclocknoMacBook-Pro:~ ticktakclock$ date
2021918日 土曜日 132307秒 JST
// 直った!

ちなみに参考はAppleのコミュニティの書き込みでした。

discussionsjapan.apple.com

普段Androidの記事などを書いていますが、 たまにはこういう記事も良いかも。

既存プロジェクトをAndroidStudio ArcticFoxでビルドできるようにするまで

こんにちは、tkyです。

現在携わっているAndroidプロジェクトの開発環境Android Studio4.1からArcticFoxに切り替えてビルドできるようにするまでをまとめてみました。

皆さんも同じようなところでハマるかもしれないのでこれを読んでおくと心構えができるかもしれません。

やることを列挙します。大きく5つくらいですが、AndroidStudioが出すエラーを1つずつ対応していけばなんとかなります。

  • ArcticFoxをインストールする
  • AGP(com.android.tools.build:gradle)を7系にする
  • JDK11にする
  • com.google.gms:google-serviceを最新(2021/8/21現在:4.3.10)にする
  • arch.lifecycle関連の依存があればandroidx.lifecycleに移行する

ArcticFoxをインストールする

当たり前ですが、新しい開発環境をインストールしましょう。

私は古いバージョンの4.x系も使用したいのもあり管理が楽な JetBrains ToolBoxを利用しています。

f:id:ticktakclock:20210821134630p:plain
Toolboxの管理例

developer.android.com

インストールしたらとりあえずビルドができないことを確認してみましょう!

AGPを7にする

今までは4系を使っていたはずですが、飛躍的に7系にアプデします。

理由は下記のリリースブログに記載の通りセマンティックバージョニングを採用したためですね。

  • 今まで -> AndroidStudioのバージョン番号に合わせていた
  • これから -> Gradle自体のバージョンに合わせる

developers-jp.googleblog.com

JDK11にする

AGP7にする要件にもなりますが、Javaのバージョンを上げる必要があります。

AndroidStudioのmenu>preferenceからGradleJDKのバージョンを11にダウンロード及び更新します。

f:id:ticktakclock:20210821135753p:plain
JDK11にする

GMSプラグインを最新にする

できる限り最新(2021/8/21現在:4.3.10)が望ましいですが、

『v4.3.9だとGradleのプラグインがうまく動作しない問題がある』

というところだけおさえておけば大丈夫だと思います。

ちなみにv4.3.9でもビルドできますが、起動時以下のエラーで即落ちることになります。

W/FirebaseApp: Default FirebaseApp failed to initialize because no default options were found. This usually means that com.google.gms:google-services was not applied to your gradle project.

androidx.lifecycleに移行する

これは人によって必要なことです。すでにサポート終了しているlifecycle-extensionsの依存がまだ残っていてビルドエラーとなって現れてきました。

以下のページを参考に自分が必要としているライブラリの依存を記述しましょう。

developer.android.com

まとめ

以上でおおよその方々がビルドできるようになったかと思います。 以降私のプロジェクトで個別で発生した問題を書いていきます。

余談ですが、この内容を自社のAndroidチームに共有したところ

「うちのプロジェクトではCIが通らなかったんですよね〜」

といった情報をもらえたり

「参考にしてうちのプロジェクトもArcticFoxになりました!」

のように参考にしてもらえたり良い影響が出ました。

みなさんもまずは身近な人に共有してみましょう!

その他ハマったこと、やったこと

'jcenter' is deprecated

まだJCenterの依存が残っていて消すと依存ライブラリでエラーが発生するので一旦このままにして、別途作業することにしました。

Firebase関連をBOM形式に記載し直した

ついでにBOMに記載し直しました。

こういう機会がないとなかなかbuild.gradleのメンテできないので良い機会でした。

firebase.google.com

protobuf-lite -> protobuf-javalifeに変更

API通信にprotoを使っています。

既存のprotobuf-liteはメンテされていない古いものだったらしく新しいprotobuf-javalifeに変更して対応しました。

その際に参考となったブログ記事がこちらです。助かりました、ありがとうございます。

h.s64.jp

Circle CIのGraldeオプションをちょっと変更

JDKのガベージコレクタの仕様が変わっているらしくSerialGCになっているようなので明示的にParallelGCに設定しました

-XX:+UseParallelGC

以上です。それでは楽しいAndroid Studioライフを👋

realmをマルチモジュール化したときに気をつけること

こんにちは、tkyです。

Androidアプリ開発のDBとしてRealmを使用して、Realmをマルチモジュール化したときにどのようなことに気をつけるとよいのか私が実際に直面した問題を説明したいと思います。

目的

マルチモジュール化の目的として、Realmの依存をappのようなコアなモジュールから切り離したかったことがあります。

管理のしやすさ、問題が発生したときのスコープを限定させる目的があります。

また、Realmオブジェクトは@RealmObjectというアノテーションを付けますが、差分ビルド毎にアノテーション処理を行っているらしく

差分ビルドにオーバーヘッドが発生している為、ビルド速度を短縮する目的もありました。

やったこと

  • realmモジュールを作成する
  • RealmModuleを作成する

前者はAndroidのモジュールとしてのrealmモジュールで、

後者はRealmのスキーマとしての依存関係をまとめるためのRealm内で使われる単位のモジュールです。この記事で言いたいこととなります。

新しいドキュメント docs.mongodb.com

古いドキュメント docs.mongodb.com

RealmMoudle

RealmModuleは何のためにいるのかというと、異なるライブラリ間で定義されたRealmのオブジェクト名などがコンフリクトしないように(しても大丈夫なように)

そのライブラリやモジュールで使用するRealmObjectを定義するというのが目的となります。

デフォルトでは自動で作られるものらしいのですがコレを明示的に定義しないと

appモジュールからrealmモジュールにアクセスするときに名前の解決ができなかったりしてちょっと厄介になります。

(そもそもアクセス依存しないように作る、というのはもちろんその通りです。)

@RealmClass
open class Foo : RealmObject()

@RealmClass
open class Bar : RealmObject()

// コレを明示的に定義することで命名の競合管理できる
@RealmModule(
    classes = [
        Foo::class,
        Bar::class,
    ]
)
class RealmModule


fun initializeRealm() {
    val realmConfiguration = RealmConfiguration.Builder()
       .modules(RealmModule())
       .build()
    Realm.setDefaultConfiguration(realmConfiguration)
}

まとめ

  • Realmをモジュールに分割するときはRealmModuleも一緒につくろう
  • 永続化領域をモジュールに分割できた
  • 差分ビルドは気持ち早くなった気がする

余談

RealmのDIモジュールもRealmModuleになってしまうので

DI側は RealmDaggerModule というように命名しています。Hiltにする場合とかもあるかもしれないので素直に RealmDIModule とかでも良いと思います。

RealmのCoroutines対応で気をつけたこと

こんにちは、tkyです。

「Realmのtransaction処理、Coroutinesで書きたいな〜」

そう思ったことありませんか?ありますよね。

最新のRealm(10.0)ではkotlin拡張がいくつか存在して executeTransactionAwait を使うと時間のかかる書き込み処理などをsuspend funで記述することができるようになります。

docs.mongodb.com

こちらでRxで書いていた処理をCotourinesで書き換える事ができるようになりますが、いくつか問題も出てきたのでその辺りをこのブログで共有できればと思います。

出てきた問題

  • UIスレッドで処理できない(初期化時に明示的にuiスレッド許可する必要あり)
  • 異なるスレッドでRealmインスタンスを生成できない

UIスレッドで処理できない

RealmException if called from the UI thread, unless an explicit opt-in has been declared in {@link RealmConfiguration.Builder#allowWritesOnUiThread(boolean)}.

対策その1ー UIスレッドの操作を許可する

Realmインスタンスを作る時にBuilderにallowWritesOnUiThreadを指定するだけです。一応これで解決自体はできますが、

UIスレッドを止めてしまうことになるのでパフォーマンスに気をつけたいところです。

val realmConfiguration = RealmConfiguration.Builder()
    .allowWritesOnUiThread(true)
    .build()

対策その2ー Realm処理用のコルーチンディスパッチャを用意する

UIスレッドを止めないようにRealmを扱うための専用のスレッドとコルーチンディスパッチャを用意する方法があります。

シングルスレッドで動作できるコルーチンディスパッチャを以下のように定義します。

// これをDaggerなどを使ってDIすると良いでしょう
fun providesRealmDispatcher(): CoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

// 関数にwithContextを使ってRealmを処理するスレッドをディスパッチャで指定する
override suspend fun write(value: String) = withContext(realmDispatcher) {
    Realm.getDefaultInstance().use { realm ->
        realm.executeTransactionAwait {
            it.copyToRealmOrUpdate(
                Hoge.create(
                    value
                )
            )
        }
    }
}

一応これで書き込み時にUIスレッドで処理できない問題は解決するのですが、別の問題も現れてきました。

異なるスレッドでRealmインスタンスを生成できない

こんなエラーが出てきます

java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.

これはWrite処理はsuspend関数対応しましたが、Read処理については通常の関数として処理してしまったことで

  • Read処理が走る・・・ViewModelScopeのIOスレッドで最初にRealmインスタンスが作られる
  • Write処理が走る・・・Realm用のディスパッチャスレッドからRealmインスタンスにアクセスする

この時RealmインスタンスはViewModelScopeのIOディスパッチャで作られたものです。

それを別のスレッド(Realm用のスレッドから作られたディスパッチャ)からアクセスしようとしたために発生したものでした

そのためWrite処理だけでなくRead処理も含めてRealm処理用のコルーチンディスパッチャ上で動作させる必要があります

この問題についてはStackOverFlowでもちょくちょくみる内容となっています。

stackoverflow.com

// 書き込むデータはString型としています

override suspend fun read(key: String): String = withContext(realmDispatcher) {
    Realm.getDefaultInstance().use { realm ->
        realm.where(Hoge::class.java).equalTo("key", key).findFirst()?.value
    }
}

override suspend fun write(value: String) = withContext(realmDispatcher) {
    Realm.getDefaultInstance().use { realm ->
        realm.executeTransactionAwait {
            it.copyToRealmOrUpdate(
                Hoge.create(
                    value
                )
            )
        }
    }
}

まとめ

Realmをコルーチンで記述するときは

  • 専用のDispacherを作成して
  • Read処理もWrite処理も全てコルーチン化すると良い

それでは良いKotlinライフを。

IntentのresolveActivityを考慮したRobolectricテスト

こんにちは、tkyです。

今日はテストの話です。

画面遷移をテストする時Robolectricを使うと比較的簡単にテストできます。

今回はRobolectric v4.3を使用して

  • 自身のアプリの別Activityに遷移する場合 (=明示的Intent)
  • 外部アプリに遷移する場合 (=暗黙的Intent)

この2つについてこんな感じでできるよ〜というのを紹介したいと思います。

また、Robolectricの説明や導入方法などは記載いたしませんのでご了承願います。

補足ですが、検証にはAssertJを使用しています。

例えば次のような画面遷移をする関数があるとします。

class MainActivity: AppComponentActivity() {

    // 自アプリ内の別Activityへ遷移する
    fun openSubActivity() {
        val intent = SubActivity.createIntent(this)
        startActivity(intent)
    }

    // 別アプリのブラウザアプリを開く
    fun openBrowser() {
        val uri = Uri.parse("https://google.com")
        startActivity(Intent(Intent.ACTION_VIEW, uri))
    }
}

テストコードは次のように書くことができます。

外部ブラウザのように暗黙的インテントによって画面遷移する場合、何が起動するかわからないのでActivityを検証することが難しいです。

そのため、発行したインテントを検証することでコードの正当性を担保します。

class MainActivityTest{

    @Test
    fun `openSubActivityしたらSubActivityに遷移すること`() {
        val activity = Robolectric.buildActivity(MainActivity::class.java)
        activity.openSubActivity() // ここでSubActivityに遷移する
        val shadowActivity = Shadows.shadowOf(activity.get())
        val intent = shadowActivity.peekNextStartedActivity()
        // intentのShadowを作成して次に起動するActivityを検証できるようにする
        val shadowIntent = Shadows.shadowOf(intent)
        val nextActivity = shadowIntent.intentClass
        // 遷移先が正しいこと
        Assertions.assertThat(nextActivity).isEqualTo(SubActivity::class.java)
    }

    @Test
    fun `openBrowser_外部ブラウザへ遷移する`() {
        val activity = Robolectric.buildActivity(RootActivity::class.java)
        activity.openBrowser() // ここで外部ブラウザに遷移する
        val shadowActivity = Shadows.shadowOf(activity.get())
        val intent = shadowActivity.peekNextStartedActivity()
        // urlスキームが正しくパースできること
        Assertions.assertThat(intent.data?.toString()).isEqualTo("https://google.com")
        // 外部ブラウザなので何が起動するか不明。暗黙的インテントのACTIONでassertする
        Assertions.assertThat(intent.action).isEqualTo(Intent.ACTION_VIEW)
    }
}

これで一応テスト可能となりますが、ここで公式のIntent起動ドキュメントを見てみましょう。

developer.android.com

注意:端末に暗黙的インテントを受け取ることができるアプリがない場合、startActivity() を呼び出すとアプリがクラッシュします。まず、インテントを受け取るアプリの存在を確認するために、Intent オブジェクトの resolveActivity() を呼び出してください。結果が null 以外の場合は、インテントを処理できるアプリが少なくとも 1 つあるということなので、startActivity() を安全に呼び出すことができます。結果が null の場合は、そのインテントは使用しないでください。可能であればそのインテントを呼び出す機能を無効にしてください。

要するに以下のようにIntentが起動できるか resolveActivity を使ってチェックしてから起動することを推奨しています。

    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }
class MainActivity: AppComponentActivity() {

    // 自アプリ内の別Activityへ遷移する
    fun openSubActivity() {
        val intent = SubActivity.createIntent(this)
        startActivity(intent)
    }

    // 別アプリのブラウザアプリを開く
    fun openBrowser() {
        val uri = Uri.parse("https://google.com")
        val intent = Intent(Intent.ACTION_VIEW, uri)
        // intentを受け取れるアプリがいるか確認してから起動する
        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        }
    }
}

こうすると何が起こるのかというとRobolectricではresolveActicvity()の戻りがnullになってしまいstartActivityが実行されないことでテストが通らなくなってしまいます。

これを解決するためにShadowPackageManagerを使用してresolveActivity()をモックできるようにします。

    @Test
    fun openByUrlScheme_open_browser_外部ブラウザへ遷移する() {
        val activity = Robolectric.buildActivity(RootActivity::class.java)
        // ▼▼▼▼ 追加ここから ▼▼▼▼
        // ダミーでComponentNameを返す。Intentを検証するのでパッケージ名とActivity名は何でも良い
        val componentName = ComponentName(“com.some.other.package”, “MainActivity”)
        val intentFilter = IntentFilter(Intent.ACTION_VIEW).apply {
            addCategory(Intent.CATEGORY_DEFAULT)
            addCategory(Intent.CATEGORY_BROWSABLE)
            addDataScheme(“https”)
        }
        // PackageManagerのShadowを作成してIntentFilterを追加しておく
        Shadows.shadowOf(ApplicationProvider.getApplicationContext<MyApplication>().packageManager).apply {
            addActivityIfNotPresent(componentName)
            addIntentFilterForActivity(componentName, intentFilter)
        }
        // ▲▲▲▲ 追加ここまで ▲▲▲▲
        activity.openBrowser() // ここで外部ブラウザに遷移する
        val shadowActivity = Shadows.shadowOf(activity.get())
        val intent = shadowActivity.peekNextStartedActivity()
        // urlスキームが正しくパースできること
        Assertions.assertThat(intent.data?.toString()).isEqualTo(“https://google.com”)
        // 外部ブラウザなので何が起動するか不明。暗黙的インテントのACTIONでassertする
        Assertions.assertThat(intent.action).isEqualTo(Intent.ACTION_VIEW)
    }

この辺調べても古いバージョンのRobolectricでの対応方法などが出てきて新しいバージョンでの解決方法がなかったので結構ハマりました。

これでGoogleが推奨する外部ブラウザを開くような暗黙的インテントの実装をした状態でテストもできるようになりました。

是非試してみてください。

kotlinで別パッケージの同じクラス名の衝突を回避する

こんにちは、tkyです。

Kotlinの小ネタです。

タイトルのとおりなのですが、こういったことってほとんどないとは思うのでいざというときどうするか迷う系のやつです。

ドメインとしてのクラス名と被ってしまう

例えば イベント情報 を取り扱うドメインがあったとして Event クラスを作ったとします。

package com.hatenablog.ticktakclock.model.Event

data class Event(val id: Int /* その他引数 */) {}

次にFirebaseに送るトラッキングイベントを Event クラスとしてモデル化します

package com.hatenablog.ticktakclock.track.Event

sealed class Event(val name: String) {
    class Favorite(): Event("user_favorite") // お気に入りしたことをトラッキング
}

この2つのクラスを一緒に使おうとしたときにクラス名の衝突が起こってしまい、どちらかはフルパッケージ名で記述する必要が出てきます。

クラス名が衝突した!その前に

まず名前が衝突しないようにクラス名の変更リファクタリングを視野に入れましょう。

この場合、ドメインとしてのEventは共通仕様なので変えることがかなり難しいですが、トラッキングイベントの方は他の領域関係なく考え直すことができます。

package com.hatenablog.ticktakclock.track.TrackingEvent のように変えることで衝突が発生しない平和な世界を作れるはずです。

※個人的にはパッケージ名にtrackが入っているので更にTrackingEventという命名をつけることに多少違和感を覚えますが、名前回避のため・・・・

import にas を使う

外部のライブラリのクラス名と衝突してしまってどうしても平和的な解決ができない場合、 こちらのドキュメントに書いてあるとおり、 as キーワードを使うことでそのファイル内で別の名前を使用することができるようになります。

package com.hatenablog.ticktakclock.model.Event

package com.hatenablog.ticktakclock.track.Event as TrackingEvent // このファイル内でのみTrackingEventとして扱える

dogwood008.github.io

まとめ

あまりこういったことに出会わないため忘れがちですがいざというときに使えるテクニックかなと思います。

とはいえ前提として名前が被らないようにクラス設計するテクニック自体も身につけていきたいところですね。

  • asキーワードで局所的に別名を指定することができる
  • asキーワード使う前にもとのクラス名をリファクタできるか考えること
  • まだまだKotlinの知らない(あまり使っていない)言語機能ありそう

apk容量削減のためにやったこと

こんにちは、tkyです。

アプリ容量が日に日に多くなっていく中、できることを探してみました。

developer.android.com

公式に削減のための方法が乗っていて、そのうちの2つをやってみました。

効果

僕が携わっているアプリプロダクトは長い間運用されているのですが、 過去の画像リソースなど使用しないものや未使用コードなどもいくつかあることを以前から確認していました。

次の公式に乗っている方法を試してみたところ 18%ほど削減できました🎉

(めっちゃ未使用リソースが多かった・・・)

ちょっとしたファイル整理だけですが、apk容量が削減されることでユーザーにもたらす影響も結構あるのでぜひ試してみると良いかもしれません。

未使用コード、未使用リソースの削除

一番着手しやすいかもしれませんね。

menu > Analyze > Inspect code で不要になっているファイルやリソースを洗い出して削除していきます。

pngからベクターDrawableにする

各種リソースを用意するとそれだけで圧迫してしまうのでベクターで書き出したリソースを使用するのが良さそうです。

実際はデザイナーと相談してデザインデータをベクターで書き出せるようにする必要があるかと思います。

material.io

上記から適当なベクター素材をDLします。

f:id:ticktakclock:20210115155924p:plain
material iconのベクター素材をDLする

AndroidStudioでメニューからNew > Vector Assetを選択します

f:id:ticktakclock:20210115160231p:plain
AndroidStudioでvector assetを選択

先程DLしたsvgを選択すると@drawable/ic_favorite_24pxでDrawableで指定することができます。

f:id:ticktakclock:20210115160254p:plain
先程DLした素材を選択