20210914のAndroidに関する記事は6件です。

Android + Kotlin での HTTP リクエストの方法

はじめに Firebase Authentication の ID トークンを HTTP ヘッダに付与したり、独自データと JSON 文字列間の変換を行ったりすることはよくあるのですが、頻繁に使う割に忘れるので記事として残します。 環境 build.gradle は次のような内容になっています。 plugins { ... id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.21' id 'com.google.gms.google-services' } dependencies { ... implementation "androidx.navigation:navigation-compose:2.4.0-alpha06" implementation platform('com.google.firebase:firebase-bom:28.4.0') implementation 'com.google.firebase:firebase-auth-ktx' implementation 'com.squareup.okhttp3:okhttp:4.9.0' } 実装 POST で送信 Firebase Authentication の ID トークンを HTTP ヘッダに付与 Firebase Authentication の認証が終えていない場合は例外を投げる Serializable なクラスのインスタンスを JSON 文字列にエンコード API サーバーから受け取った JSON 文字列をクラスのインスタンスにデコード 上記の仕様を持つ request 関数は次のようになります。 val API_PREFIX = "http://192.168.1.100:3000/api" val json = Json { ignoreUnknownKeys = true } @OptIn(ExperimentalSerializationApi::class) inline fun <reified T, reified S> request(path: String, post: T): S { val user = FirebaseAuth.getInstance().currentUser require(user != null) val idToken = Tasks.await(user.getIdToken(true)).token require(idToken != null) val client = OkHttpClient() val req = Request.Builder().apply { addHeader("Authorization", "Bearer $idToken") url("${API_PREFIX}${path}") post(json.encodeToString(post).toRequestBody("application/json".toMediaType())) }.build() client.newCall(req).execute().use { val str = it.body?.string() require(str != null) return json.decodeFromString(str) } } 正直なところ、inline や reified を使う理由がよくわかりません。コンパイルエラーを直していくと上記のコードになりました。 使用例 次のように使用します。Kotlin の型推論により、戻り値の型を request 関数で指定する必要がなくて素晴らしいです。 @Serializable data class ListUsersRequest( val offset: Int, val pageSize: Int, ) @Serializable data class User(val id: Int, val name: String) fun listUsers(req: ListUsersRequest): List<User> { return request("/users/list", req) } この関数は Jetpack Compose の Composable から使うことを想定しています。ボタンがクリックされたときなどです。その箇所のコードは次のようになります。 var users by remember { mutableStateOf<List<User>>(emptyList()) } var ajax by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val handleClick = { scope.launch(Dispatchers.IO) { try { ajax = true val req = ListUsersRequest(0, 20) val res = listUsers(req) users = res } catch (e: Exception) { // TODO: ここで Snackbar などを使ってエラー内容をユーザーに伝える Log.e("ERROR", e.toString()) return@launch } finally { ajax = false } } Unit } Button(onClick = handleClick, enabled = !ajax) { Text("click me!") } おわりに HTTP リクエストを行うライブラリを公式が用意してくれると嬉しいのですが、ないですかね?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】Looper を持つスレッドの CoroutineDispatcher を作る

関連記事:【Kotlin/JVM】CoroutineDispatcher を作る 次のようにすることで Looper を持つスレッドの CoroutineDispatcher を作ることができる。 import android.os.Handler import android.os.HandlerThread import kotlinx.coroutines.* import kotlinx.coroutines.android.HandlerDispatcher import kotlinx.coroutines.android.asCoroutineDispatcher val handlerDispatcher: HandlerDispatcher = HandlerThread("HandlerThreadDispatcher") .apply { start() } .looper .let { Handler(it) } .asCoroutineDispatcher() HandlerDispatcher クラスは Dispatchers.Main の型である MainCoroutineDispatcher を継承しており1、Dispatchers.Main と同じく immediate プロパティを持つ。 Realm では、メインスレッドではない、Looper を持つスレッドが必要になることがある。 そのときにはこの方法を使うとコルーチンで扱うことができて便利だ。 /以上 型名からは継承関係が逆のように感じるが…。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Coil]キャッシュサイズを変更する方法

