- 投稿日:2020-10-27T22:45:15+09:00
部分的Migration to MDC (Button編)
はじめに
- Android Studio 4.1 でテンプレートアプリケーションに設定されているテンプレートが Material Design Component (以下MDC)になりました。記事
- ということでそろそろAndroidアプリの標準的ThemeはMDCに準拠していかないとならないなと思っていますが、そうもいかないよーと思っていたりもします。
- ただ、MDCに適応していくことで実装上の利点なんかもあるので、そういう意味では使いやすい部分から移行して行きたいところだったり。
- よくある AppCompat時代のButtonコンポーネントを、MDC対応Buttonコンポーネントにする方法をまとめておきます。
よくあるAppCompatのButtonって?
想定
- iOSと同じようなデザインでと言われ、角丸、単色(通常色、選択色、無効色)のボタンを用意する感じです (もちろん
RippleEffect
は使っていないよ。)用意する
- アプリのテーマは
"Theme.AppCompat.DayNight.DarkActionBar"
あたりを設定と想定ボタン色
colors.xml<color name="default_button_color">#ff0000ff</color> <color name="highlight_button_color">#ff007Fff</color> <color name="disable_button_color">#550000ff</color>ボタンの選択状態と角丸設定(通常用)
shape_corner_radius_4dp_button_default_background.xml<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="4dp" /> <solid android:color="@color/default_button_color" /> </shape>ボタンの選択状態と角丸設定(選択用)
shape_corner_radius_4dp_button_highlight_background.xml<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="4dp" /> <solid android:color="@color/disable_button_color" /> </shape>ボタンの選択状態と角丸設定(選択用)
shape_corner_radius_4dp_button_disable_background.xml<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="4dp" /> <solid android:color="@color/highlight_button_color" /> </shape>ボタンのselector設定
- 通常・選択・無効の状態を切り替えさせる為に
drawable
フォルダ内に用意selector_default_button.xml<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/shape_default_button" android:state_enabled="true" android:state_pressed="false" /> <item android:drawable="@drawable/shape_default_button_highlighted" android:state_pressed="true" /> <item android:drawable="@drawable/shape_default_button_disabled" android:state_enabled="false" /> </selector>ボタンコンポーネント
<Button android:id="@+id/button_first" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/next" android:textColor="@color/white" android:background="@drawable/selector_default_button" ・・・ />こんな感じ
何が嫌か
- ボタンの角丸サイズ違いや、色違いで
drawable
のリソースが増えていくのが非常に煩わしいところですね。ButtonだけをMDC対応した場合
- まずは MDCのライブラリは必要なのでgradleで取り込んでおきましょう。
build.gradledependencies { ・・・ implementation 'com.google.android.material:material:1.2.1' }MDC用 Theme を用意
- MDC には AppCompatに存在していない必須となる設定値があるので、Button用にThemeを作成します。
colorOnPrimary
colorPrimary
colorOnSurface
は新しく必要ないろ定義です。
- 文字色の指定ですので、任意の色を指定してあげてください。
theme.xml<!-- アプリ全体でMaterialComponentを適応できれば不要になるTheme --> <style name="MDCButtonTheme" parent="Theme.MaterialComponents"> <item name="colorOnPrimary">@color/base_text_color</item> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorOnSurface">@color/base_text_color</item> </style>MDC用 Style を用意
"@style/Widget.MaterialComponents.Button.UnelevatedButton"
は MDCで用意された、高さを表現させない場合のStyle定義insetTop
とinsetBottom
は MDCのボタンに標準で設定されている Inset値(タッチ領域だけを広げる設定)を0に設定しています。style.xml<style name="MDCButtonStyle" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton"> <item name="android:insetTop">0dp</item> <item name="android:insetBottom">0dp</item> </style>ボタンのselector設定
- 通常・選択・無効の状態を切り替えさせる為に
color
フォルダ内に用意- 配置先が
drawable
からcolor
にかわり、設定も色だけの設定にしていますselector_default_button.xml<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/default_button_color" android:state_enabled="true" android:state_pressed="false" /> <item android:color="@color/default_button_highlight_color" android:state_pressed="true" /> <item android:color="@color/disable_button_color" android:state_enabled="false" /> </selector>ボタンコンポーネント
Button
からMaterialButton
に変更- Theme と Style は必須となるので設定
- MDCのStyleにボタンの角丸
4dp
が設定されているので記述していませんが、MDCではXMLからcornerSize
を利用する事で、角丸の値を設定できます。<com.google.android.material.button.MaterialButton android:id="@+id/button_first" style="@style/MDCButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:backgroundTint="@color/selector_default_button" android:text="@string/next" android:textColor="@color/white" android:theme="@style/MDCButtonTheme" ・・・ />こんな感じ
ほどんと同じボタンが出来上がりました。
- MDC のButtonを利用する事で、角丸や色の設定ごとにDrawableを増やす必要がなくなるので便利ですね。
まとめ
- MDCへの切り替えは早めにしたいが、なかなか難しい場合、Buttonだけとかでも切り替えていってみよう。
- MDCのButtonを利用する事で、無駄にリソースを作る必要がなくなります!
- 本当はアプリのThemeを切り替えられるのがいいんでしょうけど
- 投稿日:2020-10-27T22:17:24+09:00
GLSurfaceViewとUIを同時につかう。
GLSurfaceViewとUIを同時につかう。
サンプル
- 投稿日:2020-10-27T20:32:10+09:00
#22 Kotlin Koans Collections/All Any and other predicates 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans Collections/All Any and other predicatesの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
2 any()/all()/count()/find()
any():呼び出し元のコレクションの全要素のうち、引数として渡す条件式を満たすものがあればtrueを返す。
all():呼び出し元のコレクションの全要素が、引数として渡す条件式を満たせばtrueを返す。
count():呼び出し元のコレクションの要素のうち、引数として渡す条件式を満たす要素の個数を返す。
find():呼び出し元のコレクションの要素のうち、引数として渡す条件式を満たす要素の値を返す。(条件を満たす要素が複数個ある場合は、先頭に近いものが返る。)
以下に、Kotlin koansの例を示します。
val numbers = listOf(-1, 0, 2) val isZero: (Int) -> Boolean = { it == 0 } numbers.any(isZero) == true numbers.all(isZero) == false numbers.count(isZero) == 1 numbers.find { it > 0 } == 23 Collections/All Any and other predicatesの解説
Kotlin Koans Collections/All Any and other predicatesの解説です。
随時本サイトの内容を引用させていただきます。本文とコードを見てみましょう。
Implement all the functions below using all, any, count, find.
val numbers = listOf(-1, 0, 2) val isZero: (Int) -> Boolean = { it == 0 } numbers.any(isZero) == true numbers.all(isZero) == false numbers.count(isZero) == 1 numbers.find { it > 0 } == 2All_Any_and_other_predicates// Return true if all customers are from the given city fun Shop.checkAllCustomersAreFrom(city: City): Boolean = TODO() // Return true if there is at least one customer from the given city fun Shop.hasCustomerFrom(city: City): Boolean = TODO() // Return the number of customers from the given city fun Shop.countCustomersFrom(city: City): Int = TODO() // Return a customer who lives in the given city, or null if there is none fun Shop.findAnyCustomerFrom(city: City): Customer? = TODO()
checkAllCustomersAreFrom(city: City)
は、
Shopのプロパティcustomersの全要素(Customer)のcityが引数として受け取るcityと一致していればtrueを返すように実装します。
hasCustomerFrom(city: City)
は、
Shopのプロパティcustomersの要素(Customer)のcityのうち、引数として受け取るcityと一致するものが1つでもあればtrueを返すように実装します。
countCustomersFrom(city: City)
は、
Shopのプロパティcustomersの要素(Customer)のcityが引数として受け取るcityと一致する個数を返すように実装します。
findAnyCustomerFrom(city: City)
は、
Shopのプロパティcustomersの要素(Customer)のうち引数として受け取るcityを持つ(要素の先頭の)ものを返し、無ければnullを返すように実装します。したがって、以下のような実装になります。
All_Any_and_other_predicates// Return true if all customers are from the given city fun Shop.checkAllCustomersAreFrom(city: City): Boolean = customers.all{ it.city == city } // Return true if there is at least one customer from the given city fun Shop.hasCustomerFrom(city: City): Boolean = customers.any{ it.city == city } // Return the number of customers from the given city fun Shop.countCustomersFrom(city: City): Int = customers.count{ it.city == city } // Return a customer who lives in the given city, or null if there is none fun Shop.findAnyCustomerFrom(city: City): Customer? = customers.find{ it.city == city }4 最後に
次回はKotlin Koans Collections/FlatMapの解説をします
- 投稿日:2020-10-27T19:41:31+09:00
#21 Kotlin Koans Collections/Filter map 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans Collections/Filter mapの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
2-1 map()
map():呼び出し元のコレクションの要素をListの形で返す。引数として渡す関数(transform function)のルールに従ってListの内容が決まる。
kotlin koansの例を引用します。
val numbers = listOf(1, -1, 2) numbers.map { it * it } == listOf(1, 1, 4)
val numbers = listOf(1,-1,2)
の部分でListを生成しています。このListがmap()を呼び出してラムダ式
{it * it}
を渡しているので、呼び出しもとには各要素が2乗となった(
1は1
、-1は1
、2は4
になります。)Listが返ってきます。2-2 filter()
filter():呼び出し元のコレクションの要素のうち、引数として渡す関数(predicate)の条件を満たす要素のみのListを返す。
kotlin koansの例を引用します。
val numbers = listOf(1, -1, 2) numbers.filter { it > 0 } == listOf(1, 2)
val numbers = listOf(1,-1,2)
の部分でListを生成しています。このListがfilter()を呼び出してラムダ式
{ it > 0 }
を渡し、この条件を満たす要素(1,2)が要素として含まれるListが返ります。3 Collections/Filter mapの解説
Kotlin Koans Collections/Filter mapの解説です。
随時本サイトの内容を引用させていただきます。本文とコードを見てみましょう。
Implement extension functions Shop.getCitiesCustomersAreFrom() and Shop.getCustomersFrom() using functions map and filter.
val numbers = listOf(1, -1, 2) numbers.filter { it > 0 } == listOf(1, 2) numbers.map { it * it } == listOf(1, 1, 4)
Filter_map// Return the set of cities the customers are from fun Shop.getCitiesCustomersAreFrom(): Set<City> = TODO() // Return a list of the customers who live in the given city fun Shop.getCustomersFrom(city: City): List<Customer> = TODO()getCitiesCustomersAreFrom()は、顧客(Customer)の出身の街(city)のsetを返すように実装します。
ここで各インスタンスの持つプロパティを整理しましょう。
Shop:name(String型)、customers(List< Customer >型)
Customer :name(String型)、city(City型)、orders(List< Order >)
ShopがgetCitiesCustomersAreFrom()を呼び出すことで、プロパティcustomersのもつ要素(Customer)のcityをmap()を利用してListとして返します。
ListをSetに変換するためにtoSet()を利用します。
よって、以下のような実装になります。
Filter_mapfun Shop.getCitiesCustomersAreFrom(): Set<City> = customers.map{ it.city }.toSet()次に、
Shop.getCustomersFrom(city: City)は、引数として渡されたcity出身の顧客(Customer)のListを返します。getCustomersFrom(city: City)を呼び出す際に、任意の値が代入されたcityを渡して、Shopのプロパティcustomersのもつ要素(Customer)のcityについて考えます。
Customersがfilter()を呼び出すことで、Customersの各要素(Customer)が引数として渡す関数の条件を満たすのかを判定させ、満たすものだけのListを返すように実装します。
満たすべき条件は、Customerのもつcityの値とgetCustomersFrom(city: City)が引数として受け取ったcityの値が一致するかです。よって、以下のような実装になります。
Filter_mapfun Shop.getCustomersFrom(city: City): List<Customer> = customers.filter{ it.city == city }4 最後に
次回はKotlin Koans Collections/All Any and other predicatesの解説をします
- 投稿日:2020-10-27T16:47:47+09:00
新しいGoogle Play Consoleさん!mapping.txt置く場所どこですか!?
はじめに
※とにかく結論が読みたい!という方は一番下の項目 ここだよ まで飛んでください
初めまして!@hayashidamokaと申します!
会社や個人でAndroidアプリの開発をしてます
まだまだ半人前で勉強中の私は、いつもQiitaの記事に助けられています
皆さん、ありがとうございます!
私も誰かのお役に立ちたい!初投稿がんばります!新しくなったGoogle Play Console
最近Google Play Consoleのデザインがまた新しく変わりましたね!
公式様によれば、
2020 年 11 月 2 日より、Google Play Console の旧バージョンはご利用いただけなくなります
とのことです!旧バージョンを使えるのも残り数日...!!
早く新バージョンに慣れないといけませんね!mapping.txt置く場所どこよ
アプリを更新しようとしたところ、困った問題がありました。
「mapping.txtを置く場所が分からないよ!!」
以前のGoogle Play Consoleなら、
Android Vitals > 解読ファイル に置く場所があったのに
新しいGoogle Play Consoleの、
Android Vitals には 解読ファイル がないのです
ここだよ
色々と探した結果...
App Bundle エクスプローラ > ダウンロード > アセット > ReTrace マッピング ファイル
に置く場所がありました!やったー!
- 投稿日:2020-10-27T12:59:26+09:00
[android]リアルタイムデベロッパー通知を使用した定期購入でのAccount Holdテストフローとnotification_typeメモ
はじめに
既存アプリの定期購読でaccount holdとrestoreの対応をする際に
リアルタイムデベロッパー通知でサーバー側の処理を行うように変更した。
通知が13種類あり、実機テストで送られてくる番号を確認した際に若干こちらの想定と違う番号が送られてきたりしたため、テストフローに沿って通知をメモしておく。テストシナリオ
テストのフローは公式に記載されている内容を参照。
更新頻度のテストシナリオ月次定期購入(新規定期購入→更新のテスト)
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:05 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:10 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:15 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:20 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:25 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:30 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:35 定期購入が終了します(6 回の更新後) (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED 6回目の更新の時に通常だったらユーザーが定期購入を解約した時に送られる(3)が通知されてその後に有効期限切れの(13)が送られてきます。
猶予期間なしの月間定期購入(アカウントの一時停止を含む)、ユーザーが再開した場合(新規定期購入→Account Hold→再開のテスト)
今回の実装では猶予期間は設定せずaccount hold(一時停止)のみ対応したので一時停止、再開のテスト。
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:01 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に不承認」に変更します。 12:05 お支払いが不承認となり、アカウントの一時停止が開始されます (5)SUBSCRIPTION_ON_HOLD 12:15 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に承認」に変更します。 定期購入が再開され、更新されて、アカウントの一時停止が終了します。 (1)SUBSCRIPTION_RECOVERED 12:20 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:25 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:30 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:35 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:40 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:45 定期購入が終了します(6 回の更新後) (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED カード不承認状態を更新した場合はすぐに一時停止状態から定期購読状態に戻るのでGoogle Playの操作で通知が来た。
猶予期間なしの月間定期購入(アカウントの一時停止を含む)、ユーザーによる非自発的チャーンの場合(新規定期購入→Account Hold→期限切れのテスト)
account hold状態から更新をしなかった場合に期限切れになるテスト
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:01 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に不承認」に変更します。 12:05 お支払いが不承認となり、アカウントの一時停止が開始されます (5)SUBSCRIPTION_ON_HOLD 12:15 非自発的チャーンにより定期購入が解約されます (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED アカウントの一時停止から解約までの時間は前後する模様。
テストの注意点
- 定期購入を開始した直後に以前の定期購入の期限切れの通知(13)SUBSCRIPTION_EXPIREDが送られてくる。(既に有効期限切れの定期購入に再度期限切れの通知(13)が来ることがある点に注意)
- 定期購入が終了する時に(13)SUBSCRIPTION_EXPIREDが来てから(3)SUBSCRIPTION_CANCELEDが来ることがある((3)SUBSCRIPTION_CANCELEDで何かしらの処理がある場合は注意)
おわりに
実機でのテストは時間がかかるから結構大変。でもGooglePlayの動きは実機で確認しないと想定外の事が起きそうなのでちゃんとやらないとね。
猶予期間(Grace Period)を導入するとさらにテストが大変そうなのでやった時に追記するかもしれない。
- 投稿日:2020-10-27T12:59:26+09:00
[android]定期購読リアルタイムデベロッパー通知のテストフローとnotification_typeメモ
はじめに
既存アプリの定期購読でaccount holdとrestoreの対応をする際に
リアルタイムデベロッパー通知でサーバー側の処理を行うように変更した。
通知が13種類あり、実機テストで送られてくる番号を確認した際に若干こちらの想定と違う番号が送られてきたりしたため、テストフローに沿って通知をメモしておく。テストシナリオ
テストのフローは公式に記載されている内容を参照。
更新頻度のテストシナリオ月次定期購入
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:05 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:10 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:15 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:20 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:25 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:30 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:35 定期購入が終了します(6 回の更新後) (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED 6回目の更新の時に通常だったらユーザーが定期購入を解約した時に送られる(3)が通知されてその後に有効期限切れの(13)が送られてきます。
猶予期間なしの月間定期購入(アカウントの一時停止を含む)、ユーザーが再開した場合
今回の実装では猶予期間は設定せずaccount hold(一時停止)のみ対応したので一時停止、再開のテスト。
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:01 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に不承認」に変更します。 12:05 お支払いが不承認となり、アカウントの一時停止が開始されます (5)SUBSCRIPTION_ON_HOLD 12:15 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に承認」に変更します。 定期購入が再開され、更新されて、アカウントの一時停止が終了します。 (1)SUBSCRIPTION_RECOVERED 12:20 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:25 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:30 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:35 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:40 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:45 定期購入が終了します(6 回の更新後) (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED カード不承認状態を更新した場合はすぐに一時停止状態から定期購読状態に戻るのでGoogle Playの操作で通知が来た。
猶予期間なしの月間定期購入(アカウントの一時停止を含む)、ユーザーによる非自発的チャーンの場合
account hold状態から更新をしなかった場合に期限切れになるテスト
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:01 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に不承認」に変更します。 12:05 お支払いが不承認となり、アカウントの一時停止が開始されます (5)SUBSCRIPTION_ON_HOLD 12:15 非自発的チャーンにより定期購入が解約されます (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED アカウントの一時停止から解約までの時間は前後する模様。
テストの注意点
- 定期購入を開始した直後に以前の定期購入の期限切れの通知(13)SUBSCRIPTION_EXPIREDが送られてくる。(既に有効期限切れの定期購入に再度期限切れの通知(13)が来ることがある点に注意)
- 定期購入が終了する時に(13)SUBSCRIPTION_EXPIREDが来てから(3)SUBSCRIPTION_CANCELEDが来ることがある((3)SUBSCRIPTION_CANCELEDで何かしらの処理がある場合は注意)
おわりに
実機でのテストは時間がかかるから結構大変。でもGooglePlayの動きは実機で確認しないと想定外の事が起きそうなのでちゃんとやらないとね。
猶予期間(Grace Period)を導入するとさらにテストが大変そうなのでやった時に追記するかもしれない。
- 投稿日:2020-10-27T12:59:26+09:00
[android]リアルタイムデベロッパー通知の定期購入テストフローとnotification_typeメモ
はじめに
既存アプリの定期購読でaccount holdとrestoreの対応をする際に
リアルタイムデベロッパー通知でサーバー側の処理を行うように変更した。
通知が13種類あり、実機テストで送られてくる番号を確認した際に若干こちらの想定と違う番号が送られてきたりしたため、テストフローに沿って通知をメモしておく。テストシナリオ
テストのフローは公式に記載されている内容を参照。
更新頻度のテストシナリオ月次定期購入
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:05 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:10 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:15 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:20 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:25 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:30 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:35 定期購入が終了します(6 回の更新後) (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED 6回目の更新の時に通常だったらユーザーが定期購入を解約した時に送られる(3)が通知されてその後に有効期限切れの(13)が送られてきます。
猶予期間なしの月間定期購入(アカウントの一時停止を含む)、ユーザーが再開した場合
今回の実装では猶予期間は設定せずaccount hold(一時停止)のみ対応したので一時停止、再開のテスト。
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:01 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に不承認」に変更します。 12:05 お支払いが不承認となり、アカウントの一時停止が開始されます (5)SUBSCRIPTION_ON_HOLD 12:15 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に承認」に変更します。 定期購入が再開され、更新されて、アカウントの一時停止が終了します。 (1)SUBSCRIPTION_RECOVERED 12:20 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:25 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:30 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:35 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:40 定期購入が更新されます (2)SUBSCRIPTION_RENEWED 12:45 定期購入が終了します(6 回の更新後) (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED カード不承認状態を更新した場合はすぐに一時停止状態から定期購読状態に戻るのでGoogle Playの操作で通知が来た。
猶予期間なしの月間定期購入(アカウントの一時停止を含む)、ユーザーによる非自発的チャーンの場合
account hold状態から更新をしなかった場合に期限切れになるテスト
時刻 ユーザーの操作 システム イベント 送られてくるnotificationType 午後 12:00 ライセンス テスト アカウントと「テスト支払い方法 - 常に承認」を使用して、アプリ内定期購入に登録します。 定期購入が開始します。 (4)SUBSCRIPTION_PURCHASED 12:01 Google Play アプリの [アカウント] > [定期購入] に移動して、テスト用定期購入をクリックし、支払い方法を「テスト支払い方法 - 常に不承認」に変更します。 12:05 お支払いが不承認となり、アカウントの一時停止が開始されます (5)SUBSCRIPTION_ON_HOLD 12:15 非自発的チャーンにより定期購入が解約されます (3)SUBSCRIPTION_CANCELED (13)SUBSCRIPTION_EXPIRED アカウントの一時停止から解約までの時間は前後する模様。
テストの注意点
- 定期購入を開始した直後に以前の定期購入の期限切れの通知(13)SUBSCRIPTION_EXPIREDが送られてくる。(既に有効期限切れの定期購入に再度期限切れの通知(13)が来ることがある点に注意)
- 定期購入が終了する時に(13)SUBSCRIPTION_EXPIREDが来てから(3)SUBSCRIPTION_CANCELEDが来ることがある((3)SUBSCRIPTION_CANCELEDで何かしらの処理がある場合は注意)
おわりに
実機でのテストは時間がかかるから結構大変。でもGooglePlayの動きは実機で確認しないと想定外の事が起きそうなのでちゃんとやらないとね。
猶予期間(Grace Period)を導入するとさらにテストが大変そうなのでやった時に追記するかもしれない。
- 投稿日:2020-10-27T10:36:08+09:00
【Android Studio4.1】ベクターアセットが表示されない問題
Android Studio4.1に更新したら…
ベクターアセットのマテリアルアイコンが表示されなくなりました。
解決方法
C:\Users\ユーザー名\AppData\Local\Android\Sdk\icons\material\materialicons↑ここにマテリアルアイコンがすべて保存されているので、
使いたいアイコンのxmlを、プロジェクトのdrawableフォルダへ手動でコピーすることで解決しました。スタックオーバーフローでも…
スタックオーバーフローでも同じことが起きて困っている人がいた。
https://stackoverflow.com/questions/64382564/how-i-fix-nothing-to-show-in-my-vector-asset-in-android-studioアイコンが保存されている場所までのURLにスペースが入っているとダメかも、とのこと。
同じ現象で困っている人の助けになればと思います。
- 投稿日:2020-10-27T04:42:36+09:00
CleanArchitecture要素のMVVMサンプル作った
サンプル
アーキテクチャ図
動機
現場のプロダクトで、内部品質を高めるために定期的なリファクタリング習慣を取り入れて行きたく、リファクタリング計画を作るにあたり、目標とするアーキテクチャをはっきりさせたかった。
アーキテクチャサンプルの概要
CleanArchitecture の考え方を一部取り入れた MVVM です。
構成は以下のようなレイヤーのマルチモジュール構成になっています。
マルチモジュールにしている理由は依存性をシステム的に制御して人為的なミスによる間違った依存関係を生むのを減らすためです。モジュール構成と主要ライブラリ
- app
- UI、バックグラウンド系のAndroidコンポーネントを持つモジュール
- 画面遷移: Navigation Component
- 画像取得: Coil
- domain
- ビジネスロジックを持つモジュール
- ViewModel、LifeCycleのための androidx.lifecycle系
- repository
- di
- DI機能を持つモジュール
- DI: Koin
- testlib
以下に、どのようにして上図のようになったかを解説します。
(ライブラリはKotlinの言語仕様に合わせて簡単に書けるものやパフォーマンスが良いものを選んでいるつもりです。)アーキテクチャの方針
目指すところとしては、変更しやすいアーキテクチャを目指したいので、変更に特化した考え方の Clean Architecture を参考に考えます。
CleanArchitectureの基本的な考え方の1つである「関心事の分離」により、コンポーネント間、レイヤー間を切り離しを容易にすることで、柔軟性を上げます。
関心事の分離の観点は以下の5つで
- フレームワーク非依存:システムをフレームワークに縛るのではなく、フレームワークをツールとして使う。
- テスト可能:ビジネスロジックはUI、DB、外部IFなどがなくてもテストできる。
- UI非依存:UIはシステムの他の部分を変更せずとも簡単に変更できる。(例えばビジネスロジック)
- データベース非依存:例えばOracleをMySQLに簡単に置き換えることができる。
- 外部エージェント非依存:ビジネスロジックは外部IFの世界を知らなくて良い。
これらを実現するために
- ソースコードの依存性は上位レベルの方針にだけ向かっていなければならない
と言っています。
最上位レイヤーは本質的な価値を持つ部分で、書籍ではビジネスモデルで相当すると言われています。
この考え方をベースに、上位レベルのレイヤーは、アプリの価値を提供する部分として「ドメイン」と呼びたいと思います。
(このネーミングが良いかは検討)また、このアーキテクチャはクライアント・サーバーモデルを前提にしています。
では何をドメインレイヤーにするか?
アプリ内の処理の流れを大きくレイヤー分けすると、以下のように分けることができます。
- ビュー:UI。アクティビティやフラグメント、ビューなど。
- プレゼンテーションロジック:入力チェックや表示項目のフォーマットなど。
- ビジネスロジック:アプリにおけるビジネスロジック。アプリで利用するデータの加工やアプリ独自の機能のロジックなど。
- データアクセス:アプリ独自の項目を持つDBへのアクセスや、サーバーのエンティティに紐づくデータを持つDBへのアクセス、サーバーへのアクセスなど。
- データストア:SharedPreferencesやSQLiteなどのデータストア。
この中からドメインを決定したいと思いますが、結論から言いますと全体的なバランスを考慮して、プレゼンテーションロジック、ビジネスロジックのレイヤーをドメインとしたいと思います。
検討事項:データストア層のエンティティをドメインにしない理由
アプリのビジネスルールは一見、データモデルとなるエンティティ(データストアの)になりそうです。
しかし、クライアント(アプリ)・サーバー(API)を前提に考えると、サービス自体がサーバーありきで成り立つアーキテクチャであるため、実質、多くのエンティティはサーバーに依存していることになり、エンティティはアプリではコントロールしづらく安定度を高くできない可能性があります。
これを踏まえると、愚直にデータストア付近のエンティティをドメインとするのはためらわれます。
(そもそもサーバーをモデルとしているので、)検討事項:ビューをドメインにしない理由
また、アプリの特徴としてUIのためのロジックを担うことが多いです。
ビュー周りは一般的に変更頻度が高いためテストコード改修も高くなる可能性があり、ここもドメインにするにはためらわれます。残りのプレゼンテーションロジック、ビジネスロジックを見てみると、これらはビューとデータと切り離すことができるためテスト容易性が高くできます。
また、機能自体もアプリの本質的な価値と言えるためこれらをドメインとして、安定度も高い状態にしていきたいと思います。5つの観点に関する各レイヤーの評価
レイヤー FW非依存 テスト可能 UI非依存 DB非依存 外部エージェント非依存 ビュー ☓。Androidに依存 ☓。テストが難しい(変更頻度が多い、時間がかかる) ☓。UIそのもの。 ○。非依存 ○。非依存 PRロジック ☓。LiveData、ViewModelなどがAndroidに依存 ○。ロジックが切り離されているためテストしやすい △。UIとの距離が近いので変更が多い可能性あり ○。非依存 ○。非依存 ビジネスロジック △。LiveData、ViewModelを使う場合はAndroidに依存 ○。非依存 ○。非依存 ○。非依存 ○。非依存 データアクセス ☓。RoomやPreferences、RetroFitなど依存 ○。FWに依存するところが多いので難易度が高い可能性あり ○。非依存 ☓。データストアに依存したコードを書く必要がある ☓。API関連に依存するクラスは依存 データストア ☓。SQLiteやPreferencesそのもの ☓。テストが難しい ○。非依存 ☓。データストアそのもの ☓。サーバーのエンティティに依存する可能性あり アーキテクチャの詳細:ベースはMVVMとする
ベースとするGUIアーキテクチャは開発効率と参画敷居を考慮して Android JetPack と親和性の高いMVVMにする。
一部 CleanArchitecture の考え方を適用させます。
レイヤー分けとその特徴としては以下のようにします。
- ビュー・バックグラウンド
- Activityはシングルアクティビティ。アプリのフロントバックのハンドリングとフラグメント管理を行う。Activityを1つにすることでActivityバックスタックの仕様とActivity起動モードを意識しなくてよくするため。ActivityとFragmentは両方ともUIコントローラという側面があるので、UIはFragmentが持つという方針にすることで、Activityとの役割を明確にして開発の迷いをなくすため。
- 1画面1Fragment。画面遷移はNavigationComponent。
- DataBindingは使わない。レイアウトファイルにはViewModelを依存させないため。データのオブザーブはフラグメントで行う。
- ドメイン(ビジネスロジック・プレゼンテーションロジック)
- 基本的にビジネスロジック・プレゼンテーションロジックはViewModelが持つ。
- このレイヤーで使うDTO、VOが実質このアプリのエンティティの位置付けとなる。VOはValueObjectのことで通常のIntなどの値をクラスにラップすることで、可読性向上、型制約により引数を扱いやすくする、仕様を隠蔽しやすい、などの利点を持つ。
- データのやり取りはLiveDataを使う。Fragment、Activityとのやり取りが、ライフサイクル仕様を考慮して安全に連結できるため。可能であればRxは使わない。LiveDataとの使い分けがわかりにくいため。ObservableField系もデータバインディングを使わないので、実質使わない。
- Serviceなどのバックグラウンドからのビジネスロジック処理はProviderというクラスを作り、そこにアクセスする。ViewModelに統一しようかと悩んだが、バックグラウンドはViewではないので。また、ネーミングもUseCaseにしようかと考えたが、基本がMVVMなのでCleanArchitectureと混同するとわかりにくくなるかと思い、Providerというネーミングにした。(FlutterではProviderというのがあるので、それを参考にネーミングしました。)
- リポジトリレイヤーに依存しないために、RepositoryIFを作りRepositoryレイヤーのクラスはこれを実装する。このあたりがCleanArchitectureの要素で、継承することで依存関係逆転させて「ドメイン(上位レイヤー)に対してのみ依存の方向を向ける」をしています。
- リポジトリ(データアクセス)
- API、データストア、PreferenceへのアクセスをRepositoryパターンを使ってアクセスする。
ユニットテスト対象
ドメインレイヤーとRepositoryレイヤーをユニットテスト対象とし、ビューレイヤーは変更頻度の高さとテスト難易度を考慮して対象としません。
ドメインレイヤーとRepositoryレイヤーもすべてを対象とするわけではなく、効果の高いところのみを対象とします。
参考サイト
マルチモジュール化について
CleanArchitectureについて
世界一わかりやすいCleanArchitecture
Clean Architecture 達人に学ぶソフトウェアの構造と設計所感
クライアント・サーバーモデルを前提とすると、アプリでは安定度の高いビジネスロジックは意外と限られてくるなぁというイメージでした。
例えば、センサーなどデバイスに依存するようなロジックはアプリに依存するため安定度の高いビジネスロジックになる可能性が高いですが、ビューに依存するロジックはどうしてもビューの変更に依存して安定度が低くなるイメージでした。
ひとまず、リファクタリングのための目標ができて良かった!!
- 投稿日:2020-10-27T02:41:34+09:00
flutter android FAILURE: Build failed with an exception. エラーの対処方法
背景
iOSでのビルドを確認できたので、Androidでも確認しようと、Android Studioを使用してAndroidエミュレータでデバッグしようとして、起きたエラーの原因と解決法を忘却録として残します
開発環境
PC:macOS Catalina
エディター:Android Studio 4.0.1
エミュレータ:Pixel 3 Android 10.0+ x86内容
とりあえず、エミュレータ を起動して、Android Studioのデバッグボタン(Shift + F9)を押して、ビルドをしようとすると以下のようなエラーが発生
userName@MacBook-Pro flutterProject % flutter run Using hardware rendering with device sdk gphone x86. If you notice graphics artifacts, consider enabling software rendering with "--enable-software-rendering". Launching lib/main.dart on sdk gphone x86 in debug mode... FAILURE: Build failed with an exception. * Where: Build file '/Users/userName/flutterProject/android/app/build.gradle' line: 25 * What went wrong: A problem occurred evaluating project ':app'. > Plugin with id 'kotlin-android' not found. * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. * Get more help at https://help.gradle.org BUILD FAILED in 871ms Running Gradle task 'assembleDebug'... Running Gradle task 'assembleDebug'... Done 5.0s Exception: Gradle task assembleDebug failed with exit code 1※userNameとflutterProjectは各自の名前と作成したプロジェクト名に置き換えてください
解決策
kotlin-androidが見つからないとあったので、調べてみると、Android/build.gradleに1行追加すれば解決するらしい(Android/app/build.gradleではないので、注意)
Android/build.gradledependencies { classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.google.gms:google-services:4.3.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" <- 追加 }上記のようにして、もう一度ビルドすると今度は別のエラーが発生
エラーその2
Using hardware rendering with device sdk gphone x86. If you notice graphics artifacts, consider enabling software rendering with "--enable-software-rendering". Launching lib/main.dart on sdk gphone x86 in debug mode... 注意:一部の入力ファイルは非推奨のAPIを使用またはオーバーライドしています。 注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。 注意:入力ファイルの操作のうち、未チェックまたは安全ではないものがあります。 注意:詳細は、-Xlint:uncheckedオプションを指定して再コンパイルしてください。 注意:一部の入力ファイルは非推奨のAPIを使用またはオーバーライドしています。 注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。 D8: Cannot fit requested classes in a single dex file (# methods: 95607 > 65536) com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html at com.android.builder.dexing.D8DexArchiveMerger.getExceptionToRethrow(D8DexArchiveMerger.java:131) at com.android.builder.dexing.D8DexArchiveMerger.mergeDexArchives(D8DexArchiveMerger.java:118) at com.android.build.gradle.internal.transforms.DexMergerTransformCallable.call(DexMergerTransformCallable.java:102) at com.android.build.gradle.internal.tasks.DexMergingTaskRunnable.run(DexMergingTask.kt:444) at com.android.build.gradle.internal.tasks.Workers$ActionFacade.run(Workers.kt:335) at org.gradle.workers.internal.AdapterWorkAction.execute(AdapterWorkAction.java:50) at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:47) at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1$1.create(NoIsolationWorkerFactory.java:65) at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1$1.create(NoIsolationWorkerFactory.java:61) at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:98) at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.execute(NoIsolationWorkerFactory.java:61) at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:416) at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:406) at org.gradle.internal.operations.DefaultBuildOperationExecutor$1.execute(DefaultBuildOperationExecutor.java:165) at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:250) at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:158) at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:102) at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36) at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:56) at org.gradle.workers.internal.DefaultWorkerExecutor$3.call(DefaultWorkerExecutor.java:215) at org.gradle.workers.internal.DefaultWorkerExecutor$3.call(DefaultWorkerExecutor.java:210) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:215) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:131) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56) at java.lang.Thread.run(Thread.java:748) Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete at com.android.tools.r8.utils.t.a(:55) at com.android.tools.r8.D8.run(:11) at com.android.builder.dexing.D8DexArchiveMerger.mergeDexArchives(D8DexArchiveMerger.java:116) ... 34 more Caused by: com.android.tools.r8.utils.AbortException: Error: null, Cannot fit requested classes in a single dex file (# methods: 95607 > 65536) at com.android.tools.r8.utils.Reporter.a(:21) at com.android.tools.r8.utils.Reporter.a(:7) at com.android.tools.r8.dex.VirtualFile.a(:33) at com.android.tools.r8.dex.VirtualFile$h.a(:5) at com.android.tools.r8.dex.ApplicationWriter.a(:13) at com.android.tools.r8.dex.ApplicationWriter.write(:35) at com.android.tools.r8.D8.d(:44) at com.android.tools.r8.D8.b(:1) at com.android.tools.r8.utils.t.a(:23) ... 36 more FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:mergeDexDebug'. > A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: The number of method references in a .dex file cannot exceed 64K. Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. * Get more help at https://help.gradle.org BUILD FAILED in 36s Running Gradle task 'assembleDebug'... Running Gradle task 'assembleDebug'... Done 41.9s [!] The shrinker may have failed to optimize the Java bytecode. To disable the shrinker, pass the `--no-shrink` flag to this command. To learn more, see: https://developer.android.com/studio/build/shrink-code Exception: Gradle task assembleDebug failed with exit code 1原因
アプリ、アプリの参照するライブラリが65,536メソッドを超えると、ビルドエラーが発生する
解決策
65,536を超えているらしいので、回避するコードを書きます(公式参照)
今度はappディレクトリのbuild.gradleに追加Android/app/build.gradleandroid { defaultConfig { ... minSdkVersion 15 targetSdkVersion 28 multiDexEnabled true <- 追加 } ... }参考
https://github.com/react-native-webview/react-native-webview/issues/1407 (react)