20210118のAndroidに関する記事は12件です。

OSMDroidとMapQuestを使ってみた

ハッカソンで、OSMつかって経路出したいねーなんてメンバーと話をしていて、その為にちょっとやってみたのでメモついでにまとめておく。

参考にした記事

参考にした Androidで道案内アプリケーションを作る はJavaだったので、Kotlinで書きながら実装してみた。
実際の実装は参照元を参考してもらうとして、実際にこの記事でつまづいたことなどを補足していきます。
参考にした記事を投稿してくれた方には感謝!

Androidで道案内アプリケーションを作るの補足

  • OSMBounusPackの導入
  • NavigationActivity.javaのコードはMainActivity.java(MainActivity.kt)で実装
  • HTTP通信の許可が必要
  • MapEventsReceiverのsingleTapConfirmedHelperメソッドで NetworkOnMainThreadException

OSMBounusPackの導入

これ、オフィシャルのドキュメントを見ても、ハマる人はハマります。
プロジェクトのbuild.gradleとappのbuild.gradleを変更する必要があります。
また、このライブラリを追加しないと、経路検索ができません。

まず、プロジェクトのbuild.gradleに以下のように maven { url "https://jitpack.io" } を追加します。
buildscriptの方ではなく、allprojectsの方に追加してください。
知ってればなんてことないと思いますが、知らなくて、ちょっと悩みました。

build.gradle
allprojects {
    repositories {
        google()
        jcenter()
        //OSMDroid Bonus Packを利用するための準備
        maven { url "https://jitpack.io" }
    }
}

次にappのbuild.gradleに implementation 'com.github.MKergall:osmbonuspack:6.6.0' を追加します。
追加するOSMBounusPackのバージョンはオフィシャルのGitを参照すると良いと思います。

app/build.gradle
dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'org.osmdroid:osmdroid-android:6.1.8'
    implementation 'com.google.android.gms:play-services-location:17.1.0'

    implementation 'com.github.MKergall:osmbonuspack:6.6.0'

    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

MapEventsReceiverのsingleTapConfirmedHelperメソッドの実装時の注意点

以下の処理で NetworkOnMainThreadException が発生しました。

road = roadManager.getRoad(waypoints) 

そのため、コルーチンを使って回避しました

MainActivity.kt
       override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
            targetLat = p!!.latitude
            targetLng = p!!.longitude

            if(waypoints.size > 0){
                waypoints.clear()
            }

            var startPoint = GeoPoint(lat, lng)
            var endPoint = GeoPoint(targetLat, targetLng)
            waypoints.add(startPoint)
            waypoints.add(endPoint)

            // NetworkOnMainThreadException を回避する
            GlobalScope.launch(Dispatchers.IO) {
                var mapView = findViewById<MapView>(R.id.mapView)
                // 経路の取得
                road = roadManager.getRoad(waypoints)

HTTP通信の許可が必要

ざっくりと対応する場合は、AndroidManifest.xmlのApplicationタグにandroid:usesCleartextTraffic="true"追加して対応しちゃいましょう。

androidManifest.xml
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.OSMSample"
        android:usesCleartextTraffic="true">

最後に

経路検索が結構早くて驚きました。
参考サイトをベースに改造して利用したり、ほかの実行もしたので、その辺は別の記事で投稿したいと思います。

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

ハッカソンでOSMを使いたかったので、別の投稿を参考にOSMDroidとMapQuestを使ってみた話

ハッカソンで、OSMつかって経路出したいねーなんてメンバーと話をしていて、その為にちょっとやってみたのでメモついでにまとめておく。

参考にした記事

参考にした Androidで道案内アプリケーションを作る はJavaだったので、Kotlinで書きながら実装してみた。
実際の実装は参照元を参考してもらうとして、実際にこの記事でつまづいたことなどを補足していきます。
参考にした記事を投稿してくれた方には感謝!

Androidで道案内アプリケーションを作るの補足

  • OSMBounusPackの導入
  • NavigationActivity.javaのコードはMainActivity.java(MainActivity.kt)で実装
  • HTTP通信の許可が必要
  • MapEventsReceiverのsingleTapConfirmedHelperメソッドで NetworkOnMainThreadException

OSMBounusPackの導入

これ、オフィシャルのドキュメントを見ても、ハマる人はハマります。
プロジェクトのbuild.gradleとappのbuild.gradleを変更する必要があります。
また、このライブラリを追加しないと、経路検索ができません。

まず、プロジェクトのbuild.gradleに以下のように maven { url "https://jitpack.io" } を追加します。
buildscriptの方ではなく、allprojectsの方に追加してください。
知ってればなんてことないと思いますが、知らなくて、ちょっと悩みました。

build.gradle
allprojects {
    repositories {
        google()
        jcenter()
        //OSMDroid Bonus Packを利用するための準備
        maven { url "https://jitpack.io" }
    }
}

次にappのbuild.gradleに implementation 'com.github.MKergall:osmbonuspack:6.6.0' を追加します。
追加するOSMBounusPackのバージョンはオフィシャルのGitを参照すると良いと思います。

app/build.gradle
dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'org.osmdroid:osmdroid-android:6.1.8'
    implementation 'com.google.android.gms:play-services-location:17.1.0'

    implementation 'com.github.MKergall:osmbonuspack:6.6.0'

    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

MapEventsReceiverのsingleTapConfirmedHelperメソッドの実装時の注意点

以下の処理で NetworkOnMainThreadException が発生しました。

road = roadManager.getRoad(waypoints) 

そのため、コルーチンを使って回避しました

MainActivity.kt
       override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
            targetLat = p!!.latitude
            targetLng = p!!.longitude

            if(waypoints.size > 0){
                waypoints.clear()
            }

            var startPoint = GeoPoint(lat, lng)
            var endPoint = GeoPoint(targetLat, targetLng)
            waypoints.add(startPoint)
            waypoints.add(endPoint)

            // NetworkOnMainThreadException を回避する
            GlobalScope.launch(Dispatchers.IO) {
                var mapView = findViewById<MapView>(R.id.mapView)
                // 経路の取得
                road = roadManager.getRoad(waypoints)

HTTP通信の許可が必要

ざっくりと対応する場合は、AndroidManifest.xmlのApplicationタグにandroid:usesCleartextTraffic="true"追加して対応しちゃいましょう。

androidManifest.xml
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.OSMSample"
        android:usesCleartextTraffic="true">

最後に

経路検索が結構早くて驚きました。
参考サイトをベースに改造して利用したり、ほかの実行もしたので、その辺は別の記事で投稿したいと思います。

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

AAC の ViewModel で View のライフサイクルとリンクさせる時は OnLifecycleEvent でフックするといいよね、と言う話

タイトルがここで言いたい事の全てです:innocent:

Android における ViewModel 側で onCreate 等のライフサイクルが走った時に何かしたい、みたいなシーンは結構あると思います。
(例えばユーザ情報を fetch する、とか)

そういった時どうやって ViewModel 側にイベントを通知するかと言うと

SampleActivity.kt
class SampleActivity: AppCompatActivity() {
    private val viewModel: SampleViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        viewModel.onViewCreated()
    }
}
SampleViewModel.kt
class SampleViewModel: ViewModel() {
    fun onViewCreated() {
        // API 叩いたりレスポンスを LiveData に流したり etc...
    }
}

とすると思います。一番シンプルです。

しかし、せっかく Android の Architecture Component です。
ViewModel の初期化やリソースの解放なんかを他のクラスに任せるのはナンセンスな気がしました。
個人的に View -> ViewModel の方向性は少なくしてあげて、View は流れてくるデータをバインドするだけにしたい。

ViewModel の LiveData を observe してから onCreate を呼んで...
みたいな事を View 側が考慮するのはなんだかなぁ、です:thinking:
ViewModel 側でライフサイクルを考慮してあげた方がスッキリしませんか?

と、言うわけで ViewModel 側で紐づく View のライフサイクルをフックしてあげましょう。

ViewModel で Lifecycle.Event をフックする

ライフサイクル対応コンポーネントによるライフサイクルへの対応  |  Android デベロッパー  |  Android Developers

ViewModel に LifecycleObserver を implement しましょう。
ViewModel が View のライフサイクルにリンクされて画面が表示される度にリロードとかそんな事ができます。

SampleViewModel.kt
class SampleViewModel: ViewModel(), LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onViewCreated() {
        // なんやかんや
    }
}
SampleActivity.kt
class SampleActivity: AppCompatActivity() {

