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

[Android] お寿司屋で例えるAndroid Architecture Compoment 最終貫:ModelとRepository

この記事は [Android] お寿司屋で例えるAndroid Architecture Compoment 第五貫:Roomの続きです。 おさらい MVVMをこんな感じでお寿司屋に例えたとさ View(Activity/Fragment)はお客さん ViewModelが板前さん Repositoryが、漁師 Modelが、海 MVVMを海からお魚を取ってきてお客さんに提供するまでの役割を分割したものと捉えています。 詳しくは、第一貫をご覧ください。(5分位で読める内容ですよ!) こんな感じのアプリを作ったとさ マグロしか食べてなかったり、小学生女子が下校途中にお寿司屋に寄っていたりとツッコミどころがありますが、スルーしてください。 先程のMVVMの例えと、アプリの例えが少し違いがありますが、こちらもスルーで。 View(お客さん) 注文画面を表示 ViewModel(板前さん) 注文画面のデータや処理を保持 Model 注文履歴を保持 本題 どうもReoです。 過去の記事を一つでもご覧いただいた方ありがとうございます。 今回で、感動の最終巻となります。 ハンカチのご用意は、大丈夫ですか? 今回は、第三貫で作成したアプリをMVVM化させる実装をしたいと思います。 これで、MVVMを簡単に実装したことになります。 これまでで、View-ViewModel間の実装が終わっているので、ViewからModel間の実装をしていきます。 とはいっても、アプリに前回学習したRoomを実装していくだけです。 注文履歴と代金をローカルデータベースに保存します。 Repositoryについて さぁ、Repositoryを実装したいのですが、この図におけるRepositoryは必須ではありません。 そもそも、Repositoryの役割とは何か? 「漁師」の事ですよね! つまり、魚(データ)を海(Model)から取ってきて、それをRoom(最新冷蔵庫)に保管しておきます。 しかし、少し違うような気がするんです。 最終貫にして、申し訳ないですw さてどの辺りかというと、まずRoomは前回の記事で「最新冷蔵庫」と比喩しました。 冷蔵庫は、買ったときは何も入ってないですよね? あれ、ではどこから取ってくるの? と思ったかもしれませんが、それは隣です。 初めてデータを取るときは、Remote Data Sourceの部分です。 ここが、海から取るという意味です。 Retrofitという通信ライブラリを用いてWebから情報を取ります。 Retrofitについて記事書いたことあるので、詳しくはこちら! ここで改めて、MVVMの流れを確認してみましょう。 依存関係ではなく、データの流れ順に説明します。 まず、漁師(Repository)が、海(Remote Data Source)から取った魚(データ)は、冷蔵庫(Room)に保存します。 そして、板前さん(ViewModel)のいるお寿司屋は、漁師さんがとったお魚を買います。 それをView(お客さん)に提供します。 重要なのが、初めて漁師さんが取るときは、海(Remote Data Source)なのですが、それ以降は保存した冷蔵庫から取り出します。 何故かというと、漁は毎回行けるとは限らないからです。 ソフトウェアの世界に戻します。 現在の実装では、ユーザーがデバイスを回転させた場合や、ユーザーがアプリから離れてすぐに戻ってきた場合には、既存の UI が直ちに表示されます。これは、リポジトリがメモリ内キャッシュからデータを取得するためです。 しかし、ユーザーがアプリから離れてから数時間後(Android OS がプロセスを強制終了した後)に戻ってきた場合はどうなるでしょうか。現在の実装では、ネットワークからデータを再取得する必要があります。この再取得プロセスは、ユーザー エクスペリエンスに悪影響を及ぼすだけでなく、貴重なモバイルデータを消費するため、無駄が多くなります。 つまり、Repositoryというのは、APIを叩いて取得した情報をRoomに保存する事で、ネットに接続が悪いときに再取得できない場合があるし、またそもそも同じものを再取得というのは、ドキュメントの通りで、ユーザーエクスペリエンスとして良くありません。 ということで、ロカールDB(Room)だけの実装の場合は、Repositoryは必要なく、ViewModelにRoomの操作(Dao)をインジェクションで良いかなと思います。 インジェクションとは、依存性注入のことで、簡単に言うと貸し借りの関係ですね。 もうちょい学びたい方はこちら ViewModelは、LocalResourceからDaoをお借りしてRoomの操作が出来るようにするということです。 お寿司語的には、板前さんが漁師さんにお魚を取ってきてもらうという関係性を作るということです。 実装 では、Roomを追加してみたいと思います。 前回説明したので、ガンガン行きます。 ちなみに、完成形はこちら。 既に作成したものに、追加実装する形で「お皿の枚数」とお母さんへの「請求額」をRoom出来るようにします。 導入 HiltとRoomを使えるようにします。 build.gradle( def room_version = "2.3.0" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" testImplementation "androidx.room:room-testing:$room_version" implementation "com.google.dagger:hilt-android:2.32-alpha" kapt "com.google.dagger:hilt-android-compiler:2.32-alpha" implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' 漁師 LocalResource.kt @Database(entities = arrayOf(Sushi::class), version = 1,exportSchema = false) abstract class SushiDatabase : RoomDatabase() { abstract fun suhiDao(): SushiDao } @Entity data class Sushi( @PrimaryKey(autoGenerate = true) val id: Int, val orderHistory: Int?, val price: String ) @Dao interface SushiDao { @Query("SELECT * FROM sushi") suspend fun getAll(): List<Sushi> @Query("SELECT * FROM sushi where id = :id") fun getHistory(id: Int): Sushi @Insert fun insertSushi(sushi: Sushi) @Delete fun delete(sushi: Sushi) } アプリケーションクラス MyApplication.kt @HiltAndroidApp class MyApplication : Application() モジュール AppModule.kt @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideSushiDatabase( @ApplicationContext context: Context ) = Room.databaseBuilder( context, SushiDatabase::class.java, "database" ).build() @Provides @Singleton fun provideSushiDao(db: SushiDatabase) = db.suhiDao() } レイアウト fragment_order.xml <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/orderImageView" android:layout_width="240dp" android:layout_height="240dp" android:layout_marginTop="80dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/cashDisplay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="" android:textSize="16sp" app:layout_constraintTop_toBottomOf="@id/orderImageView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <Button android:id="@+id/orderButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="40dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/cashDisplay" /> <Button android:id="@+id/billButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="40dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/orderButton" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> 追加したViewは、お会計の時と請求時のテキストをセットするTextViewのみです。 お客さん(View) OrderFragment.kt @AndroidEntryPoint class OrderFragment : Fragment() { private val viewModel: OrderViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = FragmentOrderBinding.inflate(inflater, container, false).let { it.lifecycleOwner = viewLifecycleOwner it.orderButton.setOnClickListener { viewModel.orderTuna() } it.billButton.setOnClickListener { viewModel.pay() } val orderImageObserver = Observer<Int> { newImageId -> it.orderImageView.setImageResource(newImageId) } val textObserver = Observer<String> { newText -> it.orderButton.text = newText } val cashObserver = Observer<String> { newText -> it.cashDisplay.text = newText } val billObserver = Observer<String> { newText -> it.billButton.text = newText } viewModel.orderImage.observe(viewLifecycleOwner, orderImageObserver) viewModel.orderText.observe(viewLifecycleOwner, textObserver) viewModel.cashDisplay.observe(viewLifecycleOwner, cashObserver) viewModel.billText.observe(viewLifecycleOwner, billObserver) it.root } こちらも監視するViewが増えただけで、特に新しいことはありません。 板前さん(ViewModel) OrderViewModel.kt @HiltViewModel class OrderViewModel @Inject constructor(private val sushiDao: SushiDao) : ViewModel() { private val _orderImage = MutableLiveData<Int>() val orderImage: LiveData<Int> get() = _orderImage private val _cashDisplay = MutableLiveData<String>() val cashDisplay: LiveData<String> get() = _cashDisplay private val _orderText = MutableLiveData<String>() val orderText: LiveData<String> get() = _orderText private val _billText = MutableLiveData<String>() val billText: LiveData<String> get() = _billText private val _customerType = MutableLiveData(CustomerType.ENTER) private val _tunaCount = MutableLiveData<Int>() private val _totalData = MutableLiveData<List<Sushi>>() private var lastIndex: Int = 0 private var _totalDish: Int = 0 private var _amountBill: Int = 0 init { _orderImage.value = R.drawable.sushiya_building _orderText.value = "入店" _tunaCount.value = 0 } fun orderTuna() { when (_customerType.value) { CustomerType.ENTER, CustomerType.RE_ENTER -> { _cashDisplay.value = "" _orderImage.value = R.drawable.sushi_syokunin_man_mask _orderText.value = "マグロ" _billText.value = "お会計" _customerType.value = CustomerType.EAT_TUNA } CustomerType.EAT_TUNA -> { _orderImage.value = R.drawable.sushi_akami _orderText.value = "完食" _customerType.value = CustomerType.COMPLETED_EAT _tunaCount.value = _tunaCount.value?.plus(1) } CustomerType.COMPLETED_EAT -> { _orderImage.value = R.drawable.sushi_syokunin_man_mask _orderText.value = "マグロ" _customerType.value = CustomerType.EAT_TUNA } CustomerType.GO_HOME -> { viewModelScope.launch(Dispatchers.IO) { sushiDao.getAll().let { lastIndex = it.lastIndex _totalData.postValue(it) } withContext(Dispatchers.Main) { for (i in 0..lastIndex) { _totalDish += _totalData.value?.get(i)?.orderHistory!! _amountBill += _totalData.value?.get(i)?.price!!.toInt() } _orderImage.value = R.drawable.tsugaku _orderText.value = "" _cashDisplay.value = "" delay(1000) _orderImage.value = R.drawable.home_kitaku_girl _orderText.value = "再入店" _customerType.value = CustomerType.RE_ENTER _cashDisplay.value = "総皿数:${_totalDish}皿\n請求総額:¥${_amountBill}" _tunaCount.value = 0 } } } } } fun pay() { when (_billText.value) { "お会計" -> { viewModelScope.launch(Dispatchers.IO) { sushiDao.insertSushi( Sushi( 0, _tunaCount.value, calcSushi(_tunaCount) ) ) _cashDisplay.postValue("${_tunaCount.value}皿\n¥${calcSushi(_tunaCount)}") withContext(Dispatchers.Main) { _orderImage.value = R.drawable.message_okaikei_ohitori _orderText.value = "帰る" _billText.value = "" _customerType.value = CustomerType.GO_HOME } } } } } private fun calcSushi(dishCount: MutableLiveData<Int>) = dishCount.value?.times(100).toString() enum class CustomerType { ENTER, RE_ENTER, EAT_TUNA, COMPLETED_EAT, GO_HOME } } まず、@HiltViewModel,@Inject constructor(private val sushiDao: SushiDao)にて依存関係を追加しています。 これによって、板前さんと漁師の依存関係が出来上がります。 Roomの操作は、IOスレッドで行いそのネスト内にwithContextを用いてメインスレッドを走らせるとIOスレッドで、データ処理が終わってからMutableデータを変更する処理に移ることが出来ます。 いっちょあがり!!! 終わりに 以上でお寿司屋で例えるAndroid Architecture Componentを終わりにします。 お疲れ様でした。 最後は、ドキュメント通りの設計でなくて申し訳ないです。 API叩く処理がないと、Repositoryを実装する意義があまり見いだせないのは知らなかったです。 さてと、ドキュメントを読みながらなんとか書き切りました。 書き始める前よりもMVVMについての理解が深まることで、正直な所、筆者が一番学びが多いと思います。 これからも、設計に対しての理解を深めていきたいなと思います。 書き方の至らない点を見つけたら、筆者のTwitterに通報してください。 ViewModel内の!!を回避したいです ありがとうございました^^
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Flutter] デバイスID取得の方法

Androidアプリ開発で個体識別IDを取得したい場面が出てきたので、実装したのを共有します。 パッケージの取得 pubspec.yaml dependencies: flutter: sdk: flutter device_info: ^0.4.0+2 出力 device_infoパッケージをimportする .dart import 'package:device_info/device_info.dart'; .dart DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; print(androidInfo.androidId); // デバイスID 番外編 ID以外にもいろいろなデバイス情報を取得できるようです。 引用元(https://miajimyu.com/docs/flutter/flutter-use-device-info/) .dart // Comments are example. Map<String, dynamic> _readAndroidBuildData(AndroidDeviceInfo build) { return <String, dynamic>{ 'version.securityPatch': build.version.securityPatch, // 2020-09-05 'version.sdkInt': build.version.sdkInt, // 30 'version.release': build.version.release, // 11 'version.previewSdkInt': build.version.previewSdkInt, // 0 'version.incremental': build.version.incremental, // 6903271 'version.codename': build.version.codename, // REL 'version.baseOS': build.version.baseOS, // 'board': build.board, // goldfish_x86 'bootloader': build.bootloader, //unknown 'brand': build.brand, // google 'device': build.device, // generic_x86_arm 'display': build.display, // sdk_gphone_x86_arm-userdebug 11 RSR1.201013.001 6903271 dev-keys 'fingerprint': build.fingerprint, // google/sdk_gphone_x86_arm/generic_x86_arm:11/RSR1.201013.001/6903271:userdebug/dev-keys 'hardware': build.hardware, // ranchu 'host': build.host, // abfarm-us-west1-c-0007 'id': build.id, // RSR1.201013.001 'manufacturer': build.manufacturer, // Google 'model': build.model, // sdk_gphone_x86_arm 'product': build.product, // sdk_gphone_x86_arm 'supported32BitAbis': build.supported32BitAbis, // [x86, armeabi-v7a, armeabi] 'supported64BitAbis': build.supported64BitAbis, // [] 'supportedAbis': build.supportedAbis, // [x86, armeabi-v7a, armeabi] 'tags': build.tags, // dev-keys 'type': build.type, // userdebug 'isPhysicalDevice': build.isPhysicalDevice, // false 'androidId': build.androidId, 'systemFeatures': build.systemFeatures, }; } 参考 device_infoをつかってみる[Flutter] [Flutter]デバイス情報の取得の仕方(iOS、Android) 会社の紹介 私は現在、株式会社ダイアログという物流×ITの会社に勤務しております。 様々な職種を募集しているので、Wantedlyのページをご覧ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jetpack ComposeでURLから画像を取得する

利用するライブラリ Remote Image for Jetpack Compose URLを指定するだけでネットワークから簡単に画像を持ってくることができるJetpack Composeで利用できるライブラリです。 導入 プロジェクトレベルのbuild.gradle build.gradle allprojects { repositories { ... maven { url 'https://dl.bintray.com/agarasul/RemoteImage' } ... } } アプリレベルのbuild.gradle build.gradle dependencies { ... implementation 'dev.rasul:remoteimage:1.0.4' ... } 使い方 RemoteImage( url = newsItem.urlToImage, contentScale = ContentScale.Crop, modifier = Modifier.height(200.dp), error = { Text(text = "Could not load image") }, loading = { CircularProgressIndicator( strokeWidth = 2.dp, modifier = Modifier.size(16.dp) ) } ) url:取得したい画像のURL contentScale:画像のスケールタイプ(ImageViewのscaleTypeと同じ) modifier:高さや幅など、他のmodifierと同様 error:エラー発生時に表示されるView、デフォルトではText loading:画像のロード中に表示されるView、デフォルトではCircularProgressIndicator 参考リンク https://github.com/agarasul/RemoteImage
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]シークレットキーなどの秘匿情報をプロジェクトで使うための設定

