- 投稿日:2020-12-04T21:37:25+09:00
AndroidのIn-App Review�を表示テストする方法
リリース前にIn-App Reviewを表示させてみたい時あるよね?
どうにか表示させようと試行錯誤した結果、どうにか表示できたので共有しておく。
(2020/12/04時点の情報)In-App Reviewをテスト表示させる方法
表示テストをしたい場合、Play Consoleから 内部テストで配布したときのみ In-App Reviewが表示される。
もう一回言います、「内部テストで配布したときのみ」に表示される。
ローカル環境でビルドしたり、apkファイルを直接インストールしてもIn-App Reviewは表示されなかった。内部アプリ配布経由で表示できた瞬間のスクショ
自分の個人アプリを実験台にして色々と試してみた!
仕事でのアプリは色々とあって実験に使えないため、こういう時に個人アプリがあると便利だねー
好き勝手に変更できるしwPlay Consoleの内部テストの場所
Play Consoleの左のメニューから選択できる。
初めて使うには若干設定が必要かも?
(何か設定した記憶があるが忘れた)参考リンク
Play Core In-App Review API not showing the Review Activity
https://stackoverflow.com/questions/63286540/play-core-in-app-review-api-not-showing-the-review-activity/63289851公式ドキュメント: Google Play In-App Review API
https://developer.android.com/guide/playcore/in-app-review?hl=ja
- 投稿日:2020-12-04T18:39:39+09:00
だからボクはオブジェクト指向が使いこなせない Android/Java編
はじめに
本記事は、Android/Java開発において、オブジェクト指向を使いこなしたい、という方に向けた記事です。
私が10年近くAndroidアプリ開発に関わってきて思うのが、開発現場でオブジェクト指向らしいコードを書けるプログラマーは、残念ながら全体の2割にも満たない、ということです。(あくまで私個人の感想です)。
逆に言えば、オブジェクト指向らしいコードを書けるようになるだけで、開発現場で重宝される人材になれます。Androidの最新技術やKotlinの勉強をすることも大切ですが、開発現場でより即効性があり、かつ、Android以外のプログラミングにも応用できる、潰しのきく技術、それがオブジェクト指向らしいコードを書く技術です。どうしたらオブジェクト指向らしいコードを書く技術を身につけられる?
※具体的な技術的な話をすぐに読みたい方は「開発現場」の段落までスキップしてください。
昔、先輩社員に「どうしたらオブジェクト指向らしいコードを書く技術を身につけられるか」を聞いたところ「開発経験を積むこと」というあいまいな答えをもらい悩んだことがあります。この「開発経験」は自分にとって余程都合が良いプロジェクトでないと意味がありません。テスターとしてプロジェクトに参画するのはもちろん、他者が書いたコードの拡張や改修する役割で参画してもあまりよい経験にはなりません(AndroidのAPIの使い方を知るという意味では意味がありますが)。自分でソフト構成を設計できるような新規案件や大きな機能ブロックを作成できる案件でないとなかなか「オブジェクト指向らしいコードを書く技術」を磨くチャンスがないのです。しかし、多くの仕事内容は、他者が書いたコードを他者が書いたコードの拡張や改修する作業です。
オブジェクト指向らしいコードを書く技術を磨くには、自分で技術の本や記事を積極的に読み、実際に自分でコードを書き、頭をトレーニングする必要があります。おすすめのトレーニング方法
「オブジェクト指向らしいコードを書く技術」を身につける為におすすめなのが「デザインパターン」と「リファクタリング」の技術を身につけることです。どちらもオブジェクト指向らしいコードを身につける為の要素がたくさん詰まっています。Android/Javaプログラマーは比較的幸運だと思います。なぜなら、結城 浩さんの良書中の良書があるからです。
私もこれらの本を読んだのがきっかけで、オブジェクト指向らしくコードを書こうとするようになりました。ただ良書ではあるのですが、どちらもそれなりにページ数が多い為、はじめは読むのに尻込みしてしまうかもしれません。そこで、初心者でも取っつきやすい内容を目指して本稿を書きました。
デザインパターン
デザインパターンについて、もう少し掘り下げます。デザインパターンはGoFの23パターンが有名で、上記のデザインパターン本もGoFの23パターンをベースにした本になっています。私個人の意見としては、最初は23種全部覚えるよりも、重要ないくつかのパターンを使いこなすのに時間を掛けた方がよいと思います。次のパターンを優先的にマスターするのがおすすめです。
- Template Methodパターン
- Factory Methodパターン
- Facadeパターン
- Stateパターン
- Builderパターン
- Observerパターン
この中で最初に覚えた方がよいパターンは「Template Method パターン」です。プログラミングのできる方だと、わざわざデザインパターンの名前も付ける必要もないくらいオブジェクト指向の性質そのものだと言う方もいると思います。それくらい「基本でかつ奥義」みたいなパターンです。この「Template Method パターン」について本記事を通して学んでいきましょう。
開発現場
※筆者注:導入部は物語風会話形式で書いてみました。
とあるソフト開発会社「エゥーゴソフト」開発チームの二人の会話から始まります。登場人物説明
- ブライト(リーダー):社会人(&開発経験)5年目。新人の頃からAndroid/Java一筋で、厳しい先輩に教わったこともあり、現在では新規アプリ開発プロジェクトの立ち上げを任されている。
- アムロ(新人女性社員):大学生の頃は主にC言語でプログラミングしていた。Java言語は入門書の7割くらいは読んだが、正直オブジェクト指向と言われてもピンとこない。新人研修のときに初めてAndroidの作り方を学んだ。
ブライトさんの作成依頼
ブライトリーダー:アムロさん、こんな感じのAndroidアプリを作ってほしいんだ。(といって下記のイメージを見せる)
- アプリを起動すると背景が青い画面を表示し、中央に「次」ボタンを表示すること
- 「次」ボタンを押下すると、背景が黄色の画面->赤い画面->青い画面と切り替わること
- Backキーを押下するとアプリが終了すること(Androidアプリでデフォルトの動作のままでよい)
ブライトリーダー:アムロさんはAndroidを新人研修で教わったみたいだけど、いきなり全部作れって言うのは厳しいと思うから、途中まで(青い画面を表示して、ボタンが押された時のイベント処理をどう書けばよいのかがわかるところまで)作ってあるから、これをベースに作ってみて。
アムロ:わかりました。作ってみます。ベースとなるコード(Javaファイル以外は省略)
サンプル:ColorSample_base
※サンプルはGithubに格納してあります。BlueActivity.javapackage com.example.colorsample; import android.graphics.Color; import android.os.Bundle; import android.view.View; import androidx.appcompat.app.AppCompatActivity; public class BlueActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.layout).setBackgroundColor(Color.BLUE); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 別のActivityに遷移する時の処理イメージ // Intent intent = new Intent(BlueActivity.this, YellowActivity.class); // intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // startActivity(intent); } }); } }。。。。
アムロさんはAndroid Studioの操作に苦戦しつつも、アプリを作り上げました。
アムロ:できました。こんな感じでどうでしょう。(ブライトさんにAndroid端末の画面を見せながら操作する)
ブライトリーダー:いいね。動作は完璧だね。ソースコードの方を見せてもらえる?アムロさんが作ったコード
サンプル:ColorSample_amuroBlueActivity.javapackage com.example.colorsample; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.View; import android.widget.FrameLayout; import androidx.appcompat.app.AppCompatActivity; public class BlueActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.layout).setBackgroundColor(Color.BLUE); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(BlueActivity.this, YellowActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); } }YellowActivity.javapackage com.example.colorsample; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.View; import android.widget.FrameLayout; import androidx.appcompat.app.AppCompatActivity; public class YellowActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.layout).setBackgroundColor(Color.YELLOW); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(YellowActivity.this, RedActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); } }RedActivity.javapackage com.example.colorsample; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.view.View; import androidx.appcompat.app.AppCompatActivity; public class RedActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.layout).setBackgroundColor(Color.RED); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(RedActivity.this, BlueActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); } }ブライトリーダー:(そうきたか。。。)うーん。残念だけどお仕事の成果物としては、このままのコードではダメだね。
アムロ:えー。ボクのコードの何がダメなんですか。ちゃんと仕様通り動いているじゃないですか。
ブライトリーダー:BlueActivity.javaとYellowActivity.javaとRedActivity.javaってなんか「とてもよく似ている」よね。
アムロ:まあ、BlueActivity.javaをコピペしてYellowActivityクラスとRedActivityクラスを作って、画面の色とか遷移先のActivityクラスのコードを修正したので。
ブライトリーダー:(コピペしたって言っちゃった。正直というか。。。)似たようなコードがあるってことはもっとコードを少なくして、スッキリさせることができるはずなんだ。コードで説明するとわかりずらいから、日本語に置き換えて説明しよう。
3つのクラスの動作を日本語で表現すると、次のようになります。
この中で、クラス毎に異なる部分を赤字にします。
(1)は3つのクラスとも同じですね。(2)と(3)は赤字以外の部分は同じです。
この同じ部分(重複部分)が「無駄」と言えます。無駄な部分を取り消し線で書きます。
この取り消し線の部分がなくなったら、スッキリしたコードになると思いませんか。そこで次のように、赤字の部分を〇〇と△△に置き換えます。3つのクラスとも同じ内容になりました。
同じ内容なら1つのクラスで表現すればいいですよね。そこで3つのクラスのスーパークラス(親クラス)というものを考えてみます。
これをソースコードで表現すれば、スッキリしたコードになります(スーパークラスのクラス名をColorActivityとします)。
青色やYelloActivityのような情報を〇〇や△△に置き換えることを「抽象化」と言います。抽象化した情報を具体化するのはサブクラス(子クラス)側に任せ、スーパークラスは決めません。ソースコードを見ていきましょう。サンプル:ColorSample_final
ColorActivity.javapackage com.example.colorsample; import android.content.Intent; import android.os.Bundle; import android.view.View; import androidx.appcompat.app.AppCompatActivity; abstract public class ColorActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //(1) findViewById(R.id.layout).setBackgroundColor(getMyColor()); //(4) findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(ColorActivity.this, getNextClass()); //(5) intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); } /** * 自Activityの色を取得する * @return 自Activityの色 */ abstract protected int getMyColor(); //(2) /** * 次に遷移するActivity Classを取得する * @return 次に遷移するActivity Class */ abstract Class getNextClass(); //(3) }「(1)レイアウトを「activity_main.xml」に設定する」については、3つのクラスで同じ内容でした。同じならば、親クラスで1回書けば、子クラスに書く必要はなくなります。
getMyColor()やgetNextClass()はabstractが付いています。ColorActivityはどんな色か、どんなクラスかを具体的に決めません。決めるのはサブクラスに任せます。色や次のクラスを取得し、それを(4)や(5)で使用することだけをスーパークラスで書きます。
サブクラス(子クラス)のソースコードも見ていきましょう。BlueActivity.javapackage com.example.colorsample; import android.graphics.Color; public class BlueActivity extends ColorActivity { @Override protected int getMyColor() { return Color.BLUE; } @Override Class getNextClass() { return YellowActivity.class; } }YellowActivity.javapackage com.example.colorsample; import android.graphics.Color; public class YellowActivity extends ColorActivity { @Override protected int getMyColor() { return Color.YELLOW; } @Override Class getNextClass() { return RedActivity.class; } }RedActivity.javapackage com.example.colorsample; import android.graphics.Color; public class RedActivity extends ColorActivity { @Override protected int getMyColor() { return Color.RED; } @Override Class getNextClass() { return BlueActivity.class; } }
アムロ:なるほど。たしかにスッキリした感じがしますね。BlueActivityとか差分の情報しかないって感じがすごくします。サブクラスは、Android固有のメソッドが1つも出てこないんですね。。。でもこのようなコードに改造するのは大変な気がします(というかメンドクサイ)。なんでこんなことをやる必要があるのでしょうか。
ブライトリーダー:簡単に言えば
(1)「バグの量」を減らすこと
(2)クラスの役割分担を明確する
を行うことでメンテナンスしやすくなるんだ。メンテナンスしやすければ「楽に」働ける。
(1)は、ソースコードを人が書く以上、書く量が多ければ多い程ミスが出て、バグが生まれる可能性が高くなる、という考え方から来ている。だから基本的にコード量は少ない方がいいんだ。(Javaのコード量で開発工数の見積もりをしようとお客さんがいるけど、正直コード量で見積もりするのはナンセンスだよな。コピペ文化を増長するし。)
(2)は、日本語の赤字をつけた例を見ればわかると思うけど、本来BlueActivityやYelloActivityは「何色か」、「次のAcitvityクラスは何か」の2つを決めるだけでいいはずなのに黒字と赤字が混ざっている。言い換えれば、Android固有のメソッド(API)を呼ぶコード(setContentView()やstartActivity())と呼ばなくてもよいコードが混ざっている。例えば、AndroidのVersionが上がってAPIの仕様変更があって動かなくなったときにどこを修正すればいいのか調査する必要が出てきたとする。この場合、アムロさんのコードでは、BlueActivity、YelloActivity、RedActivityの3つのクラスの調査する必要がある。
<ColorSample_amuroのソフト構成>
それに比べてColorSample_finalサンプルは、ColorActivityだけを(基本的に)調査すればいいんだ。
<ColorSample_finalのソフト構成>
アムロさんには、ぜひ「ColorSample_final」のようなコードが書けるようになってほしいね。
アムロ:わかりました。がんばります!Template Method パターンの理解を深める為のQ&A
- Q.Template Method パターンとは、何かを取得する処理を抽象化するパターンのことである。Yes or No
- A. No。抽象化の対象は動作(動詞)、つまりメソッドのことです。ただし、〇〇を取得するというメソッドの例が、Template Method パターンを理解する上で一番わかりやすい例だとは思います。
- Q. 「Parent parent = new Child();」のようなコードが出てこないんだけど、これって本当にTemplate Method パターンのサンプル?
- A. 今回のサンプルでは、「Parent parent = new Child();」の部分をAndroidが行っています。Template Method パターンの例としては、微妙かもしれませんが、メソッドを抽象化する感覚を感じて頂ければと思います。プログラミング言語の入門書を読んだだけだと、なかなか「abstract」や「interface」を自分で使ってみようという気にならないと思います。Template Method パターンを通じて、「abstract」や「interface」を自分が書いているコードに適用できないか考えてみましょう。
あとがき
ここまで読んでいただきありがとうございます。
本記事はできるだけ「オブジェクト指向の用語の説明」に傾注しないように注意して書きました。もともと数年前に、本屋で見かけたJava言語向けのオブジェクト指向の本が、あまりに現場の感覚と離れていて「使えない」と思ったのがきっかけでした。オブジェクト指向の用語を絵とか書いて(インスタンスとは~をハンコの絵で描いて説明している)伝えようとしているのですが、数学の公式をマスターするのに学校の先生が一生懸命黒板に書いても説明しても生徒が公式をマスターできないように、オブジェクト指向も自分で手と頭を使って初めて習得できるものだと思います。
本記事が少しでもオブジェクト指向の理解と実戦で活かせる助けになれば幸いです。
- 投稿日:2020-12-04T18:29:16+09:00
【Kotlin】Navigation Component Architectureで安全に遷移する方法
この記事はKotlin Advent Calendar 2020の4日目の記事です。
Androidアプリ開発初心者ですが、空きがあったので勢いだけで参加してみました。はじめに
Navigation Architecture Componentを使ってアプリを作成した際、
素早く操作した場合や戻るボタンの連打などで、NavControllerと表示されているFragmentがズレてしまい、エラー(IllegalArgumentException
)が頻発しました。
これからライブラリ自体のアップデートで解消されていく可能性はありますが、現時点では、自前で対策する必要があります。普通の画面遷移
MainFragmentからSubFragmentへの遷移findNavController().navigate(R.id.action_mainFragment_to_subFragment) // パラメータを渡す場合 findNavController().navigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))SubFragmentからMainFragmentへ戻る場合の遷移findNavController().popBackStack() // または findNavController().popBackStack(R.id.MainFragment, false)特にpopBackStack()を使った場合は、連打するとNavController内で管理されているBackStackがズレやすくなります。(戻りすぎる)
対策済み
Extensionを利用しFragmentに画面遷移用のメソッドを生やします。
(画面遷移前にヒストリとの整合性確認を行う処理を追加しています)FragmentExtension.ktpackage com.sample.extensions import android.os.Bundle import androidx.fragment.app.Fragment import androidx.navigation.* import androidx.navigation.fragment.DialogFragmentNavigator import androidx.navigation.fragment.FragmentNavigator import androidx.navigation.fragment.findNavController /** * ============================================== * Fragment Extensions * ============================================== */ /** * 現在のFragmentがヒストリの最新と一致しているか */ fun Fragment.isCurrentDestination(): Boolean { // ヒストリから現在のFragmentの情報を取得 val currentDestination: NavDestination = findNavController().currentDestination ?: return false when (currentDestination) { is DialogFragmentNavigator.Destination -> { if (currentDestination.className != this.javaClass.name) { // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致 return false } return true } is FragmentNavigator.Destination -> { if (currentDestination.className != this.javaClass.name) { // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致 return false } return true } else -> { return false } } } /** * safeNavigate */ fun Fragment.safeNavigate( resId: Int, args: Bundle? = null, navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null ) { if (!isCurrentDestination()) { return } findNavController().apply { navigate(resId, args, navOptions, navigatorExtras) } } /** * safeNavigate */ fun Fragment.safeNavigate( directions: NavDirections, navOptions: NavOptions? = null ) { safeNavigate(directions.actionId, directions.arguments, navOptions) } /** * safePopBackStack */ fun Fragment.safePopBackStack() { if (!isCurrentDestination()) { return } forcePopBackStack() } /** * forcePopBackStack * note:ヒストリと現在表示されているFragmentの一致チェックなしで戻る処理を行う * DialogFragmentを表示元のFragmentから非表示にする場合などの利用を想定 */ fun Fragment.forcePopBackStack() { findNavController().apply { val navBackStackEntry : NavBackStackEntry = previousBackStackEntry ?: return // ヒストリの1つ前の画面に戻る // popBackStack()だと、連打により2つ前の画面まで戻ってしまいヒストリがおかしくなることがある popBackStack(navBackStackEntry.destination.id, false) } } /** * safePopBackStack */ fun Fragment.safePopBackStack( destinationId: Int, inclusive: Boolean ) { if (!isCurrentDestination()) { return } findNavController().apply { popBackStack(destinationId, inclusive) } }MainFragmentからSubFragmentへの遷移safeNavigate(R.id.action_mainFragment_to_subFragment) // パラメータを渡す場合 safeNavigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))SubFragmentからMainFragmentへ戻る場合の遷移safePopBackStack() // または safePopBackStack(R.id.MainFragment, false)さいごに
NavigationComponentでの
IllegalArgumentException
を検索してみると、
ボタン連打防止処理や、navigate
前のヒストリとの整合性確認がよく出てきますが、
端末の戻るボタンでNavControllerを操作(popBackStack)している場合は、
popBackStackにも対策を行う必要がありました
- 投稿日:2020-12-04T18:29:16+09:00
【Kotlin】Navigation Architecture Componentで安全に遷移する方法
この記事はKotlin Advent Calendar 2020の4日目の記事です。
Androidアプリ開発初心者ですが、空きがあったので勢いだけで参加してみました。はじめに
Navigation Architecture Componentを使ってアプリを作成した際、
素早く操作した場合や戻るボタンの連打などで、NavControllerと表示されているFragmentがズレてしまい、エラー(IllegalArgumentException
)が頻発しました。
これからライブラリ自体のアップデートで解消されていく可能性はありますが、現時点では、自前で対策する必要があります。普通の画面遷移
MainFragmentからSubFragmentへの遷移findNavController().navigate(R.id.action_mainFragment_to_subFragment) // パラメータを渡す場合 findNavController().navigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))SubFragmentからMainFragmentへ戻る場合の遷移findNavController().popBackStack() // または findNavController().popBackStack(R.id.MainFragment, false)特にpopBackStack()を使った場合は、連打するとNavController内で管理されているBackStackがズレやすくなります。(戻りすぎる)
対策済み
Extensionを利用しFragmentに画面遷移用のメソッドを生やします。
(画面遷移前にヒストリとの整合性確認を行う処理を追加しています)FragmentExtension.ktpackage com.sample.extensions import android.os.Bundle import androidx.fragment.app.Fragment import androidx.navigation.* import androidx.navigation.fragment.DialogFragmentNavigator import androidx.navigation.fragment.FragmentNavigator import androidx.navigation.fragment.findNavController /** * ============================================== * Fragment Extensions * ============================================== */ /** * 現在のFragmentがヒストリの最新と一致しているか */ fun Fragment.isCurrentDestination(): Boolean { // ヒストリから現在のFragmentの情報を取得 val currentDestination: NavDestination = findNavController().currentDestination ?: return false when (currentDestination) { is DialogFragmentNavigator.Destination -> { if (currentDestination.className != this.javaClass.name) { // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致 return false } return true } is FragmentNavigator.Destination -> { if (currentDestination.className != this.javaClass.name) { // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致 return false } return true } else -> { return false } } } /** * safeNavigate */ fun Fragment.safeNavigate( resId: Int, args: Bundle? = null, navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null ) { if (!isCurrentDestination()) { return } findNavController().apply { navigate(resId, args, navOptions, navigatorExtras) } } /** * safeNavigate */ fun Fragment.safeNavigate( directions: NavDirections, navOptions: NavOptions? = null ) { safeNavigate(directions.actionId, directions.arguments, navOptions) } /** * safePopBackStack */ fun Fragment.safePopBackStack() { if (!isCurrentDestination()) { return } forcePopBackStack() } /** * forcePopBackStack * note:ヒストリと現在表示されているFragmentの一致チェックなしで戻る処理を行う * DialogFragmentを表示元のFragmentから非表示にする場合などの利用を想定 */ fun Fragment.forcePopBackStack() { findNavController().apply { val navBackStackEntry : NavBackStackEntry = previousBackStackEntry ?: return // ヒストリの1つ前の画面に戻る // popBackStack()だと、連打により2つ前の画面まで戻ってしまいヒストリがおかしくなることがある popBackStack(navBackStackEntry.destination.id, false) } } /** * safePopBackStack */ fun Fragment.safePopBackStack( destinationId: Int, inclusive: Boolean ) { if (!isCurrentDestination()) { return } findNavController().apply { popBackStack(destinationId, inclusive) } }MainFragmentからSubFragmentへの遷移safeNavigate(R.id.action_mainFragment_to_subFragment) // パラメータを渡す場合 safeNavigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))SubFragmentからMainFragmentへ戻る場合の遷移safePopBackStack() // または safePopBackStack(R.id.MainFragment, false)さいごに
NavigationComponentでの
IllegalArgumentException
を検索してみると、
ボタン連打防止処理や、navigate
前のヒストリとの整合性確認がよく出てきますが、
端末の戻るボタンでNavControllerを操作(popBackStack)している場合は、
popBackStackにも対策を行う必要がありました
- 投稿日:2020-12-04T18:13:06+09:00
Flow 版ズンドコキヨシを Android で使用する
ACCESS Advent Calendar 2020 5日目の記事です。
と言いつつ、内容は過去に自分の書いた記事の続編です。
昔流行ったズンドコキヨシを通じて Coroutines Flow の良さを考えていきます。やりたいこと
前回、Coroutines Flow を使用してジェネリックなズンドコキヨシを実装しました。
検証はコンソール環境で行いましたが、せっかく Kotlin で実装したので Android でも使用してみたいと思います。
Android 向け Coroutines にはFlow
をLiveData
に変換するasLiveData
拡張関数が用意されています。
これを使用しつつ、その利点を追求していきます。再掲 : Flow 版ジェネリック・ズンドコキヨシ
GenericZDK.ktfun <T> randomFlow(source: List<T>) = flow<T> { while (true) { emit(source.shuffled().first()) } } fun <T> terminateIfSatisfiedFlow( scanned: List<T>, pattern: List<T>, terminal: T ): Flow<T?> = if (scanned == pattern) { flowOf(scanned.last(), terminal, null) } else { flowOf(scanned.last()) } @FlowPreview @ExperimentalCoroutinesApi fun <T> genericZDK( source: List<T>, pattern: List<T>, terminal: T ): Flow<T> = randomFlow(source) .scan(emptyList<T>()) { list, e -> (list + e).takeLast(pattern.size) } .dropWhile { it.isEmpty() } .flatMapConcat { scanned -> terminateIfSatisfiedFlow(scanned, pattern, terminal) } .takeWhile { it != null } .filterNotNull()Android でもズンドコしたい
バージョン情報
こちら
- Android Studio 4.2 Canary 15
- org.jetbrains.kotlin:kotlin-stdlib:1.4.10
- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0
- androidx.core:core-ktx:1.3.2
- androidx.fragment:fragment-ktx:1.2.5
- androidx.lifecycle:lifecycle-livedata-ktx:2.2.0
- androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0
asLiveData
について
Flow<T>
から収集(collect
)された値を保持するLiveData<T>
を作成します。
定義を確認しましょう。fun <T> Flow<T>.asLiveData( context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT ): LiveData<T>
asLiveData
で生成されたLiveData
は Cold の性質を持ちます。
つまり、誰かがobserve
してLiveData
がアクティブな状態になった時に上流Flow
が起動します。
Flow
が完了する前にLiveData
が非アクティブになると、timeoutInMs
で指定した時間経過後にFlow
をキャンセルします(デフォルトは5秒)。
キャンセル後にLiveData
が再びアクティブになると、Flow
が再起動します。なぜこんなことを説明しているのかというと、この性質がズンドコキヨシをAndroidで利用する上で重要だからです。
ズンドコキヨシはキ・ヨ・シ!
が出るまで無限ループでズン
ドコ
を放出し続けますが、キ・ヨ・シ!
が出る前にユーザが飽きてしまい、アプリを閉じたりスリープした場合にどうなるかを考える必要があります。
Flow.asLiveData
ならこのような問題にも容易に対処することができます。ではまず、
asLiveData
を使用してViewModel
を実装します。ViewModel
genericZDK
でズンドコフローを取得したら、onEach
内でdelay
してテキストの更新間隔を調整します。
上流Flow
はメインスレッドで動作させたくないので、flowOn
でデフォルトディスパッチャを指定します。
ViewModel
でasLiveData
を使用する場合はviewModelScope.coroutineContext
を指定するのが良いでしょう。
間違ってもGlobalScope
を指定してはいけません。
ズンドコが止まらなくなる恐れがあります。ZDKViewModel.kt@ExperimentalCoroutinesApi @FlowPreview class ZDKViewModel : ViewModel() { companion object { private const val ZUN = "ズン" private const val DOKO = "ドコ" private const val KI_YO_SHI = "キ・ヨ・シ!" private val SOURCE = listOf(ZUN, DOKO) private val PATTERN = listOf(ZUN, ZUN, ZUN, ZUN, DOKO) private const val INTERVAL_MS = 600L } val zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI) .onEach { delay(INTERVAL_MS) } .flowOn(Dispatchers.Default) .asLiveData(viewModelScope.coroutineContext) }View (Fragment)
Data Binding を使っても良いですが、大した規模ではないので今回は採用しません。
やることは単純で、ViewModel
で定義したzdk
プロパティをTextView
にバインドするだけです。MainFragment.kt@FlowPreview @ExperimentalCoroutinesApi class MainFragment : Fragment() { private val viewModel by viewModels<ZDKViewModel>() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return inflater.inflate(R.layout.main_fragment, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.zdk.observe(viewLifecycleOwner) { message.text = it } } }
レイアウト(ほぼテンプレ通り)
main_fragment.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.main.MainFragment"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="60dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="MainFragment" /> </androidx.constraintlayout.widget.ConstraintLayout>問題点
見てわかる通り、このままでは
ズン
(あるいはドコ
)が連続で流れてきた時にテキストが更新されたことを目視で確認できないため、テンポ感を掴みにくいです。
いくつかの解決策を模索していきましょう。
主題に沿うため、すべてFlow
のオペレータを使って解決します。解決策1 : 点滅させる
flatMapConcat
を使ってテキストを点滅させてみましょう。
flatMapConcat
は流れてきた値を別のFlow
に変換し、各Flow
を順番通りに連結します(Rx のconcatMap
に相当します)。ZDKViewModel.ktval zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI) .flatMapConcat { flow { emit(it) delay(INTERVAL_MS * 2 / 3) if (it != KI_YO_SHI) { emit("") delay(INTERVAL_MS / 3) } } } .flowOn(Dispatchers.Default) .asLiveData(viewModelScope.coroutineContext)まず流れてきた値をそのまま
emit
し、一定時間置いた後に空文字列をemit
することで点滅を表現します。
ただし最後のキ・ヨ・シ!
は消したくないのでその条件を付与します。解決策2 : 左右に振る
いまいち躍動感に欠けるので別の方法を模索します。
テキストを左右に振ってみましょう。
といってもTextView
の座標をいじるのは面倒なので、前後にスペースを挿入することにより擬似的に揺れを表現します。ZDKViewModel.ktval zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI) .withIndex() .map { when { it.value == KI_YO_SHI -> it.value it.index % 2 == 0 -> "${it.value} " else -> " ${it.value}" } } .onEach { delay(INTERVAL_MS) } .flowOn(Dispatchers.Default) .asLiveData(viewModelScope.coroutineContext)
withIndex
はFlow<T>
を、インデックス情報を付与したFlow<IndexedValue<T>>
に変換します。
map
を使用して、偶数回なら後ろにスペースを、奇数回なら前にスペースを挿入します。
ただし、最後のキ・ヨ・シ!
は常にセンターに表示するものとします。解決策3 : ランダムに文字サイズ(あるいは文字色)を変える
ランダムに文字サイズや文字色を変えることが出来ればより華やかになるでしょう(?)。
これを実現するために、前回のズンドコキヨシで実装したrandomFlow
関数を再利用します。
以下は文字サイズを変更する例です。ZDKViewModel.kt// random text size factor (0.5f ~ 1.0f) private val randomSizeFlow = randomFlow((50..100 step 10).toList()) .distinctUntilChanged() .map { it / 100f } private val zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI) .zip(randomSizeFlow) { text, size -> text to if (text == KI_YO_SHI) 1.1f else size } .onEach { delay(INTERVAL_MS) } .flowOn(Dispatchers.Default) .asLiveData(viewModelScope.coroutineContext) // LiveData<Pair<String, Float>> val zdkText = zdk.map { it.first } val zdkSizeFactor = zdk.map { it.second }サイズ(の倍率)候補からランダムに1つを選び続ける
Flow
をrandomFlow
で作成します。
ただし、前回とは必ず異なるサイズを選択したいので、distinctUntilChanged
をつけます。ズンドコフローとサイズフローの2本を
zip
関数で1本のFlow
にまとめます。川が合流するイメージです。
ここでテキストとサイズ倍率のPair
をつくります。
ただし、最後のキ・ヨ・シ!
は固定の特大サイズ(1.1倍)で表示するものとします。
最後にLiveData
のmap
でテキストとサイズ倍率に分解します(これは必須ではありません)。
こうすることでテキストと文字サイズの更新タイミングを揃えることができます。
Fragment
を以下のように書き換えたら完成です。MainFragment.ktoverride fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.zdkText.observe(viewLifecycleOwner) { message.text = it } val textSize = message.textSize viewModel.zdkSizeFactor.observe(viewLifecycleOwner) { message.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize * it) } }ズンドコキヨシはバッテリーに優しいか
ズンドコキヨシは無限ループを使用しています。
冒頭で述べたとおり、もしアプリ終了やスリープしても無限ループが止まらなかったら困りものです。
バッテリーに優しくないズンドコキヨシという烙印を押されてしまいます。
randomFlow
関数に以下のようにログを仕込み、この点について検証してみましょう。GenericZDK.ktfun <T> randomFlow(source: List<T>) = flow<T> { try { while (true) { emit(source.shuffled().first().also { Log.i("ZUNDOKO", "emit $it") }) } } catch (e: Throwable) { Log.i("ZUNDOKO", "Thrown $e") throw e } }「解決策3」の実装で動作を確認します。
アプリ起動したら、キ・ヨ・シ!
が出現する前に端末をスリープ状態にします。Logcat2020-11-21 22:34:29.657 22142-22176/com.example.zdk I/ZUNDOKO: emit ドコ 2020-11-21 22:34:29.659 22142-22176/com.example.zdk I/ZUNDOKO: emit 70 2020-11-21 22:34:30.301 22142-22176/com.example.zdk I/ZUNDOKO: emit ズン 2020-11-21 22:34:30.303 22142-22176/com.example.zdk I/ZUNDOKO: emit 70 2020-11-21 22:34:30.303 22142-22176/com.example.zdk I/ZUNDOKO: emit 90 2020-11-21 22:34:30.945 22142-22176/com.example.zdk I/ZUNDOKO: emit ズン 2020-11-21 22:34:30.947 22142-22502/com.example.zdk I/ZUNDOKO: emit 60 2020-11-21 22:34:31.561 22142-22176/com.example.zdk I/ZUNDOKO: emit ドコ 2020-11-21 22:34:31.565 22142-22502/com.example.zdk I/ZUNDOKO: emit 50 2020-11-21 22:34:32.171 22142-22502/com.example.zdk I/ZUNDOKO: emit ズン 2020-11-21 22:34:32.173 22142-22502/com.example.zdk I/ZUNDOKO: emit 80 2020-11-21 22:34:32.782 22142-22502/com.example.zdk I/ZUNDOKO: emit ズン 2020-11-21 22:34:32.785 22142-22176/com.example.zdk I/ZUNDOKO: emit 90 2020-11-21 22:34:33.411 22142-22502/com.example.zdk I/ZUNDOKO: emit ズン 2020-11-21 22:34:33.413 22142-22502/com.example.zdk I/ZUNDOKO: emit 60 2020-11-21 22:34:33.965 22142-22502/com.example.zdk I/ZUNDOKO: Thrown kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@53327ff 2020-11-21 22:34:33.965 22142-22176/com.example.zdk I/ZUNDOKO: Thrown kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@53327ff「解決策3」では2本のランダムフローが動作していますが、そのどちらもキャンセルされていることがわかります。
これはLiveData
が非アクティブになることで未完了の上流Flow
がキャンセルされるためです。
なお、アプリ(Activity)を閉じた場合はviewModelScope
が閉じられることによりJobCancellationException
がスローされ、やはりキャンセルされます。
これにより、バッテリーに優しいズンドコキヨシであることが示されました。
Flow
とLiveData
の組み合わせでは、このようにライフサイクルのケアが手厚く行われる点が大きな利点といえます。
また、map
とdistinctUntilChanged
程度しかオペレータの無かったLiveData
と違い、Flow
には豊富なオペレータが用意されているので、両者を組み合わせることで実装の幅が大きく広がると思います。
- 投稿日:2020-12-04T17:47:15+09:00
xamarin-forms-tab-badgeでバッジ表示してみる(Xamarin, Android)
はじめに
モバイルアプリケーションを開発するときに、Xamarinを採用することもあると思います。
Xamarinについての説明はここでは割愛し、xamarin-forms-tab-badgeを利用して、通知のバッジを表示することに関して記載します。
ライブラリのドキュメントに使い方があるのでそれで十分なのですが、日本語の情報があまり見当たらなかったので、何かの参考になれば嬉しいです。
(なお、Androidエミュレータでしか確認していないため、iOSでは未確認です。)■ 参考
Xamarin:Xamarin(ザマリン) とはなんぞや
xamarin-forms-tab-badge:https://github.com/xabre/xamarin-forms-tab-badge完成イメージ
通知タブに赤丸でバッジが表示される。
タブの位置やスタイルに関しての実装については割愛します。
参考:TabbedPage実装
注:タブ機能の簡易な実装方法として、Xamarin.Forms Shellというものがありますが、こちらを利用するとバッジ表示ができないようなので、TabbedPageを利用します。
xamarin-forms-tab-badgeのインストール
VisualStudioのソリューションエクスプローラ上でプロジェクトを右クリック>NuGetパッケージ管理をクリック
Plugin.Badgeを検索し、[インストール]ボタンをクリックAssemblyInfo.csを修正
[プロジェクト名].Android/Properties/AssemblyInfo.cs を開き、以下を追加
AssemblyInfo.cs[assembly: ExportRenderer(typeof(TabbedPage), typeof(BadgedTabbedPageRenderer))]必要に応じて、usingを追加
TabbedPageの実装
TabbedPageのサブクラスをMainTabPage(任意)として実装する。
バッジを付けたい項目に属性としてplugin:TabBadge.BadgeText="{Binding NotificationCount}"
を追加MainTabPage.xaml<NavigationPage Title="通知" IconImageSource="{FontImage FontFamily={StaticResource MaterialFontFamily}, Glyph={Static icons:MaterialIcon.Bell}, Size=50}" plugin:TabBadge.BadgeText="{Binding NotificationCount}"> <x:Arguments> <local:NotificationListPage /> </x:Arguments> </NavigationPage>コードビハインドにバッジを変更するメソッドを用意
MainTabPage.cspublic partial class MainTabPage : TabbedPage { public MainTabPage() { InitializeComponent(); BindingContext = new { NotificationCount = "" }; } public void ChangeNotificationBadge(int count) { if (count == 0) { BindingContext = new { NotificationCount = "" }; } else { BindingContext = new { NotificationCount = "" + count }; } } }バッジ表示の実装
今回はAppから通知をつける場合を実装する。
上で実装したバッジ変更のメソッドを呼び出すApp.csif (Current.MainPage is MainTabPage tabPage) { tabPage.ChangeNotificationBadge(1); }まとめ
Shellが使えず、自分でTabbedPageを実装しないといけないのは少し面倒ではありますが、ライブラリを利用すると簡単にバッジ表示機能が実装できました。
iOSの場合はLinkerの設定などあるらしいので、もう一手間必要そうです。
- 投稿日:2020-12-04T15:34:21+09:00
Jetpack Compose における TalkBack 対応
本記事は Android Advent Calendar 2020 の 6 日目の記事です。
Jetpack Compose 以前はアクセシビリティ対応の一環として、
ImageView
にcontentDescription
を設定したり View のグルーピングで TalkBack の対応をしていました。
大きな一つの View の中でレンダリングされる Jetpack Compose ではどのように TalkBack の対応をするのかについて書いていきます。※ Jetpack Compose 1.0.0-alpha08 時点の記事で、今後大きな変更もあり得ます
Semantics
Jetpack Compose でアクセシビリティに対応するときは Semantics フレームワークを使用します。
これまでであれば読み上げツールが View のコンポーネントを識別していましたが、View のコンポーネントを操作できない Compose では Semantics を使用して構造化された UI に対して意味を与える必要があります。また Semantics はアクセシビリティだけでなく、Jetpack Compose で UI テストをするときにも使用されます。
ref : https://developer.android.com/jetpack/compose/testing#semantics
accessibilityLabel
accessibilityLabel
では読み上げる文言の設定ができます。IconButton( onClick = {}, modifier = Modifier.semantics { accessibilityLabel = "戻るボタン" } ) { Icon(imageVector = Icons.Default.ArrowBack) }これは ← のアイコンがついたボタンに
Modifier#semantics
でアクセシビリティに関することを設定します。
accessibilityLabel
でボタンに対して読み上げて欲しい文言を設定します。
これまでのcontentDescription
に近いものがこれになります。mergeDescendants
mergeDescendants
は複数の要素をグループ化します。Column( modifier = Modifier.semantics(mergeDescendants = true) { } ) { Text( text = "タイトル", style = MaterialTheme.typography.subtitle1 ) Providers(AmbientContentAlpha provides ContentAlpha.medium) { Text( text = "サブタイトル", style = MaterialTheme.typography.caption ) } }
mergeDescendants
を true にすると要素がまとめて読み上げられます。
上記の例では 「タイトル サブタイトル」 と 2 つのテキストがまとめて読み上げられます。accessibilityValue
accessibilityValue
は要素の状態を設定することができます。var checked by remember { mutableStateOf(false) } Row( modifier = Modifier.semantics(mergeDescendants = true) { accessibilityValue = if (checked) { "チェック済み" } else { "未チェック" } } ) { Checkbox(checked = checked, onCheckedChange = { checked = it }) Text("アイテム") }チェックボックスとテキストをグループ化し、チェックボックスの状態を
accessibilityValue
に設定しています。
上記の例では 「チェック済み アイテム」 と読み上げられます。clickable
Compose でタップイベントを実装する
Modifier#clickable
も内部の実装ではsemantics
で読み上げに対応しています。Box( modifier = Modifier.fillMaxWidth() .clickable( onClick = { ... }, onClickLabel = "詳細を開く" ) ) { ... }
Modifier#clickable
にはonClickLabel
とonLongClickLabel
を設定でき、それぞれにタップ時に読み上げる文言を設定することができます。val semanticModifier = Modifier.semantics(mergeDescendants = true) { if (enabled) { onClick(action = { onClick(); true }, label = onClickLabel) if (onLongClick != null) { onLongClick(action = { onLongClick(); true }, label = onLongClickLabel) } } else { disabled() } }
Modifier#clickable
の内部ではこのような実装になっており、semantics
が設定されています。customActions
customActions
はフォーカスされている項目に対してアクションを設定することができます。Box( modifier = Modifier.semantics(mergeDescendants = true) { customActions = listOf( CustomAccessibilityAction("アクション") { // return a boolean result indicating whether the action is successfully handled true } ) } ) { ... }TalkBack では選択された項目に対してジェスチャーでコンテキストメニューを呼び出して操作する場合もあります。
CustomAccessibilityAction
でアクションを定義することで、コンテキストメニューでの項目を追加することができます。Jetpack Compose における基本的な TalkBack の対応について書いてみました。
Jetpack Compose はまだ alpha とはいえアクセシビリティに対応する仕組みが用意されているので、置き換えるにしても新規で作るにしても意識しておきましょう。Compose のサンプルプロジェクトでも要所要所に
semantics
の実装がされているので、そちらをみるのも参考になると思います。ドキュメント
- 投稿日:2020-12-04T00:32:57+09:00
DependabotをAndroidプロジェクトで使う方法
Dependabotは、依存しているライブラリのアップデートをチェックし、自動でPRを作ってくれる便利なbotです。
Androidプロジェクトでも、GitHub上からポチポチするだけで簡単にセットアップすることができたので、手順をご紹介したいと思います。
※ 個人のプライベートなAndroidプロジェクトで試しました。
app
モジュールのみのオーソドックスなAndroidプロジェクトです。手順
色々と設定を記述することもできますが、一旦無視して package-ecosystem のところに 「gradle」 を記述します。
ちなみに、変数を用いてバージョンを共通して書くやり方をしている箇所でも、うまいこと変数のところだけアップデートされるようでした。(賢い!)
まとめ
DependabotをAndroidプロジェクトで使う方法をご紹介しました。
ポチポチするだけで設定できるので、かなり簡単に導入することができるなぁと感じました。自動でPRを出してくれるのも非常に便利です。
ただ、ライブラリのアップデートのPRは出してくれますが、機械的なアップデートなため、アプリの機能が動かなくなったり、レイアウトが崩れたりなどの問題が起こる可能性もあるので、PR起点のCIでテストを回したり、PRをpullして手元で確認したりなどは必要なのかなと思いました。
Dependabotのドキュメントは こちら を御覧ください。