- 投稿日:2020-04-30T17:45:33+09:00
[Kotlin]Firebase Remote Configの実装
はじめに
- 公式はこちら
- メンテナンスモードや強制アップデートの実装に使える
画面
Firebase consoleでRemoteConfigの設定
FirabseConsoleのRemoteConfigで設定する値は2つの為、それぞれ表示する用のTextViewを2つ用意
スクショは実装結果ですactvity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" tools:context=".MainActivity"> <TextView android:id="@+id/main_first_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="first text" /> <TextView android:id="@+id/main_second_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:text="second text" /> </LinearLayout>実装
build.gradle(app)dependencies { 〜 // Remote config implementation 'com.google.firebase:firebase-config:19.1.4' // Remote Config (Kotlin) implementation 'com.google.firebase:firebase-config-ktx:19.1.4'Activity
MainActivity.ktclass MainActivity : AppCompatActivity() { private lateinit var remoteConfig: FirebaseRemoteConfig override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // firebase-config-ktx により簡潔に書ける remoteConfig = Firebase.remoteConfig // debug modeの有効化 val configSettings = remoteConfigSettings { minimumFetchIntervalInSeconds = 3600 // fetch間隔 。頻繁に取得できないので、debug時は短く設定する } remoteConfig.setConfigSettingsAsync(configSettings) // デフォルト値設定 remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) // apply default main_first_text.text = remoteConfig[LOADING_PHRASE_CONFIG_KEY].asString() main_second_text.text = remoteConfig[LOADING_PHRASE_CONFIG_KEY].asString() fetchWelcome() } private fun fetchWelcome() { remoteConfig.fetchAndActivate() .addOnCompleteListener(this) { task -> if (task.isSuccessful) { val updated = task.result Timber.d("Config params updated: $updated") Toast.makeText(this, "Fetch and activate succeeded", Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, "Fetch failed", Toast.LENGTH_SHORT).show() } displayWelcomeMessage() } } private fun displayWelcomeMessage() { // 失敗してたらxmlより反映。成功してたら firebase console にて設定した値 main_first_text.text = remoteConfig.getBoolean(WELCOME_MESSAGE_CAPS_KEY).toString() main_second_text.text = remoteConfig.getString(WELCOME_MESSAGE_KEY) } companion object { // Remote Config keys private const val LOADING_PHRASE_CONFIG_KEY = "loading_phrase" private const val WELCOME_MESSAGE_CAPS_KEY = "welcome_message_caps" private const val WELCOME_MESSAGE_KEY = "welcome_message" } }所感
ソースコードはこちら
- 投稿日:2020-04-30T17:13:52+09:00
AndroidでPush通知を表示する
Firebase Cloud Messaging(FCM)を使って AndroidでPush通知を実装したときのメモです。(2020/4/29時点)
Firebase Cloud Messagingとは
Firebase Cloud Messaging(FCM)は、メッセージを無料で確実に配信するためのクロスプラットフォーム メッセージング ソリューションです。以前は Google Cloud Messaging API が使われていたようですが、現在はFCMに移行しています。
FCMで端末にメッセージを送信すると、スリープ画面でも通知メッセージを表示できます。
Push通知の実装なおおまかな流れ
- Androidアプリの作成 - Android Studio
- Firebaseプロジェクトの作成 - Firebaseコンソール
- Androidアプリでサービスを実装 - Android Studio
- テスト通知 - Firebaseコンソール
それでは、順番にやっていきます。
Androidアプリの作成
最初にAndroid Studioでスケルトンアプリを作成します。
Android StudioでEmpty Activityプロジェクトを作成
ここで指定したPackage nameは後ほどFirebaseプロジェクトに設定します。
Firebaseのデバッグ用証明書キーの取得
Firebaseプロジェクトの作成時にSHA-1が必要になります。画面右のGradleタブを引き出して、app > Tasks > singningReportを右クリックして「Run」をクリックします。
Buildペインにキーストア情報が表示されます。ここで表示されたSHA-1は後ほどFirebaseプロジェクトに設定します。
Firebaseプロジェクトの作成
Firebaseのコンソールから新規にプロジェクトを作成
Firebaseコンソール
https://console.firebase.google.com/
ダウンロードした設定ファイルgoogle-services.jsonをAndroid Studioに追加します。
Finderで設定ファイルをコピー(command+C)し、プロジェクトのappフォルダーにフォーカスを当てた状態でペースト(command+V)します。
Androidアプリの設定
プロジェクトレベルのbuild.gradleを修正
dependenciesにclasspath 'com.google.gms:google-services:4.3.3'を追加します。build.gradle(Project)// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.6.3' // Add this line classpath 'com.google.gms:google-services:4.3.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }アプリレベルのbuild.gradleを修正
dependenciesにimplementation 'com.google.firebase:firebase-messaging:20.0.0'を追加します。
必要なライブラリは使用可能なライブラリの一覧(https://firebase.google.com/docs/android/setup#available-libraries) の「Cloud Messaging」の項目を確認します。デバイス登録トークンを取得するために、getToken()を呼び出す場合は、末尾に
apply plugin: 'com.google.gms.google-services'を追加しておきます。build.gradle(app)apply plugin: 'com.android.application' android { compileSdkVersion 29 buildToolsVersion "29.0.1" defaultConfig { applicationId "jp.yuppe.push1" minSdkVersion 24 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' // Add this line implementation 'com.google.firebase:firebase-messaging:20.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } // Add this line apply plugin: 'com.google.gms.google-services'修正したら「Sync now」をクリックします。
FirebaseMessagingServiceを継承したMyFirebaseMessagingServiceクラスを追加
Firebaseのガイド(https://firebase.google.com/docs/cloud-messaging/android/receive) を参考にサービスクラスを追加します。MyFirebaseMessagingService.javapackage jp.yuppe.push1; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.util.Log; import android.widget.Toast; import androidx.core.app.NotificationCompat; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; public class MyFirebaseMessagingService extends FirebaseMessagingService { private static final String TAG = MyFirebaseMessagingService.class.getSimpleName(); @Override public void onNewToken(String token) { Log.d(TAG, "Refreshed token: " + token); // If you want to send messages to this application instance or // manage this apps subscriptions on the server side, send the // Instance ID token to your app server. // sendRegistrationToServer(token); } @Override public void onMessageReceived(RemoteMessage remoteMessage) { // ... // TODO(developer): Handle FCM messages here. // Not getting messages here? See why this may be: https://goo.gl/39bRNJ Log.d(TAG, "From: " + remoteMessage.getFrom()); // Check if message contains a data payload. if (remoteMessage.getData().size() > 0) { Log.d(TAG, "Message data payload: " + remoteMessage.getData()); if (/* Check if data needs to be processed by long running job */ true) { // For long-running tasks (10 seconds or more) use WorkManager. //scheduleJob(); } else { // Handle message within 10 seconds //handleNow(); } } // Check if message contains a notification payload. String messageBody = ""; if (remoteMessage.getNotification() != null) { messageBody = remoteMessage.getNotification().getBody(); Log.d(TAG, "Message Notification Body: " + messageBody); } // Also if you intend on generating your own notifications as a result of a received FCM // message, here is where that should be initiated. See sendNotification method below. sendNotification(messageBody); } /** * Create and show a simple notification containing the received FCM message. * * @param messageBody FCM message body received. */ private void sendNotification(String messageBody) { Intent intent = new Intent(this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, PendingIntent.FLAG_ONE_SHOT); //String channelId = getString(R.string.default_notification_channel_id); String channelId = "001"; Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, channelId) //.setSmallIcon(R.drawable.ic_stat_ic_notification) .setSmallIcon(R.drawable.ic_launcher_foreground) .setContentTitle("Push1 App") .setContentText(messageBody) .setAutoCancel(true) .setSound(defaultSoundUri) .setContentIntent(pendingIntent); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); // Since android Oreo notification channel is needed. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(channelId, "Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT); notificationManager.createNotificationChannel(channel); } notificationManager.notify(0 /* ID of notification */, notificationBuilder.build()); } }AndroidManifest.xmlにサービスを登録
AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.yuppe.push1"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- add ここから --> <service android:name=".MyFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> <!-- add ここまで --> </application> </manifest>これで完成
Androidアプリを実行するとPush通知が受け取れるようになります。テストメッセージを送信
Firebaseコンソール(https://console.firebase.google.com/) でテスト用の通知メッセージを作成します。
メッセージを送信するとAndroidアプリにPush通知が表示されます。
アプリがバックグランドにまわっている、アプリがKillされている、端末がスリープ状態のいづれも通知を受け取ることができます。アプリがフォアグラウンド以外の場合はFMCによって自動的に通知が処理されます。
デバイス登録トークンを取得する
通知対象を単一のデバイスを対象にする場合、またはデバイス グループを作成する場合は、FirebaseMessagingService を拡張して onNewToken をオーバーライドすることで、このトークンにアクセスする必要があります。
MainActivity.javapublic class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FirebaseInstanceId.getInstance().getInstanceId() .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() { @Override public void onComplete(@NonNull Task<InstanceIdResult> task) { if (!task.isSuccessful()) { Log.w(TAG, "getInstanceId failed", task.getException()); return; } // Get new Instance ID token String token = task.getResult().getToken(); // Log and toast String msg = "token:" + token; Log.d(TAG, msg); Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show(); } }); } }例外が発生したら
この例外が出る場合は、プロジェクトの設定が不十分な場合に発生します。
Default FirebaseApp is not initialized in this process <パッケージ名>. Make sure to call FirebaseApp.initializeApp(Context) first.
アプリレベルのbuild.gradleに、
apply plugin: 'com.google.gms.google-services'が追加されているか確認してください。これは公式のチュートリアルに書かれていますので、参考にしてください。curlでメッセージを投げる
POSTでリクエストを投げるときに、「サーバーキー」が必要になります。Firebaseコンソールのプロジェクト認証情報で確認しておきます。
2つタイプのメッセージを送信できます。
- 通知メッセージ: 「表示メッセージ」とみなされることもあります。FCM SDK によって自動的に処理されます。
- データ メッセージ: クライアント アプリによって処理されます。参考: https://firebase.google.com/docs/cloud-messaging/concept-options
通知メッセージ
curl \ -X POST \ --header "Authorization: key=<your_server_key>" \ --Header "Content-Type: application/json" \ https://fcm.googleapis.com/fcm/send \ -d "{ \ \"to\":\"<device_token>\", \"notification\":{ \ \"title\" : \"FCM Message\" \ \"body\" : \"Hello FCM\" \ }, \ \"priority\":10 \ }"データメッセージ
curl \ -X POST \ --header "Authorization: key=<your_server_key>" \ --Header "Content-Type: application/json" \ https://fcm.googleapis.com/fcm/send \ -d "{ \ \"to\":\"<device_token>\", \"data\":{ \ \"key1\" : \"val1\", \ \"key2\" : \"val2\", \ \"key3\" : \"val3\" \ }, \ \"priority\":10 \ }"公式ドキュメント
- 投稿日:2020-04-30T16:32:44+09:00
startActivityForResult / requestPermissions が deprecated になる話
はじめに
影響範囲が大きそうなのでまとめておきます。
※本記事は Medium にも投稿済みです。 Qiita の方が見る人が多そうなのでこちらにも投稿しました。
androidx.fragment:fragment-ktx:1.3.0-alpha04から、以下のメソッドがdeprecatedになります。
- startActivityForResult
- onActivityResult
- requestPermissions
- onRequestPermissionsResult
代わりに 新しく追加された Activity Result APIs を使えとのアナウンスがあります。
Getting a result from an activity
While the underlying startActivityForResult() and onActivityResult() APIs are available on the Activity class on all API levels, it is strongly recommended to use the Activity Result APIs introduced in AndroidX Activity 1.2.0-alpha02 and Fragment 1.3.0-alpha02.
どうやら、
ActivityResultContractの 「registerForActivityResult」 を使えば良いようです。
そこで、registerForActivityResultの使い方をまとめました。リポジトリ
以下にサンプルリポジトリがあります。
https://github.com/nanaten/Activity-Contract-Example使い方
build.gradle
build.gradleに以下を追加します。
build.gradleimplementation "androidx.activity:activity-ktx:1.2.0-alpha04" implementation "androidx.fragment:fragment-ktx:1.3.0-alpha03"シンプルな使い方
シンプルにActivityの結果だけを受け取りたい場合は以下のように書けます。
MainActivity// 普段どおりIntentを生成する val intent = Intent(this, SecondActivity::class.java) // registerForActivityResult を定義する val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> // 呼び出し先のActivityを閉じた時に呼び出されるコールバック if(result.resultCode == Activity.RESULT_OK) { // RESULT_OK時の処理 val intent = result.data intent?.getStringExtra("result_key") } } // Activityを起動 launcher.launch(intent)
registerForActivityResultにActivityResultContracts.StartActivityForResult()を引数として渡すことによって、ActivityResult型の結果をコールバックで受け取ることが出来ます。
ActivityResultには resultCode (onActivityResultの resultCode と同様)と、 data (Intent) というプロパティがあるので、そちらから結果を処理します。呼び出し先のActivityは以前と変わりません。
SecondActivityintent.putExtra("result_key", "result") // 結果をセット setResult(Actiivty.RESULT_OK, intent) finish()カスタムの ActivityResultContracts
ActivityResultContractsを継承した class を定義することで、カスタムのActivityResultContractsを利用することもできます。ActivityResultContracts 継承クラスの作成
今回はobjectで定義しました。
ResultContractsobject ResultContracts: ActivityResultContract<Int, String?>() { // Activity起動時の処理 override fun createIntent(context: Context, input: Int): Intent { val intent = Intent(context, SecondActivity::class.java) intent.putExtra("select_id", input) return intent } // 結果受け取り時の処理 override fun parseResult(resultCode: Int, intent: Intent?): String? { if(resultCode == Activity.RESULT_OK) { return intent?.getStringExtra("result_key") } return null } }class に
ActivityResultContract<{Inputの型}, {Outputの型}>の形で継承させます。作成したクラスには、Activityを起動する時に呼ばれる
createIntent()とActivityから結果を受け取る時に呼ばれるparseResult()をoverrideする必要があります。
createIntent()にはregisterForActivityResult.launch()が呼び出された時の処理を、parseResult()には Activityから結果を受け取った時の処理を書きます。あとは、作成した class を
registerForActivityResultに渡してあげます。val launcher = registerForActivityResult(ResultContracts) { result -> // 受け取った結果を処理する text_view.text = result } launcher.launch(0) // InputにInt型を定義したのでInt型の引数を渡すどんな時に使う?
上記ではInputとOutputで定義した型をそのまま渡しているため恩恵はありませんが、Activityから受け取った値を加工してから渡したい場合などに便利です。
object ResultContracts: ActivityResultContract<Int, User?>() { // Outputの型をUser型に修正 // Activity起動時の処理 ... // 結果受け取り時の処理 override fun parseResult(resultCode: Int, intent: Intent?): User? { if(resultCode == Activity.RESULT_OK) { // Activityから文字列を受け取る val text = intent?.getStringExtra("result_key") // 受け取った値を独自クラスに詰め替えて返す val user = User(name = text) return user } return null } }val launcher = registerForActivityResult(ResultContracts) { result: User? -> // OutputをUser?型で定義したのでUser?型が返ってくる text_view.text = result?.name } launcher.launch(0) // Inputは変えていないのでInt型のままrequestPermissions
requestPermissions も ActivityResultContracts を利用して代替可能です。
ActivityResultContracts.RequestPermissionという関数が用意されているので、registerForActivityResultに引数として渡してあげます。val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { result -> if(result) { // リクエスト許可時の処理 Toast.makeText(this, "Permission Accepted.", Toast.LENGTH_SHORT).show() } else { // リクエスト拒否時の処理 Toast.makeText(this, "Permission Denied.", Toast.LENGTH_SHORT).show() } } // アクセスしたい権限を引数に渡す requestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION)コールバックの引数(result)はBoolean型です。権限が許可された場合 true が、拒否された場合は false が渡されてきます。
AndroidManifest.xmlに要求する権限を記載する必要があるのはいつも通りです。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>注意点
すたぜろさんのこちらの記事の注意点は
registerForActivityResultの場合にも当てはまると思われます。prepareCall->registerForActivityResultに置き換えて読んでください。(このあたりは自分で検証出来てません。すみません)
おわりに
Activityへの値の受け渡しは長い間
startActivityForResultが使われてきましたが、ここにきて大きな変更が加わりました。これから新規作成するアプリは、将来的に
registerForActivityResultへ置き換えることを念頭に入れて作る必要があると思われます。本記事を書くにあたり、以下の記事を参考にさせて頂きました。
- 投稿日:2020-04-30T13:07:27+09:00
ExoPlayerで、暗号化されたHLSを任意のkeyで復号・再生するには
※この記事はDRMが施されたHLSの復号についてではなく、AES等で暗号化されたHLSを復号する方法について記載しています。
HLSの暗号化の仕様
HLSストリームの暗号化に関する情報はm3u8ファイルの
#EXT-X-KEYに記載されています。
例えば、以下のファイルでは暗号化の方法・暗号化キーのurl・初期化ベクトルの値がわかります。#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-MEDIA-SEQUENCE:1653 #EXT-X-DISCONTINUITY-SEQUENCE:36 #EXT-X-KEY:METHOD=AES-128,URI="https://hogehoge",IV=0xdad51a9c61cc4344c03a566980ac21e7 #EXTINF:8.333333, /ts/CD8xZLiy68pVp3/h264/480/a1qLvavaVwX.ts #EXTINF:8.333334, /ts/CD8xZLiy68pVp3/h264/480/LAAK1hXHVLa.ts #EXTINF:8.333333, /ts/CD8xZLiy68pVp3/h264/480/KXzsNPf8KYx.ts #EXTINF:8.333333, /ts/CD8xZLiy68pVp3/h264/480/JmHJgZGPZQA.tsこのキーファイルのURIにロカールUriを記載してExoPlayerで再生することはできませんが、キーファイルをData URI schemeに変換して記載すればExoPlayerで再生することができます(参考)。
つまり、HLSを暗号化したうえで、暗号化キーのUrlではなくキーそのものをm3u8ファイルにベタ書きしてストリームを配信・再生することが可能だということです。パーサをカスタマイズする
HLSに限らず、ExoPlayerではDataSourceを自由にinjectすることが可能です。
ここではデフォルトで使用されているHlsPlaylistParserの代わりに自前でカスタマイズしたHLSMediaParserを使用してExoPlayerを初期化します。class CustomHlsPlaylistParserFactory() : HlsPlaylistParserFactory { override fun createPlaylistParser(): ParsingLoadable.Parser<HlsPlaylist> = CustomHlsPlaylistParser() override fun createPlaylistParser(masterPlaylist: HlsMasterPlaylist): ParsingLoadable.Parser<HlsPlaylist> = CustomHlsPlaylistParser(masterPlaylist) } class CustomHlsPlaylistParser() : ParsingLoadable.Parser<HlsPlaylist> { @Throws(IOException::class) override parse(uri: Uri, inputStream: InputStream): HlsPlaylist { // パーサを実装 // HlsPlaylistParser#parseMediaPlaylist`のfullSegmentEncryptionKeyUri`を任意のData Uri schemeに書き換えます } private fun toDataUriScheme(file: File): String { val base64str = Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)//NO_PADDINGは使用できません return "data:text/plain;base64,$base64str" } }fun initPlayer(remoteUrl: String): HlsMediaSource { val userAgent = WebView(getApplication()).settings.userAgentString val hppf = CustomHlsPlaylistParserWrapper(getApplication(), this) val dsf = DefaultDataSourceFactory(getApplication(), userAgent)//DefaultDataSourceFactory val mediaSource = HlsMediaSource.Factory(dsf) .setPlaylistParserFactory(hppf) .createMediaSource(remoteUrl.toUri()) player.prepare(mediaSource) player.playWhenReady = true }なお、HLSの仕様書によればAES-128ではキーのパディングを省略できないようです。
An encryption method of AES-128 signals that Media Segments are completely encrypted using the Advanced Encryption Standard (AES) [AES_128] with a 128-bit key, Cipher Block Chaining (CBC), and Public-Key Cryptography Standards #7 (PKCS7) padding [RFC5652].
また、DataSourceFactoryにはDefaultHttpDataSourceFactoryではなくDefaultDataSourceFactoryを用います(DRMを使用する場合はDataSchemeDataSource.Factory)。もしDefaultHttpDataSourceFactoryを使用すると、キーのURIがUrlとして扱われてしまい、MalformedURLExceptionがスローされます。
これで任意のキーに書き換えることができました。
- 投稿日:2020-04-30T12:12:46+09:00
FragmentでActionBarのTitleを設定する方法
- 投稿日:2020-04-30T11:03:24+09:00
Android - AsyncTaskの直列処理と並列処理の順番は保証されるのか??
Android開発において、AsyncTaskで直列処理と並列処理について、ほぼ同時に動いた時はどのような動きになるのか、実際に順番は保証されるのかわからなかったので、軽くコードを組んでみて確認してみましたので、メモとして残しておきたいと思います。
【参考URL】
developer - AsyncTask
Android - AsyncTaskの直列処理?並列処理?
AsyncTaskの非同期処理はどのように実現しているのか【試作1】タスク1:直列処理、タスク2:並列処理の場合
task1:execute()
task2:executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)// 1つ目の非同期タスク AsyncTask task1 = new AsyncTask() { @Override protected Object doInBackground(Object[] objects) { Log.v("task1", "start!!!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Object o) { super.onPostExecute(o); Log.v("task1", "done!!!"); } } .execute(); // 2つ目の非同期タスク AsyncTask task2 = new AsyncTask() { @Override protected Object doInBackground(Object[] objects) { Log.v("task2", "start!!!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Object o) { super.onPostExecute(o); Log.v("task2", "done!!!"); } } .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);実行結果
順番通りになっているかを確認するため、数回実行してみました。
【1回目】 5970-5995/com.example.testcode V/task2: start!!! 5970-5994/com.example.testcode V/task1: start!!! 5970-5970/com.example.testcode V/task2: done!!! 5970-5970/com.example.testcode V/task1: done!!! 【2回目】 7138-7157/com.example.testcode V/task2: start!!! 7138-7156/com.example.testcode V/task1: start!!! 7138-7138/com.example.testcode V/task2: done!!! 7138-7138/com.example.testcode V/task1: done!!! 【3回目】 7383-7403/com.example.testcode V/task1: start!!! 7383-7404/com.example.testcode V/task2: start!!! 7383-7383/com.example.testcode V/task2: done!!! 7383-7383/com.example.testcode V/task1: done!!!2回目のスタートが逆になっていますね。。。
【試作2】タスク1:並列処理、タスク2:直列処理の場合
task1:executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
task2:execute()
※ソースコードは試作1とほぼ変わりません。// 1つ目の非同期タスク AsyncTask task1 = new AsyncTask() { @Override protected Object doInBackground(Object[] objects) { Log.v("task1", "start!!!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Object o) { super.onPostExecute(o); Log.v("task1", "done!!!"); } } .execute(); // 2つ目の非同期タスク AsyncTask task2 = new AsyncTask() { @Override protected Object doInBackground(Object[] objects) { Log.v("task2", "start!!!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Object o) { super.onPostExecute(o); Log.v("task2", "done!!!"); } } .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);実行結果
こちらも順番通りになっているかを確認するため、数回実行してみました。
【1回目】 7267-7273/com.example.testcode V/task2: start!!! 7267-7272/com.example.testcode V/task1: start!!! 7267-7267/com.example.testcode V/task2: done!!! 7267-7267/com.example.testcode V/task1: done!!! 【2回目】 7643-7662/com.example.testcode V/task1: start!!! 7643-7663/com.example.testcode V/task2: start!!! 7643-7643/com.example.testcode V/task2: done!!! 7643-7643/com.example.testcode V/task1: done!!! 【3回目】 7871-7891/com.example.testcode V/task1: start!!! 7871-7892/com.example.testcode V/task2: start!!! 7871-7871/com.example.testcode V/task2: done!!! 7871-7871/com.example.testcode V/task1: done!!!こちらの実行結果も試作1と似たような結果となっています。
まとめ
直列処理と並列処理をほぼ同時に実行した場合の処理の順番は保証されないようです。
確実に順番を保証したい場合は、両方とも直列処理で行ったほうが良いですね。最後に
それ違うぞ!っという点もあるかと思いますが、参考程度になれば幸いです。
以上です。
- 投稿日:2020-04-30T01:09:39+09:00
【Android】Dagger2 ~基本編
はじめに
前回の記事(DI(依存性の注入)とは依存性を注入するということである、、?)では
DIとは何かについて記述しました。今回はDagger2を導入してみたので記事にまとめます。
言語はKotlinです!参考にしたチュートリアルはこちらです。
準備
まず、
build.gradleにてDagger2を導入します。
参照:Dagger README.mdbuild.gradle(app)apply plugin: 'kotlin-kapt' // Kotlinでアノテーションを使用するために必要 dependencies { .. // 2020/4/26現在、 // バージョンによって、使用できるアノテーションの種類が異なる. // また、2.25 ではコードが自動生成されない問題があることを確認済み. def dagger_version = 2.27 implementation "com.google.dagger:dagger:$dagger_version" annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version" // Kotlinで開発する場合は必要. // KotlinでもDaggerに関するアノテーションを使用できるようにするためのもの. kapt "com.google.dagger:dagger-compiler:$dagger_version" }Daggerの概要
Daggerとは、オブジェクト間の依存関係を管理するコードを自動生成してくれるツールです。
Daggerがコードを自動生成するために必要なこと:
- 提供するオブジェクトを把握する
- 注入先のオブジェクトを把握する
- 注入を実行する
1. 提供するオブジェクトを把握する
まずは、どのオブジェクトが何に依存しているのかを開発者が把握する必要がある。
どのオブジェクトを注入するのか
自分が依存関係を把握できたら、次はアノテーションを用いて「Dagger」にそれを教えてあげる
e.g.
@Module class DatabaseModule { @Provides fun providePlayCallDao(): PlayCallDao { return App.database.playCallDao() } }
@Provides
@Provides(提供)したいオブジェクトのインスタンス化メソッドにつけることで、何を@ProvideしたいかをDaggerに知らせる。
@Module
@Provideするためのメソッドを、@Moduleクラスにまとめる。他の
@Moduleも含めることができる。e.g.
@Module class DatabaseModule @Module(includes = [DatabaseModule::class]) class RepositoryModuleちょこっとまとめ
提供するオブジェクトをDaggerに知らせることができた!
-> これで、Daggerはコードを自動生成することができる。そして、
@Provideするオブジェクトの生成方法を覚えてくれる。2. 注入先のオブジェクトを把握する
Daggerは、1.の過程で何を注入するのかを把握することができた。
しかし、@Provideされるオブジェクトをどこで@Injectするのかは把握できていない。-> どこで使うためのものなのかを知らせてあげよう!
注入したいタイミングで
@Injectすれば良いが、後述の Field Injection を用いる場合は@Componentを作成する必要があります。
ここでは、その@Componentについて説明します。
@Component
@Componentを定義し、以下の役割を担わせる。
これは後述の Field Injection で使用します。役割:
@Provideするオブジェクトを解放する- 目標のクラスに依存性を注入する
含まれるもの:
- 使用する
@Moduleの宣言- inject用メソッドのインターフェース
このメソッドの引数に injectする対象のクラス(@Injectを記述するクラス)の型を設定します。
ここで定義しておけば、あとは Dagger が処理内容を自動生成してくれます。
inject()の使用方法は次節に持ち越します。e.g.
@Component(modules = [DatabaseModule::class]) interface AppComponent { // Daoの注入 fun inject(repository: PlayCallListRepository) }3. 注入を実行する
@Injectを用いて実装します。
@Inject
@Injectしたいオブジェクトを宣言している箇所につけます。
例は後述します。Daggerが提供するDIの種類
Daggerが提供するDIの種類は基本的に以下の2つがあります。
*数字は優先度
- Constructor Injection
- Field Injection
優先度の理由と各種詳細を以下に記述します。
1. Constructor Injection
e.g.
class PlayCallRepository @Inject constructor( private val dao: PlayCallDao ) { // このメソッドの実装は Dagger には関係ありません suspend fun loadAllPlayCall(): List<PlayCallEntity> { return dao.loadAllPlayCall() } }1番良さげな理由:
- オブジェクト生成時に依存関係をまとめて設定できるから
(ここでは注入するものがPlayCalldaoのみですが)- 実装が手軽だから
@Componentを作成しなくても良いなので、可能であれば Contructor Injection を適用するのが良いと思います。
ただし、
ActivityやFragmentなどの Android環境独自の Lifecycleを持つオブジェクトに対しては使えません。
-> Field Injectionを使用する2. Field Injection
e.g.
PlayCallListFragment.ktclass PlayCallListFragment : Fragment() { @Inject lateinit var viewModel: PlayCallListViewModel .. }PlayCallRepository.ktclass PlayCallRepository { @Inject lateinit var dao: PlayCallDao ..
@Injectアノテーションをつけるだけだと、Daggerは依存性を注入してくれません
-> @Component を作成し、inject用メソッドを用意する必要がある。e.g.
AppComponent.kt@Component(modules = [AppModule::class]) interface AppComponent { fun inject(repository: PlayCallListRepository) }ここで定義した
inject(repository:)を使用し対象オブジェクト(ここではPlayCallListRepository)に依存性を注入します。
このメソッドを呼んだ後に、@Injectを使用することができます。
* 逆にいうと、inject(repository:)より前に@Injectを通ってしまうとコンパイルエラーが発生します。e.g.
PlayCallRepositoryにてPlayCallRepository.ktclass PlayCallRepository { @Inject lateinit var dao: PlayCallDao init { DaggerAppComponent.create().inject(this) } }e.g.
PlayCallListFragmentにてPlayCallListFragment.ktclass PlayCallListFragment : Fragment() { @Inject lateinit var viewModel: PlayCallListViewModel override fun onAttach(context: Context) { DaggerPlayCallListComponent.create().inject(this) } }1つ目の例で少し解説します。
DaggerAppComponent.create()でDaggerAppComponentを生成します
AppComponent内で定義したinject(repository:)メソッドを呼ぶことで@Injectアノテーションが付いているプロパティに依存性を注入することができます補足:
DaggerAppComponentはDaggerが自動生成したクラスです。
このクラス内で、AppComponentで定義したインターフェースの処理を実装しています。
正確に言うと、DaggerAppComponent内から別クラスのメソッドを呼ぶ処理を実装しています。
Daggerが自動生成したDaggerAppComponentクラスを見てみましょう!DaggerAppComponent.javapublic final class DaggerAppComponent implements AppComponent { .. @Override public void inject(PlayCallRepository repository) { // ここで、inject(repository:) を実装している injectPlayCallRepository(repository); // メソッドA } // メソッドA private PlayCallRepository injectPlayCallRepository(PlayCallRepository instance) { // 次に添付のクラス内の injectDao() を呼ぶ PlayCallRepository_MembersInjector.injectDao(instance, DatabaseModule_ProvidePlayCallDaoFactory.providePlayCallDao(databaseModule)); return instance; } .. }PlayCallRepository_MembersInjector.javapublic final class PlayCallRepository_MembersInjector implements MembersInjector<PlayCallRepository> { .. @InjectedFieldSignature("io.github.itakahiro.architecturefootball.repository.PlayCallRepository.dao") public static void injectDao(PlayCallRepository instance, PlayCallDao dao) { // ここで、PlayCallRepository に PlayCallDao を注入している!! instance.dao = dao; } }まとめ
Dagger の基礎的な部分をまとめてみました。
Dagger では、他にも様々な機能・アノテーションが用意されています。
それらを使いこなすのは難しく感じていますが、少しずつ理解していこうと思います。サンプルコード
https://github.com/iTakahiro/ArchitectureFootball
参考資料
- Dagger: 公式ガイド
- Dagger: リポジトリ
- Dagger 2 Tutorial For Android: Advanced: チュートリアル
- 投稿日:2020-04-30T00:03:51+09:00
ViewModelProviders is deprecated になっているので、ViewModelProviderを使う
ViewModelProviders が非推奨になっている
公Android Developer 公式: ViewModelに書いてある
ViewModelProviders.of(this).get(UserModel.class)
をつかうと deprecated(非推奨) と表示されてしまう。
公式のViewModelProvidersの説明によるともうメンテナンスがされないらしい。
今後はViewModelProviderを使わないといけないらしいので今回はその使い方を紹介する。用意するファイル
Activity
プロジェクト開始時に通常通り生成されるもの
解説は省くViewModel
MVVM アーキテクチャで View と Model をつなぐもの。メリットとしては下記の2つが考えられる。
・画面回転時に View に持たせていたデータが消えるのを防ぐ
・疎結合にしてくれるので Activity が太るのを防ぐFactory
ViewModel を Activity で呼ぶ際に使うクラス。
Factory クラスでインスタンスの作り方を管理して、Activity でクラスを呼び出す際にで具体的な処理を行っている。使い方
以下のコードの通りに使うことができます。
ViewModelに設定した文字が Activity から呼び出されるだけのコードです。MainActivitypackage com.example.viewmodel_fragment import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider class MainActivity : AppCompatActivity() { lateinit var factory: MainViewModelFactory lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) factory = MainViewModelFactory() viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java) viewModel.loadData() viewModel.user.observe(this, Observer { println(it) }) } }MainViewModelpackage com.example.viewmodel_fragment import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel class MainViewModel : ViewModel(){ val user: MutableLiveData<String> = MutableLiveData() fun loadData(){ user.postValue("higu") } }MainViewModelFactorypackage com.example.viewmodel_fragment import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider class MainViewModelFactory : ViewModelProvider.NewInstanceFactory(){ override fun <T : ViewModel?> create(modelClass: Class<T>):T { return MainViewModel() as T } }Factory に関して補足をすると T というのはジェネリクスといい、メソッドやクラスに型を
つけることができる機能です。





















