20210913のAndroidに関する記事は5件です。

AlertDialogのカスタマイズその1:引数を渡そう

何かと便利なAlertDialogですが、引数を渡してその内容を表示させることも簡単にできます。 ViewBindingを使い柔軟にカスタマイズすることもできますが、今回はシンプルにテキストのみを呼び出し元から渡すサンプルを実装してみたいと思います。 AlertDialogについて 基本的にAndroidのDialogはタイトル、コンテンツエリア、PositiveButton、NegativeButton、NeutralButtonといった部品で構成されています(ボタンの総称をActionButtonとも言います)。 DialogとしてはAlertDialogが最もよく使われ、Alertとはありますが別に警告に限らず様々な用途で使うことができます。 今回はMainAcitivtyでMaterialAlertDialogBuilder()を使用する方法と、DialogFragmentを使う方法の2種類を紹介します。 作成するサンプル ListViewに英単語の一覧を表示し、アイテムをクリックするとAlertDialogが起動し、その単語の品詞、意味を表示するサンプルを作成します。 なおListViewのカスタマイズについては本記事では触れません。 なお今回紹介する2つの方法どちらも同じ動きをします。 コード それでは両者のコードを見てみましょう。 MaterialAlertDialogBuilder()で実装 たぶん一番楽なやり方です。 MainActivity.kt class MainActivity : AppCompatActivity() { // (省略) private inner class ListItemClickListener : AdapterView.OnItemClickListener{ // リストのアイテムがクリックされたときのイベントハンドラ override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val item = parent?.getItemAtPosition(position) as MutableMap<String, Any> val wordName = item["name"] as String val wordSpeech = item["speech"] as String val wordMeaning = item["meaning"] as String val builder = MaterialAlertDialogBuilder(this@MainActivity) builder.setTitle(wordName) builder.setMessage("$wordSpeech:$wordMeaning") val dialog = builder.create() dialog.show() } } } シンプルですね。 MaterialAlertDialogBuilderにcontextを渡しbuilderオブジェクトを生成します。 生成したオブジェクトにsetTitle()、setMessage()で値を設定するだけですね。 ただまぁ「引数を渡す」ってのとはちょっとイメージが違うか。 今回は触れていませんがもちろん各ボタンがクリックされた場合の処理もMainAcitivty内部に記述できます。 DialogFragmentで実装 ちゃんと(?)DialogFragmentを生成し、そこに引数を渡します。 拡張性はこちらの方が高いですね。 WordDialogFragment.kt class WordDialogFragment : DialogFragment() { // 表示する「単語」、「品詞」、「意味」のプロパティ private var _word: String = "" private var _speech: String = "" private var _meaning: String = "" companion object{ // インスタンス生成のメソッド // 引数として読みだし元から「単語」、「品詞」、「意味」を受け取る fun create(word: String, speech: String, meaning:String): WordDialogFragment{ val bundle = Bundle() bundle.putString("Word", word) bundle.putString("Speech", speech) bundle.putString("Meaning", meaning) val dialog = WordDialogFragment().also { it.arguments = bundle } return dialog } } // 引数の内容をプロパティに設定 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _word = arguments?.getString("Word") as String _speech = arguments?.getString("Speech") as String _meaning = arguments?.getString("Meaning") as String } // ダイアログの表示 // プロパティから画面部品に値を設定 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = activity?.let{ val builder = AlertDialog.Builder(it) builder.setTitle(_word) builder.setMessage("$_speech:$_meaning") builder.create() } return dialog ?: throw IllegalStateException("Error") } } すこし注意していただきたいのがメンバ変数にあたる_word、_speech、_meaningのデータの持ち方(与え方?)です。 コンストラクタの引数としてこれらのデータを受け取り、onCreateDialogでsetTitle()しようとすると画面回転させたときにアプリが落ちます。 というのもDialogFragment(実際にはそれ以外にもFragmentを継承しているクラスも)は、画面を回転させたりするとActivityが破棄され、そしてその都度再度生成される動きとなっているからです。 そしてその際の初期化は引数なしのコンストラクタで行われます。 なので落ちちゃうんですね。 というわけで今回のサンプルではonCreateで引数をメンバ変数に設定し、onCreateDialogでタイトル、メッセージにsetするという書き方をしています。 MainActivity.kt Dialogの生成は以下の通りです。 class MainActivity : AppCompatActivity() { // (省略) private inner class ListItemClickListener : AdapterView.OnItemClickListener{ // リストのアイテムがクリックされたときのイベントハンドラ override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val item = parent?.getItemAtPosition(position) as MutableMap<String, Any> val wordName = item["name"] as String val wordSpeech = item["speech"] as String val wordMeaning = item["meaning"] as String // ダイアログの生成 var dialogFragment = WordDialogFragment.create(wordName, wordSpeech, wordMeaning) dialogFragment.show(supportFragmentManager, "WordDialog") } } } こちらはDialogFragmentを呼び出しているだけですね。 create()メソッドで引数を渡すのだけ忘れないでください。 まとめ シンプルに引数を渡して表示するだけの内容ですが、DialogFragment側でのデータの持ち方に少し工夫が必要ですね。 次はDialogから引数を受け取る方法についてまとめてみたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フォアグラウンドサービスの作り方

はじめに 通常のサービスには制限がありますが、フォアグラウンドサービスにすることにより制限を緩和できます。 実装 次のコードは通常のサービスの実装です。 class MyService : Service() { override fun onBind(intent: Intent): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return START_STICKY } } それに対して次のコードはフォアグラウンドサービスの実装です。 class MyService : Service() { override fun onBind(intent: Intent): IBinder? { return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val CHANNEL_ID = "asdf" // Android 8.0 以降では必ず通知チャネルを作成する必要がある if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val mChannel = NotificationChannel( CHANNEL_ID, "ここが通知チャネル名として表示される", NotificationManager.IMPORTANCE_DEFAULT, ) mChannel.apply { description = "ここが通知チャネルの説明として表示される" } val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager // 既存の通知チャネルを作成しても問題ない manager.createNotificationChannel(mChannel) } // Android 8.0 よりも前では `CHANNEL_ID` は無視される val notification = NotificationCompat.Builder(this, CHANNEL_ID).build() startForeground(1, notification) return START_STICKY } } 通知チャネルと通知を作成し、startForeground() に通知を渡しています。この通知はサービス起動中は常に通知領域に表示されます。 フォアグラウンドサービスを生成する側では、次の2行のコードを実行するだけでOKです。 val intent = Intent(this, MyService::class.java) startForegroundService(intent) 通知チャネルの注意点 一度作成した通知チャネルの名前、説明文、優先度は変更できない フォアグラウンドサービスを起動するだけであれば setSmallIcon は必須ではない(通常の通知では必須) ただし、setSmallIcon を設定しないと setContentTitle と setContentText の設定は反映されない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1年間Flutterで開発をしていてわかったアクセシビリティの問題(Android編)

この1年コンスタントにFlutterでコーディングをしていてわかったスクリーンリーダー(TalkBack)の問題についての記録です。 iOSについては1年間Flutterで開発していてわかったアクセシビリティの問題(iOS編)をご覧ください。 文字入力時にボリュームボタンでカーソル移動した時の文字読み上げがない iOSに比べAndroidは動作の安定性や不具合の修正があり、現状ほとんど問題なく開発ができています。 ただ最近のバージョンで上記の問題が発生しています。 これはAndroid特有のアクセシビリティ機能であり、大変協力な物です。 文字入力中に ボリュームダウンボタン → 左カーソルキー - ボリュームアップボタン → 右カーソルキー の役割を果たします。 スワイプによるジェスチャーでも同様のことができますが、ボタン操作のほうが確実です。 が、残念ながらこのカーソル移動の時にフォーカスされた文字を読み上げない問題が起きています。 これは以前からあった問題ではなくバージョンアップに伴い起きている物です。 私が確認しているところではバージョン2.1.0-12.2.preまでは正常に動作していました。 全てではありませんが、バージョン2.5.0-5.3.preまでのいくつかで試してみましたが、読み上げがされませんでした。 まとめ 上記問題はissueとして報告していますが、暖簾に腕押しな状況です。 ほんとうは最新のバージョンで開発をしたいのですが、この問題のため古いバージョンを使っています。 Flutterの開発に関わっている方やバグ修正の得意な方がいらっしゃいましたら、改善に力を貸していただけますと幸いです。 下記サンプルでTalkBackをONにすると問題の確認ができます。 TalkBackは 設定 → ユーザ補助 → TalkBack から ショートカットの設定 TalkBackを使用する の順で有効にしてください。 TalkBackがONになるとジェスチャーが変わり、操作に戸惑うことと思います。 上記の順番で有効にすることでいつでもボリュームボタンのアップとダウンの同時押しでON/OFFを切り替えることができます。 端末内にTalkBackの項目がない場合は Android ユーザー補助設定ツール - Google Play のアプリ からインストールをお願いします。 TalkBackサンプル
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Empty Compose Activity から色々なものを作る

2021年09月13日現在、Deprecated でない機能を使って色々なものを作る方法を記載します。 各機能の実装方法 Composable に ViewModel を渡す① build.gradle の修正は不要 ViewModel のインスタンス化は Activity の onCreate で行う class MyViewModel : ViewModel() { } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModel = ViewModelProvider(this).get(MyViewModel::class.java) setContent { Foo(viewModel) } } } @Composable fun Foo(viewModel: MyViewModel) { Text("hello, world") } 作られる ViewModel は MainActivity のライフサイクルと紐付けられるため、MainActivity と ViewModel の生存期間は同じです。 Composable 内で ViewModel の変更を検知する build.gradle の修正は不要 ViewModel 内で State<*> を作成する class MyViewModel : ViewModel() { val count = mutableStateOf(0) } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModel = ViewModelProvider(this).get(MyViewModel::class.java) setContent { Foo(viewModel) } } } @Composable fun Foo(viewModel: MyViewModel) { Row { Text(viewModel.count.value.toString()) Button(onClick = { viewModel.count.value += 1 }) { Text("click me!") } } } このように、State<*> が ViewModel に存在していても正常に動作します。 Composable 内で ViewModel にある LiveData の変更を検知する androidx.compose.runtime:runtime-livedata:$composeVersion アーティファクトをインストールする必要がある class MyViewModel : ViewModel() { private val _count = MutableLiveData(0) val count: LiveData<Int> = _count fun onChange(value: Int) { _count.value = value } } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModel = ViewModelProvider(this).get(MyViewModel::class.java) setContent { Foo(viewModel) } } } @Composable fun Foo(viewModel: MyViewModel) { val count = viewModel.count.observeAsState(0) Row { Text(count.value.toString()) Button(onClick = { viewModel.onChange(count.value + 1) }) { Text("click me!") } } } ひとつ前の mutableStateOf を使う方法に比べてコード量が増えています。mutableStateOf で要件を満たせるのであればそちらを使いたいところです。しかし、mutableStateOf を ViewModel 内で使うコードが公式サイトで見当たらないため、もしかするとバッドプラクティスかもしれません。 Composable に ViewModel を渡す② viewModel 関数を使用するために androidx.lifecycle:lifecycle-viewmodel-compose:$latestVersion アーティファクトをインストールする必要がある。$latestVersion はこちらから確認できる viewModel 関数は、ViewModel がなければ作成して返し、あればそれを返す class MyViewModel : ViewModel() { } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Foo() } } } @Composable fun Foo(vm: MyViewModel = viewModel()) { } 作成される ViewModel は、その Composable を利用するアクティビティが終了するまで同じものが使われます。そして、Composable が異なっていてもアクティビティが同じであればひとつの ViewModel が使われます。 @Composable fun Foo(vm: MyViewModel = viewModel()) { Button(onClick = { vm.count.value += 1 }) { Text("click me!") } } @Composable fun Bar(vm: MyViewModel = viewModel()) { Text(vm.count.value.toString()) } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Column { Foo() Bar() } } } } 上記コードの Foo Composable のボタンをタップすると、Bar Composable のテキストが更新されます。 単方向データフローのバケツリレー方式は冗長になりがち、React の Context 方式は依存関係がわかりづらいという欠点がありますが、viewModel 関数は両方の欠点をうまく取り除いています。 Navigation を追加する androidx.navigation:navigation-compose:$latestVersion アーティファクトをインストールする必要がある(バージョンはこちらから確認できる) @Composable fun Hoge() { val navController = rememberNavController() Column { Button(onClick = { navController.navigate("bar") }) { Text("click me!") } NavHost(navController, startDestination = "foo") { composable("foo") { Text("foo") } composable("bar") { Text("bar") } } } } ボタンを押すと、画面表示が "foo" から "bar" に切り替わります。 Destination が切り替わったときに Composable に何らかの変化を起こしたいときは、navController.currentBackStackEntryAsState() でオブザーバブルなインスタンスを取得します。 val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route 上記コードは、Destination が切り替わったときに再評価されます。それにより、currentRoute が常に今の Destination を表すようになります。 navBackStackEntry が Nullable である理由は、Destination が存在しないケースがあるからです。NavHost 関数の startDestination 引数により最初の Destination が設定されるため、それまでは Destination が存在しない状態になります。 Navigation で、一覧画面から詳細画面に遷移して一覧画面に戻ったときに、元の状態を保持しておく androidx.navigation:navigation-compose:$latestVersion アーティファクトをインストールする必要がある rememberSaveable を使うことで前回の状態を保存できる @Composable fun Hoge() { val navController = rememberNavController() Column { NavHost(navController, startDestination = "foo") { composable("foo") { Foo { navController.navigate("bar") } } composable("bar") { Bar { navController.popBackStack() } } } } } @Composable fun Foo(toBar: () -> Unit) { var done by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { delay(1000) done = true } Column { Button(onClick = toBar, enabled = done) { Text("show detail") } Text(if (done) "Done!" else "Loading...") } } @Composable fun Bar(back: () -> Unit) { Column { Button(onClick = back) { Text("back") } } } // 1 composable("bar") { Bar { navController.popBackStack() } } // 2 composable("bar") { Bar { navController.navigate("foo") } } 1 を 2 に書き換えたときは前回の状態がクリアされます。これは、ナビゲーションコントローラのバックキューに存在する前回の状態と .navigate("foo") により作成される Composable は実体が別々だからです。 Navigation で .navigate() を使って 画面 A -> 画面 B -> 画面 A と遷移したときに、前回の 画面 A の状態が残るようにする たとえ rememberSaveable を使ったとしても、最初の 画面 A と最後の 画面 A では実体が異なるため状態が共有されません。このようなときは、状態を上に持ち上げることで共有できます。 // build.gradle に追加するコード implementation "androidx.navigation:navigation-compose:2.4.0-alpha06" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07" class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Hoge() } } } @Composable fun Hoge() { var done by remember { mutableStateOf(false) } val navController = rememberNavController() NavHost(navController, startDestination = "foo") { composable("foo") { Foo( done = done, onDone = { done = true }, toBar = { navController.navigate("bar") }, ) } composable("bar") { Bar { navController.navigate("foo") } } } } @Composable fun Foo(done: Boolean, onDone: () -> Unit, toBar: () -> Unit) { LaunchedEffect(done) { if (!done) { delay(3000) onDone() } } Column { Button(onClick = toBar) { Text("to bar") } Text(if (done) "Done!" else "Loading...") } } @Composable fun Bar(toFoo: () -> Unit) { Button(onClick = toFoo) { Text("to foo") } } Foo の持っていた done という状態を上に移動しました。こうすることで、Navigation で画面を切り替えたときであっても状態を保つことができます。 LaunchedEffect に渡した suspend 関数の実行は .navigate("bar") のときにキャンセルされるため、再度 Foo を表示した時点から3秒後に done = true となります。 終わりに この記事は随時更新していこうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fluter doctorで--android-licensesが出来ないときの対処

