- 投稿日:2021-01-18T23:49:02+09:00
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.gradleallprojects { 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.gradledependencies { 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.ktoverride 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">最後に
経路検索が結構早くて驚きました。
参考サイトをベースに改造して利用したり、ほかの実行もしたので、その辺は別の記事で投稿したいと思います。
- 投稿日:2021-01-18T23:49:02+09:00
ハッカソンで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.gradleallprojects { 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.gradledependencies { 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.ktoverride 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">最後に
経路検索が結構早くて驚きました。
参考サイトをベースに改造して利用したり、ほかの実行もしたので、その辺は別の記事で投稿したいと思います。
- 投稿日:2021-01-18T22:54:58+09:00
AAC の ViewModel で View のライフサイクルとリンクさせる時は OnLifecycleEvent でフックするといいよね、と言う話
タイトルがここで言いたい事の全てです
Android における ViewModel 側で onCreate 等のライフサイクルが走った時に何かしたい、みたいなシーンは結構あると思います。
(例えばユーザ情報を fetch する、とか)そういった時どうやって ViewModel 側にイベントを通知するかと言うと
SampleActivity.ktclass SampleActivity: AppCompatActivity() { private val viewModel: SampleViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) viewModel.onViewCreated() } }SampleViewModel.ktclass SampleViewModel: ViewModel() { fun onViewCreated() { // API 叩いたりレスポンスを LiveData に流したり etc... } }とすると思います。一番シンプルです。
しかし、せっかく Android の Architecture Component です。
ViewModel の初期化やリソースの解放なんかを他のクラスに任せるのはナンセンスな気がしました。
個人的に View -> ViewModel の方向性は少なくしてあげて、View は流れてくるデータをバインドするだけにしたい。ViewModel の LiveData を observe してから onCreate を呼んで...
みたいな事を View 側が考慮するのはなんだかなぁ、です
ViewModel 側でライフサイクルを考慮してあげた方がスッキリしませんか?と、言うわけで ViewModel 側で紐づく View のライフサイクルをフックしてあげましょう。
ViewModel で Lifecycle.Event をフックする
ライフサイクル対応コンポーネントによるライフサイクルへの対応 | Android デベロッパー | Android Developers
ViewModel に LifecycleObserver を implement しましょう。
ViewModel が View のライフサイクルにリンクされて画面が表示される度にリロードとかそんな事ができます。SampleViewModel.ktclass SampleViewModel: ViewModel(), LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun onViewCreated() { // なんやかんや } }SampleActivity.ktclass SampleActivity: AppCompatActivity() { private val viewModel: SampleViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) } }実際には Dagger を使ったりしていると、こんな事をして上げたりするかもしれませんね。
SampleActivity.ktinline 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 側は通知されてくるデータに専念できそうです
ライフサイクルのオーバーライドと 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.javafinal 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.javapublic 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.javavoid 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.javafinal 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.javapublic 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 が走るみたいな流れになりますね。
なので、通知順は以下になります。
Conclusion
と、いうわけで
ViewModel でOnLifecycleEvent
をフックした方が、View 側でオーバーライドしたライフサイクルで viewmodel.on~ みたいに呼ぶより View 側が考慮する事がなくなってスッキリした気がしますね。今回の話は ViewModel に限った話ではありませんが、
オーバーライドしたライフサイクル内であれこれやりすぎた結果
うわっ...私の Activity、太りすぎ...?
みたいな事にならない様上手く LifecycleObserver を使って責任を分散させましょう!これは個人的な意見なので誰かの参考になればと思います。
後、いろいろ間違ってたりしたらご指摘いただけると嬉しいです...参考
- 投稿日:2021-01-18T22:54:58+09:00
AAC の ViewModel で View のライフサイクルとリンクさせる時は Lifecycle.Event をフックするといいよね、と言う話
タイトルがここで言いたい事の全てです
Android における ViewModel 側で onCreate 等のライフサイクルが走った時に何かしたい、みたいなシーンは結構あると思います。
(例えばユーザ情報を fetch する、とか)そういった時どうやって ViewModel 側にイベントを通知するかと言うと
SampleActivity.ktclass SampleActivity: AppCompatActivity() { private val viewModel: SampleViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) viewModel.onCreate() } }SampleViewModel.ktclass SampleViewModel: ViewModel() { fun onCreate() { // API 叩いたりレスポンスを LiveData に流したり etc... } }とすると思います。一番シンプルです。
しかし、せっかく Android の Architecture Component です。
ViewModel の初期化やリソースの解放なんかを他のクラスに任せるのはナンセンスな気がしました。
個人的に View -> ViewModel の方向性は少なくしてあげて、View は流れてくるデータをバインドするだけにしたい。ViewModel の LiveData を observe してから onCreate を呼んで...
みたいな事を View 側が考慮するのはなんだかなぁ、です
ViewModel 側でライフサイクルを考慮してあげた方がスッキリしませんか?と、言うわけで ViewModel 側で紐づく View のライフサイクルをフックしてあげましょう。
ViewModel で Lifecycle.Event をフックする
ライフサイクル対応コンポーネントによるライフサイクルへの対応 | Android デベロッパー | Android Developers
ViewModel に LifecycleObserver を implement しましょう。
ViewModel が View のライフサイクルにリンクされて画面が表示される度にリロードとかそんな事ができます。SampleViewModel.ktclass SampleViewModel: ViewModel(), LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun onViewCreated() { // なんやかんや } }SampleActivity.ktclass SampleActivity: AppCompatActivity() { private val viewModel: SampleViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) } }実際には Dagger を使ったりしていると、こんな事をして上げたりするかもしれませんね。
SampleActivity.ktinline 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 側は通知されてくるデータに専念できそうです
ライフサイクルのオーバーライドと 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.javafinal 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.javapublic 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.javavoid 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.javafinal 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.javapublic 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 が走るみたいな流れになりますね。
なので、通知順は以下になります。
Conclusion
と、いうわけで
ViewModel でOnLifecycleEvent
をフックした方が、View 側でオーバーライドしたライフサイクルで viewmodel.on~ みたいに呼ぶより View 側が考慮する事がなくなってスッキリした気がしますね。今回の話は ViewModel に限った話ではありませんが、
オーバーライドしたライフサイクル内であれこれやりすぎた結果
うわっ...私の Activity、太りすぎ...?
みたいな事にならない様上手く LifecycleObserver を使って責任を分散させましょう!これは個人的な意見なので誰かの参考になればと思います。
後、いろいろ間違ってたりしたらご指摘いただけると嬉しいです...参考
- 投稿日:2021-01-18T19:02:16+09:00
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を開いてみる。上の画像のようにインストールしてもアプリ一覧には追加されないため開けない。
設定を開く
アプリを開く
インストールしたアプリが出てくる。
今回インストールしたアプリを開く
この場合CPU-Zを開くアプリの情報が出るため、開くでアプリが立ち上がる。
これでadbでインストールしたアプリが開ける。
最後に
昔のAndroidTVだったら「インストール済みのアプリ」という項目があり、アプリが簡単に開けたが今のLauncherだとアプリ一覧には追加されないようなので、開くのが一苦労。
標準のLauncherでインストールしたアプリを一覧についかされる方法を探りたいと思う。
- 投稿日:2021-01-18T18:55:10+09:00
ピンあてゲーム「Hit and Blow」をFlutterでリリースした話【個人開発】
概要
- アプリの説明
- 作ろうと思ったきっかけ
- Hit and Blowってどんなゲーム?
- Flutterでどう実現するか
- リリース準備について!
- 現状・今後実現していきたいこと、意気込み
- 最後に
はじめましてtomoと申します。
インターン生として大阪のスタートアップでエンジニアをしながら個人開発をしている大学生です。
Qiita初投稿になります!
自分と同じようにアプリを作る方またこれからアプリを作りたいと思っている方に向けて何かのヒントになれば嬉しいです!リリースしたアプリについて
Hit and Blow(ヒット&ブロー)
というピンの色を推測するゲームです。シングルプレーヤーゲームでアプリ自体は3ヶ月ほど前にリリースしました。
Nintendo Switchの世界のアソビ大全51というゲームに搭載されているのでご存知のかたもいるかと思います。
Numer0n(ヌメロン)という数あてゲームが日本だと有名かと思いますが、それとほぼ同等のゲームになります。
なぜこのアプリを作ったか
このアプリはFlutterを使って開発しました。
Flutter自体は業務でよく触っていましたんですが、ちょうど開発しようと思っていた時期は業務でFlutterを触ることが少なくなり、勉強も含め何かアプリを個人で作ろうと思ったのがきっかけです。
ちょうどそのタイミングでNintendo Switchの世界のアソビ大全51というゲームが人気を博し、YouTubeで様々な動画が公開されていました。
そのアソビ大全51の一つにこの「Hit and Blow」というゲームがあり、このくらいのシンプルなゲームならUnityなどのゲームエンジン等必要なくFlutterだけで作れるじゃんと思い作りはじめました。
朝と夜の時間を使って一週間ほどで開発し、なんやかんや(アイコン作ったり、ストアに申請したり)あって二週間ほどでリリースしました。
Hit and Blowとはどのようなゲームなのか。
Hit and Blow(ヒット & ブロー) はピンの色を当てるゲームです。マスターマインドとも言われてるみたいですね。
■ ヒット&ブローの遊び方 ■(Wikiより)
プレイヤーは、出題者と解答者に分かれる。
(このアプリの場合回答者がプレーヤーになります。)
- 出題者は解答者から見えないように、ピンを4本選び並べる。
- 解答者は、配置を予想する。
- 出題者は解答者の予想を判定する。
- 位置も色も正しいピン(これをヒットという)があったら赤いピンを立てる。
- 色は正しいが位置が違うピン(これをブローという)があったら白いピンを立てる。
- 2-3 を繰り返し、赤いピンが4本立つ(配置を完全に答える)までの回数で勝負を決める。
という推理力が問われるゲームになります。
このようにルールが非常にシンプルなのでプログラムもしやすいです。
アプリの要件としては
- ランダムな4つのピンを決める。
- ユーザーに4つのピンを答えさせる。
- ランダムに生成したピンとユーザーが回答したピンを比較し、判定する。
最初に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です。
操作手順
- 下のピンをドラッグして上部の設置箇所にドラッグします。
- 4つのピンを設置すると表示されるOKボタンを押して確定
- Hit Blowの結果がモーダルで表示される。(正解の場合はクリア画面)
このようにUIは非常にシンプルかつ、カラフルに仕上げています。
ピンの設置にはFlutterのDraggableとDragTargetを使っています。
DraggableをDragTargetにドロップすることで、Widgetを設置したり、任意の値を渡したりできます。実際のコードです。
Draggableに数値を持たせていて、DragTargetにドロップすることで配列に数値をいれるようになっています。便利ですね。
詳しくはこちらのQiita記事を参考にしました。
// ピン 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で背景色を白に設定しました。
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に突っ込んでいい感じに文字を書いて、目を引く背景色を設定して完成です
説明文の作成
説明文は英語でHit and Blowのゲームの説明を書きました。Hit and Blow自体海外のゲームですし、非常にシンプルなのでターゲットは広い方がいいだろうということで全て英語で書いています。
文章に関しては、Googleさんに「Hit and Blow Rule」とかで調べて出てきたものを参考に書きました。
既存のゲームなので深く説明文書く必要なくて楽ですね。
現状の数値・今後の展望
現状の数値
ダウンロード数は累計で300くらいです。
下のグラフを見てもらったらわかるように年末年始は数字が伸びてますね。こういう暇つぶしゲームは可処分時間が増えるとダウンロードも増える傾向にあります。
半分は日本でのダウンロードですが、残り半分はアメリカやイギリスなど海外でのダウンロードになっています。英語で説明文を書いた甲斐がありました。
もうちょっと海外からのダウンロードも伸びていい気はしますが、ASOとかで最適化されていないんですかね。もしくはHIt and Blowの知名度があまりないんでしょうか。
広告はゲーム終了後に全画面広告を設置しています。全画面なので、バナー広告とは違い単価が大きいです。先月の売上は年末に跳ねましたね。
ゲーム系は動画広告が多いので、全画面広告ではなく動画広告に変更するのもありかなと思っています。
現状の課題・今後の展望
現状の課題はなんと言ってもユーザーの減少数が多いことです。今のアプリは非常にシンプルなので、ユーザーの定着率が悪くアンインストール数が増えてしまします。
改善策として、
①ピンの数や重複ありorなしなどを自ら設定できるようにする
②オンライン対戦をできるようにするを考えています。
①に関しては判定処理などが少し複雑になりますが、現実的ですね。
②に関しては課題が多そうです。ユーザーが集まらないとマッチング自体ができないので実装するにしてもタイミングをしっかり考慮する必要がありそうです。最後に
Flutterでシンプルなゲームアプリを作ることができました!
面白そうだと感じてくれたら是非一度ダウンロードしてみてください!
コメント欄などで感想をいただけると嬉しいです!
- 投稿日:2021-01-18T18:02:38+09:00
【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[] valuesSensorEventListener
センサー関連のイベントを取得するためのイベントリスナー。SensorManagerのメソッドでこのイベントリスナーの登録や解除をすると、センサーからの情報を取得できる。
SensorEventListenerのメソッド// センサーの精度が変わったときに呼び出されるメソッド onAccuracyChanged(Sensor sensor, int accuracy) // センサーの値が変わったときに呼び出されるメソッド onSensorChanged(SensorEvent event)参考文献
- 投稿日:2021-01-18T17:16:04+09:00
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
エラーが発生した、複数の画面でデータを共有する方法を調べたらInheritedWidget
やProvider
といった知らない仕組みが出てきて混乱した、など、 少し込み入ったアプリを作ろうと思ったら「こう書けば、こう表示される」の対応表だけではやりたいことを実現できない 場合も少なくありません。これは、Flutter が提供するほとんどの Widget が「Widget のツリー構造」を前提に設計されているためです。
逆に言うと、この Widget のツリー構造がイメージできるようになれば、より効率的に Flutter に対する理解を深めることができ、「Flutter の考え方」に則って安全で無駄のないアプリが開発できるようになる はずです。
Widget ツリーを組み立てる
では、先ほどのサンプルコードを使いながら Widget ツリーがどのように組み立てられるのかを見ていきましょう。
基本は
child
/children
プロパティに指定した通り
Text
やImage
といった一部の 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 ツリーができあがります。
- ソースコードの先頭に
Center
があるためツリーの先頭はCenter
が配置され、Center
のchild
プロパティに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 ツリーで表すと以下のようになります。(前の例と変わらない部分は半透明にしてあります)
見ての通り、自分で作った
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 ツリーで表すと以下のようになります。
ということで、 StatelessWidget の場合と特に変わらないことが分かるかと思います。
一点だけ注意しなければならないのは、 StatefulWidget の場合は
build()
メソッドが Widget 自身(この例ではCounter
)ではなくState
(この例では_CounterState
)のに定義されている、という点です。
child
やchildren
でも Widget のbuild()
でもなく、StatefulWidget の場合はState
のbuild()
メソッドが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 その様子を表したのが以下のイメージです。この「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 ツリーの図を正確に書くと以下のようになります。
図の通り、 Text の下に
Semantics
とExcludeSemantics
、さらにRichText
といった別の Widget がつながっていることが読み取れます。また、
Semantics
とExcludeSemantics
は Text のsemanticsLabel
プロパティに何も指定しなかった場合は省略するような記述も、 Text のbuild()
メソッドの処理として書かれています。このように、一見ひとつの Widget でも、実はその Widget が
build()
メソッドを持っていて(つまり StatelessWidget / StatefulWidget のサブクラスで)、私たちアプリ開発者が作る StatelessWidget / StatefulWidget と同様、他のいくつもの Widget を組み合わせてツリー上に配置する場合が少なからず存在します。例えばMaterialApp
やScaffold
などは、大量の Widget をbuild()
で返却する Widget の例です。ただし、ここまで正確に Widget ツリーを把握しなければならない場面はそれほどありません。開発ツール等で実際の Widget ツリーを細かく確認するようになったときに、「Flutter 標準の Widget は他の Widget の組み合わせでできている場合がある」ことを思い出す程度で良いでしょう。
まとめ
多くの初学者にとって Flutter で UI の構築方法を学ぶ際、まずやるのは「どの Widget をどのように書けばどう表示されるか」を体験することだと思います。
一方で、Flutter の仕組みを少し詳しく知ろうと思ったとき、公式ドキュメントを含めほとんどの記事は Widget がツリー構造であることを前提に説明されています。また、
Provider
を使ったりNavigator
で画面遷移する仕組みなどもこの Widget ツリーについてのイメージがあるかどうかで理解度が大きく変わります。まずは自分の書いたプログラムがどのような Widget ツリーを構築するのかイメージできるようになることで、そのような「一歩踏み込んだ」説明を理解する土台を作ると、学習効率の観点からも良いと思います。
話が複雑になるため省略していますが、Element にもいくつかの種類があります。 ↩
- 投稿日:2021-01-18T15:57:56+09:00
【Android】Code Reformatに関するメモ
- 投稿日:2021-01-18T14:38:41+09:00
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=2testapp.v1://test?open=item&id=2 ⇔ https://www.test.com/jp/item/2 が開く
参考サイト
https://qiita.com/tkc_tsuchiya/items/1fd9953bc1337150dff1
ありがとうございました!
- 投稿日:2021-01-18T10:07:25+09:00
Windows10 adbコマンドを実行できるようにする
目的
- Windows10のPCでadbコマンドを実行できるようにする方法をまとめる
方法
- 下記にアクセスする。
「DOWNLOAD ANDROID STUDIO」をクリックする。
利用規約に目を通し同意するなら「上記の利用規約を呼んだうえで利用規約に同意します。」にチェックを入れ「ダウンロードする」をクリックする。
「platform-tool_rXX.X.X-windows.zip」というファイルがインストールされるのでダウンロードが完了したら解凍する。
解凍したフォルダの中にある「platform-tools」のフォルダを任意のディレクトリに移動する。
画面左下のWindowsマークを右クリックし「システム」をクリックする。
「関連設定」にある「システムの詳細設定」をクリックする。
「環境変数」をクリックする。
「システム環境変数」の中にある「Path」選択して「編集」をクリックする。
先程設置した「platform-tools」フォルダをエクスプローラで開く。
ファイルの階層を表示している部分をクリックしてパスをコピーする。
「新規」をクリックする。
コピーしたパスを貼り付ける。貼り付けたら「OK」をクリックする。
コマンドプロンプトを開き、下記コマンドを実行する。
adbコマンドが正常に実行できればadbコマンドが実行できるようになっている。
- 投稿日:2021-01-18T01:39:14+09:00
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.51msdelayが即時実行され処理が完了していることがわかる。
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.
参考
[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]