- 投稿日:2020-04-21T23:37:05+09:00
[LiveData] 更新されたときだけ値を受けとる
二通りの方法を示します。
1. LiveData を BroadcastChannel に変換する
いきなりテストです。
class MyTest { private val dispatcher = TestCoroutineDispatcher() @[Rule JvmField] val rule = InstantTaskExecutorRule() @Before fun before() { Dispatchers.setMain(dispatcher) } @After fun after() { Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() } @Test fun test() = dispatcher.runBlockingTest { // ソースとなる LiveData val source = object : MutableLiveData<Int>(0) { override fun onActive() = println("onActive") override fun onInactive() = println("onInactive") } // BroadcastChannel に変換 val broadcastChannel = source.asFlow().broadcastIn(this) // Flow に変換 (都度 broadcastChannel.openSubscription() しても良い) val flow = broadcastChannel.asFlow() val job1 = launch { flow.collect { println("coroutine1: $it") } } source.value = 1 job1.cancel() source.value = 2 val job2 = launch { flow.collect { println("coroutine2: $it") } } source.value = 3 job2.cancel() broadcastChannel.cancel() } }実行結果は以下です。
onActive coroutine1: 0 coroutine1: 1 coroutine2: 3 onInactive
coroutine1
では初期値0
および1
を受信していますが、coroutine2
では3
だけを受信しており、直前の値2
を受信していないことがわかります。利点
- 特別な仕組みを作りこむ必要がない
欠点
- おまじないっぽい?
- 多くの関数が
@ExperimentalCoroutinesApi
だったり@FlowPreview
だったりする2. LiveData.getVersion() を使う
androidx.lifecycle
パッケージ配下に以下の関数を用意します。package androidx.lifecycle internal fun LiveData<*>.version() = version次に、任意のパッケージ配下に以下の関数を用意します。
@MainThread fun <T> LiveData<T>.observeChangesForever(action: (T) -> Unit): Observer<T> = object : Observer<T> { private val version = version() override fun onChanged(t: T) { if (version() > version) action(t) } }.also { observeForever(it) }省略していますが、LifecycleOwner を受け取る関数も同じように作ることができます。
テストは以下です。
class MyTest { @[Rule JvmField] val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() @Test fun test() { // ソースとなる LiveData val source = object : MutableLiveData<Int>(0) { override fun onActive() = println("onActive") override fun onInactive() = println("onInactive") } val observer1 = source.observeChangesForever { println("observer1: $it") } source.value = 1 source.removeObserver(observer1) source.value = 2 val observer2 = source.observeChangesForever { println("observer2: $it") } source.value = 3 source.removeObserver(observer2) } }実行結果は以下です。
onActive observer1: 1 onInactive onActive observer2: 3 onInactive
observer1
・observer2
共に直前の値 (0
および2
) を受信していないことがわかります。利点
- わかりやすい (かも)
欠点
- 作りこみが必要
- LiveData.getValue() はパッケージプライベートなので、今後動かなくなるかも
二通りの方法を示しましたが両者は挙動が異なるので、使い方には注意が必要です。
以上です。
- 投稿日:2020-04-21T23:36:19+09:00
[SavedStateHandle] やっぱりDaggerとうまく付き合う
以前 SavedStateHandle を DI する方法について記事を書きましたが、今は正直以下の方法でも良いのではないかと思っています。
class MyViewModel(handle: SavedStateHandle) : ViewModel() { @Inject lateinit var myRepository: MyRepository val myData: LiveData<MyData> = liveData { emitSource(myRepository.getMyData()) } }class MyFragment : Fragment() { private val viewModel by viewModels<MyViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (requireActivity().applicationContext as App) .component .inject(viewModel) } }SavedStateHandle はデフォルトの ViewModelFactory におまかせして、それ以外のコンポーネントをフィールドインジェクションでセットする方法です。
テストはしていません。
以上です。
- 投稿日:2020-04-21T21:56:22+09:00
targetSdkVersion を 29 に上げたら Robolectric のテストが動かなくなる
概要
Java 8 の環境で targetSdkVersion を 29 に上げたら RobolectricTestRunner を使うユニットテストが動かなくなったので、対応方法を調べました。
問題
テストを実行すると、以下の例外が出てテストが失敗します。
java.lang.IllegalArgumentException: failed to configure jp.toastkid.yobidashi.media.image.preview.RotateMatrixFactoryTest.test: Package targetSdkVersion=29 > maxSdkVersion=28 at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:240) at org.junit.runners.ParentRunner.getFilteredChildren(ParentRunner.java:426) // 中略 at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: java.lang.IllegalArgumentException: Package targetSdkVersion=29 > maxSdkVersion=28 at org.robolectric.plugins.DefaultSdkPicker.configuredSdks(DefaultSdkPicker.java:118) at org.robolectric.plugins.DefaultSdkPicker.selectSdks(DefaultSdkPicker.java:69) at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:206)対応
現時点の最新バージョンの 4.3.1 を使えば SDK 29に対応はしているようですが、Java 9 以上が必要です。
java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8) at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:265) at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:63) at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:215) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)Workaround
諸事情で Java 8 を使わざるを得ない場合は、 src/test/resources/robolecric.properties というファイルを置き、以下の設定を入れることで動くようになりました。Java 9 を導入するまではこれで対応しましょう。
robolecric.propertiessdk=28今後、sdk 28を指定していたことでテストに不具合が起こる可能性も否定はできないので、早めに Java と Robolectric をアップグレードする方が良さそうです。
参考
- 投稿日:2020-04-21T21:56:22+09:00
targetSdkVersion を 29 に上げても Robolectric のテストを動くようにする
概要
Java 8 の環境で targetSdkVersion を 29 に上げたら RobolectricTestRunner を使うユニットテストが動かなくなったので、対応方法を調べました。
問題
テストを実行すると、以下の例外が出てテストが失敗します。
java.lang.IllegalArgumentException: failed to configure jp.toastkid.yobidashi.media.image.preview.RotateMatrixFactoryTest.test: Package targetSdkVersion=29 > maxSdkVersion=28 at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:240) at org.junit.runners.ParentRunner.getFilteredChildren(ParentRunner.java:426) // 中略 at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: java.lang.IllegalArgumentException: Package targetSdkVersion=29 > maxSdkVersion=28 at org.robolectric.plugins.DefaultSdkPicker.configuredSdks(DefaultSdkPicker.java:118) at org.robolectric.plugins.DefaultSdkPicker.selectSdks(DefaultSdkPicker.java:69) at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:206)対応
現時点の最新バージョンの 4.3.1 を使えば SDK 29に対応はしているようですが、Java 9 以上が必要です。
java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8) at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:265) at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:63) at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:215) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)Workaround
諸事情で Java 8 を使わざるを得ない場合は、 src/test/resources/robolecric.properties というファイルを置き、以下の設定を入れることで動くようになりました。Java 9 を導入するまではこれで対応しましょう。
robolecric.propertiessdk=28今後、sdk 28を指定していたことでテストに不具合が起こる可能性も否定はできないので、早めに Java と Robolectric をアップグレードする方が良さそうです。
参考
- 投稿日:2020-04-21T20:54:53+09:00
Google認証で ApiException:10 が発生
Google認証をやっていたら、下記の例外が発生
com.google.android.gms.common.api.ApiException: 10:
下記で解決できるらしい。
■参考サイト
https://medium.com/@riscait/apiexception-10-error-in-sign-in-with-google-using-firebase-auth-in-flutter-1be6a44a2086
https://stackoverflow.com/questions/47437678/why-do-i-get-com-google-android-gms-common-api-apiexception-10
- 投稿日:2020-04-21T18:07:33+09:00
Android studioのemulatorでインターネットに繋がらない現象を解決する
Android studioでemulatorを起動し外部APIへGETリクエストを実行しようとするもレスポンスが取得できずなんでだろうと思っていたらそもそもインターネットにできない状態でした.
環境
端末: Nexus5X
API version: 28現象
対応
ホスト側のDNSを8.8.8.8(GoogleのDNS)に変更後,emulatorを再起動することで解決
参考
Android emulator not able to access the internet
Android 9(Pie)でHTTP通信を有効にする
- 投稿日:2020-04-21T16:29:27+09:00
[Android]GoogleMap上ランドマークのストリートビュー表示
GoogleMap上ランドマークのストリートビュー表示
以下の手順となります。
・ランドマークからPlaceIDを使ってplace/details API 実行して情報取得
・imageViewに表示するためライブラリ導入
・ストリートビュー表示するランドマークからPlaceIDを使ってplace/details API 実行して情報取得
https://qiita.com/myatthinkyu/items/efc901d88a5f9d031c4f
imageViewに表示するためライブラリ導入
implementation "com.github.bumptech.glide:glide:4.8.0"ストリートビュー表示する
buildingInfo.firstOrNull()?.let { var location = it.result!!.geometry!!.location var locationlatlng = location!!.lat.toString()+"," + location!!.lng.toString() val imageUri = Uri.Builder().apply { scheme("https") path("maps.googleapis.com/maps/api/streetview") appendQueryParameter("size", "400x300") appendQueryParameter("location", locationlatlng) appendQueryParameter("key", BuildConfig.GOOGLE_API_KEY) } var imageview = findViewById<ImageView>(R.id.street_view_image) Glide.with(this).load(imageUri.toString()).into(imageview) }
- 投稿日:2020-04-21T14:57:21+09:00
同一のRadioGroupに入っているRadioButtonを画面上の自由な位置に配置したい
概要
Androidの標準APIで提供されている
RadioButton
とRadioGroup
。複数の
RadioButton
を1つのRadioGroup
の中に入れ子にすれば、その中で同時に一つだけが選択可能になる、とここまではいいのだが、RadioButton
は自由に配置することが許されない。なんでRadioButtonは自由に動かせない?
ドキュメントでもなんでも見ればわかるのだが、
android.widget.RadioGroup
はandroid.widget.LinearLayout
を継承しているため。LinearLayoutが何だかはみんな知ってるでしょう。
つまり横1列か縦1列にしか並べられないということ。なんでやねん!!
解決策
ということで、作っちゃいました。そういうクラス。
FreeRadioGroup.javaimport android.util.Log; import android.view.View; import android.widget.CompoundButton; import android.widget.RadioButton; import android.widget.RadioGroup; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; public class FreeRadioGroup { private List<RadioButton> registeredRadioButtons = new ArrayList<>(); public FreeRadioGroup(){ } public void addChild(RadioButton rb){ if(registeredRadioButtons.contains(rb)){ Log.w("FreeRadioGroup", "RadioButton already registered. Abort registering"); }else { registeredRadioButtons.add(rb); rb.setOnCheckedChangeListener(OCL); } } public void removeChild(RadioButton rb){ if(!registeredRadioButtons.contains(rb)){ Log.w("FreeRadioGroup", "RadioButton is not registered. Abort unregistering"); }else { registeredRadioButtons.remove(rb); rb.setOnClickListener(null); } } public int getSelectedButtonIndex(){ for(int i=0,n=registeredRadioButtons.size();i<n;i++){ if(registeredRadioButtons.get(i).isChecked()){ return i; } } return -1; } @Nullable public RadioButton getSelectedButton(){ for(int i=0,n=registeredRadioButtons.size();i<n;i++){ RadioButton r = registeredRadioButtons.get(i); if(r.isChecked()){ return r; } } return null; } private final CompoundButton.OnCheckedChangeListener OCL = new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { if(registeredRadioButtons.contains((RadioButton)compoundButton) && b){ for(RadioButton r : registeredRadioButtons){ if(r.equals((RadioButton)compoundButton)){ continue; } r.setChecked(false); } } } }; }とりあえず丸コピして使っていただいて構いません。
addChild(RadioButton rb)
で登録したいRadioButton
を登録すると、登録したものの中で同時に1つだけが選択される仕組み。逆になんでこれが公式に実装されてないのか疑問に思うレベルで必要だと思うのだが……。
現状の問題点
・ボタンの状態が変わった時に呼ばれるリスナーを他に登録しようとすると上書きになってしまうため使えない
自分が使う分には今のところこれで困ってないので、時間があるときに考えておきます。あるいは何かいい方法があったらコメントで教えて頂ければと。OnClickの方は普通に使えるのでそれだけで済むのであれば問題ないはず。
- 投稿日:2020-04-21T14:56:44+09:00
Androidエミュレータの日本語化(Android9)
- 投稿日:2020-04-21T13:29:13+09:00
M5Stackで測定した温度と気圧をBLEで飛ばし、Androidアプリで受け取ってみる
本記事は、仙台のメイカースペース"FabLab Senda FLAT"で、Arduino等を使いながらIoT的機器を作っている集まりで発表した内容をまとめたものです。
この集まりはハード寄りの人が多いため、スマホアプリのBLEの開発についてのさわりについて発表しました。概要
M5StackのENV.Sencorから温度と気圧を読み取り、そのデータをBLEのAdvertisingで飛ばします。
そして、Androidアプリで温度と気圧のデータを受け取りアプリ上で表示します。BLEでは双方向通信のポイントトゥーポイントと単方向のブロードキャストモードがありますが、今回は簡単なブロードキャストモードモードのみを扱います。
ソースコード
https://github.com/yhirano/ble_mobile_app_sample/
送信側(M5Stack)
M5Stackとは
M5Stackは液晶ディスプレイ、microSDカードスロット、3つのボタン、USB、Groveのコネクタ5cm四方の基板に搭載しプラスチックのケースに詰め込んだモジュールです。
CPUにはEspressifのESP32が搭載されており、Wi-FiおよびBluetooth v4.2とBLEを使用することができます。いろいろな拡張基板が公式・非公式を問わずに用意されています。
M5Stack上で動くソフトの開発はEspressifのESP-IDF(C/C++言語)のほか、Arduino IDEや、MicroPythonなどを利用できます。
このように、M5StackはIoT機器の試作にはとても重宝するモジュールします。
M5Stackの準備
本記事では、M5GOに付属の本体とENV.Sensorを使用します。
写真のように本体のPORT AにENV.Sensorを接続します。
M5Stackの開発環境の構築
今回はArduino言語で開発します。
開発環境にはVSCodeとPlatformIO IDEプラグインを使います。PlatformIOとは
Arduino言語で開発する際には一般的にはArduino IDEが使われますが、開発になれてくるとArduino IDEで開発をするのはツラいものがあります。
PlatformIOはいろいろなマイコン+いろいろなフレームワークに対応した高機能エディタにプラグインできる組み込み用IDEです。
VSCodeの拡張としてインストールすることができ、簡単に開発を始めることができます。PlatformIOはVSCodeの他にもAtom、Vim、CLionといったエディタにも対応しています。
VSCodeのインストール
VSCodeのサイトからダウンロードし、インストールして下さい。
VSCodeへのPlatformIO IDE拡張のインストール
VSCodeを起動し、
使用するライブラリのダウンロード
M5Stack開発用のライブラリと、ENV.Sensorのライブラリをインストールします。
M5Stack開発用のライブラリをインストールします。
VSCodeのPIO Homeのタブより次にENV.Sensorのライブラリをインストールします。
BMP280とはENV.Sensorに搭載されている温度・気圧センサーのチップです。
データシート等はこちら。プロジェクトの作成
プロジェクトを作成します。
VSCodeのPIO Homeのタブより各種ファイルが生成されます。
M5Stackのセンサーの読み取りとBLEでのデータのアドバタイジング
src/main.cpp
を下記のように書き換えます。
処理の内容についてはコード内のコメントを参照してください。src/main.cpp#include <M5Stack.h> #include <Adafruit_BMP280.h> #include <BLEDevice.h> #include <BLE2902.h> // BLE上のこのデバイスの名前。適当な名前でOK。 #define BLE_LOCAL_NAME "M5GO Env.Sensor Advertiser" // BLEのサービスUUID。適当なUUID(ランダムで生成したものがよい)を設定して下さい。 #define BLE_SERVICE_UUID "133fe8d4-5197-4675-9d76-d9bbf2450bb4" // BLEのCharacteristic UUID。適当なUUID(ランダムで生成したものがよい)を設定して下さい。 #define BLE_CHARACTERISTIC_UUID "0fc10cb8-0518-40dd-b5c3-c4637815de40" // BMP280のインスタンスを作成 static Adafruit_BMP280 bmp280; static BLEServer* pBleServer = NULL; static BLECharacteristic* pBleNotifyCharacteristic = NULL; // 温度と気圧のデータを指定したバッファーに書き込みます。 // バッファーには、1〜4バイト目にはリトルエンディアンで温度を格納し、 // 5〜8バイト目にはリトルエンディアンで気圧を格納します。 // // - buffer uint8_tの8つ分以上のサイズのバッファを指定すること static void pack(uint8_t* buffer, float temperature, float pressure) { int32_t temperature100 = (int32_t)(temperature * 100); buffer[0] = temperature100 & 0x000000ff; buffer[1] = (temperature100 & 0x0000ff00) >> 8; buffer[2] = (temperature100 & 0x00ff0000) >> 16; buffer[3] = (temperature100 & 0xff000000) >> 24; int32_t pressure100 = (int32_t)(pressure * 100); buffer[4] = pressure100 & 0x000000ff; buffer[5] = (pressure100 & 0x0000ff00) >> 8; buffer[6] = (pressure100 & 0x00ff0000) >> 16; buffer[7] = (pressure100 & 0xff000000) >> 24; } // 起動時に最初の1回だけ呼ばれる処理は setup 関数内に書きます void setup() { // M5Stackの初期化 M5.begin(); // BMP280の初期化。 // 初期化の結果が bmp280InitResult の変数に格納されます。 // 引数に指定している BMP280_ADDRESS_ALT はBMP280センサーのI2Cアドレスです。 bool bmp280InitResult = bmp280.begin(BMP280_ADDRESS_ALT); // M5StackのLCD(液晶ディスプレイ)を黒で塗りつぶします M5.Lcd.fillScreen(BLACK); // M5StackのLCDで表示する文字色を白、背景色を黒に設定します。 M5.Lcd.setTextColor(WHITE ,BLACK); // M5StackのLCDで表示する文字の大きさを設定します。 M5.Lcd.setTextSize(2); // "BMP280" という文字列をLCDの座標 (10, 20) の位置に表示します。 // LCDの座標系は左上が(0, 0)、右下が(320, 240)です。 M5.Lcd.setCursor(10, 20); M5.Lcd.print("BMP280"); // "temperature:" という文字列をLCDの座標 (30, 50) の位置に表示します。 M5.Lcd.setCursor(30, 50); M5.Lcd.print("temperature:"); // "temperature:" という文字列をLCDの座標 (30, 80) の位置に表示します。 M5.Lcd.setCursor(30, 80); M5.Lcd.print("pressure:"); // BMP280の初期化が失敗した場合(bmp280InitResultがfalseの場合)は… if (!bmp280InitResult) { // M5StackのLCDで表示する文字色を黄色にします。 M5.Lcd.setTextColor(YELLOW ,BLACK); // "Failed BMP280 init." という文字列をLCDの座標 (10, 200) の位置に表示します。 M5.Lcd.setCursor(10, 200); M5.Lcd.print("Failed BMP280 init."); } // BLE環境の初期化 BLEDevice::init(BLE_LOCAL_NAME); // BLEサーバの生成 pBleServer = BLEDevice::createServer(); // BLEのサービスの生成。引数でサービスUUIDを設定する。 BLEService* pBleService = pBleServer->createService(BLE_SERVICE_UUID); // BLE Characteristicの生成 pBleNotifyCharacteristic = pBleService->createCharacteristic( // Characteristic UUIDを指定 BLE_CHARACTERISTIC_UUID, // このCharacteristicのプロパティを設定 BLECharacteristic::PROPERTY_NOTIFY ); // BLE Characteristicにディスクリプタを設定 pBleNotifyCharacteristic->addDescriptor(new BLE2902()); // BLEサービスの開始 pBleService->start(); // BLEのアドバタイジングを開始 pBleServer->getAdvertising()->start(); } // 電源が入っている間は、この loop 関数で書いた処理が繰り返されます void loop() { // M5StackのボタンA/B/Cの読み取り状態を更新しています。 // ボタンを使わない場合でも、loop関数の冒頭で M5.update() を呼んでおくといいでしょう。 M5.update(); // BMP280から温度を取得します float temperature = bmp280.readTemperature(); // BMP280から気圧を取得します。取得した気圧を100でわることで、hPaの単位になります。 float pressure = bmp280.readPressure() / 100; // M5StackのLCDで表示する文字色を白、背景色を黒に設定します。 M5.Lcd.setTextColor(WHITE ,BLACK); // LCDの座標 (180, 50) の位置に温度を小数二桁で表示します。 M5.Lcd.setCursor(180, 50); M5.Lcd.printf("%.2fC", temperature); // LCDの座標 (180, 50) の位置に気圧を小数二桁で表示します。 M5.Lcd.setCursor(180, 80); M5.Lcd.printf("%.2fhPa", pressure); // BLEでのデータ通知用バッファを定義 uint8_t dataBuffer[8]; // 温度と気圧のデータをdataBufferに格納します pack(dataBuffer, temperature, pressure); // データをBLEに設定し、送信します pBleNotifyCharacteristic->setValue(dataBuffer, 8); pBleNotifyCharacteristic->notify(); // 33ミリ秒停止します delay(33); }プログラムの転送と実行
M5GO本体のUSB-CポートとPCを接続します。
そして、VSCodeの下部の「→」のをクリックするとプログラムが本体に転送され、本体でプログラムが実行されます。
受信側(Android)
Android端末(実機)の用意
Androidアプリの開発はPC上のAndroidエミュレータでもできますが、エミュレータではBLEがサポートされていませんので、BLEが搭載されたAndroid端末が必要です。
Androidアプリを実機で開発するには、実機で
デバイスの開発者向けオプション
を有効にする必要があります。
端末の [設定] > [デバイス情報] > [ビルド番号](端末によってメニュー階層が異なる場合があります) を7回タップするとデバイスの開発者向けオプション
を有効にすることができます。
- Android 7.1(API レベル 25)以下: [設定] > [デバイス情報] > [ビルド番号]
- Android 8.0.0(API レベル 26)および Android 8.1.0(API レベル 26): [設定] > [システム] > [デバイス情報] > [ビルド番号]
- Android 9(API レベル 28)以上: [設定] > [デバイス情報] > [ビルド番号]
デバイスの開発者向けオプション
を有効にすると [設定] > [システム] > [詳細設定] > [開発者向けオプション](端末によってメニュー階層が異なる場合があります) のメニューが出現します。Androidアプリ開発のためには、 [設定] > [システム] > [詳細設定] > [開発者向けオプション]でオプションをオンにし、 さらに
[USBデバッグ]
オプションもオンにする必要があります。
- Android 7.1(API レベル 25)以下: [設定] > [開発者向けオプション] > [USBデバッグ]
- Android 8.0.0(API レベル 26)および Android 8.1.0(API レベル 26): [設定] > [システム] > [開発者向けオプション] > [USBデバッグ]
- Android 9(API レベル 28)以上: [設定] > [システム] > [詳細設定] > [開発者向けオプション] > [USBデバッグ]
詳しくはグーグルのドキュメントを参照して下さい。
Android Studioのインストール
Androidアプリの開発にはAndroid Studioを使用します。
Android Studioのインストールはグーグルのドキュメントと動画を参照して下さい。プロジェクトの作成
Android Studioを起動し、
プロジェクトが作成されます。
Androidアプリでのデータの受け取りと表示
app/src/main/AndroidManifest.xml
app/src/main/AndroidManifest.xml
を書き換えます。app/src/main/AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.github.yhirano.ble_mobile_app_sample.android"> <!-- BLEを搭載した端末でのみインストールできるように設定 --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> <!-- BLEを使用するにはBluetoothのパーミッションが必要 --> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <!-- Android 6以降はBLEを使用するには位置情報取得のパーミッションが必要。 Android 9以下は ACCESS_FINE_LOCATION の代わり にACCESS_COARSE_LOCATION でもOK。 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <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> </application> </manifest>app/src/main/res/layout/activity_main.xml
app/src/main/res/layout/activity_main.xml
を書き換えます。
(このファイルは画面のレイアウトを指定するためのファイルです)app/src/main/res/layout/activity_main.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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="センサーデータの表示欄"/> </androidx.constraintlayout.widget.ConstraintLayout>app/src/main/src/java/your-package-path/MainActivity.kt
app/src/main/src/java/your-package-path/MainActivity.kt
を書き換えます。app/src/main/src/java/your-package-path/MainActivity.ktpackage your-package-path.android // パッケージ名は書き換えないでください import android.Manifest import android.bluetooth.* import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.* class MainActivity : AppCompatActivity() { /** 温度と湿度を表示するTextView */ private val sensorTextView by lazy { findViewById<TextView>(R.id.sensorTextView) } /** RSSI(電波強度)を表示するTextView */ private val rssiTextView by lazy { findViewById<TextView>(R.id.rssiTextView) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (allPermissionsGranted()) { // 位置情報のパーミッションが取得できている場合は、BLEのスキャンを開始 bleScanStart() } else { // 位置情報のパーミッションが取得できていない場合は、位置情報の取得のパーミッションの許可を求める ActivityCompat.requestPermissions( this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS ) } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { // 位置情報のパーミッションが取得できている場合は、BLEのスキャンを開始 bleScanStart() } else { sensorTextView.text = "パーミッションが許可されていません" rssiTextView.text = null } } } /** REQUIRED_PERMISSIONSで指定したパーミッション全てが許可済みかを取得する */ private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED } /** BLEのスキャンを開始 */ private fun bleScanStart() { val manager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val adapter = manager.adapter if (adapter == null) { sensorTextView.text = "Bluetoothがサポートされていません" rssiTextView.text = null return } if (!adapter.isEnabled) { sensorTextView.text = "Bluetoothの電源が入っていません" rssiTextView.text = null return } val bluetoothLeScanner = adapter.bluetoothLeScanner // "M5GO Env.Sensor Advertiser" というデバイス名のみの通知を受け取るように設定 val scanFilter = ScanFilter.Builder() .setDeviceName("M5GO Env.Sensor Advertiser") .build() val scanSettings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_BALANCED) .build() Log.d(TAG, "Start BLE scan.") bluetoothLeScanner.startScan(listOf(scanFilter), scanSettings, scanCallback) } /** スキャンでデバイスが見つかった際のコールバック */ private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { super.onScanResult(callbackType, result) rssiTextView.text = "RSSI(受信信号強度) ${result.rssi}" // デバイスのGattサーバに接続 val bluetoothGatt = result.device.connectGatt(this@MainActivity, false, gattCallback) val resultConnectGatt = bluetoothGatt.connect() if (resultConnectGatt) { Log.d(TAG, "Success to connect gatt.") } else { Log.w(TAG, "Failed to connect gatt.") } } } /** デバイスのGattサーバに接続された際のコールバック */ private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { super.onConnectionStateChange(gatt, status, newState) if (gatt == null) { Log.w(TAG, "Gatt is empty. Maybe Bluetooth adapter not initialized.") return } if (newState == BluetoothGatt.STATE_CONNECTED) { Log.d(TAG, "Discover services.") // GATTサーバのサービスを探索する。 // サービスが見つかったら onServicesDiscovered が呼ばれる。 gatt.discoverServices() } } override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { super.onServicesDiscovered(gatt, status) Log.d(TAG, "Services discovered.") if (status == BluetoothGatt.GATT_SUCCESS) { if (gatt == null) { Log.w(TAG, "Gatt is empty. Maybe Bluetooth adapter not initialized.") return } val service = gatt.getService(BLE_SERVICE_UUID) val characteristic = service?.getCharacteristic(BLE_CHARACTERISTIC_UUID) if (characteristic == null) { Log.w(TAG, "Characteristic is empty. Maybe Bluetooth adapter not initialized.") return } // Characteristic "0fc10cb8-0518-40dd-b5c3-c4637815de40" のNotifyを監視する。 // 変化があったら onCharacteristicChanged が呼ばれる。 gatt.setCharacteristicNotification(characteristic, true) val descriptor = characteristic.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") ) descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor) } } override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) { super.onCharacteristicChanged(gatt, characteristic) Log.v(TAG, "onCharacteristicChanged") this@MainActivity.runOnUiThread { val data = Data.parse(characteristic?.value) ?: return@runOnUiThread val sb = StringBuilder() sb.append("Temperature: ${String.format("%.2f", data.temperature)}\n") sb.append("Pressure: ${String.format("%.2f", data.pressure)}") sensorTextView.text = sb.toString() } } } /** 温度と気圧を持つデータクラス */ private data class Data(val temperature: Float, val pressure: Float) { companion object { /** * BLEから飛んできたデータをDataクラスにパースする */ fun parse(data: ByteArray?): Data? { if (data == null || data.size < 8) { return null } val temperatureBytes = ByteBuffer.wrap(data, 0, 4) val pressureBytes = ByteBuffer.wrap(data, 4, 4) val temperature = temperatureBytes.order(ByteOrder.LITTLE_ENDIAN).int.toFloat() / 100.toFloat() val pressure = pressureBytes.order(ByteOrder.LITTLE_ENDIAN).int.toFloat() / 100.toFloat() return Data(temperature, pressure) } } } companion object { private val TAG = MainActivity::class.java.simpleName private const val REQUEST_CODE_PERMISSIONS = 10 private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) /** BLEのサービスUUID */ private val BLE_SERVICE_UUID = UUID.fromString("133fe8d4-5197-4675-9d76-d9bbf2450bb4") /** BLEのCharacteristic UUID */ private val BLE_CHARACTERISTIC_UUID = UUID.fromString("0fc10cb8-0518-40dd-b5c3-c4637815de40") } }アプリの実行
アプリを実行すると、M5Stackから飛んできた温度・気圧のデータを受け取り、画面に温度と気圧を表示します。
BLEを使うアプリ開発での気にすべきこと
BLEを搭載していない端末のサポートをする
BLEを搭載していない端末のサポートが必要です。
本記事のアプリのようにBLEがない端末でインストールできないようにするか、BLEがない端末で動くように設計をする必要があります。
最近ではBLEがない端末は見なくなったので、BLEがない端末にはインストールできないようにするのもよいかもしれません。AndroidアプリでBLEを使用する場合はたくさんの端末でテストする
Android端末によってはBluetoothの挙動が異なっていることが結構あるので、可能な限りいろいろなOSバージョンと端末で確認することをおすすめします。(不特定多数の端末で使うアプリの場合は特に)
iOSアプリは、最新のiOSバージョンから1〜2つ前のバージョンをサポートしておけば市場的には充分で、かつ端末の種類もAndroidに比べれば少ないので、BLEを使うアプリを開発するならiOSの方が楽です。
AndroidのBLEのAPIはひどい
AndroidのBLEのAPIはひどいと評判です。実際書いててツラいものがあります。
BLEを使うAndroidアプリを開発する際は工数等の見積もりには注意しましょう。参照資料
M5Stack + BLE
PlatformIO
PlatformIO + M5Stack
Android + BLE
- 投稿日:2020-04-21T11:59:56+09:00
Flutter環境構築 - 4(VSCodeセットアップ編)
はじめに
今回はFlutter環境構築第4弾 「VSCodeセットアップ編」です!
Flutter SDKのインストール・Flutterコマンドのセットアップがお済みでない方は、
先に「Flutter SDKインストール編」をご覧ください。Flutter環境構築シリーズ
・Flutter環境構築 - 1(Flutter SDKインストール編)
・Flutter環境構築 - 2(Xcode・iOSシミュレーターセットアップ編)
・Flutter環境構築 - 3(Android Studio・Androidエミュレータセットアップ編)「VSCodeセットアップ編」でやること
本編では以下の項目を行います
- VSCodeのインストール
- VSCodeのセットアップ
VSCodeのインストール
1.VSCodeをインストールする
VSCode公式サイトへ行き、今すぐ無料でダウンロードするをクリックします。
ダウンロードが完了したら解凍し、VSCodeを起動します。
VSCodeのセットアップ
1.Flutterプラグインをインストールする
VSCodeが起動できたら、左タブのExtensionsをクリックします。
検索欄でFlutterと入力し、Flutterプラグインをインストールします。
2.Androidライセンスを承諾する
ターミナルにて以下を実行します。
flutter doctor —android-licensesyを複数回入力していき、ライセンス条項を承諾していきます。
3.flutter doctorを実行
ターミナルにて以下を実行します。
flutter doctor実行すると以下のように表示されます。
ここで、Connected device以外のものに全て✔︎(チェックマーク)が入っていたらセットアップ完了となります。
もし!(ビックリマーク)がついているものがありましたら、そこに記述されている内容通りに行い、再度flutter doctorを実行してください。
最後に
以上で「VSCodeセットアップ編」は終了となります。お疲れ様でした。
また、これにてFlutter環境構築シリーズは終了となります。
最後までご覧くださりありがとうございました。
Flutter環境構築シリーズ
・Flutter環境構築 - 1(Flutter SDKインストール編)
・Flutter環境構築 - 2(Xcode・iOSシミュレーターセットアップ編)
・Flutter環境構築 - 3(Android Studio・Androidエミュレータセットアップ編)参考サイト[Flutter公式サイト]
- 投稿日:2020-04-21T10:39:30+09:00
[Android]Google アカウントを利用して Firebase にサインインする
はじめに
Firebase には FireabaseUI Auth という予め用意された UI を使ってサインインする方法と Fireabase SDK を利用してサインインする 2 つの方法があります。今回は Fireabase SDK を利用して Firebase にサインインする方法を試してみます。また Firebase には Mail・Google・Twitter・Facebook などでサインインする方法があるのですが、色々試すのは大変なので Google を使ったサインインするようにしてみます。
準備
Firebase を利用するには Firebase プロジェクト、 Android プロジェクトの作成が必要になります。なので下記の手順でセットアップをしていきます。
Firebase のプロジェクトを作成する
次の手順で Firebase のプロジェクトを作成していきます。
- [Firebase] にサインインし、「プロジェクト」の作成を選択する。
- 「プロジェクト名」に任意の名称を入力して、「続行」を選択する。
- 「Googleアナリティクス」を選択し、「続行」を選択する
- 「Googleアナリティクスアカウント」を選択し、「続行」を選択する
- Firebase のコンソールに移動し、作成した Firebase プロジェクトを選択する。
作成したプロジェクトに次の設定を行い、 Android アプリから Firebase を利用できるようにします。
- Firebase のコンソールで Firebase プロジェクトを選択する。
- アプリを追加 を選択し、プラットフォーム で Android を選択する。
- Androidパッケージ名の入力が求められるので、Android プロジェクトのパッケージ名を入力する。
- デバッグ用の署名証明書を入力し、アプリを登録を選択する。(署名証明書の作成はこちら)
- 設定ファイルのダウンロード や Firebase SDK の追加 や
アプリを実行してインストールを確認 は後で確認するので、次へを選択する。- Firebase のコンソールに戻り、Authentication -> Sign-in methodを選択する
- Sign-in method で Google を有効にする。
- 最後に プロジェクトの概要 -> 作成したアプリ名称 設定アイコン -> google-service.json でファイルをダウンロードする。
Android プロジェクトを作成する
そして次の手順で Android Studio で Android プロジェクトを作成します。これで Android から Firebase が利用できるようになります。
- [Android Studio]Android プロジェクトを作成する。
- [Andriod Studio]app に google-service.json をコピーする
- [Android Studio]app の build.gradle で必要なライブラリを加える
buildscript { ext.kotlin_version = '1.3.71' repositories { google() // 追加する jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.6.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.3' // 追加する } } allprojects { repositories { google() // 追加する jcenter() } }apply plugin: 'com.google.gms.google-services' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // Firebase Authentication implementation 'com.firebaseui:firebase-ui-auth:6.2.1' // 追加 implementation 'com.google.firebase:firebase-auth:19.3.0' // 追加 // Kotlin Coroutine implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' // 必要に応じて追加 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5' // 必要に応じて追加 // KTX implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha01' // 必要に応じて追加 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha01' // 必要に応じて追加 // LiveEvent implementation "com.github.hadilq.liveevent:liveevent:1.0.1" // 必要に応じて追加 }実装
初期化する
次のコードで GoogleSignInClient と FirebaseAuth クラスのオブジェクトの初期化をします。GoogleSignInClient は Google アカウントへのサインインを制御するクラス、 FirebaseAuth は Firebase へのサインインを制御するクラスになります。これらの 2つのオブジェクトを利用して Google アカウントを利用した Firebase へのサインインを実装していきます。
class GoogleAuthController(private val activity: AppCompatActivity) { // FirebaseAuth の生成 private val firebaseAuth: FirebaseAuth by lazy { FirebaseAuth.getInstance() } // GoogleSignInClient の生成 private val googleSignInClient: GoogleSignInClient by lazy { GoogleSignIn.getClient(activity, googleSignInOptions) } // GoogleSignInClient のオプション、今回は特に設定が必要ないのでデフォルト値的なものを利用する private val googleSignInOptions: GoogleSignInOptions by lazy { val signInOption = GoogleSignInOptions.DEFAULT_SIGN_IN val idToken = activity.getString(R.string.default_web_client_id) GoogleSignInOptions.Builder(signInOption).requestIdToken(idToken).requestEmail().build() } }サインインする
まずはFirebaseへのサインインに利用する Google アカウントを選択する必要があります。アカウントの選択は GoogleSignInClient の signInIntent 経由で選択できるようになっているので signInIntent を取得して startActivityForResult にセットして Intent を起動してやります。
class GoogleAuthController(private val activity: AppCompatActivity) { fun startSignIn() { val signInIntent = googleSignInClient.signInIntent activity.startActivityForResult(signInIntent, RC_SIGN_IN) } companion object { private const val RC_SIGN_IN = 100 } }startActivityForResult で signInIntent を起動してやると次のような画面が出てくるので、表示された Google アカウントを選択して Firebase にサインインに利用するアカウントを決めてやります。
アカウントの選択が完了したら、onActivityResult で選択した Google のアカウント情報(GoogleSignInAccount) を 引数の data(Intent) から取得してやります。あとは取得した アカウント情報(GoogleSignInAccount) から credential を生成し、FirebathAuth . signInWithCredential に credential を渡して実行すればサインインできます。あとサインイン完了の通知を受け取りたいこともあると思います、その場合は firebaseAuth.signInWithCredential の戻り値の Task の addOnCompleteListener でリスナーを追加してやります。今回はサインイン完了の処理はあとからカスタマイズできるようにサインイン開始時に引数で関数を渡し、完了時に呼び出す形にしています。
class GoogleAuthController(private val activity: AppCompatActivity) { private var completed: (FirebaseUser) -> (Unit) = {} fun startSignIn(completed: (FirebaseUser) -> (Unit)) { this.completed = completed val signInIntent = googleSignInClient.signInIntent activity.startActivityForResult(signInIntent, RC_SIGN_IN) } fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { ︙ val credential = GoogleAuthProvider.getCredential(account.idToken, null) firebaseAuth.signInWithCredential(credential).addOnCompleteListener(activity) { task -> if (!task.isSuccessful) { return@addOnCompleteListener } if (firebaseAuth.currentUser == null) { return@addOnCompleteListener } completed(firebaseAuth.currentUser!!) } } }サインアウトする
サインアウトは簡単です、Google と Firebase 両方をサインアウトしてやれば良いです。
class GoogleAuthController(private val activity: AppCompatActivity) { fun startSignOut() { firebaseAuth.signOut() googleSignInClient.signOut() } }最終的なコード
最終的にこのようなコードになりました、あとはこいつを Activity で呼び出してやればサインイン・サインアウトができるようになります。
class GoogleAuthController(private val activity: AppCompatActivity) { private val firebaseAuth: FirebaseAuth by lazy { FirebaseAuth.getInstance() } private val googleSignInOptions: GoogleSignInOptions by lazy { val signInOption = GoogleSignInOptions.DEFAULT_SIGN_IN val idToken = activity.getString(R.string.default_web_client_id) GoogleSignInOptions.Builder(signInOption).requestIdToken(idToken).requestEmail().build() } private val googleSignInClient: GoogleSignInClient by lazy { GoogleSignIn.getClient(activity, googleSignInOptions) } private var completed: (FirebaseUser) -> (Unit) = {} val currentUser: FirebaseUser? get() = firebaseAuth.currentUser fun startSignIn(completed: (FirebaseUser) -> (Unit)) { this.completed = completed val signInIntent = googleSignInClient.signInIntent activity.startActivityForResult(signInIntent, RC_SIGN_IN) } fun startSignOut() { firebaseAuth.signOut() googleSignInClient.signOut() } fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode != RC_SIGN_IN) { return } val clazz = ApiException::class.java val account = GoogleSignIn.getSignedInAccountFromIntent(data).getResult(clazz) if (account == null) { return } val credential = GoogleAuthProvider.getCredential(account.idToken, null) firebaseAuth.signInWithCredential(credential).addOnCompleteListener(activity) { task -> if (!task.isSuccessful) { return@addOnCompleteListener } if (firebaseAuth.currentUser == null) { return@addOnCompleteListener } completed(firebaseAuth.currentUser!!) } } companion object { private const val RC_SIGN_IN = 100 } }動作確認
あと動作を確認するための UI を作ってやります。Button を押すとサインイン・サインアウト、サインインできたらユーザートークンを取得し更新するシンプルなUIです。
class MainActivity : AppCompatActivity() { private val coroutineScope = CoroutineScope(Dispatchers.IO) private val googleAuthController: GoogleAuthController by lazy { GoogleAuthController(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val userIdTextView = findViewById<TextView>(R.id.user_id_text_view) val signInButton = findViewById<Button>(R.id.sign_in_button) signInButton.setOnClickListener { googleAuthController.startSignIn { coroutineScope.launch { val token = Tasks.await(it.getIdToken(true)).token withContext(Dispatchers.Main) { userIdTextView.text = token } } } } val signOutButton = findViewById<Button>(R.id.sign_out_button) signOutButton.setOnClickListener { googleAuthController.startSignOut() userIdTextView.text = "" } coroutineScope.launch { val currentUser = googleAuthController.currentUser if (currentUser != null) { val token = Tasks.await(currentUser.getIdToken(true)).token withContext(Dispatchers.Main) { userIdTextView.text = token } } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) googleAuthController.onActivityResult(requestCode, resultCode, data) } }<?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" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/user_id_text_view" android:layout_width="0dp" android:layout_height="0dp"a android:gravity="center" android:textSize="16sp" app:layout_constraintBottom_toTopOf="@id/sign_in_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/sign_in_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Sign In" app:layout_constraintTop_toBottomOf="@id/user_id_text_view" app:layout_constraintBottom_toTopOf="@id/sign_out_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <Button android:id="@+id/sign_out_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Sign Out" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/sign_in_button" app:layout_constraintBottom_toBottomOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>サインインするとユーザートークンが表示され、
サインアウトするとユーザートークンが消えますね。
まとめ
Google アカウントを利用した Firebase へのサインインは Google アカウント情報を取得するために Intent の起動、 Intent の結果取得を自身で制御するので少しややこしいです。あと Google アカウントを利用したサインインは公式ドキュメントもあまり充実していないような気がするので、自分で少しずつ試していくしかなさそうです。
参考文献