Android アプリを開発していく中で、シークレットキーなどの秘匿情報をプロジェクトで扱うことがあるかと思います。 例えば Qiita API を使った Android アプリを開発する場合、アクセストークンを発行するためには POST /api/v2/access_tokens を実行する必要がありますが、この時リクエストパラメータに client_id と client_secret が必要になります。 これらのパラメータは Qiita のアプリケーション ページからアプリケーションを登録することによって発行できますが、このような秘匿すべきパラメータを、以下のような条件で Android プロジェクトで扱うための設定について考えます。 漏洩防止のため Git のバージョン管理には含めない BuildConfig のフィールドとして、これらのパラメータをプログラムから参照できるようにする 今回は以下の3つの方法について紹介したいと思います。 gradle.properties を使う 環境変数として設定する secrets-gradle-plugin を使う 1. gradle.properties を使う 一つ目の方法は gradle.properties を使う方法です。 gradle.properties の作成 gradle.propeties を以下のように作成し、~/.gradle/gradle.properties に配置します。 ~/.gradle/gradle.properties QIITA_CLIENT_ID=xxxxxxxxxxxxxxxxxx QIITA_CLIENT_SECRET=xxxxxxxxxxxxxxxxxx build.gradle への設定 そして Android プロジェクトの app/build.gradle で以下のように設定します。 app/build.gradle ... android { ... defaultConfig { ... buildConfigField "String", "QIITA_CLIENT_ID", "\"${project.properties['QIITA_CLIENT_ID']}\"" buildConfigField "String", "QIITA_CLIENT_SECRET", "\"${project.properties['QIITA_CLIENT_SECRET']}\"" } ... } Gradle で project.properties['QIITA_CLIENT_ID'] のようにして、gradle.properties に設定した値を取得することができます。 これでプログラムから BuildConfig.QIITA_CLIENT_ID、BuildConfig.QIITA_CLIENT_SECRET として設定した値を参照することができます。 CI でのビルド もし CI でビルドするのであれば、必要なパラメータを CI で環境変数として設定しておいて、ビルドを実行する前に一度以下のコマンドを実行して gradle.properties を作成しておく必要があります。 (以下の例では CI で環境変数 QIITA_CLIENT_ID と QIITA_CLIENT_SECRET が設定されているものとします) cat <<EOF > ~/.gradle/gradle.properties QIITA_CLIENT_ID=$QIITA_CLIENT_ID QIITA_CLIENT_ID=$QIITA_CLIENT_SECRET EOF もしくは、ビルド時に -P オプションを指定することによってパラメータを渡すことも可能です。 (以下の例でも環境変数 QIITA_CLIENT_ID と QIITA_CLIENT_SECRET が設定されているものとします) ./gradlew assembleRelease -PQIITA_CLIENT_ID=$QIITA_CLIENT_ID -PQIITA_CLIENT_SECRET=$QIITA_CLIENT_SECRET 2. 環境変数として設定する 先ほど紹介したように CI でビルドする際に環境変数として設定する必要があるのであれば、ローカルで開発する際にも環境変数で設定してしまう方法もあります。 環境変数への設定 OS や使用しているシェルによって設定方法は異なるかと思いますが、私の場合は Mac で zsh を使用しているので ~/.zshrc に以下の設定を追記します。 export QIITA_CLIENT_ID=xxxxxxxxxxxxxxxxxx export QIITA_CLIENT_SECRET=xxxxxxxxxxxxxxxxxx direnv を使って環境変数を設定する 余談ですが Unix 系の OS であれば direnv を使うのがオススメです。 これを使うことによって、指定したディレクトリ以下に対して環境変数が適用されるようになります。 また ~/.zshrc に環境変数を設定する必要がないため、プロジェクトごとにいくつか設定が必要な場合でも ~/.zshrc に追加していく必要がなくなります。 導入方法や使い方については、こちらの記事 などが参考になるかと思います。 build.gradle への設定 そして Android プロジェクトの app/build.gradle で以下のように設定します。 app/build.gradle ... android { ... defaultConfig { ... buildConfigField "String", "QIITA_CLIENT_ID", "\"${System.getenv('QIITA_CLIENT_ID')}\"" buildConfigField "String", "QIITA_CLIENT_SECRET", "\"${System.getenv('QIITA_CLIENT_SECRET')}\"" } ... } 環境変数の場合は System.getenv('QIITA_CLIENT_ID') のようにして環境変数を取得できます。 CI でのビルド CI 側に環境変数を設定しておくだけで OK です。 3. secrets-gradle-plugin を使う google/secrets-gradle-plugin という Gradle プラグインを使うことによって、local.properties などに設定されているプロパティを BuildConfig のフィールドに設定してくれます。 設定はとても簡単で、まずは app/build.gradle に以下のようにプラグインを導入します。 app/build.gradle plugins { ... id 'com.google.secrets_gradle_plugin' version '0.6' } そして、local.properties に以下のように設定を追記します。 local.properties QIITA_CLIENT_ID=xxxxxxxxxxxxxxxxxx QIITA_CLIENT_SECRET=xxxxxxxxxxxxxxxxxx この状態でビルドを行うとプログラムから BuildConfig.QIITA_CLIENT_ID、BuildConfig.QIITA_CLIENT_SECRET として設定した値を参照することができるようになります。 また、このプラグインを使うことによって、設定した値が AndroidManifest.xml 内でも参照できるようになります。 その他に local.properties 以外にも設定ファイルを指定することができたり、指定したキーを無視するなどのオプションが設定できるようなので ドキュメント を参照してみてください。 CI でのビルド local.properties のファイルがあればよいので、ビルドを実行する前に以下のコマンドでファイルを作成する必要があります。 (以下の例では CI で環境変数 QIITA_CLIENT_ID と QIITA_CLIENT_SECRET が設定されているものとします) cat <<EOF > local.properties QIITA_CLIENT_ID=$QIITA_CLIENT_ID QIITA_CLIENT_ID=$QIITA_CLIENT_SECRET EOF 参考サイト Android Developers / カスタム フィールドとリソース値をアプリのコードと共有する google/secrets-gradle-plugin direnv/direnv direnvを使おう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

How to Update Android Apps

I'll explain how to update Android Apps. I am using Android Studio. [Steps] 1. Open "build.gradle" file on the Android Studio. 2. Update Version. We must set the versionCode as an Integer. (ex.) You must write the versionCode and version name in the file like this. versionCode 4 to 5 versionName "1.0.4" to "1.0.5" 3. Click "Sync Now" on the upper right. 4. After the sync, Click Build on the menu. And then, choose Generate Signed APK... 5. Choose "Android App Bundle" on another pop-up window and click next. 6. Set your Key store path and fill in the Key store password, Key alias, and Key password. After that, click next. 7. Choose your Build Variants, debug or release and click finish. 8. After executing tasks, the android studio generates the signed bundle apps. 9. Login Google Play Developer Console. 10. Pick the app up and click the production of the Release 11. Open the Releases Tab, and then you click "Create new release." 12. Upload the app bundle (.aab) file. 13. Fill in the release notes. 14. Click the save on the bottom. 15. After saving, click the review release. 16. Click "Start rollout to production" after checking the detail. Android アプリ更新の際の自分用のメモです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む