    private val viewModel: SampleViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        lifecycle.addObserver(viewModel)
    }
}

実際には Dagger を使ったりしていると、こんな事をして上げたりするかもしれませんね。

SampleActivity.kt
inline fun <reified T : ViewModel> AppCompatActivity.provideViewModels(crossinline viewModels: () -> T): Lazy<T> {
    return viewModels {
        object : ViewModelProvider.NewInstanceFactory() {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return viewModels() as T
            }
        }
    }
}

class SampleActivity: AppCompatActivity() {
    private val viewModel: SampleViewModel by provideViewModels {
        SampleViewModel(repository).also {
            lifecycle.addObserver(it)
        }
    }
}

これで View 側は通知されてくるデータに専念できそうです:clap:

ライフサイクルのオーバーライドと OnLifecycleEvent のコールスタック

結論は ↑ までになりますが、どっちが先に呼ばれるかみたいな記事がなかなか見つからなかったので

実際 View 側のライフサイクルイベントと Lifecycle.Event をフックしている関数はどっちが先に呼ばれるのか。
Lifecycle.Event がどうハンドリングされているかは、コールスタックを追って見ていくと分かります。

なぜか API が 28 以下と 29 以上でアルゴリズムが変わっていましたが通知される順番は変わりません。

ポイントになる人たち

  • LifecycleRegistory

ライフサイクルイベントをハンドリングして一括で通知してくれるクラスです。
イベントが来たら登録された LifecyclerObserver に対してイベントを通知します。
(実際には通知されるまでに Adapter とかありますが)

  • ReportFragment

ライフサイクルを監視するための不可視の Fragment です。
Activity 生成時に勝手にヒエラルキーに入れられています。
この Fragment の on~ で対応したライフサイクルのイベントが LifecyleRegisotry に渡されます。

API 28 (Android 9.0) 以下の場合

API 28 以下の場合の Lifecycle.Event.ON_CREATE のコールスタックは以下です。
(アノテーション付けたメソッドはリフレクションで呼び出されてますね。)

onCreated:47, SampleViewModel (com.example.lifecycle)
invoke:-1, Method (java.lang.reflect)
invokeCallback:216, ClassesInfoCache$MethodReference (androidx.lifecycle)
invokeMethodsForEvent:194, ClassesInfoCache$CallbackInfo (androidx.lifecycle)
invokeCallbacks:185, ClassesInfoCache$CallbackInfo (androidx.lifecycle)
onStateChanged:37, ReflectiveGenericLifecycleObserver (androidx.lifecycle)
dispatchEvent:361, LifecycleRegistry$ObserverWithState (androidx.lifecycle)
forwardPass:300, LifecycleRegistry (androidx.lifecycle)
sync:339, LifecycleRegistry (androidx.lifecycle)
moveToState:145, LifecycleRegistry (androidx.lifecycle)
handleLifecycleEvent:131, LifecycleRegistry (androidx.lifecycle)
dispatch:68, ReportFragment (androidx.lifecycle)
dispatch:144, ReportFragment (androidx.lifecycle)
onActivityCreated:102, ReportFragment (androidx.lifecycle)
performActivityCreated:2531, Fragment (android.app)
moveToState:1318, FragmentManagerImpl (android.app)
moveFragmentToExpectedState:1576, FragmentManagerImpl (android.app)
moveToState:1637, FragmentManagerImpl (android.app)
dispatchMoveToState:3050, FragmentManagerImpl (android.app)
dispatchActivityCreated:3002, FragmentManagerImpl (android.app)
dispatchActivityCreated:183, FragmentController (android.app)
performCreate:7334, Activity (android.app)
performCreate:7318, Activity (android.app)
callActivityOnCreate:1271, Instrumentation (android.app)
performLaunchActivity:3094, ActivityThread (android.app)
handleLaunchActivity:3257, ActivityThread (android.app)
execute:78, LaunchActivityItem (android.app.servertransaction)
executeCallbacks:108, TransactionExecutor (android.app.servertransaction)
execute:68, TransactionExecutor (android.app.servertransaction)
handleMessage:1948, ActivityThread$H (android.app)
dispatchMessage:106, Handler (android.os)
loop:214, Looper (android.os)
main:7050, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:494, RuntimeInit$MethodAndArgsCaller (com.android.internal.os)
main:964, ZygoteInit (com.android.internal.os)

実際コードを順に追っていくと、

Activity.java
    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        dispatchActivityPreCreated(icicle);
        mCanEnterPictureInPicture = true;
        restoreHasCurrentPermissionRequest(icicle);

        // Activity の onCreate のオーバーライド達が走る
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }

        ...

        // Lifecycle.State が CREATED に変わるところ
        mFragments.dispatchActivityCreated();
        mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());

        // API 29 以上はここで post される
        dispatchActivityPostCreated(icicle);
    }
FragmentManager.java
    public void dispatchActivityCreated() {
        mStateSaved = false;
        dispatchMoveToState(Fragment.ACTIVITY_CREATED);
    }

    private void dispatchMoveToState(int state) {
        if (mAllowOldReentrantBehavior) {
            moveToState(state, false);
        } else {
            try {
                mExecutingActions = true;
                moveToState(state, false);
            } finally {
                mExecutingActions = false;
            }
        }
        execPendingActions();
    }

    void moveToState(int newState, boolean always) {
        ...

        if (mActive != null) {
            boolean loadersRunning = false;

            // Must add them in the proper order. mActive fragments may be out of order
            final int numAdded = mAdded.size();
            for (int i = 0; i < numAdded; i++) {
                // ReportFragment の State を更新する
                Fragment f = mAdded.get(i);
                moveFragmentToExpectedState(f);
                if (f.mLoaderManager != null) {
                    loadersRunning |= f.mLoaderManager.hasRunningLoaders();
                }
            }
            ...
        }
    }

    void moveFragmentToExpectedState(final Fragment f) {
        int nextState = mCurState;
        if (f.mRemoving) {
            if (f.isInBackStack()) {
                nextState = Math.min(nextState, Fragment.CREATED);
            } else {
                nextState = Math.min(nextState, Fragment.INITIALIZING);
            }
        }

        moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false);
        ...
    }

    void moveToState(Fragment f, int newState, int transit, int transitionStyle,
            boolean keepActive) {
        ...
        switch (f.mState) {
            case Fragment.CREATED:
                ...
                // 
                f.performActivityCreated(f.mSavedFragmentState);                
            ...
        }
        ...
    }

Fragment.java
    void performActivityCreated(Bundle savedInstanceState) {
        if (mChildFragmentManager != null) {
            mChildFragmentManager.noteStateNotSaved();
        }
        mState = ACTIVITY_CREATED;
        mCalled = false;
        // ReportFragment の onActivityCreated が呼ばれる
        onActivityCreated(savedInstanceState);
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onActivityCreated()");
        }
        if (mChildFragmentManager != null) {
            mChildFragmentManager.dispatchActivityCreated();
        }
    }
ReportFragment.java
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        dispatchCreate(mProcessListener);
        dispatch(Lifecycle.Event.ON_CREATE);
    }

    private void dispatch(@NonNull Lifecycle.Event event) {
        if (Build.VERSION.SDK_INT < 29) {
            // Only dispatch events from ReportFragment on API levels prior
            // to API 29. On API 29+, this is handled by the ActivityLifecycleCallbacks
            // added in ReportFragment.injectIfNeededIn
            dispatch(getActivity(), event);
        }
    }

    static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
         // ここで Lifecycle.Event がハンドリングされる -> OnLifecycleEvent がフックしてるところがはしる
        if (activity instanceof LifecycleRegistryOwner) {
            ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
            return;
        }

        if (activity instanceof LifecycleOwner) {
            Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
            if (lifecycle instanceof LifecycleRegistry) {
                ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
            }
        }
    }