あらすじ flutter doctorの初期設定の一つ。--android-licensesについて、戸惑うところと、その思考過程の一部始終について解説。初心者向けの内容です。 ドクター!アンドロイドライセンス! flutter doctor --android-licensesが効かなかった。 flutter doctor --android-licenses Android sdkmanager not found. Update to the latest Android SDK and ensure that the cmdline-tools are installed to resolve this. sdkマネージャーが見つからないらしい。 パスを書き換えてみる その1 探してみると、それっぽいのが見つかったため、 C:\Users\who\AppData\Local\Android\Sdk\tools\bin\sdkmanager.bat pathにC:\Users\who\AppData\Local\Android\Sdk\tools\binを追記。 ひとまず、もう一度ドクターを読んでみる。 flutter doctor --android-licenses Android sdkmanager not found. Update to the latest Android SDK and ensure that the cmdline-tools are installed to resolve this. やっぱりエラー。 パスを書き換えてみる その2 公式からcommand-toolsが出てるっぽいので、zip解凍してみると、中から、ドクターのエラーメッセージにあるcommand-toolsというフォルダ名が見つかり、「これは?!」と若干の期待。解凍したものをC:\Users\who\AppData\Local\Android\Sdk\の下に移動する。 先ほどpathに追加したものを書き換え、 C:\Users\who\AppData\Local\Android\Sdk\cmdline-tools\binに変更。 それでは、もう一度ドクターを読んでみる。 flutter doctor --android-licenses Android sdkmanager not found. Update to the latest Android SDK and ensure that the cmdline-tools are installed to resolve this. やっぱりエラー。 しかもなんだかエラーが出てる。 Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156) at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75) at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81) at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:73) at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:48) Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) ... 5 more sdkmanager Error: Could not determine SDK root. Error: Either specify it explicitly with --sdk_root= or move this package into its expected location: <sdk>\cmdline-tools\latest\ C:\Users\who\AppData\Local\Android\Sdk\cmdline-tools\latest\binというディレクトリ構成になるようにしろってことらしい。そうなるようにフォルダ配置しなおせば行けるのだろう。だが何か違う。こんなの新しいのが出てるか頻繁に手動確認が必要ではないか! Android SDKさんに頼ってみる VScodeを使う予定のため、二度と開かないであろうAndroid Studioさんを開いてみる。Android Studioからもインストールできるだろうし、IDEなら更新を自動でお知らせしてくれるだろう。 見つけた。Command-line tools (latest)と親切にもそれっぽい単語が並んでいる。ステキだ。 applyボタンとかそれっぽいことをしていくと、更新処理が走って完了する。ただなぜか、statusがNot Installedのままだ。気に入らない。 よく見ると、C:\Users\who\AppData\Local\Android\Sdk\cmdline-tools\binのとなりに、latestフォルダがあって、その下にちゃんとbinがある。インストールは出来たようだ。Not Installedのままだが。 パスを書き換えてみる その3 すでにディレクトリ構成は出来上がった。Android Studioさんありがとう。 先ほど書き換えたばかりのpathを再度書き換えてC:\Users\who\AppData\Local\Android\Sdk\cmdline-tools\latest\binとする。 sdkmanagerを呼んでみる。 PS C:\Users\who> sdkmanager [=======================================] 100% Computing updates... なんか勝手に更新した。 PS C:\Users\who> sdkmanager --version 5.0 バージョンちゃんと出ました。 これでOK PS C:\Users\who> flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 2.5.0, on Microsoft Windows [Version 10.0.19043.1165], locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0) [✓] Chrome - develop for the web [✓] Android Studio (version 2020.3) [✓] VS Code, 64-bit edition (version 1.60.0) [✓] Connected device (2 available) ! Device emulator-5554 is offline. • No issues found! Excelsior!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む