Androidの画像ローディングライブラリCoilでディスクキャッシュのサイズを変更する方法を調べたのでメモがてら記事にします。 方法 CoilはOkHttpのディスクキャッシュ機構を利用しています。 したがって、キャッシュのサイズを変更するにはOkHttpClientを置き換えます。 Coil.kt val cache = Cache( directory = File(context.cacheDir, "image_cache"), maxSize = 500L * 1024L * 1024L // 500 MB ) val imageLoader = ImageLoader.Builder(context) .okHttpClient { OkHttpClient.Builder() .cache(cache) .build() } .build() アプリ内で共通のキャッシュ(ImageLoader)を利用したい場合は、ApplicationクラスでCoil.setImageLoader()メソッドを利用してImageLoaderをセットします。 MyApp.kt class MyApp : Application() { override fun onCreate() { val cache = Cache( directory = File(cacheDir, "image_cache"), maxSize = 500L * 1024L * 1024L ) val imageLoader = ImageLoader.Builder(applicationContext) .okHttpClient { OkHttpClient.Builder() .cache(cache) .build() } .build() Coil.setImageLoader(imageLoader) } } キャッシュを削除する場合は、Cache.delete()やCache.evictAll()メソッドを利用します。 CacheインスタンスはDIなどを用いてシングルトンとして扱うのが良いと思います。 MyRepository.kt class MyRepository @Inject constructor( private val cache: Cache, ) { fun deleteCache() { cache.evictAll() } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】ViewPager2+FragmentStateAdapterでFragmentを置き換える

ViewPager2 + FragmentStateAdapterでFragmentを追加・削除しようとしてハマった時のまとめ。 やりたいこと ViewPagerにセットしたFragmentを別のFragmentに置換したい 方針 Android Developersに以下のような記載がありました。 ViewPager2 は、編集可能なフラグメント コレクションのページングをサポートしています。基盤コレクションが変更されたときに、notifyDatasetChanged() を呼び出して UI を更新します。 これにより、アプリは、実行時にフラグメント コレクションを動的に編集できるようになり、編集されたコレクションを ViewPager2 が正確に表示します。 そのため方針としては以下のように実装していきます。 FragmentStateAdapterで表示するFragmentのリストを保持して、表示するFragmnetを操作する(add/remove) Fragmentのリストを操作したら、notify系のメソッドでUIを更新する 試したこと FragmentStateAdapterを実装してみる 今回は以下の2パターンの置き換えを実装します。 FragmentOne -> FragmentA FragmentTwo -> FragmentA, FragmentB 置き換える方法の判別はEnum classを利用して行います。 ReplacePattern.kt enum class ReplacePattern { OneToA, TwoToAB } ViewPagerAdapter.kt class ViewPagerAdapter (fragment: Fragment): FragmentStateAdapter(fragment) { private val fragmentList = mutableListOf<Fragment>() /** * 中略 **/ fun replaceFragment(pattern: ReplacePattern){ //FragmentOneをremoveして、FragmentAに置き換える if (pattern == ReplacePattern.A) { fragmentList.removeFirst() fragmentList.add(0, (FragmentA())) } //FragmentTwoをremoveして、FragmentA, Bに置き換える if (pattern == ReplacePattern.AB){ fragmentList.removeAt(1) fragmentList.add(1, (FragmentA())) fragmentList.add(2, (FragmentB())) } //notifyを呼んでUIを更新する notifyDataset(pattern) } fun notifyDataSet(pattern: ReplacePattern){ //add,removeするFragmentに対応したpositionを指定 if (pattern == ReplacePattern.A){ notifyItemRemoved(0) notifyItemInserted(0) } if (pattern == ReplacePattern.AB){ notifyItemRemoved(1) notifyItemRangeInserted(1,2) } } } ただ、このコードでFragment操作を実行すると以下のエラーが出ます。 java.lang.IllegalStateException: Fragment already added これはFragmentStateAdapterがコレクションをIDで管理しているためです。 そのためFragmentを操作する場合は、以下の2つのメソッドをoverrideする必要があります。 getItemId(position: Int): Long getItemCount(position: Int): Long FragmentをHashCodeで管理する IDはLongで管理しているため、FragmentのHashCodeをIDとして利用することにします。 完成系が以下になります。 ViewPagerAdapter.kt class ViewPagerAdapter (fragment: Fragment): FragmentStateAdapter(fragment) { private val fragmentList = mutableListOf<Fragment>() private val idsList = mutableListOf<Long>() init { fragmentList.apply { add(FragmentOne()) add(FragmentTwo()) add(FragmentThree()) //FragmentをHashCodeで管理 forEach { idsList.add(it.hashCode().toLong()) } } } override fun getItemCount(): Int = fragmentList.size override fun createFragment(position: Int): Fragment = fragmentList[position] //以下の2つをoverrideしないと"java.lang.IllegalStateException"が発生する override fun getItemId(position: Int): Long = idsList[position] override fun containsItem(itemId: Long): Boolean = idsList.contains(itemId) fun replaceFragment(pattern: ReplacePattern){ //FragmentOneをremoveして、FragmentAに置き換える if (pattern == ReplacePattern.OneToA) { fragmentList.removeFirst() fragmentList.add(0, (FragmentA())) } //FragmentTwoをremoveして、FragmentA , Bに置き換える if (pattern == ReplacePattern.TwoToAB){ fragmentList.removeAt(1) fragmentList.add(1, (FragmentA())) fragmentList.add(2, (FragmentB())) } //idを更新 idsList.clear() fragmentList.forEach { idsList.add(it.hashCode().toLong()) } notifyDataSet(pattern) } private fun notifyDataSet(pattern: ReplacePattern){ if (pattern == ReplacePattern.OneToA){ notifyItemRemoved(0) notifyItemInserted(0) } if (pattern == ReplacePattern.TwoToAB){ notifyItemRemoved(1) notifyItemRangeInserted(1,2) } } } 後はViewPagerAdapter#replaceFragment()をよしなに呼び出せば期待した動作が実行されます。 今回作成したサンプルアプリ↓ 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

孫から祖父へテレビ電話(LINEのできるタブレット)をプレゼントした話

この記事では 齢80を超えた祖父に孫(筆者)から初めてのスマホ(タブレット)をプレゼントしてLINEでおしゃべりするようになるまでを振り返ります。 なお「孫」の発音は「まご」。 発端 新型コロナの流行拡大が止まらない2020年。田舎の祖父(農家。パソコンはワープロ、ネット環境はない)が高熱で入院。 ごく小さいながら年相応に前立腺がんがあり、その治療でトイレが近くなっていたらしい祖父は水分を控えて結果膀胱炎か何かになったらしい。 何が起こった コロナ時代の入院といえば面会制限。 面会できるのは1人だけ、1日30分。洗濯物を引き取らなければならないし、日々の細かな対応も必要というので面会者になったのは祖母(パソコンもワープロも知らない、電話は固定電話のみ)。 車で1時間のところに住んでいる叔父が駆けつけるも、面会制限に引っかかって門前払い。会話はできる状態らしいと聞けば、声が聞きたいし聞かせたいのが心情というものでしょう。 この時点で入院は1か月くらい?と言われていた。そんなこと言われたら叔父も母も(もちろん私も)余計に声が聞きたくなるじゃないか。 そこで叔父、叔父のスマホを祖父に渡してLINEで通話したらどうかと考えたらしい。 ところがどっこい この叔父は停波ギリギリまでTu-Kaガラケーを使っていた猛者だったのである。話をするなら電話で、LINEは文字チャットの位置づけ。そもそもよく自分の携帯電話を人に貸し出すつもりになったな。その間携帯なしでやっていけると考えているあたり、半分くらいネット上に生きている私にはない発想である。 で、この叔父が祖父にスマホを渡す前に、姉である我が母とLINE通話をしようとして「なぜかできない」と母から私に連絡があったのが出社していた私の定時過ぎたころ。 「Androidのマイクとカメラの使用許可が通ってない」と直感したSE私、解決方法を教えつつこの後の展開を想像する。 祖父に叔父のスマホを渡す 祖父、使い方がわからない 直接話ができる祖母ももちろんわからない 病院の公衆電話から叔父の家に電話をかける(叔父の携帯は祖父の手元にあるから、家にかけるしかない。なお、叔父は叔母の両親と同居している) 叔父、スマホがどういう状態かわからないから説明しようがない(そもそもLINE通話したことなかった人だからあてにできない) 結局公衆電話で話すことになる ……想像が容易すぎて笑う。 そこで孫(私)は考えた。簡単にLINEができる、むしろLINEしかできないくらいのスマホを献上すればいいのだ。 そうと決まれば 幸い、大都市勤務の私の帰路にはヨドバシカメラがある。端末も買えるし即時開通SIMも手に入る。ということで、会社からヨドバシに向かって歩きながら作戦を練った。 日ごろ持ち歩きはしないが出先に持ち出しても使える「テレビ電話」的な位置づけとする 老人が操作する&顔を見ながら話すものだから、画面はデカければデカいほどいい → 端末はHuawei T3(必要十分、オマケで充電器とスタンドがついてくる)   老人は指が乾燥して操作が難しかったりするそうだから、タッチペンを別途付ける 使われなくても惜しくないくらいの設備にして、祖父に使用を強要しない どこでも設定なしに使えるように、SIMもつける(そもそも祖父母宅にネット回線は引かれていない) 祖父は携帯電話は持っているので、それを置き換えることはしない → 回線はLINEモバイルのベーシックプラン 500MB / データSIM(SMS付き)、LINEデータフリー 下手に陰謀論なんかに染まっても困るから、あれもこれもできるようにしない(特にYouTube、お前はだめだ) → ホームランチャーアプリを導入してLINEビデオ通話のショートカットを配置   その他のアプリのアイコンはホームに置かない ひとまずLINEのかけ方と受け方だけは祖父母だけでわかるようにする → LINE通話をかける・受ける最低限の手順書を作成し、印刷して一緒に送る   (端末の説明書にアプリの使い方は含まれていないし、アプリの説明書をデータで渡しても見られない) 調達 ヨドバシにかかればワンストップで全部揃う。取説を印刷するための用紙とバインダーも買った。 ホームランチャーアプリを調べながら電車で帰路に就く。 製造 端末セットアップ 帰宅し、まずは祖父専用Googleアカウントの作成から着手。 次にLINEをインストールしてアカウントを作成する。 さらに帰路に調べておいたホームランチャーアプリをインストール。ホームに母と叔父と私(ヘルプデスクのつもり)への通話ショートカットを配置。 追加で弟妹、大叔母、従弟妹など祖父母が連絡しそうな親族のLINEアカウントを聞いて回り、友達登録しておいた。 取説作成 セットアップした端末のスクショを取りつつPCに取り込み、PowerPointで操作方法をわかりやすく説明していく。 基本事項として、「タップ」「スワイプ」などの専門用語は使わないよう心掛ける。 ショートカットは3人分なので、その他の親族と連絡を取るために「友だち」の一覧から通話する手順も載せておく。 また、何か困りごとがあって私以外の手を借りなければならなくなった時のため、Googleのアカウント情報・SIMの契約情報・端末の機種名とその読み方(これ結構大事だと思うんだけどどうだろう)を末尾に記載しておいた。 これを印刷して買ってきたバインダーに綴じ、アカウント情報には上から付箋を貼って「親族以外に教えてはいけません」と注記しておく。本当は親族だって教えたらだめなんだろうけど、支払情報とかは入っていないし、本当にサポートが必要な時に「誰にも教えるな」と書いてあったら戸惑うだろうという判断。 梱包 幸い我が家にはAmazonの箱がアホほどある。タブレットをケースに装着した状態で化粧箱に戻し、タッチペンと手作りの取説を添えて適当なサイズの箱に入れる。あと緩衝材も取っておくタイプなのでめちゃくちゃある。これを詰める。 「まごじるしのおじいちゃんタブレットセット」の完成である。 発送 翌日の通勤途上で、コンビニに寄って発送。 なお、タブレットはすぐに受話できるよう満充電にして電源も入れたまま送った。配送途中に何かの通知音が鳴ってたらクロネコさんビビったかもしれない(ごめんなさい) 発送した次の日には自宅にいた祖母の手元に端末が届いたらしい。 評判 祖父は祖母の助けで何とかLINE電話がかけられるようになった。 やはりこういうものはモチベーションの違いが結果に影響するのか、おしゃべり好きな祖母のほうが習得が早かった。今では祖父のアイコンからかかってくると祖母だな、と思う。大叔母とは2時間とか平気でしゃべっているらしい。おしゃべりは健康にいいのでどしどし話してもらいたいものだ。 手製の取説は祖父曰く「ようわからん」とのことだったが、特に説明を求められるでもなくビデオ通話がかかってきたのでおそらく祖母が理解してつなげてくれたと推察する。一方入院先のリハビリスタッフさんや従弟妹には取説も評判がよかったらしい。パワポ使う人には手間具合がわかるのだろう。 あと、LINEのアイコンを結婚式の写真から見つけてきたオフショットにしたら、大叔母たちからの評判が上々であった。オメカシしているし、力の抜けたいい笑顔なので私も気に入っているが、控室全景写真の隅っこに写っているのを切り取っているので解像度が低くて今後のことを考えると以下略。写真屋さんに行きたい。 その後 祖父はその後も2度ほど入院したが、いずれも当初の目安より短い期間で出てくる。どうやら胃腸と足腰が強いらしい。退院した日にカツ定食を食っている。 お百姓は体がつよい。どんなに鍛えても泳いでも机上労働者の自分が弱っちく思えてくることであるなあ。 かつて山へタケノコ狩りに行ってわれら若者がふらふらと振り回す鍬を横から奪い取って掌に唾を飛ばし、斜面のタケノコをメゴッと一発で打ち抜いた姿がめちゃくちゃかっこよくて印象に残っている。これからも長生きしてくれると孫は嬉しい。 追記 なお、この件から1年以上経って今度は父方の祖母にも同様に「テレビ電話」をプレゼントしようとしたら、LINEモバイルはなくなってるわ大手のLTE対応タブレットも新品市場から消えてるわで頭を悩ましている現在。つまりこの記録にある調達はこの当時にしかできなかったということになる。なんとも惜しい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Application を ViewModelStoreOwner にする方法

はじめに 気軽に Activity 間でデータを共有する場合は、Application を継承したクラスを ViewModelStoreOwner にすると良いです。このように設計することで、Application を継承したクラスが肥大化することなく、Application にグローバルな値を持たせることができます。 実装 class MyApplication : Application(), ViewModelStoreOwner { private val store = ViewModelStore() override fun getViewModelStore(): ViewModelStore { return store } } Application の実装はこれだけです。今後グローバルな状態が増えたとしても、Application が肥大化することはありません。代わりに ViewModel の数が増えます。 使用例 次のコードは、SubActivity で加算した値が MainActivity にすぐに反映されるコードです。Jetpack Compose を使用しています。 class MyViewModel : ViewModel() { var count by mutableStateOf(0) } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ViewModel の実体を取得するコード。このコードは SubActivity でも使われる val vm = ViewModelProvider(application as MyApplication).get(MyViewModel::class.java) setContent { MainScreen(vm, ::move) } } private fun move() { val intent = Intent(this, SubActivity::class.java) startActivity(intent) } } @Composable fun MainScreen(vm: MyViewModel, onClick: () -> Unit) { Button(onClick = onClick) { Text("click me! ${vm.count}") } } class SubActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val vm = ViewModelProvider(application as MyApplication).get(MyViewModel::class.java) setContent { SubScreen(vm) } } } @Composable fun SubScreen(vm: MyViewModel) { Button(onClick = { vm.count += 1 }) { Text("click add! ${vm.count}") } } おわりに この実装パターンを使うことで、いいね問題が解決すると思います。 余談 次のようにシングルトンオブジェクトを作れば、Application を継承したクラスを作る必要すらありません。 object MyData { var count by mutableStateOf(0) } このオブジェクトを使うようにしたとき、MainActivity と SubActivity のコードにある val vm = ViewModelProvider(application as MyApplication).get(MyViewModel::class.java) がなくなります。もし Application とシングルトンオブジェクトの生存期間が同じなのであれば、こちらでもいい気がします。ただ、Composable の依存する状態が引数として表れないのは少し良くないかもしれないです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む