となり、 各 onCreate のオーバーライドが走った後 ReportFragment が Activity の LifecycleRegistory に対してイベントを通知 します。
(長くなるのでいろいろ端折ってます...)

余談ですが、ちょっとややこしいですが、Fragment の mState は moveToState が終わってから更新されるので
dispatchActivityCreated 時の ReportFragment の mState は Fragment.CREATED です。
(FragmentManager.moveToState 内 switch のお話)

API 29 以上

ReportFragment にイベントが来るまでのシーケンスが変わっています。
が、ReportFragment 以降は API 28 以下の場合と同じです。
こっちの方がわかりやすいですね。

Activity.java
    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        ...

        // Activity の onCreate のオーバーライド達が走る
        // (ReportFragment の ActivityLifecycleCallbacks が登録される)
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }

        ...

        // API 29 以上はここで ReportFragment に post される
        dispatchActivityPostCreated(icicle);
    }
ComponentActivity.java
    @SuppressLint("RestrictedApi")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // (ActivityLifecycleCallbacks が登録される)
        ReportFragment.injectIfNeededIn(this);
    }
ReportFragment.java
    public static void injectIfNeededIn(Activity activity) {
        if (Build.VERSION.SDK_INT >= 29) {
            // On API 29+, we can register for the correct Lifecycle callbacks directly
            activity.registerActivityLifecycleCallbacks(
                    new LifecycleCallbacks());
        }
        ...
    }

    static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
        ...

        @Override
        public void onActivityPostCreated(@NonNull Activity activity,
                @Nullable Bundle savedInstanceState) {
            // 以降 API 28 以下と一緒
            dispatch(activity, Lifecycle.Event.ON_CREATE);
        }
        ...
}

onPause 以降の View が死んでいく時の通知順は逆になります。
(Lifecycle.Event.ON_PAUSE -> Activity.onPause())

先に ViewModel で解放してから View の destroy が走るみたいな流れになりますね。

なので、通知順は以下になります。

image.png

Conclusion

と、いうわけで
ViewModel で OnLifecycleEvent をフックした方が、View 側でオーバーライドしたライフサイクルで viewmodel.on~ みたいに呼ぶより View 側が考慮する事がなくなってスッキリした気がしますね。

今回の話は ViewModel に限った話ではありませんが、
オーバーライドしたライフサイクル内であれこれやりすぎた結果
うわっ...私の Activity、太りすぎ...?
みたいな事にならない様上手く LifecycleObserver を使って責任を分散させましょう!

これは個人的な意見なので誰かの参考になればと思います。
後、いろいろ間違ってたりしたらご指摘いただけると嬉しいです...

参考

Android Architecture Components 雑感2。 - なるようになるかも

Android Architecture Component -- Lifecycle 浅析 - 简书

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

AAC の ViewModel で View のライフサイクルとリンクさせる時は Lifecycle.Event をフックするといいよね、と言う話

タイトルがここで言いたい事の全てです:innocent:

Android における ViewModel 側で onCreate 等のライフサイクルが走った時に何かしたい、みたいなシーンは結構あると思います。
(例えばユーザ情報を fetch する、とか)

そういった時どうやって ViewModel 側にイベントを通知するかと言うと

SampleActivity.kt
class SampleActivity: AppCompatActivity() {
    private val viewModel: SampleViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        viewModel.onCreate()
    }
}
SampleViewModel.kt
class SampleViewModel: ViewModel() {
    fun onCreate() {
        // API 叩いたりレスポンスを LiveData に流したり etc...
    }
}

とすると思います。一番シンプルです。

しかし、せっかく Android の Architecture Component です。
ViewModel の初期化やリソースの解放なんかを他のクラスに任せるのはナンセンスな気がしました。
個人的に View -> ViewModel の方向性は少なくしてあげて、View は流れてくるデータをバインドするだけにしたい。

ViewModel の LiveData を observe してから onCreate を呼んで...
みたいな事を View 側が考慮するのはなんだかなぁ、です:thinking:
ViewModel 側でライフサイクルを考慮してあげた方がスッキリしませんか?

と、言うわけで ViewModel 側で紐づく View のライフサイクルをフックしてあげましょう。

ViewModel で Lifecycle.Event をフックする

ライフサイクル対応コンポーネントによるライフサイクルへの対応  |  Android デベロッパー  |  Android Developers

ViewModel に LifecycleObserver を implement しましょう。
ViewModel が View のライフサイクルにリンクされて画面が表示される度にリロードとかそんな事ができます。

SampleViewModel.kt
class SampleViewModel: ViewModel(), LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onViewCreated() {
        // なんやかんや
    }
}
SampleActivity.kt
class SampleActivity: AppCompatActivity() {

    private val viewModel: SampleViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        lifecycle.addObserver(viewModel)
    }
}

実際には Dagger を使ったりしていると、こんな事をして上げたりするかもしれませんね。

SampleActivity.kt
inline fun <reified T : ViewModel> AppCompatActivity.provideViewModels(crossinline viewModels: () -> T): Lazy<T> {
    return viewModels {
        object : ViewModelProvider.NewInstanceFactory() {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return viewModels() as T
            }
        }
    }
}

class SampleActivity: AppCompatActivity() {
    private val viewModel: SampleViewModel by provideViewModels {
        SampleViewModel(repository).also {
            lifecycle.addObserver(it)
        }
    }
}

これで View 側は通知されてくるデータに専念できそうです:clap:

ライフサイクルのオーバーライドと OnLifecycleEvent のコールスタック

結論は ↑ までになりますが、どっちが先に呼ばれるかみたいな記事がなかなか見つからなかったので

実際 View 側のライフサイクルイベントと Lifecycle.Event をフックしている関数はどっちが先に呼ばれるのか。
Lifecycle.Event がどうハンドリングされているかは、コールスタックを追って見ていくと分かります。

なぜか API が 28 以下と 29 以上でアルゴリズムが変わっていましたが通知される順番は変わりません。

ポイントになる人たち

  • LifecycleRegistory

ライフサイクルイベントをハンドリングして一括で通知してくれるクラスです。
イベントが来たら登録された LifecyclerObserver に対してイベントを通知します。
(実際には通知されるまでに Adapter とかありますが)

  • ReportFragment

ライフサイクルを監視するための不可視の Fragment です。
この Fragment の on~ で対応したライフサイクルのイベントが LifecyleRegisotry に渡されます。

API 28 (Android 9.0) 以下の場合

API 28 以下の場合の Lifecycle.Event.ON_CREATE のコールスタックは以下です。
(リフレクションで呼び出されてますね。)

