- 投稿日:2019-07-09T22:52:01+09:00
RyzenでのAVDでつまづいた話
Xamarin.Formsで初めてモバイルアプリ開発をしようとした矢先、いきなり大量のエラーメッセージが...
Android Studioはどうだ??と試してみてもエラーメッセージの嵐...
初めてのモバイル開発がこんなスタートだったら萎えますね。このエラーを治すために2日間ハマってしまいました...環境
CPU:Ryzen1700
OS:Windows10 Pro-バージョン1903問題
Xamarin.Forms,Android Studioでエミュレーターでのデバッグができない...
解決
この問題はRyzenシリーズを使っているWindows10バージョン1903に起こるバグらしいです。解決策は単純。前のバージョンに戻す、またはWindows Insider Previewに登録するかです。
僕の場合、バージョン1903にしてから10日以上たっていてバージョンダウンできなかったので泣く泣くInsider Previewに...
プレリリースのバージョンなので不安定だけど、とりあえず無事(?)にAVDができるようになりました。
早くWindows公式バージョンでもこのバグ直してほしいですね....Microsoftさん頼みます!
- 投稿日:2019-07-09T22:32:15+09:00
ナビゲーションドローワーのUIテスト
EspressoでUIテストを実施していて、ふと、ナビゲーションドローワーがある場合はどうなるんだろうと思い、やってみました。
dependencyの追加
ナビゲーションドローワーのアクションは
espresso-contribのライブラリに入っているので追加しました。
その時、バージョン違いでコンフリクトが起こったので、重複しているものは除外します。build.gradle(app)androidTestImplementation ('com.android.support.test.espresso:espresso-contrib:3.0.2') { exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-v4' exclude group: 'com.android.support', module: 'recyclerview-v7' exclude group: 'com.android.support', module: 'appcompat-v7' exclude group: 'com.android.support', module: 'design' exclude group: 'com.android.support', module: 'support-compat' exclude group: 'com.android.support', module: 'support-core-utils' }これでボタンクリックと同じようにナビゲーションドローワーを開くアクションが使えるようになります。
テストの記載
NavigatinViewは下記になります。
activity_main.xml<android.support.design.widget.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" app:headerLayout="@layout/nav_header_main" app:menu="@menu/activity_main_drawer" />MainActivityTest@Test fun navigationMenu_updatetime_exist() { onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) Thread.sleep(1500) onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.nav_slideshow)) Thread.sleep(1500) onView(ViewMatchers.withId(R.id.updateTimeLabel)).check(matches(not(doesNotExist()))) }ナビゲーションドローワーを開くアクションは
DrawerActions.open()になります。
開いたら、NavigationViewActions.navigateTo(R.id.nav_slideshow)の部分で、開いたドローワーに存在するメニューをタップしています。実際にテストしてみて、思ったより画面の操作は担保できるなあと感じました。
参照
- 投稿日:2019-07-09T17:30:57+09:00
EspressoでRecyclerViewのチェックを行うカスタムMatcherを作成
背景
RecyclerViewのUIテストを作ろうとすると、どうしても標準APIでは満たせないテスト項目が出てきます。
https://qiita.com/AAkira/items/26ee7b18b11635cfc621 を参考にさせていただき、独自のRecyclerViewMatcherを作成しました。
下記の項目ではそれぞれバラバラに記載していますが、全てのメソッドは1つのRecyclerViewMatcherの中に定義されています。実装
【isExist】RecyclerViewの指定した位置に、指定したViewHolderが存在すること
RecyclerViewMatcher.ktobject RecyclerViewMatcher { inline fun <reified T : RecyclerView.ViewHolder> isExist(position: Int) = object : TypeSafeMatcher<View>() { override fun describeTo(description: Description) { description.appendText("RecyclerViewの指定した位置に、指定したViewHolderが存在すること") } override fun matchesSafely(view: View): Boolean { if (view !is RecyclerView) return false return view.findViewHolderForAdapterPosition(position) is T } } }【contains】RecyclerViewの指定した範囲内に、指定したViewHolderが存在すること
RecyclerViewMatcher.ktobject RecyclerViewMatcher { inline fun <reified T : RecyclerView.ViewHolder> contains(range: IntRange) = object : TypeSafeMatcher<View>() { override fun describeTo(description: Description) { description.appendText("RecyclerViewの指定した範囲内に、指定したViewHolderが存在すること") } override fun matchesSafely(view: View): Boolean { if (view !is RecyclerView) return false return range.any { position -> view.findViewHolderForAdapterPosition(position) is T } } } }【isVisible】RecyclerViewの指定した位置のViewHolderに、指定したViewが表示されていること
RecyclerViewMatcher.ktobject RecyclerViewMatcher { inline fun <reified T : RecyclerView.ViewHolder> isVisible(position: Int, @IdRes id: Int) = object : TypeSafeMatcher<View>() { override fun describeTo(description: Description) { description.appendText("RecyclerViewの指定した位置のViewHolderに、指定したViewが表示されていること") } override fun matchesSafely(view: View): Boolean { if (view !is RecyclerView) return false val viewHolder = view.findViewHolderForAdapterPosition(position) as? T ?: return false return viewHolder.itemView.findViewById<View>(id)?.visibility == View.VISIBLE } } }
- 投稿日:2019-07-09T15:32:31+09:00
Espressoを用いたUIテストで、指定のViewが表示されるまで待機する
背景
これまで、EspressoのUIテストにおけるネットワーク通信待機処理を
Thread.sleep()で行なっていました。
この方法では、通信が完了した後も指定した時間待つことになり、テスト実行に余計な時間が掛かってしまいます。
これを改善するために、今回はUiAutomatorを利用して指定したViewが表示されたタイミングで待機処理を終了する実装に置き換えました。実装
まずは、UiAutomatorの依存関係を追加します。
build.gradleandroidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'そして、Kotlinの拡張関数を用いてトップレベル関数として待機処理を実装します。
テスト設計にはPageObjectパターンを適用しているため、戻り値にはthisを指定してレシーバを返すようにしています。
※PageObjectパターンはDroidKaigi2019のこちらのセッションを参考にさせていただきました。(https://droidkaigi.jp/2019/timetable/70791/ )PageObjectExt.kt/** * [resId]に指定したViewが表示されるまで待つ * [timeout]はデフォルトで7秒 */ fun <T : Any> T.waitShown(resources: Resources, @IdRes resId: Int, timeout: Long = 7_000): T { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) .wait(Until.findObject(By.res( "[YOUR_APP_PACKAGE_NAME]", resources.getResourceEntryName(resId))), timeout) return this }テストメソッドで使うとこのようになります。
画面の挙動としては、SampleActivityで通信を行い、通信中はローディングが表示され、通信完了後にtarget_viewが表示されるイメージです。SampleActivityTest.kt@RunWith(AndroidJUnit4::class) @LargeTest class SampleActivityTest { @Rule @JvmField val activityRule: ActivityTestRule<SampleActivity> = ActivityTestRule(SampleActivity::class.java) @Test fun test() { val resources = activityRule.activity.resources SampleActivityPage .fetchData() // 時間の掛かる通信処理 .waitShown(resources, R.id.target_view) // `target_view`が表示されるまで待機 .assert() // 任意のアサーション } }解説
waitShownの処理を分解して見てみます。PageObjectExt.kt/** * [resId]に指定したViewが表示されるまで待つ * [timeout]はデフォルトで7秒 */ fun <T : Any> T.waitShown(resources: Resources, @IdRes resId: Int, timeout: Long = 7_000): T { // デバイスの状態を提供するクラス。ホームボタンなどの制御も可能。 val uiDevice: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) // UI要素を指定するクラス。パッケージ名とリソースID名で指定する。 val bySelector: BySelector = By.res("[YOUR_APP_PACKAGE_NAME]", resources.getResourceEntryName(resId)) // waitメソッドに渡す条件。最初に一致するUI要素(BySelector)を条件として返す。 val searchCondition: SearchCondition<UiObject2> = Until.findObject(bySelector) // 条件(SearchCondition)に一致するか、タイムアウトになるまで待機する。 uiDevice.wait(searchCondition, timeout) return this }
- 投稿日:2019-07-09T13:42:07+09:00
Android Kotlin+DataBinding+ViewModelでとりあえず動くモノを作る
概要
AndroidのDataBindingやViewModelの実装について手続き的な面でつまづいたことがあったので自分なりにまとめました!
「とにかく動かすための手続きが知りたい」という方に向けてサンプルコードを具体的に記述します。作るもの
ボタンのタップによって画像の表示⇔非表示がトグルするやつを作ります!
![]()
作りましょう
プロジェクトを作ろう
登場人物
プロジェクトを作ったらこの人達をいじっていきます!
- build.gradle(app)
- MainViewModel.kt
- MainActivity.kt
- layout/activity_main.xml
build.gradle(app)
dataBinding{ enabled = true }をいい感じの場所に追加します!
MainViewModel.kt
android.arch.lifecycle.ViewModelを継承したMainViewModelを新規作成します!
こいつにデータ(今回だとフラグ)を保持させます。
toggle()が呼び出される度に_flag.valueがtrue⇔falseと切り替わるイメージです。MainViewModel.ktclass MainViewModel : ViewModel() { private val _flag = MutableLiveData<Boolean>().apply { value = false } val flag: LiveData<Boolean> get() = _flag fun toggle() { _flag.postValue(!(flag.value ?: false)) } }activity_main.xml
レイアウトファイルと、ついでにstringsファイルを弄ります!
バインディングされたオブジェクトのgetterは勝手に剥いてくれるので、三項演算子の部分はviewModel.flagと書いています。
(viewModel.getFlag().getValue()でも動きます)activity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="com.rodentia6.sampledatabinding.MainViewModel"/> <import type="android.view.View"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.flag ? View.VISIBLE : View.GONE}" app:srcCompat="@android:drawable/star_on" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintBottom_toTopOf="@id/button" app:layout_constraintLeft_toLeftOf="parent" android:contentDescription="@null"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="80dp" android:text="@{viewModel.flag ? @string/kesu : @string/tukeru}" android:onClick="@{(v) -> viewModel.toggle()}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>values/strings.xml<resources> <string name="app_name">SampleDataBinding</string> <string name="tukeru">つける</string> <string name="kesu">けす</string> </resources>一旦リビルドをかける
DataBindingのクラスを自動生成してもらうために一旦リビルトします!(上メニューから Build > Rebuild Project)
何かが間違っていた場合はこの時点でエラーが発生します。MainActivity.kt
Activityのクラスを編集します!
ViewModelオブジェクトとDataBindingオブジェクトを作成して色々紐づけます。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // ViewModelオブジェクトを生成する val viewModelFactory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory() val viewModelProvider = ViewModelProvider(this, viewModelFactory) val viewModel: MainViewModel = viewModelProvider.get(MainViewModel::class.java) // 上記3行のコードは、build.gradle(app)にimplementation 'android.arch.lifecycle:extensions:1.1.1'を追加すると下記で代替できるようになる // val viewModel: MainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) // Bindingオブジェクトを生成する val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) // 色々紐付ける binding.viewModel = viewModel binding.lifecycleOwner = this } }完成!
おめでとうございます、完成です!起動して触ってみましょう!
画面を回転してもフラグの状態が維持されていることがわかりますね。
- 投稿日:2019-07-09T13:42:07+09:00
Android Kotlin+DataBinding+ViewModelでとりあえず動くモノを作るための手続き
概要
AndroidのDataBindingやViewModelの実装について手続き的な面でつまづいたことがあったので自分なりにまとめました!
「とにかく動かすための手続きが知りたい」という方に向けてサンプルコードを具体的に記述します。作るもの
ボタンのタップによって画像の表示⇔非表示がトグルするやつを作ります!
![]()
作りましょう
プロジェクトを作ろう
登場人物
プロジェクトを作ったらこの人達をいじっていきます!
- build.gradle(app)
- MainViewModel.kt
- MainActivity.kt
- layout/activity_main.xml
build.gradle(app)
dataBinding{ enabled = true }をいい感じの場所に追加します!
MainViewModel.kt
android.arch.lifecycle.ViewModelを継承したMainViewModelを新規作成します!
こいつにデータ(今回だとフラグ)を保持させます。
toggle()が呼び出される度に_flag.valueがtrue⇔falseと切り替わるイメージです。MainViewModel.ktclass MainViewModel : ViewModel() { private val _flag = MutableLiveData<Boolean>().apply { value = false } val flag: LiveData<Boolean> get() = _flag fun toggle() { _flag.postValue(!(flag.value ?: false)) } }activity_main.xml
レイアウトファイルと、ついでにstringsファイルを弄ります!
バインディングされたオブジェクトのgetterは勝手に剥いてくれるので、三項演算子の部分はviewModel.flagと書いています。
(viewModel.getFlag().getValue()でも動きます)activity_main.xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="com.rodentia6.sampledatabinding.MainViewModel"/> <import type="android.view.View"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.flag ? View.VISIBLE : View.GONE}" app:srcCompat="@android:drawable/star_on" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintBottom_toTopOf="@id/button" app:layout_constraintLeft_toLeftOf="parent" android:contentDescription="@null"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="80dp" android:text="@{viewModel.flag ? @string/kesu : @string/tukeru}" android:onClick="@{(v) -> viewModel.toggle()}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>values/strings.xml<resources> <string name="app_name">SampleDataBinding</string> <string name="tukeru">つける</string> <string name="kesu">けす</string> </resources>一旦リビルドをかける
DataBindingのクラスを自動生成してもらうために一旦リビルトします!(上メニューから Build > Rebuild Project)
何かが間違っていた場合はこの時点でエラーが発生します。MainActivity.kt
Activityのクラスを編集します!
ViewModelオブジェクトとDataBindingオブジェクトを作成して色々紐づけます。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // ViewModelオブジェクトを生成する val viewModelFactory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory() val viewModelProvider = ViewModelProvider(this, viewModelFactory) val viewModel: MainViewModel = viewModelProvider.get(MainViewModel::class.java) // 上記3行のコードは、build.gradle(app)にimplementation 'android.arch.lifecycle:extensions:1.1.1'を追加すると下記で代替できるようになる // val viewModel: MainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) // Bindingオブジェクトを生成する val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) // 色々紐付ける binding.viewModel = viewModel binding.lifecycleOwner = this } }完成!
おめでとうございます、完成です!起動して触ってみましょう!
画面を回転してもフラグの状態が維持されていることがわかりますね。
- 投稿日:2019-07-09T12:16:40+09:00
LiveData + Coroutine + RetrofitでAPI呼び出しを簡潔に実装する
はじめに
LivedataがCoroutineに対応したので、
これらを利用してretrofitを用いたAPIの呼び出しを書きました。使用ライブラリ
app.gradleimplementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha02" implementation "com.squareup.retrofit2:retrofit:2.6.0"実装
GitHubのAPIを用いてユーザー情報を取得するサンプルです。
ModelやDIの実装については割愛させていただきます。API定義
GithubApi.ktinterface GithubApi { @GET("/users/{user}") fun getUser(@Path("user") loginName: String): LiveData<GithubUser> }Retrofitを用いたAPI呼び出しの定義部分。返り値の型をLiveDataにしています。
API呼び出し
GithubRepository.ktclass GithubRepository(val githubApi: GithubApi) { fun getUser(loginName: String) = liveData { emitSource(githubApi.getUser(loginName)) } }上で定義したAPIの呼び出しです。
liveDataブロック内で受け取るliveDataを処理します。
emitSource()するとliveDataに値が流れ込みます。LiveDataの取得
GithubViewModel.ktclass GithubViewModel(application: Application, private val repository: GithubRepository) : AndroidViewModel(application) { fun getUser(loginName: String) = repository.getUser(loginName) }あとはViewModelやFragmentなどでLiveDataとして値を取得できるので、
observeしたりdatabindingに値をセットしたりして利用します。以上、Coroutine対応したLiveDataを用いてAPI呼び出しを実装する例を書きました。
参考
- 投稿日:2019-07-09T11:18:24+09:00
Flutterウィークリー #66
Flutterウィークリーとは?
FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/この記事は#66の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-66※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。
読み物&チュートリアル
#Hack19 - インターナショナルFlutterハッカソン結果
これで、 Flutter Hackathon Webサイトのすべての参加者を確認できます。また、参加した場合は、証明書を取り戻すことができます。FlutterとGoogle Cloud Platformが「学習」問題を解決するためにどのように連携したか
Tapas Adhikaryが彼女の娘がFlutterとGoogleクラウドを使用してライティングスキルを向上させるのに役立った経緯。Flutterスクロールアニメーションを作成する
https://itnext.io/creating-scroll-animations-in-flutter-74eb340627d9
Kenneth Reillyによるチュートリアルで、リストスクロールによるアニメーションの調整がいかに簡単かを示しています。プロバイダを使用した下部ナビゲーションバー
https://medium.com/flutterdevs/bottom-navigation-bar-using-provider-flutter-8b607beb2e5a
Ashish Rawatが、BottomNavigationBarでFlutter Providerパッケージを使用する方法を説明します。カスタムクリッパー:ウェーブカーブ - パート2
https://medium.com/@bhoomiharkhani22/custom-clipper-wave-curve-part-2-26cd54dc30f
Bhoomika HarkhaniはFlutter使ってクレイジーカーブを作り続けていますFlutterカスタムスティッキヘッダ
https://medium.com/flutter-community/custom-sticky-headers-in-flutter-248d3c26863
Dane Mackierのこの記事でスティッキヘッダーを使用する方法を学んでください。ディープリンクとFlutterアプリケーションそれらを適切に処理する方法。
Aleksandr Denisovが、 Flutter AndroidとiOSからのディープリンクを処理する方法についてこのチュートリアルを書きました。Flutterアニメーション例 - #2
https://medium.com/flutterdevs/example-animations-in-flutter-2-1034a52f795b
これらの基本的なアニメーションの例を使って、Mohak Guptaによるアニメーションの学習を続けてください。指紋と顔認識によるローカル認証
Daniele Cambiによるこのチュートリアルでは、データを保護するために指紋と顔認識を使用する方法を学びます。Flutter + MobX +チョッパー
https://medium.com/@rishabhv1509/flutter-mobx-chopper-8a1b0d7954a5
Rishabh Vermaによるアプリケーションで、Chopper(残りのクライアントライブラリ)とMobX(状態管理ライブラリ)を一緒に使用する。Flutter学習パス
https://medium.com/@kingwu/flutter-learning-path-d6b3b0235799
Wu Chun WaはFlutterための彼の学習経路を共有しています。Flutter :ステートフルウィジェットとステートレスウィジェット
https://medium.com/flutter-community/flutter-stateful-vs-stateless-db325309deae
Souvik Biswasは、ステートフルウィジェットとステートレスウィジェットの違いを説明します。Flutter :開発における自己署名SSL証明書の使用
Reme Le Haneは、 FlutterカスタムSSL証明書を使用する際に問題がありました。Flutter Proのようにネットワーク通話を処理する
https://medium.com/@xsahil03x/handling-network-calls-like-a-pro-in-flutter-31bd30c86be1
Sahil Kumarによる、すべてのネットワーク通話を処理するためのアプリの構成方法に関する素晴らしい記事。Flutterペイントする方法
https://medium.com/@studymongolian/how-to-paint-in-flutter-d18c6c26df10
Flutter開発時間を毎日10%節約するにはどうすればいいですか?
自動テストでCI / CDを使用することで、どのように時間が短縮され、より良い品質が得られるかを学ぶための数字をご覧ください。RouteObserverを使ってFlutter画面遷移を追跡する方法
David AnayaがFlutter RouteObserverとRouteAwareを使って自動的に画面遷移を追跡する方法を説明します。Flutter電池残量を確認する
https://medium.com/flutter-community/provide-battery-level-in-flutter-1b69fc4e6b2d
Dane Mackierは、バッテリーパーセンテージ値のストリームを提供するDeviceInformationSerivceを構築します。Flutterを学ぶためのリソース
https://medium.com/flutter-community/resources-to-learn-flutter-2ade7aa73305
LaraMartínは、 Flutterを学ぶのに最適なリソースのリストをまとめました。クロスプラットフォームのランディングページでのFlutterテーマの使用(Web、Android、iOS)
Priyanka Tyagiによるこの記事でコードの一部を変更するだけで、ランディングページの外観やテーマをすばやく変更する方法を学びましょう。ビデオ&メディア
Flutterカスタムリストビューカードを作成する方法
https://www.youtube.com/watch?v=1Y6o1DOUJcU
このビデオでは、カード用のカスタムウィジェットを作成し、それをカードのリストに使用する方法を紹介します。Dartストリーム - フォーカスのFlutter
このビデオでは、 Flutterストリームを使用したリアクティブコーディングの基本について説明します。Flutter Flare Basics - GiphyのNavメニューを作ろう
https://www.youtube.com/watch?v=hwBUU9CP4qI&feature=youtu.be
Giphyモバイルアプリから素晴らしいナビゲーションメニューを再作成することで、 Flutter Flare 2Dでベクトルアニメーションの基本をマスターしましょう。プレースホルダー(今週のFlutterウィジェット)
https://www.youtube.com/watch?v=LPe56fezmoo
作曲が終わっていないウィジェットの代わりになるものが必要ですか。プレースホルダーはここにあります。プロバイダを用いた実用的な状態管理(The Boring Flutter Development Show、Ep。25)
https://www.youtube.com/watch?v=HrBiNHEqSYU&feature=youtu.be&linkId=69927151
このエピソードでは、FilipとMattがProviderパッケージをFlutterます。これは、 Flutterアプリで状態を管理するための簡単な方法です。ライブラリ&コード
AmitJoki /エニグマ
https://github.com/AmitJoki/Enigma
Flutter完全に開発された本格的な一対一のチャットアプリ。
codemissions /フラッタービデオストリーミングアプリ
https://github.com/codemissions/flutter-video-streaming-app
flutterでビデオストリーミングアプリを構築する方法を示すチュートリアルのコードJideGuru / Flutterトラベル
https://github.com/JideGuru/FlutterTravel
UpLabsからの旅行アプリのコンセプトのFlutter表現。leisim / logger
https://github.com/leisim/logger#log-console
nickwu241 / instagroot
https://github.com/nickwu241/instagroot
グルートのためのInstagramのクローンを使用して作成したFlutter !
- 投稿日:2019-07-09T09:35:46+09:00
ByteBufferの複製には、バイトオーダーの再設定が必要
バイト列を扱う際、バイトオーダーを意識しないといけない場合があります。
このような場合、Javaでは、ByteBufferクラスを使用すると便利です。ByteBufferにはバイトオーダーを設定でき、複数バイトから構成される値を簡単に扱えるためです。例えば、次のコードでは、リトルエンディアンの値を扱っています。
LittleEndianSample.javaimport java.nio.ByteBuffer; import java.nio.ByteOrder; public class LittleEndianSample { public static final void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES); // リトルエンディアンの16ビット整数。下位バイトが0x11、上位バイトが0x22。 buffer.order(ByteOrder.LITTLE_ENDIAN) .put((byte) 0x11) .put((byte) 0x22) .rewind(); System.out.printf("0x%x\n", buffer.getShort()); } }実行結果は次の通りです。
0x2211ところが、上記のByteBufferを複製すると、バイトオーダーが想定外のものになってしまいます。例えば、リトルエンディアンのByteBufferを返すメソッドを書いているとします。その呼び出し元がByteBufferの内容を変更できないように、読み取り専用で返すことにします。しかし、次のようなコードでは、呼び出し元がビッグエンディアンのByteBufferを受け取ってしまいます。
BadSample.javaimport java.nio.ByteBuffer; import java.nio.ByteOrder; public class BadSample { private static class BufferObject { private final ByteBuffer buffer; public BufferObject() { buffer = ByteBuffer.allocate(Short.BYTES); // リトルエンディアンの16ビット整数。下位バイトが0x11、上位バイトが0x22。 buffer.order(ByteOrder.LITTLE_ENDIAN) .put((byte) 0x11) .put((byte) 0x22) .rewind(); } private ByteBuffer getBuffer() { // 読み取り専用で複製(誤り。ビッグエンディアンになる) return buffer.asReadOnlyBuffer(); } } public static final void main(String[] args) { BufferObject bufferObject = new BufferObject(); ByteBuffer buffer = bufferObject.getBuffer(); System.out.printf("0x%x\n", buffer.getShort()); } }実行結果は次のようになります。上位バイトと下位バイトとが入れ替わり、ビッグエンディアンになっていることがわかります。
0x1122なぜこのような挙動となるのでしょうか。
OpenJDKのIssue Trackerに投稿されたIssue JDK-7178109によると、この挙動は仕様とのことです。
新規に作成されたByteBufferのバイトオーダーは、常にビッグエンディアンとなります。つまり、allocate()やallocateDirect()以外にも、下記のメソッドから返されるByteBufferのバイトオーダーは、元のByteBufferのバイトオーダーにかかわらず、常にビッグエンディアンです。
- duplicate()
- asReadOnlyBuffer()
- wrap()
- slice()
- alignedSlice()
Java SEでは、バージョン9のAPIドキュメントから、上記の各メソッドに、作成されたByteBufferのバイトオーダーがビッグエンディアンになることが記されています。しかし、AndroidのAPIドキュメントでは、現時点(2019/7/9)ではそのような記述はありません。
以上から、バイトオーダーを保ったままByteBufferを複製するには、複製後にorder()でバイトオーダーを再度設定する必要があります。
DuplicationSample.javaimport java.nio.ByteBuffer; import java.nio.ByteOrder; public class DuplicationSample { private static class BufferObject { private final ByteBuffer buffer; public BufferObject() { buffer = ByteBuffer.allocate(Short.BYTES); // リトルエンディアンの16ビット整数。下位バイトが0x11、上位バイトが0x22。 buffer.order(ByteOrder.LITTLE_ENDIAN) .put((byte) 0x11) .put((byte) 0x22) .rewind(); } private ByteBuffer getBuffer() { // 読み取り専用で複製。バイトオーダーを設定する必要がある。 return buffer.asReadOnlyBuffer().order(buffer.order()); } } public static final void main(String[] args) { BufferObject bufferObject = new BufferObject(); ByteBuffer buffer = bufferObject.getBuffer(); System.out.printf("0x%x\n", buffer.getShort()); } }実行結果は次の通りです。バイトオーダーが正しくリトルエンディアンになりました。
0x2211
- 投稿日:2019-07-09T01:54:08+09:00
Flutter で MLKit のカスタムモデルを使う
Flutter ではバッテリー、カメラ、イメージピッカーなど、React Native ではサードパーティから提供されているようなプラグインが公式で多数提供されています。公式で提供されているプラグインは flutter/plugins というリポジトリで開発されていますが、README を見るとその充実ぶりが分かるかと思います。公式から提供されていることによって、似たようなサードパーティのプラグインが乱立することが避けられていると思います。
残念なのは、細かいところに微妙に手が届いていないプラグインがあるということです。さらに悪いことに、Flutter の開発者がまだ多くないということが原因で、サードパーティもまだあまり充実していません。今回は MLKit for Firebase のカスタムモデルを Flutter から利用した例を紹介します。
使用したライブラリ
公式からは firebase_ml_vision というライブラリが提供されています。Firebase から提供されている機械学習モデルをそのまま利用する場合はこれでよいのですが、残念ながらカスタムモデルを使いたい場合には対応していません。
サードパーティのライブラリとしては flutter_mlkit というものがあります。このライブラリはカスタムモデルに対応しているので、これを使います。今回は v0.13.1 ベースのバージョンを使いました。カスタムモデルによっては複数入力、複数出力となることもありこれまで対応していなかったのですが、最近のプルリクエスト で複数入力、複数出力にも対応しています。ちなみに複数入力の扱いにバグを見つけたので プルリクエストを送っているところ です。
Flutter から Firebase を使う共通設定を行う
カスタムモデルを使う場合に限らずいつも必要な設定として Flutter から Firebase を使うため共通設定が必要です。以下の手順に従いました。
Flutter アプリに Firebase を追加する | Firebase
プロジェクト内の変更としては以下のものが必要になります。
com.google.gms:google-servicesを依存関係に追加(4.2.0 を使いました)- ↑ を Gradle プラグインとして有効化 (
apply plugin: 'com.google.gms.google-services')AndroidX を有効化する
flutter_mlkit のほうで AndroidX の機能を使っている箇所があります。具体的には以下のものです。
import androidx.annotation.NonNull; import androidx.annotation.Nullable;というわけで、利用している側でも設定を有効にしなければビルドできません。プロジェクトレベルの
gradle.propertiesに以下の設定を追加しました。org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true公式の情報としては以下を参照してください。
https://developer.android.com/jetpack/androidx?hl=ja#using_androidx
flutter_mlkit にパッチをあてる
複数入力の扱いにバグがあったので修正しました。本家のほうが治っていなければ以下のパッチをあててください。
https://github.com/azihsoyn/flutter_mlkit/pull/71
サンプルコード
2×5 の大きさの行列の和を計算するカスタムモデルを提供いただいたので、以下のようなサンプルコードを作成しました。
initTflite()ではカスタムモデルの読み込みを行っています。実際に処理を実行する直前までモデルのダウンロードを遅延させたいと思って_onActionButtonPressed()の中でawait initTflite()のように書いていたのですが、なぜかモデルがダウンロードできませんでした。flutter_mlkit の中で非同期処理にバグがあるのではないかと疑っています。_onActionButtonPressed()の中でモデルの計算処理を行っています。FirebaseModelInputOutputOptionsは最近のアップデートで複数入力、複数出力に対応しました。- Firebase ML とのデータのやりとりは
Uint8Listを使って行います。残念なことに、多次元配列を1次元にうまくパックしてやる必要があるだけでなく、複数入力の場合には連結して渡してやるといった処理が必要になります。Float32ListからUint8Listへの変換でバイトオーダーなどを気にしないといけないかと不安になりますが、ここについては多少マシでFloat32List#buffer.asUint8List()というメソッドを使うことができます。FirebaseModelInterpreter#run()の戻り値がかなり厄介でList<dynamic>となっています。実態としては 2×5 の大きさの行列が1つ出力されるのでList<List<List<double>>>のようなものがなのですが、これがなぜかキャストできませんでした。これについては Dart の型システムを理解して出直して来たいと思っています。import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:mlkit/mlkit.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Tflite add', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { String _left = ""; String _right = ""; String _output = ""; FirebaseModelInterpreter interpreter = FirebaseModelInterpreter.instance; FirebaseModelManager manager = FirebaseModelManager.instance; Future<void> initTflite() { return this.manager.registerRemoteModelSource( FirebaseRemoteModelSource( modelName: "add", enableModelUpdates: true, ), ); } void _onLeftOpChanged(String op) { setState(() { _left = op; }); } void _onRightOpChanged(String op) { setState(() { _right = op; }); } @override void initState() { super.initState(); initTflite(); } void _onActionButtonPressed() async { final leftNum = double.parse(_left); final rightNum = double.parse(_right); const ROW = 2; const COLUMN = 5; const options = FirebaseModelInputOutputOptions([ FirebaseModelIOOption(FirebaseModelDataType.FLOAT32, [ROW, COLUMN]), FirebaseModelIOOption(FirebaseModelDataType.FLOAT32, [ROW, COLUMN]), ], [ FirebaseModelIOOption(FirebaseModelDataType.FLOAT32, [ROW, COLUMN]), ]); final left = List<double>(ROW * COLUMN); final right = List<double>(ROW * COLUMN); for (var i = 0; i < ROW; i++) { for (var j = 0; j < COLUMN; j++) { left[COLUMN * i + j] = leftNum; right[COLUMN * i + j] = rightNum; } } final concat = <double>[]..addAll(left)..addAll(right); print(concat); final input = float32ListToUint8List(Float32List.fromList(concat)); print(input); final output = await this.interpreter.run("add", options, input); print(output); setState(() { _output = (output[0][0][0] as double).toStringAsFixed(2); }); } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(16.0), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextField( decoration: InputDecoration( labelText: 'left-hand operand', ), onChanged: _onLeftOpChanged, keyboardType: TextInputType.numberWithOptions( signed: true, decimal: true, ), ), TextField( decoration: InputDecoration( labelText: 'right-hand operand', ), onChanged: _onRightOpChanged, keyboardType: TextInputType.numberWithOptions( signed: true, decimal: true, ), ), Padding( padding: const EdgeInsets.only(top: 32.0), child: Text( _output, style: Theme.of(context).textTheme.display1, ), ), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: _onActionButtonPressed, tooltip: 'Execute', child: Icon(Icons.add), ), ); } } Uint8List float32ListToUint8List(Float32List list) { return list.buffer.asUint8List(); }結果
Tensorflow Lite のカスタムモデルを使って計算する簡単なデモができました。