onCreated:47, SampleViewModel (com.example.lifecycle)
invoke:-1, Method (java.lang.reflect)
invokeCallback:216, ClassesInfoCache$MethodReference (androidx.lifecycle)
invokeMethodsForEvent:194, ClassesInfoCache$CallbackInfo (androidx.lifecycle)
invokeCallbacks:185, ClassesInfoCache$CallbackInfo (androidx.lifecycle)
onStateChanged:37, ReflectiveGenericLifecycleObserver (androidx.lifecycle)
dispatchEvent:361, LifecycleRegistry$ObserverWithState (androidx.lifecycle)
forwardPass:300, LifecycleRegistry (androidx.lifecycle)
sync:339, LifecycleRegistry (androidx.lifecycle)
moveToState:145, LifecycleRegistry (androidx.lifecycle)
handleLifecycleEvent:131, LifecycleRegistry (androidx.lifecycle)
dispatch:68, ReportFragment (androidx.lifecycle)
dispatch:144, ReportFragment (androidx.lifecycle)
onActivityCreated:102, ReportFragment (androidx.lifecycle)
performActivityCreated:2531, Fragment (android.app)
moveToState:1318, FragmentManagerImpl (android.app)
moveFragmentToExpectedState:1576, FragmentManagerImpl (android.app)
moveToState:1637, FragmentManagerImpl (android.app)
dispatchMoveToState:3050, FragmentManagerImpl (android.app)
dispatchActivityCreated:3002, FragmentManagerImpl (android.app)
dispatchActivityCreated:183, FragmentController (android.app)
performCreate:7334, Activity (android.app)
performCreate:7318, Activity (android.app)
callActivityOnCreate:1271, Instrumentation (android.app)
performLaunchActivity:3094, ActivityThread (android.app)
handleLaunchActivity:3257, ActivityThread (android.app)
execute:78, LaunchActivityItem (android.app.servertransaction)
executeCallbacks:108, TransactionExecutor (android.app.servertransaction)
execute:68, TransactionExecutor (android.app.servertransaction)
handleMessage:1948, ActivityThread$H (android.app)
dispatchMessage:106, Handler (android.os)
loop:214, Looper (android.os)
main:7050, ActivityThread (android.app)
invoke:-1, Method (java.lang.reflect)
run:494, RuntimeInit$MethodAndArgsCaller (com.android.internal.os)
main:964, ZygoteInit (com.android.internal.os)

実際コードを順に追っていくと、

Activity.java
    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        dispatchActivityPreCreated(icicle);
        mCanEnterPictureInPicture = true;
        restoreHasCurrentPermissionRequest(icicle);

        // Activity の onCreate のオーバーライド達が走る
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }

        ...

        // Lifecycle.State が CREATED に変わるところ
        mFragments.dispatchActivityCreated();
        mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());

        // API 29 以上はここで post される
        dispatchActivityPostCreated(icicle);
    }
FragmentManager.java
    public void dispatchActivityCreated() {
        mStateSaved = false;
        dispatchMoveToState(Fragment.ACTIVITY_CREATED);
    }

    private void dispatchMoveToState(int state) {
        if (mAllowOldReentrantBehavior) {
            moveToState(state, false);
        } else {
            try {
                mExecutingActions = true;
                moveToState(state, false);
            } finally {
                mExecutingActions = false;
            }
        }
        execPendingActions();
    }

    void moveToState(int newState, boolean always) {
        ...

        if (mActive != null) {
            boolean loadersRunning = false;

            // Must add them in the proper order. mActive fragments may be out of order
            final int numAdded = mAdded.size();
            for (int i = 0; i < numAdded; i++) {
                // ReportFragment の State を更新する
                Fragment f = mAdded.get(i);
                moveFragmentToExpectedState(f);
                if (f.mLoaderManager != null) {
                    loadersRunning |= f.mLoaderManager.hasRunningLoaders();
                }
            }
            ...
        }
    }

    void moveFragmentToExpectedState(final Fragment f) {
        int nextState = mCurState;
        if (f.mRemoving) {
            if (f.isInBackStack()) {
                nextState = Math.min(nextState, Fragment.CREATED);
            } else {
                nextState = Math.min(nextState, Fragment.INITIALIZING);
            }
        }

        moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false);
        ...
    }

    void moveToState(Fragment f, int newState, int transit, int transitionStyle,
            boolean keepActive) {
        ...
        switch (f.mState) {
            case Fragment.CREATED:
                ...
                // 
                f.performActivityCreated(f.mSavedFragmentState);                
            ...
        }
        ...
    }

Fragment.java
    void performActivityCreated(Bundle savedInstanceState) {
        if (mChildFragmentManager != null) {
            mChildFragmentManager.noteStateNotSaved();
        }
        mState = ACTIVITY_CREATED;
        mCalled = false;
        // ReportFragment の onActivityCreated が呼ばれる
        onActivityCreated(savedInstanceState);
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onActivityCreated()");
        }
        if (mChildFragmentManager != null) {
            mChildFragmentManager.dispatchActivityCreated();
        }
    }
ReportFragment.java
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        dispatchCreate(mProcessListener);
        dispatch(Lifecycle.Event.ON_CREATE);
    }

    private void dispatch(@NonNull Lifecycle.Event event) {
        if (Build.VERSION.SDK_INT < 29) {
            // Only dispatch events from ReportFragment on API levels prior
            // to API 29. On API 29+, this is handled by the ActivityLifecycleCallbacks
            // added in ReportFragment.injectIfNeededIn
            dispatch(getActivity(), event);
        }
    }

    static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
         // ここで Lifecycle.Event がハンドリングされる -> OnLifecycleEvent がフックしてるところがはしる
        if (activity instanceof LifecycleRegistryOwner) {
            ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
            return;
        }

        if (activity instanceof LifecycleOwner) {
            Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
            if (lifecycle instanceof LifecycleRegistry) {
                ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
            }
        }
    }

となり、 各 onCreate のオーバーライドが走った後 ReportFragment が Activity の LifecycleRegistory に対してイベントを通知 します。
(長くなるのでいろいろ端折ってます...)

余談ですが、ちょっとややこしいですが、Fragment の mState は moveToState が終わってから更新されるので
dispatchActivityCreated 時の ReportFragment の mState は Fragment.CREATED です。
(FragmentManager.moveToState 内 switch のお話)

API 29 以上

ReportFragment にイベントが来るまでのシーケンスが変わっています。
が、ReportFragment 以降は API 28 以下の場合と同じです。
こっちの方がわかりやすいですね。

Activity.java
    final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        ...

        // Activity の onCreate のオーバーライド達が走る
        // (ReportFragment の ActivityLifecycleCallbacks が登録される)
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }

        ...

        // API 29 以上はここで ReportFragment に post される
        dispatchActivityPostCreated(icicle);
    }
ComponentActivity.java
    @SuppressLint("RestrictedApi")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // (ActivityLifecycleCallbacks が登録される)
        ReportFragment.injectIfNeededIn(this);
    }
ReportFragment.java
    public static void injectIfNeededIn(Activity activity) {
        if (Build.VERSION.SDK_INT >= 29) {
            // On API 29+, we can register for the correct Lifecycle callbacks directly
            activity.registerActivityLifecycleCallbacks(
                    new LifecycleCallbacks());
        }
        ...
    }

    static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
        ...

        @Override
        public void onActivityPostCreated(@NonNull Activity activity,
                @Nullable Bundle savedInstanceState) {
            // 以降 API 28 以下と一緒
            dispatch(activity, Lifecycle.Event.ON_CREATE);
        }
        ...
}

onPause 以降の View が死んでいく時の通知順は逆になります。
(Lifecycle.Event.ON_PAUSE -> Activity.onPause())

先に ViewModel で解放してから View の destroy が走るみたいな流れになりますね。

なので、通知順は以下になります。

image.png

Conclusion

と、いうわけで
ViewModel で OnLifecycleEvent をフックした方が、View 側でオーバーライドしたライフサイクルで viewmodel.on~ みたいに呼ぶより View 側が考慮する事がなくなってスッキリした気がしますね。

今回の話は ViewModel に限った話ではありませんが、
オーバーライドしたライフサイクル内であれこれやりすぎた結果
うわっ...私の Activity、太りすぎ...?
みたいな事にならない様上手く LifecycleObserver を使って責任を分散させましょう!

これは個人的な意見なので誰かの参考になればと思います。
後、いろいろ間違ってたりしたらご指摘いただけると嬉しいです...

参考

Android Architecture Components 雑感2。 - なるようになるかも

Android Architecture Component -- Lifecycle 浅析 - 简书

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

AndroidTV9にadbでapkインストールし開くメモ

はじめに

某中国電機メーカーのスティックAndroidTV買った。
自分はお初AndroidTVでは無いが、最近のAndroidTVに野良apkをadbで入れても自動的にAndroidTV HOME Launcherに登録はされないことを思い出した。
検索しても日本語の記事が無かったようなので、今更ではあるがそれの開き方のメモである。

adbでアプリインストール

分からない場合はここを参照。
https://developer.android.com/studio/command-line/adb?hl=ja

※AndroidTVは基本的にUSBでadb接続できない機種が多い。そのためTCP/IPで接続を行う。

インストールしてもadbからの指示で起動はできるが、Launcher自体に登録されない。

開き方

今回はその某中国電機メーカーのスティックAndroidTVでやっていく。
サンプルとして今回はCPU-Zを開いてみる。

image.png

上の画像のようにインストールしてもアプリ一覧には追加されないため開けない。

設定を開く

image.png

アプリを開く

image.png

インストールしたアプリが出てくる。

今回インストールしたアプリを開く
この場合CPU-Zを開く

image.png

アプリの情報が出るため、開くでアプリが立ち上がる。

これでadbでインストールしたアプリが開ける。

最後に

昔のAndroidTVだったら「インストール済みのアプリ」という項目があり、アプリが簡単に開けたが今のLauncherだとアプリ一覧には追加されないようなので、開くのが一苦労。

標準のLauncherでインストールしたアプリを一覧についかされる方法を探りたいと思う。

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

ピンあてゲーム「Hit and Blow」をFlutterでリリースした話【個人開発】

概要

  • アプリの説明
  • 作ろうと思ったきっかけ
  • Hit and Blowってどんなゲーム?
  • Flutterでどう実現するか
  • リリース準備について!
  • 現状・今後実現していきたいこと、意気込み
  • 最後に

はじめましてtomoと申します。

インターン生として大阪のスタートアップでエンジニアをしながら個人開発をしている大学生です。

Qiita初投稿になります!
自分と同じようにアプリを作る方またこれからアプリを作りたいと思っている方に向けて何かのヒントになれば嬉しいです!

リリースしたアプリについて

Hit and Blow(ヒット&ブロー)
というピンの色を推測するゲームです。

シングルプレーヤーゲームでアプリ自体は3ヶ月ほど前にリリースしました。

Nintendo Switchの世界のアソビ大全51というゲームに搭載されているのでご存知のかたもいるかと思います。

Numer0n(ヌメロン)という数あてゲームが日本だと有名かと思いますが、それとほぼ同等のゲームになります。

1.png

4.png

なぜこのアプリを作ったか

このアプリはFlutterを使って開発しました。

Flutter自体は業務でよく触っていましたんですが、ちょうど開発しようと思っていた時期は業務でFlutterを触ることが少なくなり、勉強も含め何かアプリを個人で作ろうと思ったのがきっかけです。

ちょうどそのタイミングでNintendo Switchの世界のアソビ大全51というゲームが人気を博し、YouTubeで様々な動画が公開されていました。

そのアソビ大全51の一つにこの「Hit and Blow」というゲームがあり、このくらいのシンプルなゲームならUnityなどのゲームエンジン等必要なくFlutterだけで作れるじゃんと思い作りはじめました。

朝と夜の時間を使って一週間ほどで開発し、なんやかんや(アイコン作ったり、ストアに申請したり)あって二週間ほどでリリースしました。

Hit and Blowとはどのようなゲームなのか。

Hit and Blow(ヒット & ブロー) はピンの色を当てるゲームです。マスターマインドとも言われてるみたいですね。

■ ヒット&ブローの遊び方 ■(Wikiより)

プレイヤーは、出題者と解答者に分かれる。

(このアプリの場合回答者がプレーヤーになります。)

  1. 出題者は解答者から見えないように、ピンを4本選び並べる。
  2. 解答者は、配置を予想する。
  3. 出題者は解答者の予想を判定する。
    • 位置も色も正しいピン(これをヒットという)があったら赤いピンを立てる。
    • 色は正しいが位置が違うピン(これをブローという)があったら白いピンを立てる。
  4. 2-3 を繰り返し、赤いピンが4本立つ(配置を完全に答える)までの回数で勝負を決める。

という推理力が問われるゲームになります。

このようにルールが非常にシンプルなのでプログラムもしやすいです。

アプリの要件としては

  1. ランダムな4つのピンを決める。
  2. ユーザーに4つのピンを答えさせる。
  3. ランダムに生成したピンとユーザーが回答したピンを比較し、判定する。

最初に1を行った後は2,3をひたすらターンが終了する、もしくはユーザーが正解するまで繰り返せばいいんですね(あら単純)

どうFlutterで実現したか。

プログラム自体は単純で「Hit and Blow Algorithm」とかなんとかGoogleさんに聞くと色々出てくるので実装自体はそこまで難しくはないと思います。

ただ数字を答えてあってるか確認するだけの機能・UIだとアプリストアで映えないし、ユーザーも使ってくれません。

このアプリを開発していく中でいかにゲームらしさ、UXをよくするかということを考えて開発しました。

使ったパッケージ

trotter
→組み合わせやランダムな抽出がうまくできる。初期値設定に利用

google_fonts
→GoogleFontが使える。

flutter_launcher_icons
→アイコンいい感じにできる。

firebase_admob
→広告載せれる。

まあ、普通ですね。

UI

作成した画面は

  • Top画面(スタート画面)
  • ゲーム画面

の2画面のみです。

ゲーム画面ではピンをドラッグアンドドロップして操作するようにしました。

タップで選択したり、入力したりするより直感的だしゲームやってる感が出る且つFlutterでそれを可能にできるWidgetがあったので。

以下ゲーム画面UIです。

Hit_and_Blow (1).png

_2021-01-09_22.48.51 (1).png

_2021-01-09_22.47.14 (1).png

操作手順

  1. 下のピンをドラッグして上部の設置箇所にドラッグします。
  2. 4つのピンを設置すると表示されるOKボタンを押して確定
  3. Hit Blowの結果がモーダルで表示される。(正解の場合はクリア画面)

このようにUIは非常にシンプルかつ、カラフルに仕上げています。
ピンの設置にはFlutterのDraggableとDragTargetを使っています。
DraggableをDragTargetにドロップすることで、Widgetを設置したり、任意の値を渡したりできます。

Hit_and_Blow (2).png

実際のコードです。

Draggableに数値を持たせていて、DragTargetにドロップすることで配列に数値をいれるようになっています。便利ですね。

詳しくはこちらのQiita記事を参考にしました。

【Flutter】ドラッグ&ドロップの実装

// ピン
Widget draggableColor(Color color) {
    return Draggable<int>(
      data: colors.indexOf(color),
      child: Container(
        margin: EdgeInsets.symmetric(horizontal: 20),
        height: colorButtonSize,
        width: colorButtonSize,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle
        ),
      ),
      feedback: Container(
        margin: EdgeInsets.symmetric(horizontal: 20),
        height: colorButtonSize,
        width: colorButtonSize,
        decoration: BoxDecoration(
          color: color,
          shape: BoxShape.circle
        ),
      ),
      childWhenDragging: Container(
        margin: EdgeInsets.symmetric(horizontal: 20),
        height: colorButtonSize,
        width: colorButtonSize,
        decoration: BoxDecoration(
          color: Colors.grey,
          shape: BoxShape.circle
        ),
      ),
    );
  }

// ピンを設置する場所
Widget dragTarget(int index) {
    return DragTarget<int>(
      builder: (context, candidateData, rejectedData) {
        return Container(
          margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
          height: colorButtonSize,
          width: colorButtonSize,
          decoration: answer[index] == null ? BoxDecoration(
            color: Colors.grey,
            shape: BoxShape.circle
          ) : BoxDecoration(
            color: colors[answer[index]],
            shape: BoxShape.circle
          ),
        );
      },
      onAccept: (data) {
        setState(() {
          setColor(data, index);
        });
      },
    );
  }

リリース準備について

リリースする際には色々準備する必要があります。

  • アイコンの作成
  • スクリーンショット画像の作成
  • アプリの説明文

などなど。

アイコンの作成

アプリの作成についてはCanvaというデザインツールを使って作りました。

Androidの場合はAdaptive Iconに対応しないといけないので、Canvaで以下のような画像を作成して、removebgで背景画像を消したのち、 flutter_launcher_iconsで背景色を白に設定しました。

Screenshot_2021-01-10-01-35-54-59.png

flutter_icons:
  android: true
  ios: true
  image_path: "assets/logo.png"
  adaptive_icon_foreground: "assets/logo.png"
  adaptive_icon_background: "#ffffff"

最終的なアイコンはこんな感じです。

いつもアイコンサイズの調整に苦労してます。

スクリーンショット の作成

スクリーンショット の作成はFigmaとCanvaを使って作りました。

Figmaはデザイン目的で利用したのではなく、モックアップ 作成の目的で利用しました。

Figmaでいい感じのスマホフレームテンプレートを見つけたので、アプリのスクリーンショット をとって枠にはめた感じです。

Figmaで作ったモックアップ をCanvaに突っ込んでいい感じに文字を書いて、目を引く背景色を設定して完成です

1.png

説明文の作成

説明文は英語でHit and Blowのゲームの説明を書きました。Hit and Blow自体海外のゲームですし、非常にシンプルなのでターゲットは広い方がいいだろうということで全て英語で書いています。

文章に関しては、Googleさんに「Hit and Blow Rule」とかで調べて出てきたものを参考に書きました。

既存のゲームなので深く説明文書く必要なくて楽ですね。

現状の数値・今後の展望

現状の数値

ダウンロード数は累計で300くらいです。

下のグラフを見てもらったらわかるように年末年始は数字が伸びてますね。こういう暇つぶしゲームは可処分時間が増えるとダウンロードも増える傾向にあります。

_2021-01-10_2.18.38.png

半分は日本でのダウンロードですが、残り半分はアメリカやイギリスなど海外でのダウンロードになっています。英語で説明文を書いた甲斐がありました。

もうちょっと海外からのダウンロードも伸びていい気はしますが、ASOとかで最適化されていないんですかね。もしくはHIt and Blowの知名度があまりないんでしょうか。

_2021-01-10_2.21.04.png

広告はゲーム終了後に全画面広告を設置しています。全画面なので、バナー広告とは違い単価が大きいです。先月の売上は年末に跳ねましたね。

ゲーム系は動画広告が多いので、全画面広告ではなく動画広告に変更するのもありかなと思っています。

_2021-01-10_2.20.07.png

現状の課題・今後の展望

現状の課題はなんと言ってもユーザーの減少数が多いことです。今のアプリは非常にシンプルなので、ユーザーの定着率が悪くアンインストール数が増えてしまします。

改善策として、

①ピンの数や重複ありorなしなどを自ら設定できるようにする
②オンライン対戦をできるようにする

を考えています。

①に関しては判定処理などが少し複雑になりますが、現実的ですね。
②に関しては課題が多そうです。ユーザーが集まらないとマッチング自体ができないので実装するにしてもタイミングをしっかり考慮する必要がありそうです。

最後に

Flutterでシンプルなゲームアプリを作ることができました!

面白そうだと感じてくれたら是非一度ダウンロードしてみてください!

コメント欄などで感想をいただけると嬉しいです!

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

【Android】AndroidセンサーAPIの種類

プログラミング勉強日記

2021年1月18日
Android端末には様々なセンサーが搭載されている。具体的には温度や方位、方角、明るさ、傾き、加速度センサーなどがある。そういったセンサーを使うために必要なセンサーAPIについてまとめる。

AndroidのセンサーAPI

 Androidでは、センサーによって変換された電気信号を取得するためのセンサーAPIが提供されている。このAPIによってセンサーを用いたアプリを簡単に開発できる。
 android.hardwareパッケージには、SensorManager、Sensor、SensorEvent、SensorEventListenerの4つのセンサーAPIが提供されている。

SensorManager

 センサーにアクセスするためのクラス。各センサーのイベントリスナーの登録や解除、センサーの情報の取得を行う。

SensorManagerクラスのメソッド
// イベントリスナーを登録するメソッド
registerListener(SensorEventListener listener, Sensor sensor, int rate, Handler handler)
registerListener(SensorEventListener listener, Sensor sensor, int rate)

// イベントリスナーを解除するメソッド
unregisterListener(SensorEventListener listener, Sensor sensor)
unregisterListener(SensorEventListener listener)

// 使用可能なセンサーをリストで取得するメソッド
getSensorList(int type)
定数名 説明
SENSOR_DELAY_FASTEST 最速通知頻度(デバイスのスペック次第)
SENSOR_DELAY_NORMAL 通常のサンプリング
SENSOR_DELAY_GAME ゲームに適した早い反応速度
SENSOR_DELAY_UI UI向けの遅い反応速度(ゲームではないアプリ向け)

Sensor

 各センサーに関する情報を管理するクラス。

Sensorクラスのメソッド
// センサーが検出できる最大値を取得するメソッド
getMaximumRange() 
// センサーの名前を取得するメソッド
getName() 
// センサーからの電気信号をmAで取得するメソッド
getPower() 
// センサーの精度を取得するメソッド
getResolution() 
// センサーの種類を取得するメソッド
getType() 
// センサーのベンダー名を取得するメソッド
getVendor() 
// センサーのバージョンを取得するメソッド
getVersion() 

センサーの種類

名前 説明
TYPE_ACCELEROMETER 加速度センサー
TYPE_MAGNETIC_FIELD 磁界(磁気)センサー
TYPE_ORIENTATION 方位(傾き)センサー
TYPE_GYROSCOPE ジャイロスコープセンサー
TYPE_LIGHT 照度センサー
TYPE_PRESSURE 圧力センサー
TYPE_TEMPERATURE 温度センサー
TYPE_PROXIMITY 近接センサー
TYPE_GRAVITY 重力センサー
TYPE_LINEAR_ACCELERATION 直線加速度センサー
TYPE_ROTATION_VECTOR 回転角度センサー
TYPE_ALL 指定したセンサーの全ての値が取得できる

SensorEvent

 センサーが取得できる情報を1つにまとめて管理するクラス。

SensorEventクラスのフィールド
// センサーの精度を格納するフィールド
int accuracy 
// Sensorオブジェクトを保持するフィールド
Sensor sensor
// タイムスタンプを格納するフィールド
long timestamp 
// センサーの値を格納するフィールド
float[] values 

SensorEventListener

 センサー関連のイベントを取得するためのイベントリスナー。SensorManagerのメソッドでこのイベントリスナーの登録や解除をすると、センサーからの情報を取得できる。

SensorEventListenerのメソッド
// センサーの精度が変わったときに呼び出されるメソッド
onAccuracyChanged(Sensor sensor, int accuracy)
// センサーの値が変わったときに呼び出されるメソッド
onSensorChanged(SensorEvent event)

参考文献

センサーの概要

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

Flutter で仕事したい人のための Widget ツリー入門

ご存知の通り、 Flutter は Widget を入れ子の構造で記述することで UI を構築します。

Center(
  child: Column(
    children: const <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Text('This is my first app.'),
    ],
  ),
),

Flutter の Widget 同士の関係は、このようなソースコードの形状から「入れ子」の構造をイメージしがちですが、 Flutter の仕組みを理解する上では、このソースコードから Widget の ツリー構造 がイメージできるようになっておくと何かと役に立ちます。

この記事では、Flutter の仕組みを説明する上でよく使われる 「Widget ツリー」 について、初学者向けに基本から解説していきたいと思います。

なぜ Widget ツリーを理解するのか

例えば「中央寄せしたければ Center で囲う」のように簡単な UI を構築するだけであれば Flutter が用意してくれている色々な Widget とそのプロパティの使い方を理解すれば作れてしまうのが Flutter の良いところです。

しかし一方で、 Snackbar を表示しようと思ったら Scaffold.of() called with a context that does not contain a Scaffold エラーが発生した、複数の画面でデータを共有する方法を調べたら InheritedWidgetProvider といった知らない仕組みが出てきて混乱した、など、 少し込み入ったアプリを作ろうと思ったら「こう書けば、こう表示される」の対応表だけではやりたいことを実現できない 場合も少なくありません。

これは、Flutter が提供するほとんどの Widget が「Widget のツリー構造」を前提に設計されているためです。

逆に言うと、この Widget のツリー構造がイメージできるようになれば、より効率的に Flutter に対する理解を深めることができ、「Flutter の考え方」に則って安全で無駄のないアプリが開発できるようになる はずです。

Widget ツリーを組み立てる

では、先ほどのサンプルコードを使いながら Widget ツリーがどのように組み立てられるのかを見ていきましょう。

基本は child / children プロパティに指定した通り

TextImage といった一部の Widget を除き、 Flutter が提供するほとんどの Widget には child または children プロパティが用意されていて、 Widget ツリーの構造もそれに従った形になります。

例えば、以下のようにサンプルコード(再掲)があったとき

Center(
  child: Column(
    children: const <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Text('This is my first app.'),
    ],
  ),
),

以下の図のような Widget ツリーができあがります。

image.png

  • ソースコードの先頭に Center があるためツリーの先頭は Center が配置され、
  • Centerchild プロパティに Column が渡されているため、ツリー上も Center の下(子 Widget)に Column が配置され、
  • 以下同様に Column の子 Widget として children プロパティの Text, SizedBox, Text が配置され、

という具合です。

まずはこれが Widget ツリーのイメージの基本となります。ソースコードと見比べて Widget ツリーのイメージを脳内で変換できるようになると良いでしょう。

StatelessWidget が入る場合

さて、以下のように child(もしくは children) に StatelessWidget が渡された場合はどうなるでしょうか。

Center(
  child: Column(
    children: <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      UserCard(),  // 自作の StatelessWidget
    ],
  ),
),

.. 省略..

// ユーザーのアイコンと名前を横並びにした Widget
class UserCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(Icons.people),
        Text('User name'),
      ],
    );
  }
}

このソースコードを Widget ツリーで表すと以下のようになります。(前の例と変わらない部分は半透明にしてあります)

image.png

見ての通り、自分で作った UserIcon もひとつの Widget としてツリーに組み込まれ、その子 Widget には build() メソッドで return した Widget(この例では Row)が続く形になっています。

Widget ツリーにおいて、 子 Widget は必ずしも child / children で指定したものとは限らない という点は覚えておいてください。

StatefulWidget をはさんだ場合

では、 StatefulWidget はどうでしょうか。以下のコードの場合を考えてみましょう。

Center(
  child: Column(
    children: <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Counter(),  // 自作 StatefulWidget
    ],
  ),
),

.. 省略..

// シンプルなカウンター Widget
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  var _counter = 0;
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('$_counter'),
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () => setState(() => _counter++),
        ),
      ],
    );
  }
}

このコードを Widget ツリーで表すと以下のようになります。

image.png

ということで、 StatelessWidget の場合と特に変わらないことが分かるかと思います。

一点だけ注意しなければならないのは、 StatefulWidget の場合は build() メソッドが Widget 自身(この例では Counter)ではなく State(この例では _CounterState)のに定義されている、という点です。

childchildren でも Widget の build() でもなく、StatefulWidget の場合は Statebuild() メソッドが return した Widget が自身の子 Widget となります。

Widget ツリーを構築する Element (一歩踏み込んだ話)

というわけで、Flutter のソースコードからどのように Widget ツリーが構築されるかをざっと見てみました。

特に、ある Widget の子 Widget がどう決まるかは

  • child / children プロパティに指定した Widget
  • Widget の build() メソッドで返却した Widget (StatelessWidget の場合)
  • State の build() メソッドで返却した Widget (StatefulWidget の場合)

などいくつかのパターンはあるものの、概ね Flutter で書いた Widget の入れ子がそのままツリー構造になっていることがイメージできたのではないかと思います。

さて、ここにリストアップした「この場合は、この Widget を子として配置する」の判断はどこでやっているのでしょうか。

その答えは、 Flutter においてとても重要な役割を持っている Element というクラスのソースコードに書かれています。

実はここまで説明してきた「Widget ツリー」というのはあくまで「イメージ」であり、必ずしも Widget 同士が親(もしくは子) Widget を参照しているわけではありません。

実際は、 Widget 1つひとつが Element という別のクラスと紐づいており、 Element 同士が親子の参照を保持する「Element ツリー」を構築することで Flutter における「ツリー構造」が成り立っています。1 その様子を表したのが以下のイメージです。

image.png

この「Elementツリー」を理解することは Flutter のより深い理解につながりますが、一方で Flutter は "Everything is a Widget" のスローガンの通り Widget さえ理解していればある程度のアプリが開発できるようにデザインされたフレームワークですので、今の段階で Element まで踏み込む必要はないでしょう。

もしここまでの内容が問題なく理解できて、さらに Element についても理解したいという方向は、Element ツリーについて詳しく解説した以下の記事(と、その参考記事)を読んでみてください。

【Flutter】Navigator.of(context) から理解する 3つのツリー | Zenn

実は Text は StatelessWidget

さて、これで Widget ツリーの基本を一通り説明してきましたが、最後に1つ、 Text は StatelessWidget のサブクラスである という点について説明します。

詳しくは別記事 【Flutter】Text とは何か を読んでいただければと思いますが、 Text も StatelessWidget のサブクラスですので、 StatelessWidget をはさんだ場合 で説明した通り build() メソッドの中で他のいくつかの Widget を組み立てて return し、それが Widget ツリーに反映されています。

そのことを考慮して最初の Widget ツリーの図を正確に書くと以下のようになります。

image.png

図の通り、 Text の下に SemanticsExcludeSemantics、さらに RichText といった別の Widget がつながっていることが読み取れます。

また、 SemanticsExcludeSemantics は Text の semanticsLabel プロパティに何も指定しなかった場合は省略するような記述も、 Text の build() メソッドの処理として書かれています。

このように、一見ひとつの Widget でも、実はその Widget が build() メソッドを持っていて(つまり StatelessWidget / StatefulWidget のサブクラスで)、私たちアプリ開発者が作る StatelessWidget / StatefulWidget と同様、他のいくつもの Widget を組み合わせてツリー上に配置する場合が少なからず存在します。例えば MaterialAppScaffold などは、大量の Widget を build() で返却する Widget の例です。

ただし、ここまで正確に Widget ツリーを把握しなければならない場面はそれほどありません。開発ツール等で実際の Widget ツリーを細かく確認するようになったときに、「Flutter 標準の Widget は他の Widget の組み合わせでできている場合がある」ことを思い出す程度で良いでしょう。

まとめ

多くの初学者にとって Flutter で UI の構築方法を学ぶ際、まずやるのは「どの Widget をどのように書けばどう表示されるか」を体験することだと思います。

一方で、Flutter の仕組みを少し詳しく知ろうと思ったとき、公式ドキュメントを含めほとんどの記事は Widget がツリー構造であることを前提に説明されています。また、 Provider を使ったり Navigator で画面遷移する仕組みなどもこの Widget ツリーについてのイメージがあるかどうかで理解度が大きく変わります。

まずは自分の書いたプログラムがどのような Widget ツリーを構築するのかイメージできるようになることで、そのような「一歩踏み込んだ」説明を理解する土台を作ると、学習効率の観点からも良いと思います。


  1. 話が複雑になるため省略していますが、Element にもいくつかの種類があります。 

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

【Android】Code Reformatに関するメモ

XML

  • タグの一番初めの行を改行していてもReformatされないようにする:
    Code Style→XML→Other→Keep line breaksにチェックを付ける

  • スラッシュの手前にスペースを入れない:
    Code Style→XML→Other→In empty tagのチェックを外す

スクリーンショット 2021-01-18 15.33.09.png

Kotlin

  • コメントの前の行を勝手に改行させない:
    Code Style→Kotlin→Blank Lines→Before declaration with comment or annoationを0にするスクリーンショット 2021-01-18 15.41.49.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android初学者 adbコマンドでターミナルから操作してみた

adbコマンドを使ってターミナルからAndroid端末を操作してみた。

インテント直で投げる

adb shell am start -n パッケージ名/アクティビティクラス

adb shell am start -n com.example.bookmanager_android/.MainActivity

上記を叩くと端末を触らなくてもbookmanagerアプリのMainActivityが開く。

Security exception: Permission Denialが出た場合
AndroidManifestファイルの開きたいアクティビティ部分にandroid:exported="true"を追加する。

端末にまかせる

adb shell am start -a アクション -d URL

adb shell am start -a android.intent.action.VIEW -d http://www.google.com/

上記を叩くと端末を触らなくてもgoogleサイトが開く。

おまけ

アプリ側でクエリをもってるとき

adb shell am start -a android.intent.action.VIEW" -d "testapp.v1://test?open=item\&id=2

testapp.v1://test?open=item&id=2 ⇔ https://www.test.com/jp/item/2 が開く

参考サイト

https://qiita.com/tkc_tsuchiya/items/1fd9953bc1337150dff1
ありがとうございました!

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

Windows10 adbコマンドを実行できるようにする

目的

  • Windows10のPCでadbコマンドを実行できるようにする方法をまとめる

方法

  1. 下記にアクセスする。
  2. 「DOWNLOAD ANDROID STUDIO」をクリックする。

    キャプチャ.PNG

  3. 利用規約に目を通し同意するなら「上記の利用規約を呼んだうえで利用規約に同意します。」にチェックを入れ「ダウンロードする」をクリックする。

    キャプチャ (1).PNG

  4. 「platform-tool_rXX.X.X-windows.zip」というファイルがインストールされるのでダウンロードが完了したら解凍する。

  5. 解凍したフォルダの中にある「platform-tools」のフォルダを任意のディレクトリに移動する。

  6. 画面左下のWindowsマークを右クリックし「システム」をクリックする。

  7. 「関連設定」にある「システムの詳細設定」をクリックする。

  8. 「環境変数」をクリックする。

    キャプチャ (2).PNG

  9. 「システム環境変数」の中にある「Path」選択して「編集」をクリックする。

    キャプチャ (3).PNG

  10. 先程設置した「platform-tools」フォルダをエクスプローラで開く。

  11. ファイルの階層を表示している部分をクリックしてパスをコピーする。

    キャプチャ (6).PNG

  12. 「新規」をクリックする。

    キャプチャ (5).PNG

  13. コピーしたパスを貼り付ける。貼り付けたら「OK」をクリックする。

  14. コマンドプロンプトを開き、下記コマンドを実行する。

    adb
    
  15. コマンドが正常に実行できればadbコマンドが実行できるようになっている。

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

kotlinx-coroutines-testのrunBlockingTestについて

前回に引き続きCoroutinesの単体テストの話です。

[https://iwsksky.hatenablog.com/entry/2020/12/09/014603:title]

今回はrunBlockingTest[1]について取り扱いたいと思います。

⚠理解が曖昧な状態で記述している可能性があります、間違いがあれば訂正お願いします。

環境

implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.20"

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9"

runBlockingTestとは

kotlinx-coroutines-testに含まれるAPIでrunBlocking同様に現在のスレッドをブロックしてコルーチンを起動することが可能。

@ExperimentalTime
@Test
fun exampleTest() = runBlocking {
   val deferred = async {
       println("SEC1 is " + measureTime { delay(10_000) })
       println("SEC2 is " + measureTime { async { delay(10_000) }.await()})
   }
   deferred.await()
}

// sec1 is 10.0s
// sec2 is 10.0s

上記例ではrunBlockingに渡しているブロックで2度コルーチンが起動されておりそれぞれで10secディレイを入れているため以下のような出力になる。

次にrunBlockingをrunBlockingTestに変更した場合

ExperimentalTime
@Test
fun exampleTest() = runBlockingTest {
   val deferred = async {
       println("SEC1 is " + measureTime { delay(10_000) })
       println("SEC2 is " + measureTime { async { delay(10_000) }.await()})
   }
   deferred.await()
}

// sec1 is 8.78ms
// sec2 is 1.51ms

delayが即時実行され処理が完了していることがわかる。

runBlockingTestの用途

runBlockingTestが導入された背景はTesting Coroutines on Android (Android Dev Summit '19)[2]とKotlinConf 2019: Testing with Coroutines by Sean McQuillan[3]が詳しい。Android Dev Summitでは良い単体テストは速く(fast)、信頼性があり(reliable)、独立している(isolated)と言われており、runBlockingTestは特に速さと信頼性の観点で導入されたように思う。

ではrunBlockingTestを利用したいのはどういう場合だろうか。

普段ViewModelが呼び出すRepositoryのsuspend関数は実行結果をmockすることが多くあまりピンと来なかったが、KotlinConfのプレゼンテーションではsuspend関数のタイムアウト処理をテストするケースが例として挙げられていた。例えば以下のようなタイムアウトが設定されたsuspend関数があった場合、これをrunBlockingで実行すると5秒待つ必要がある。

suspend fun foo(resultDeferred: Deferred<Foo>) {
    try {
        withTimeout(5_000) {
            resultDeferred.await()
        }
    } catch (e: Exception) {
        println("e: ${e}")
        throw FooException()
    }
}

@ExperimentalCoroutinesApi
@Test(expected = FooException::class)
fun testFooWithTimeout() = runBlocking {
    val uncompleted = CompletableDeferred<Foo>() // this Deferred<Foo> will never complete
    foo(uncompleted)
}

一方同じ処理をrunBlockingTestで書くと以下のようになり即座にテストが通る、またDelayControllerによりCoroutineDispatcherのvirtual timeを変更することでtimeOutが絡むテストが容易に記述できる。

// success
@ExperimentalCoroutinesApi
@Test(expected = FooException::class)
fun testFooWithTimeout() = runBlockingTest {
    val uncompleted = CompletableDeferred<Foo>() 
    launch {
        foo(uncompleted)
    }
    advanceTimeBy(5_000)
    uncompleted.complete(Foo("bar"))
}

// failure
@ExperimentalCoroutinesApi
@Test(expected = TitleRefreshError::class)
fun testFooWithTimeout() = runBlockingTest {
    val uncompleted = CompletableDeferred<Foo>() 
    launch {
        foo(uncompleted)
    }
    advanceTimeBy(6_000)
    uncompleted.complete(Foo("bar"))
}

補足

ここまでrunBlockingTestの用途について書いてみたが現状ExperimentalCoroutinesApiであることに加えて、いくつかクリティカルに思えるissueが上がっていたため導入については要検討という印象である。続報に期待したい。

AbstractMethodError when use withTimeout

[https://github.com/Kotlin/kotlinx.coroutines/issues/2307:title]

2021/01/18時点で最新版1.4.2のkotlinx-coroutines-testを利用すると発生するエラー

Provided example test for withTimeout fails

[https://github.com/Kotlin/kotlinx.coroutines/issues/1390:title]

ReadMeのExample[5]をそのまま書くと発生するエラー

Replace TimeoutCancellationException with TimeoutException

上述のsuspend fun foo でTimeoutExceptionをそのままthrowせずにtry/catchしてFooExceptionを投げ直している理由。TimeoutCancellationException is CancellationException, thus is never reported. とのこと。

[https://github.com/Kotlin/kotlinx.coroutines/issues/1374:title]

Use Kotlin Coroutines in your Android App

[https://developer.android.com/codelabs/kotlin-coroutines#9:title]

runBlockingTest is experimental, and currently has a bug that makes it fail the test if a coroutine switches to a dispatcher that executes a coroutine on another thread. The final stable is not expected to have this bug.

参考

[1] [https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-blocking-test.html:title]

[2]

[https://www.youtube.com/watch?v=KMb0Fs8rCRs&t=469s&ab_channel=AndroidDevelopers:embed:cite]

[3]

[https://www.youtube.com/watch?v=hMFwNLVK8HU&feature=emb_title&ab_channel=JetBrainsTV:embed:cite]

[4] [https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test#testing-withtimeout-using-runblockingtest:title]

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