- 投稿日:2019-12-03T19:30:50+09:00
zxing-android-embedded を使ってみる (Kotlinで)
はじめに
この前、授業成果物として簡易的なQRコード読み取りアプリケーションを制作したときに使ったライブラリについてメモを残しておきます。
今回使用したライブラリ
- ZXing Android Embedded
https://github.com/journeyapps/zxing-android-embedded
できること
簡単に QR 読み込み機能のアプリケーションが作成できます。
使ってみる
公式ドキュメントもあり書かれている通りに進めれば、動作できると思います。
あまり面倒な作業は必要なく、インポートするだけで実行可能です。Gradle ファイルにライブラリを記載する
build.gradle (app)
に以下の内容を追記します。repositories { jcenter() } dependencies { implementation 'com.journeyapps:zxing-android-embedded:4.0.0' implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'com.google.zxing:core:3.3.0' } android { buildToolsVersion '28.0.3' }ハードウェアアクセラレーションを有効にする
AndroidManifest.xml
にandroid:hardwareAccelerated="true"
を追記します。<application android:hardwareAccelerated="true" ... >QR 読み取り用のアクティビティを追記する
QR コード読み取り部分のアクティビティが必要なので、ライブラリに存在している QR コード読み取りアクティビティを
AndroidManifest.xml
に追記する。<activity android:name="com.journeyapps.barcodescanner.CaptureActivity" android:screenOrientation="fullSensor" tools:replace="screenOrientation" />コードを書く
基本的にコピペで OK です。
適当なサンプルを置いておくので参考にどうぞ!class QRReaderActivity : AppCompatActivity() { internal var qrScanIntegrator: IntentIntegrator? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_qrreader) qrScanIntegrator = IntentIntegrator(this) // 画面の回転をさせない (今回は縦画面に固定) qrScanIntegrator?.setOrientationLocked(false) // QR 読み取り後にビープ音がなるのを止める qrScanIntegrator?.setBeepEnabled(false) // スキャン開始 (QR アクティビティ生成) qrScanIntegrator?.initiateScan() } // 読み取り後に呼ばれるメソッド override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { // 結果の取得 val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { // result.contents で取得した値を参照できる Toast.makeText(this, result.contents, Toast.LENGTH_LONG).show() } else { super.onActivityResult(requestCode, resultCode, data) } } }これで QR コードを読み取るアプリを簡単に作成することができます!
まとめ
正直、自作するのだるいですからね。サボれるところはサボってしまいましょう。
今回は QR コードを読み込む最低限の機能しかないアプリケーションを作成しましたが、サンプルを見た感じだともっと細かく作ることもできそうだなあと思いました。楽に QR コードを読み込むアプリケーションを作成したい人は使ってみてはどうでしょうか
ありがとうございましたー ?
- 投稿日:2019-12-03T18:06:16+09:00
Androidアプリのデバッグ中に "Source code does not match the bytecode"というエラーが出る
- 投稿日:2019-12-03T17:02:45+09:00
Android 2.xなどで基本認証が通らないときのTIPS
話題:ベーシック認証のトラブル
Android 4.x以降では発生する頻度が下がったが、Android 2.x時代に頻発したトラブル。
基本認証で保護したページを、他の端末からは閲覧できるのに、
Android 2.xの端末からだけは閲覧できない何回id/pwを入力しても、幾度でも尋ね直してくる無間地獄。
解決方法
前提
URI ユーザ名 パスワード https://foo.net user password の場合
解決方法
https://user:password@foo.netこれだけ。
もしSSL未対応ならhttp://user:password@foo.netでも全然OK。
いったんこのURIでアクセスした後は既に認証が通っている状態になるらしく
https://foo.net http://foo.netとアドレス欄の内容を修正してから再アクセスしてもid/pwを尋ね直されることは、もう、ない。
注意点・リスク
- idとpwの文字列が(リクエストURIとして)インターネット上を流れていく、ということの危険性を理解し、割り切ったうえで使用せねばなりません。
- ひとたび
https://user:password@foo.net
でアクセスすると、サイト内の遷移リンク、たとえば<a href="directory">link</a>
を押下しても、https://user:password@foo.net/directory/
という具合にホスト名の前にuser:password
がズ~ッとついて来続けます(外れません)。
- つまり、その試験中のスクリーンショットを開発チーム内で共有したりすると、ついでにid/pwを喧伝しているようなことになります。危険。
*あとがき
ベーシック認証をURLに直接書く
を参照にしました。というかこのテクニック自体は他の場面で以前から利用していたけれど、 Android 2.xでのトラブルに効く、と教えてくれたのは仕事場の同僚のおかげです。感謝。
- 投稿日:2019-12-03T13:54:50+09:00
Android Data Binding(Codelabs for the 2019 Android Dev Summit: Mountain View, October 23-24, 2019.)
Android Data Bindingの日本語訳
1.はじめに
Data Binding Library
Data Binding LibraryはAndroid Jetpack libraryの一つであり、プログラムではなく宣言形式を使用して、XMLレイアウト内のUIコンポーネントをアプリのデータソースにバインドできます。
これにより、定型コードを削減できます。 このコードラボでは、次の方法を学習します。
- 既存のアプリでデータバインディングを設定する
- レイアウト式を使用
- 監視可能なデータオブジェクトの使用
- カスタムバインディングアダプターを作成
ビルドするもの
このコードラボでは、このアプリをデータバインディングに変換します。
このアプリには、いくつかの静的データといくつかの監視可能なデータを表示する1つの画面があり、データが変更されると、UIが自動的に更新されます。
データはViewModelによって提供されます。Model-View-ViewModelはデータバインディングで非常にうまく機能するプレゼンテーションレイヤーパターンです。以下に図を示します。
アーキテクチャコンポーネントライブラリのViewModelクラスにまだ慣れていない場合は、公式ドキュメントをご覧ください。要約すると、ビューにUIの状態(アクティビティ、フラグメントなど)を提供するクラスです。向きの変更に耐え、アプリの残りのレイヤーへのインターフェイスとして機能します。必要なもの
Android Studio 3.4以降
2.データバインディングなしでアプリを試す
この手順では、コードラボ全体のコードをダウンロードしてから、簡単なサンプルアプリを実行します。
$ git clone https://github.com/googlecodelabs/android-databinding
または、リポジトリをzipファイルとしてダウンロードできます:
- 1 Zipをダウンロード
- 2 コードを解凍します
- 3 Android Studio(バージョン3.4以降)でプロジェクトを開きます
![]()
プロジェクトが開いたら、ツールバーの
をクリックしてアプリを実行します。
ビルドが完了し、アプリがデバイスまたはエミュレーターにデプロイされると、デフォルトのアクティビティが開き、次のようになります。
この画面にはいくつかのデータが表示され、ユーザーはボタンをクリックしてカウンターを増分し、プログレスバーを更新できます。SimpleViewModelを使用します。それを開いて見てみましょう。
Ctrl + Nを使用して、Android Studioでクラスをすばやく見つけます。 Ctrl + Shift + Nを使用して、名前でファイルを見つけます。
Macでは、Command + Oでクラスを見つけ、Command + Shift + Oでファイルを見つけます。SimpleViewModelクラスは以下を公開します。
- "name"と"lastName"
- "likes"の数
- 人気のレベルを説明する値
SimpleViewModelを使用すると、ユーザーはonLike()メソッドで"likes"の数を増やすことができます。
最も興味深い機能はありませんが、この演習にはSimpleViewModelで十分です。
一方、PlainOldActivityクラスにあるUI実装には、いくつかの問題があります。
- findViewById()を複数回呼び出します。これは遅いだけでなく、コンパイル時にチェックされないため安全ではありません。 findViewById()に渡すIDが間違っていると、実行時にアプリがクラッシュします。
- onCreate()で初期値を設定します。 自動的に設定される適切なデフォルトを設定する方がはるかに良いでしょう
- XMLレイアウト宣言のButton要素で"android:onClick"属性を使用しますが、これも安全ではありません。onLike()メソッドがアクティビティに実装されていない(または名前が変更されている)場合、アプリは実行時にクラッシュします。
- たくさんのコードがあります。アクティビティとフラグメントは非常に急速に成長する傾向があるため、できるだけ多くのコードをアクティビティとフラグメントから移動することをお勧めします。また、アクティビティとフラグメントのコードはテストと保守が困難です。
Data Binding Libraryを使用すると、アクティビティからロジックを再利用可能かつテストしやすい場所に移動することにより、これらの問題をすべて修正できます。
3.データバインディングを有効にして、レイアウトを変換します
このプロジェクトでは既にデータバインディングが有効になっていますが、自分のプロジェクトで使用する場合、最初のステップは、それを使用するモジュールでライブラリを有効にすることです。
build.gradleandroid { ... dataBinding { enabled true } }次に、レイアウトをデータバインディングレイアウトに変換します。
通常のレイアウトをデータバインディングレイアウトに変換するには:
- <layout>タグでレイアウトをラップします
- レイアウト変数を追加する(オプション)
- レイアウト式を追加する(オプション)
plain_activity.xmlを開きます。これは、ConstraintLayoutをルート要素とする通常のレイアウトです。
レイアウトをデータバインディングに変換するには、ルート要素を<layout>タグでラップする必要があります。また、名前空間定義(xmlns:で始まる属性)を新しいルート要素に移動する必要があります。
レイアウトは次のようになります。plain_activity.xml<layout 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"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView ...<data>タグにはレイアウト変数が含まれます。
レイアウト変数は、レイアウト式を記述するために使用されます。レイアウト式は要素属性の値に配置され、@ {expression}形式を使用します。 ここではいくつかの例を示します。// 複雑なレイアウト式のいくつかの例 android:text="@{String.valueOf(index + 1)}" android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}'レイアウト式言語は多くの機能を提供しますが、ビュー内に複雑なロジックをネストしないようにすることが最善です。 複雑な式は、レイアウトの読み取りと保守を難しくします。
レイアウト式を使用してレイアウトファイル内のコンポーネントをバインドすることにより、次のことができます。
- アプリのパフォーマンスを改善する
- メモリリークとNULLポインタ例外を防ぐのに役立ちます
- UIフレームワークの呼び出しを削除して、アクティビティのコードを合理化しますここではいくつかの例を示します。
// viewmodelのnameプロパティをtext属性にバインドします android:text="@{viewmodel.name}" // viewmodelのnameVisibleプロパティをvisibility属性にバインドします android:visibility="@{viewmodel.nameVisible}" // ビューがクリックされたときに、viewmodelのonLike()メソッドを呼び出します android:onClick="@{() -> viewmodel.onLike()}"ここで言語の完全な説明をご覧ください。
それでは、いくつかのデータをバインドしましょう!4.最初のレイアウト式を作成します
とりあえず、いくつかの静的データバインディングから始めましょう。
- <data>タグ内に2つの文字列レイアウト変数を作成します。<data> <variable name="name" type="String"/> <variable name="lastName" type="String"/> </data>
- "android:id"がplain_nameのTextViewを探し、"android:text"属性にレイアウト式を追加します
<TextView android:id="@+id/plain_name" android:text="@{name}" ... />レイアウト式は@記号で始まり、中括弧{}で囲まれます。
名前は文字列であるため、データバインディングはTextViewでその値を設定する方法を認識します。 後で、さまざまなレイアウト式のタイプと属性を処理する方法を学習します。
- "android:id"がplain_lastNameのTextViewでも同じことを行います。
<TextView android:id="@+id/plain_lastname" android:text="@{lastName}" ... />これらの操作の結果は、plain_activity_solution_2.xmlにあります。
ここで、アクティビティを変更して、データバインディングレイアウトを正しく拡張する必要があります。5.インフレーションを変更し、アクティビティからUI呼び出しを削除します
レイアウトの準備はできていますが、アクティビティのいくつかの変更が必要です。 PlainOldActivityを開きます。
データバインディングレイアウトを使用しているため、インフレーションは別の方法で行われます。
onCreateで、以下を置き換えます。setContentView(R.layout.plain_activity)を
val binding : PlainActivityBinding = DataBindingUtil.setContentView(this, R.layout.plain_activity)この変数の目的は何ですか? <data>ブロックで宣言したレイアウト変数を設定するために必要になります。 バインディングクラスは、ライブラリによって自動的に生成されます。
生成されたクラスがどのように見えるかを確認するには、PlainActivitySolutionBindingを開いて、見て回ってください。
- これで、変数値を設定できます。
binding.name = "Your name" binding.lastName = "Your last name"以上です。 ライブラリを使用してデータをバインドしました。
古いコードの削除を開始できます
- updateName()メソッドを削除します。これは、新しいデータバインディングコードがIDを見つけてテキスト値を設定しているためです。
- onCreate()のupdateName()呼び出しを削除します。
これらの操作の結果は、PlainOldActivitySolution2で確認できます。
これでアプリを実行できます。 Adaの名前があなたの名前に置き換わっていることがわかります。6.ユーザーイベントの処理
これまで、ユーザーにデータを表示する方法を学習しましたが、データバインディングライブラリを使用して、ユーザーイベントを処理し、レイアウト変数でアクションを呼び出すこともできます。
イベント処理コードを変更する前に、レイアウトを少しクリーンアップできます。
- 最初に、単一のViewModelの2つの変数を置き換えます。これは、プレゼンテーションのコードと状態を1か所に保持するため、ほとんどの場合に使用する方法です。
plain_activity.xml<data> <variable name="viewmodel" type="com.example.android.databinding.basicsample.data.SimpleViewModel"/> </data>変数に直接アクセスする代わりに、viewmodelプロパティを呼び出します。
- 両方のTextViewのレイアウト式を変更します。
plain_activity.xml<TextView android:id="@+id/plain_name" android:text="@{viewmodel.name}" ... /> <TextView android:id="@+id/plain_lastname" android:text="@{viewmodel.lastName}" ... />また、「Like」ボタンのクリックの処理方法を更新します。
- "android:id"がlike_buttonのボタンを探して置き換えます
plain_activity.xmlandroid:onClick="onLike"を
plain_activity.xmlandroid:onClick="@{() -> viewmodel.onLike()}"前のonClick属性は、ビューがクリックされたときにアクティビティまたはフラグメントのonLike()メソッドが呼び出される安全でないメカニズムを使用していました。その正確な署名を持つメソッドが存在しない場合、アプリはクラッシュします。
新しい方法は、コンパイル時にチェックされ、ラムダ式を使用してビューモデルのonLike()メソッドを呼び出すため、はるかに安全です。Android Studioの[ビルド]メニューで[プロジェクトの作成]をクリックして、データバインディングエラーを確認します。 問題が発生すると、アプリのビルドが妨げられ、ビルドログにエラーが表示されます。
これらの操作の結果は、plain_activity_solution_3.xmlにあります。
次に、不要なものをアクティビティから削除します。
1.リプレイス
binding.name = "Your name" binding.lastName = "Your last name"を
binding.viewmodel = viewModel2.バイパスされているため、アクティビティのonLike()メソッドを削除します。
これらの操作の結果は、PlainOldActivitySolution3にあります。
アプリを実行すると、ボタンは何もしないことがわかります。 これは、updateLikes()をもう呼び出していないためです。 次のセクションでは、それを適切に実装する方法を学びます。7.データの監視
前のステップで、静的バインディングを作成しました。
ViewModelを開くと、nameとlastNameが単なる文字列であることがわかりますが、これらは変更されないため問題ありません。 ただし、LIKEはユーザーが変更します。var likes = 0この値が変更されたときにUIを明示的に更新する代わりに、監視可能にします。
データバインディングを使用すると、監視可能な値が変更されると、バインドされているUI要素が自動的に更新されます。
可観測性を実装する方法は複数あります。監視可能なクラス、監視可能なフィールド、または優先的な方法であるLiveDataを使用できます。 その完全なドキュメントはこちらです。
ObservableFieldsはよりシンプルなので使用します。
リプレイス
val name = "Grace" val lastName = "Hopper" var likes = 0 private set // This is to prevent external modification of the variable.新しいLiveDatasの場合
private val _name = MutableLiveData("Ada") private val _lastName = MutableLiveData("Lovelace") private val _likes = MutableLiveData(0) val name: LiveData<String> = _name val lastName: LiveData<String> = _lastName val likes: LiveData<Int> = _likes次もリプレイス
fun onLike() { likes++ } /** * Returns popularity in buckets: [Popularity.NORMAL], * [Popularity.POPULAR] or [Popularity.STAR] */ val popularity: Popularity get() { return when { likes > 9 -> Popularity.STAR likes > 4 -> Popularity.POPULAR else -> Popularity.NORMAL } }を
// popularity is exposed as LiveData using a Transformation instead of a @Bindable property. val popularity: LiveData<Popularity> = Transformations.map(_likes) { when { it > 9 -> Popularity.STAR it > 4 -> Popularity.POPULAR else -> Popularity.NORMAL } } fun onLike() { _likes.value = (_likes.value ?: 0) + 1 }ご覧のとおり、LiveDataの値はvalueプロパティで設定されており、変換を使用して1つのLiveDataを別のLiveDataに依存させることができます。 このメカニズムにより、ライブラリは値が変更されたときにUIを更新できます。
LiveDataはライフサイクルを認識するオブザーバブルなので、使用するライフサイクル所有者を指定する必要があります。 これはバインディングオブジェクトで行います。PlainOldActivityを開き(PlainOldActivitySolution3のように見えるはずです)、バインディングオブジェクトでライフサイクルの所有者を設定します。
binding.lifecycleOwner = thisプロジェクトをリビルドすると、アクティビティがコンパイルされていないことがわかります。 アクティビティから「likes」に直接アクセスしていますが、これはもう必要ありません。
private fun updateLikes() { findViewById<TextView>(R.id.likes).text = viewModel.likes.toString() findViewById<ProgressBar>(R.id.progressBar).progress = (viewModel.likes * 100 / 5).coerceAtMost(100) ...PlainOldActivityを開き、アクティビティとその呼び出しのすべてのプライベートメソッドを削除します。 これで、アクティビティは簡単になりました。
class PlainOldActivity : AppCompatActivity() { // Obtain ViewModel from ViewModelProviders private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding : PlainActivityBinding = DataBindingUtil.setContentView(this, R.layout.plain_activity) binding.lifecycleOwner = this binding.viewmodel = viewModel } }SolutionActivityでこれらの操作の結果を見つけることができます。
一般に、アクティビティからコードを移動することは、保守性とテスト容易性に優れています。
LIKEの数を示すTextViewを監視可能な整数にバインドしましょう。 plain_activity.xmlで<TextView android:id="@+id/likes" android:text="@{Integer.toString(viewmodel.likes)}" ...ここでアプリを実行すると、LIKEの数が予想どおりに増加します。
これまでに行われたことを要約しましょう。
1. "name"と"last name"は、ビューモデルから文字列として公開されます。
2. ボタンのonClick属性は、ラムダ式を介してビューモデルにバインドされます。
3. LIKEの数は、監視可能な整数を介してビューモデルから公開され、テキストビューにバインドされるため、変更時に自動的に更新されます。これまで、"android:onClick"や"android:text"などの属性を使用してきました。 次のセクションでは、他のプロパティについて学習し、独自のプロパティを作成します。
8.バインディングアダプターを使用してカスタム属性を作成する
文字列(または監視可能な文字列)を"android:text"属性にバインドすると、何が起こるのかは明らかですが、どのように起こっていますか?
Data Binding Libraryを使用すると、ほとんどすべてのUI呼び出しは、バインディングアダプターと呼ばれる静的メソッドで実行されます。
ライブラリには、大量のバインディングアダプタが用意されています。 こちらをご覧ください。 次に、android:text属性の例を示します。@BindingAdapter("android:text") public static void setText(TextView view, CharSequence text) { // Some checks removed for clarity view.setText(text); }またはandroid:background one
@BindingAdapter("android:background") public static void setBackground(View view, Drawable drawable) { if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { view.setBackground(drawable); } else { view.setBackgroundDrawable(drawable); } }データバインディングには魔法はありません。 コンパイル時にすべてが解決され、生成されたコードを読むことができます。
進行状況バーで作業しましょう。 私たちはそれをしたい:
- LIKEがなければ見えない
- 5つのLIKEでいっぱいになる
- いっぱいになったら色を変更
このためのカスタムバインディングアダプターを作成します。
utilsパッケージのBindingAdapters.ktファイルを開きます。それらをどこで作成しても、ライブラリはそれらを見つけます。Kotlinでは、Kotlinファイルの最上位に関数を追加するか、クラスの拡張関数として静的メソッドを作成できます。最初の要件、hideIfZeroのバインディングアダプターを探します。
@BindingAdapter("app:hideIfZero") fun hideIfZero(view: View, number: Int) { view.visibility = if (number == 0) View.GONE else View.VISIBLE }このバインディングアダプタ
- "app:hideIfZero"属性に適用されます。
- すべてのビューに適用できます(最初のパラメーターはビューであるため、このタイプを変更することで特定のクラスに制限できます)
- レイアウト式が返すものでなければならない整数を取ります。
- 数値がゼロの場合、View GONEを作成します。 それ以外の場合は可視。plain_activityレイアウトで、進行状況バーを探し、hideIfZero属性を追加します。
<ProgressBar android:id="@+id/progressBar" app:hideIfZero="@{viewmodel.likes}" ...アプリを実行すると、ボタンを初めてクリックしたときにプログレスバーが表示されます。 ただし、値と色を変更する必要があります。
これらの手順の結果は、plain_activity_solution_4.xmlにあります。
9.複数のパラメーターを持つバインディングアダプターを作成する
進捗値については、最大値とLIKEの数をとるバインディングアダプターを使用します。BindingAdaptersファイルを開き、これを探します。
/** * Sets the value of the progress bar so that 5 likes will fill it up. * * Showcases Binding Adapters with multiple attributes. Note that this adapter is called * whenever any of the attribute changes. */ @BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true) fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) { progressBar.progress = (likes * max / 5).coerceAtMost(max) }属性のいずれかが欠落している場合、このバインディングアダプタは使用されません。 これはコンパイル時に発生します。 このメソッドは、3つのパラメーター(適用されるビューに加えて、アノテーションで定義された属性の数)を取ります。
requireAllパラメーターは、バインディングアダプターを使用するタイミングを定義します
- trueの場合、すべての要素がXML定義に存在する必要があります。
- falseの場合、欠落している属性はnull、ブール値の場合はfalse、プリミティブの場合は0になります。
次に、属性をXMLに追加します。
<ProgressBar android:id="@+id/progressBar" app:hideIfZero="@{viewmodel.likes}" app:progressScaled="@{viewmodel.likes}" android:max="@{100}" ...progressScaled属性をLIKEの数にバインドし、リテラル整数をmax属性に渡しているだけです。 @{}形式を追加しないと、データバインディングは正しいバインディングアダプターを見つけることができません。
これらの手順の結果は、plain_activity_solution_5.xmlにあります。
アプリを実行すると、プログレスバーが期待どおりにいっぱいになる様子がわかります。10.バインディングアダプタの作成の練習
習うより慣れよう。作成
- LIKEの値に応じてプログレスバーの色を調整し、対応する属性を追加するバインディングアダプター
- 人気度に応じて異なるアイコンを表示するバインディングアダプタ
- 黒のic_person_black_96dp
- 薄いピンクのic_whatshot_black_96dp
- 濃いピンクのic_whatshot_black_96dp
ソリューションは、BindingAdapters.ktファイル、SolutionActivityファイル、およびsolution.xmlレイアウトにあります。
11.きっと成功します!
おめでとうございます!コードラボを完了したので、データバインディングレイアウトの作成方法、それに変数と式を追加する方法、監視可能なデータを使用し、カスタムバインディングアダプターを介してカスタム属性でXMLレイアウトをより意味のあるものにします。
次に、より高度な使用法のサンプルと全体像のドキュメントを確認してください。
- 投稿日:2019-12-03T13:50:58+09:00
[Android] 加速度センサのサンプリング周波数が知りたいとき
端末依存で最大・最低・間隔が決まるので,確認用のコードを書いた。
メモです。Logcatに周期が[ns]で出力される
package com.example.getrotationtest import android.content.Context import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.opengl.Matrix import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import kotlinx.android.synthetic.main.activity_main.* import java.io.BufferedReader import java.io.File import java.io.InputStreamReader import java.math.BigDecimal import java.util.* import kotlin.properties.Delegates import kotlin.io.* import kotlin.math.PI class MainActivity : AppCompatActivity(), SensorEventListener { private var mManager: SensorManager by Delegates.notNull<SensorManager>() private var mSensor: Sensor by Delegates.notNull<Sensor>() private var maSensor: Sensor by Delegates.notNull<Sensor>() private var mgSensor: Sensor by Delegates.notNull<Sensor>() private var prevtime = 0L override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main); mManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager mSensor = mManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) maSensor = mManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) mgSensor = mManager.getDefaultSensor(Sensor.TYPE_GRAVITY) } override fun onSensorChanged(event: SensorEvent) { when (event.sensor.type) { Sensor.TYPE_ACCELEROMETER -> { Log.d("tag", (event.timestamp - prevtime).toString()) prevtime = event.timestamp } } } //センサー精度が変更されたときに発生するイベント override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { } //アクティビティが閉じられたときにリスナーを解除する override fun onPause() { super.onPause() //リスナーを解除しないとバックグラウンドにいるとき常にコールバックされ続ける mManager.unregisterListener(this) } override fun onResume() { super.onResume() //リスナーとセンサーオブジェクトを渡す //第一引数はインターフェースを継承したクラス、今回はthis //第二引数は取得したセンサーオブジェクト //第三引数は更新頻度 SensorManagerから選択するか,usで指定する mManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_FASTEST) mManager.registerListener(this, maSensor, 238000) // 加速度のセンサ mManager.registerListener(this, mgSensor, SensorManager.SENSOR_DELAY_FASTEST) } }
- 投稿日:2019-12-03T13:15:06+09:00
[ Android ] 戻るボタン無効化
Androidで端末の戻るボタンを無効にする方法について述べる。
方法
とても簡単でActivityクラスのonBackPressed()をオーバライドし、処理を書かなければ完了である。
@Override public void onBackPressed() { }参考サイト
以下のサイトがとても参考になった。
https://minpro.net/onbackpressed-android
- 投稿日:2019-12-03T12:59:31+09:00
【Android/Kotlin】円を拡大するアニメーションの実装方法
基本画面
- FrameLayoutをmatch/matchで設定しています。
- 円のカスタムビューをvisible=GONEで設定しています。
- 星のImageViewを2種類おいています。
- 星をクリックすると、円が拡大描画されていきます。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <FrameLayout android:id="@+id/back_color" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center"> <com.kiyota.scaleanimationapp.CircleCustomView android:id="@+id/main_circle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone"></com.kiyota.scaleanimationapp.CircleCustomView> <ImageView android:id="@+id/star_pink" android:layout_width="50dp" android:layout_height="50dp" android:layout_gravity="center" android:src="@drawable/ic_star_pink"></ImageView> <ImageView android:id="@+id/star_blue" android:layout_width="50dp" android:layout_height="50dp" android:layout_gravity="center" android:layout_marginTop="100dp" android:src="@drawable/ic_star"></ImageView> </FrameLayout> </androidx.appcompat.widget.LinearLayoutCompat>CircleCustomView.ktclass CircleCustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { /** * サークルの色 */ private val paint = Paint() /** * サークルの座標位置 */ private val rect = RectF() /** * サークルの円周の長さ */ private var angle = 360F /** * 中心座標(x) */ private var x: Int = 0 /** * 中心座標(y) */ private var y: Int = 0 /** * 初回フラグ */ var isInit = true /** * コンパニオンオブジェクト */ companion object { /** * サークルの開始位置 */ private const val angleTarget = 0F } /** * コンストラクタ */ init { // サークルの幅 this.paint.isAntiAlias = true this.paint.style = Paint.Style.FILL_AND_STROKE // サークルの色 this.paint.color = (context.resources.getColor(R.color.star_color)) } /** * ビュー描画メソッド * * @param canvas 描画領域 */ @SuppressLint("CanvasSize") public override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // 初回は中心スタート if (isInit) { // サークル開始地点(中央揃え) this.x = canvas.width / 2 this.y = canvas.height / 2 isInit = false } // サークルの背景色(透明) canvas.drawColor(this.context.resources.getColor(R.color.color_back_circle)) // サークルの半径()大きさ var radius = 40 val left = this.x - radius val top = this.y - radius val right = this.x + radius val bottom = this.y + radius // サークルの領域設定 this.rect.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) // サークルの描画 canvas.drawArc(this.rect, angleTarget, this.angle, false, this.paint) } /** * 中心座標変更メソッド */ fun changeCenter(x: Int, y: Int) { // サークルの中心座標 this.x = x this.y = y } /** * サークル色変更メソッド * * @param index 押されたボタンの番号 */ fun changeColor(index: Int) { when (index) { 0 -> { this.paint.color = context.resources.getColor(R.color.colorAccent) } 1 -> { this.paint.color = context.resources.getColor(R.color.star_color) } else -> { this.paint.color = context.resources.getColor(R.color.colorPrimary) } } } }画面の中心で円を拡大する場合(ピンク)
MainActivity.ktpackage com.kiyota.scaleanimationapp class MainActivity : AppCompatActivity(), Animation.AnimationListener { private var index = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // クリックされたらアニメーションをスタートする star_pink.setOnClickListener { index = 0 // 端末(Nexus5x)の中心座標が(540,792)という意味です startScalingXml(540, 792, index) } star_blue.setOnClickListener { index = 1 startScalingXml(540, 792, index) } } /** * サークル拡大アニメーションメソッド * * @param x x座標 * @param y y座標 * @param index クリックされたビューの順番 */ private fun startScalingXml(x: Int, y: Int, index: Int) { main_circle.changeCenter(x, y) main_circle.changeColor(index) var scaleAnimation = ScaleAnimation( 1.0f, 30.0f, 1.0f, 30.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ) scaleAnimation.duration = 20000 scaleAnimation.repeatCount = 0 scaleAnimation.fillAfter = true main_circle.visibility = VISIBLE scaleAnimation.setAnimationListener(this) main_circle.startAnimation(scaleAnimation) } override fun onAnimationRepeat(p0: Animation?) { } /** * アニメーション終了時処理メソッド * * @param p0 アニメーション */ override fun onAnimationEnd(p0: Animation?) { setColorByIndex(index) main_circle.visibility = GONE } override fun onAnimationStart(p0: Animation?) { } /** * 背景色切り替えメソッド * * @param index クリックされたビューの順番 */ private fun setColorByIndex(index: Int) { when (index) { 0 -> { back_color.setBackgroundColor(this.resources.getColor(R.color.colorAccent)) } 1 -> { back_color.setBackgroundColor(this.resources.getColor(R.color.star_color)) } else -> { back_color.setBackgroundColor(this.resources.getColor(R.color.colorPrimary)) } } } }画面の左側で円を拡大する場合(水色) :失敗
- 中心座標(270 , 792)
- pivot(0.5f , 0.5f)
画面の左側で円を拡大する場合(水色) :成功
- 投稿日:2019-12-03T12:55:12+09:00
Material Components時代のAndroid Themeを改めて理解する
AndroidのMaterial Componentsを利用する場合、
theme.xml<style name="Theme.App" parent="Theme.MaterialComponents.Light"> <!-- ... --> </style>上記のように、
Theme.MaterialComponents.Light
などを継承させてthemeを定義しているかと思います。
Dark Themeに対応する場合はTheme.MaterialComponents.DayNight
などを継承させているでしょう。世はまさにDark Theme時代と言っても過言ではなくなってきています。
今一度、Androidにおけるtheme/styleとは何かを簡単に整理していこうという記事になります。Material ComponentsのTheming
デフォルトのThemeはどうなっているのか
Android Studioからプロジェクトを作ると、
colorPrimary
などのThemeが既に定義されています。
いきなりカスタムするのではなく、まずは素っ裸のTheme.MaterialComponents.DayNight
を見てみましょう。theme.xml<?xml version="1.0" encoding="utf-8"?> <resources> <style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar"/> <style name="ThemeStudy" parent="Base.ThemeStudy"/> </resources>このように定義し、わかりやすいようにいくつかコンポーネントを置いてみるとこのようになります。
(レイアウトファイルの方でもtheme/styleやattributesの定義は全てなくしています。)
なんとなく
colorPrimary
が紫でcolorSecondary
が青緑かな〜というのがわかりますね。
はてさてこの色はどこに定義されているのでしょう?⌘を押しながらクリック(あるいはショートカット)で定義元を探ってみます
values.xml<!-- 継承しているTheme.MaterialComponents.DayNight.NoActionBarからスタート --> <style name="Theme.MaterialComponents.DayNight.NoActionBar" parent="Theme.MaterialComponents.Light.NoActionBar"/> ↓ <style name="Theme.MaterialComponents.Light.NoActionBar"> ↓ <style name="Theme.MaterialComponents.Light" parent="Base.Theme.MaterialComponents.Light"/> ↓ <style name="Base.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light"/> ↓ <style name="Base.V14.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light.Bridge"> <!-- ... --> <!-- Colors --> <item name="colorPrimary">@color/design_default_color_primary</item> <!-- !!!! --> <item name="colorPrimaryDark">@color/design_default_color_primary_dark</item> <item name="colorAccent">?attr/colorSecondary</item> <!-- ... --> </style>なかなか深いところにいましたね。
colorPrimary
はどうやら#6200EE
のようです。これらのデフォルトカラーはmaterial.ioのColor Themingにも記載されています。
ここを見るとわかるのですが、Material ComponentではAppCompatとは違ったテーマ属性が使われます。というわけで、次は色について簡単に説明していきます。
Material Componentsのカラー属性(テーマ属性)
マテリアルデザインではベースラインとして使える色の属性が定義されていて
基本的にはこれを用いて自分のアプリに合うようカスタマイズしていくことになります。
便宜上、テーマ属性における色に関する属性をカラー属性と呼びます。
ref: https://material.io/design/color/the-color-system.html#color-theme-creation
各色についてはUsing The Color Theming Systemに記載されているので割愛します。
Material Components for Androidでは、
Widget.MaterialComponents.~
のスタイルをデフォルトで使うよう定義されています。
またこのスタイルではテーマ属性を参照して構成されています。
そのため、このベーステーマを継承させ、オーバーライドすることで簡単に各コンポーネントの色を変えることができるのです。カラー属性を変えてみる
なんとなくどのカラー属性が、どのコンポーネントのスタイルから参照されているかが掴めるかと思います。
しかし、TextInputLayout
の背景色のように、これはどのカラー属性だ?というものがいくつか出てきますね。前述したように、各コンポーネントはテーマ属性を参照してスタイルが定義されています。次はその定義を見てみましょう。
Material ComponentsのWidget Style
Theme.MaterialComponents.DayNight
などのテーマでは、テーマ属性で各コンポーネントのスタイルを指定しています。例として、
TextInputLayout
のスタイルを見てみましょう。themeの継承をたどっていくと、textInputStyle
というテーマ属性が定義されています。values.xml<style name="Base.V14.Theme.MaterialComponents.Light.Bridge" parent="Platform.MaterialComponents.Light"> <!-- ... --> <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.FilledBox</item> <!-- ... --> </style>その中身を見ると、どうやら背景色は
@color/mtrl_filled_background_color
に定義されているということがわかります。values.xml<style name="Widget.MaterialComponents.TextInputLayout.FilledBox" parent="Base.Widget.MaterialComponents.TextInputLayout"> <!-- ... --> <item name="boxBackgroundColor">@color/mtrl_filled_background_color</item> <!-- ... --> <item name="boxStrokeColor">@color/mtrl_filled_stroke_color</item> <!-- ... --> </style>
mtrl_filled_background_color
では、Viewの状態に応じて透明度は変わるものの、colorOnSurface
を使っていることがわかりました!
先程の図で、うすく赤い色になっていたのはcolorOnSurface
にアルファがかかっていたからなのです。
(黒みがかっているのは内部的にsurfaceレイヤーがあり、colorSurface
の色が見えているからです。)mtrl_filled_background_color.xml<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:alpha="0.16" android:color="?attr/colorOnSurface" android:state_hovered="true"/> <item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_focused="true"/> <item android:alpha="0.04" android:color="?attr/colorOnSurface" android:state_enabled="false"/> <item android:alpha="0.12" android:color="?attr/colorOnSurface"/> </selector>同様に、
boxStrokeColor
は@color/mtrl_filled_stroke_color
に定義がされており、TextInputLayout
にフォーカスがあたったときに色が変わる謎も解けますmtrl_filled_stroke_color.xml<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="?attr/colorPrimary" android:state_focused="true"/> <!-- 4% overlay over 42% colorOnSurface --> <item android:alpha="0.46" android:color="?attr/colorOnSurface" android:state_hovered="true"/> <item android:alpha="0.38" android:color="?attr/colorOnSurface" android:state_enabled="false"/> <item android:alpha="0.42" android:color="?attr/colorOnSurface"/> </selector>このように、テーマ属性を適切に定義することで、マテリアルコンポーネントではアプリ全体に統一感のあるデザインを提供しています。
テーマ属性の参照
「テーマ属性を参照しているので」 と説明もなく言っていましたが、これはどうやるのでしょう?
私自身、今回ちゃんとThemeと向き合うまで、なにげなく使っていたけど知りませんでした。すでに説明したコードにも出てきていますが、
?attr/colorPrimary
と書くことで、現在のテーマ属性を参照できます。
この場合は、現在のテーマにおけるcolorPrimary
を参照しています。color
タグで定義したものではないことに注意です。
現在のというのが肝です。
テーマ属性の参照を使ってコンポーネントの色を変える
例えば、ユーザのステータスに応じて
colorPrimary
を変えたいと言った場合を考えてみましょう。単純に思いつくのは、プログラムで動的に色をコンポーネントに指定してあるげる方法です。
毎回Activity/Fragmentで処理を書くのはつらいのでBindingAdapter
を作ってよしなに色を変えたりもできそう。こういった場合、コンポーネントごとに色を変える処理を書くのは難儀なものです。
この場合、themeをまるまる変えてしまうとよいでしょう。
colorPrimary
/colorSecondary
など、必要な分だけ定義を上書きましょう。theme.xml<resources> <style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorSecondary">@color/colorSecondary</item> </style> <style name="ThemeStudy" parent="Base.ThemeStudy" /> <style name="ThemeStudy.Premium"> <item name="colorPrimary">#f44336</item> <item name="colorSecondary">#ffc107</item> </style> <style name="ThemeStudy.Etc"> <item name="colorPrimary">#36f443</item> <item name="colorSecondary">#07ffc1</item> </style> </resources>あとはthemeを条件に応じて指定してあげるだけです。
MainActivity.ktclass MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 条件に応じてテーマを設定 setTheme(if (isPremium()) R.style.ThemeStudy_Premium else R.style.ThemeStudy) ... } ... }画面単位や、部分的な適用の場合はレイアウトで指定してあげると良いでしょう。
これに関しては公式のMaterial DesignサンプルのOwlが上手に使い分けているので参考になります。ThemeとStyleの違い
Styleは1つのViewのみに適用され、Themeは適用したViewの子Viewにまで適用されるという違いがあります。
また、Themeはコンテキストに紐づきます。ActivityでThemeを設定していれば画面全体に適用されます。
設定していなければApplicationコンテキストで設定されたThemeが適用されます。
ThemeOverlayとは
ThemeOverlay自体はただの命名規則です。Style/Themeの継承を利用し、この命名規則も納得の動きをします。
<style>
タグではparent
を指定しThemeを継承、そして<item>
タグでテーマ属性などを定義し自分のThemeでオーバーライドすることが多いかと思います。
例えば、colorPrimary
を定義すると、継承元のcolorPrimary
を上書きしますね。theme.xml<style name="Base.ThemeStudy" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorSecondary">@color/colorSecondary</item> </style>このとき、
parent
を指定せずにcolorPrimary
などのテーマ属性を定義すると、その属性のみが上書きされ、定義されていないテーマ属性はそのViewにすでに適用されているThemeのものが使われるのです。(親のViewのThemeやApplication/Activity/Fragmentなどで適用されているTheme)
この例では、便宜上
Custom
というスタイルを定義していますが、命名規則を守り、ThemeOverlay.AppName.Red
のように定義すると管理がしやすいでしょう。
TextInputLayout
はViewの属性としてboxBackgroundColor
やboxStrokeColor
を持っているので、それを用いて色を変えられるには変えられます。
しかし、Material Componentsではテーマ属性を用いて丁寧にスタイルが定義されています。都度都度、ViewごとにViewの属性を設定していると管理が煩雑になっていまします。
ThemeOverlayの考え方を使って、ViewあるいはViewGroupに、colorPrimary
などのテーマ属性を適切に設定し統一感を守っていきましょう。ThemeOverlayの使い所
なぜThemeOverlayを使うかというと、必要なテーマ属性だけ上書きできるという利点が大きいのではないかと思います。
公式サンプルのOwlを見てみると非常にわかりやすいです。
Base.Owl
ではtextAppearanceHeadline1
やbottomNavigationStyle
といったアプリ全体として共通のテーマ属性を定義しています。
そしてcolorPrimary
などの色に関するテーマ属性は、Owl.Yellow
/Owl.Blue
それぞれで定義されています。theme.xml<!-- ... --> <style name="Owl" parent="@style/Base.Owl"/> <style name="Owl.Yellow"> <item name="colorPrimary">@color/owl_yellow_500</item> <item name="colorPrimaryVariant">@color/owl_yellow_400</item> <item name="colorSecondary">@color/owl_blue_700</item> <item name="colorSecondaryVariant">@color/owl_blue_800</item> </style> <style name="Owl.Blue"> <item name="colorPrimary">@color/owl_blue_700</item> <item name="colorPrimaryVariant">@color/owl_blue_800</item> <item name="colorSecondary">@color/owl_yellow_500</item> <item name="colorSecondaryVariant">@color/owl_yellow_400</item> </style> <!-- ... -->こうすることで変更にも強くなり見通しも良いですね。
(Owlでは
ThemeOverlay.MaterialComponents.Light
を継承したThemeOverlay.Owl.Blue
がありますが、Theme.MaterialComponents.Light
との差分までは面倒なので追えてないです)よく見る
ThemeOverlay
でいうと、プロジェクトを作るとAppBarLayout
にデフォルトで定義されているThemeOverlay.Material.Dark.ActionBar
(あるいはThemeOverlay.Material.Light.ActionBar
)があります。
おなじみChris BanesさんのTheme vs Styleによると、colorControlNormal
をandroid:textColorPrimary
にしているだけ、とあります。(実際にコードを追うとcolorControlNormal
が?android:attr/textColorPrimary
と定義されています。)
これによって、AppBarLayout
のタイトルテキストと、メニューなどのアイコンの色を揃えているというわけです。(メニューなどの色はcolorControlNormal
によって決まる)まとめ
- Material Componentsはテーマ属性によってThemingされている
?atrr/
を使うことでテーマ属性を参照できる
- これをうまく使っていくの大事そう
- Styleは1つのViewに適用され、Themeは子Viewにも適用される
- テーマ属性を適切に設定することで統一感があり管理も煩雑にならない
- ThemeとStyleを見ればView属性のデフォルトがどのテーマ属性を参照しているかわかる
それこそTheme vs Styleや、Dark Themeについても取り上げたかったんですが長くなるのでいつかの機会に。
カラー命名の良い方法などはAndroid Dev Summit動画または こちらのQiita記事を見ると良いです!
(本記事の中では<color name="colorPrimary">#6200EE</color>
のように定義してますが、あまりよくないです。)間違ったところや疑問点などがアレば歓迎しますぜひ教えて下さい!
参考にしたリンクなども書いておくのでThemeをみんなで攻略しましょう!Links
- Developing Themes with Style (Android Dev Summit '19)
- Developing Themes with Style
- Androidのthemeにつけられた命名のメモ
- Theme vs Style
- Android Dev Summit '19で紹介されたサンプルから学ぶ、AndroidのTheme、Style、Colorの設定方法
- DroidKaigi2019で発表したAndroid Themeの話のスライド補足
- Color theme creation
- Color Theming
- Dark Theme
- Setting up a Material Components theme for Android
- material-components-android | MaterialColors.java
- 投稿日:2019-12-03T10:07:29+09:00
AndroidStudioのProject Viewで、パッケージをまとめずに全階層表示する
環境
Android Studio 3.5
連結されたパッケージ表示
プロジェクト内にファイルやパッケージを追加する時、AndroidStudioのProject Viewから直接追加している人も多いかと思います。
この時、設定によっては以下のようにパッケージが連結されて表示されています。model直下にrequestしかないため、個別に表示する必要がないと判断されていますね。
パッケージを全階層表示
これだと、modelの下に
response
というpackageを追加したい時に少し面倒です。
そんな時はこのようにして全ての階層を表示させましょう。
- 投稿日:2019-12-03T09:58:52+09:00
Firebaseのサーバーレスアプリ開発について(Android編)
Firebaseサーバーレスアプリのニーズ
2~3年前からサーバーレスのアプリ開発(Android/iOS)が増えています。
特にFirebaseを使っているスタートアップをよく見かけます。アプリの競争が激しい中、今はいいアプリを作るだけで売れる時代は終わっているかも知りません。
特に資金力の弱いスタトアップだと限られた資金で短期間に成果をあげることは難しいでしょう
資金調達の為にもより早くアプリを見せないといけないです。
そういったニーズの答えがFirebaseのサービスだと思います。Firebaseサーバーレスのメリット&デメリット
メリット&デメリットはあくまで私の個人意見です。
以下の内容以外にもいっぱいあると思います。メリット
- サーバー開発期間がゼロ
- サーバー管理が不要で開発に集中できる
- 無料枠でも結構使える
- Firestoreだとほぼリアルタイムで更新するのでチャット機能も簡単に実現できる
デメリット
- 無料枠を超えると料金がちょっと高めになること
- Firebaseの詳細料金表:https://firebase.google.com/pricing/?hl=ja
- Firebaseサービスに依存することになる
その他
Firebaseサーバーレスの実装サンプル
本投稿ではサーバーレスの実装がどれだけ簡単にできるかをコードを中心に伝えたいと思います。
導入方法については次回に紹介します。1.Firebase Authentication
会員登録とログイン関係の全ての機能を提供しています。
サーポートする認証方式は以下の画像を参考にしてください。
FirebaseでサーポートしてないLINEなどの認証は別の方法で実装ができます。
それについては次回に紹介します。
サーポートする認証方式は色々ありますが、
本投稿では「メール/パスワード」の認証方式だけをkotlinで書いて見ました。
- 会員登録、プロフィール変更、パスワード変更、ログイン、ログアウト、退会a.会員登録
会員登録fun signup() { val auth = FirebaseAuth.getInstance() val email = "test@bizreach.co.jp" val password = "12345678" auth.createUserWithEmailAndPassword(email, password) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }会員登録後のAuthentication管理画面
b.プロフィール変更
プロフィール変更fun changeProfile() { val auth = FirebaseAuth.getInstance() val user = auth.currentUser ?: return val name = "山田" val profileUrl = "https://example.com/jane-q-user/profile.jpg" val profile = UserProfileChangeRequest.Builder() .setDisplayName(name) .setPhotoUri(Uri.parse(profileUrl)) .build() user.updateProfile(profile) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }c.パスワード変更
パスワード変更fun changePassword() { val auth = FirebaseAuth.getInstance() val user = auth.currentUser ?: return val newPassword = "abcdefgh" user.updatePassword(newPassword) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }d.ログイン
ログインfun login() { val auth = FirebaseAuth.getInstance() val email = "test@bizreach.co.jp" val password = "abcdefgh" auth.signInWithEmailAndPassword(email, password) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }e.ログアウト
ログアウトfun logout() { FirebaseAuth.getInstance().signOut() }f.退会
退会fun withdrawal() { val auth = FirebaseAuth.getInstance() val user = auth.currentUser ?: return user.delete() .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }退会後のAuthentication管理画面
2.Cloud Firestore
NoSQLデータベースで、データの保存・変更・削除などの機能が簡単に使えます。
サンプルコードではToDo一覧、ToDo詳細機能をベースにデータを構成して実装して見ました。a.ToDo登録
ToDo登録data class Todo(val title: String, val done: Boolean = false, val createdAt: Timestamp = Timestamp(Date())) fun addTodo() { val db = FirebaseFirestore.getInstance() val col = db.collection("todo_list") col.add(Todo("todo1")) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } col.add(Todo("todo2")) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }ToDo登録後のFirestore管理画面
b.ToDo編集
ToDo編集fun changeToDone() { val db = FirebaseFirestore.getInstance() val doc = db.collection("todo_list") .document("Hi2mlwN3lguMmfu76ZqX") doc.update("done", true) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }ToDo編集後のFirestore管理画面
c.ToDoの一覧取得
ToDoの一覧取得fun getTodoList() { val db = FirebaseFirestore.getInstance() db.collection("todo_list") .get() .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ result?.forEach { val title = it["title"] as String val done = it["done"] as Boolean val createdAt = it["createdAt"] as Timestamp val todo = Todo(title, done, createdAt) Log.d("TAG", "todo_list: documentId=${it.id} todo=$todo") } } } ---- logcat //todo_list: documentId=1aPGHKce0Fn4Osu65ioW todo=Todo(title=todo2, done=false, createdAt=Timestamp(seconds=1573460794, nanoseconds=820000000)) //todo_list: documentId=Hi2mlwN3lguMmfu76ZqX todo=Todo(title=todo1, done=true, createdAt=Timestamp(seconds=1573460794, nanoseconds=801000000))d.ToDo削除
ToDo削除fun removeTodo() { val db = FirebaseFirestore.getInstance() val doc = db.collection("todo_list") .document("Hi2mlwN3lguMmfu76ZqX") doc.delete() .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }ToDo削除後のFirestore管理画面
3.Cloud Storage
ファイルの保存、変更、削除、URL参照ができます。
例えば会員のプロフィール写真などを保存して画面に表示する際に使います。
※内部的にはAWSのS3を使っています。a.プロフィール画像のアップロード
プロフィール画像のアップロードfun uploadProfile() { val storage = FirebaseStorage.getInstance() val profileRef = storage.reference.child("images/profile.jpg") val bitmap = BitmapFactory.decodeResource(resources, R.drawable.profile) val baos = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos) val data = baos.toByteArray() profileRef.putBytes(data) .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }プロフィール画像のアップロード後のStorage管理画面
b.プロフィール画像URLの取得
プロフィール画像URLの取得fun getProfileUrl() { val storage = FirebaseStorage.getInstance() val profileRef = storage.reference.child("images/profile.jpg") profileRef.downloadUrl .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ Log.d("TAG", "profileUrl = $result") } } ----logcat profileUrl = https://firebasestorage.googleapis.com/v0/b/fir-serverlessdemo.appspot.com/o/images%2Fprofile.jpg?alt=media&token=8bbb1678-76a7-470f-8773-9f3d8d8363f4d.プロフィール画像の削除
プロフィール画像の削除fun deleteProfile() { val storage = FirebaseStorage.getInstance() val profileRef = storage.reference.child("images/profile.jpg") profileRef.delete() .addOnFailureListener { exception -> /* 失敗 */ } .addOnSuccessListener { result -> /*成功*/ } }プロフィール画像の削除後のStorage管理画面
まとめ
Firebaseを使えば認証管理、DB管理、ファイル管理がどれだけ簡単にできるか理解頂けたと思います。
伝統的なネイティブアプリの開発方法がだんだん変わっています。
昔の開発では3人(インフラエンジニア/サーバーサイドエンジニア/フロントエンジニア)でやってたのが
今はだった一人でできる時代になりました。
Firebaseを使えば質の高いアプリ(Android/iOS/Web)をたった一人で開発することができます。以前は物理サーバーを立ててから開発することが普通でしたが、
現在ではクラウドサービス(AWS、AZUL、GCP...)を使うのが当たり前のようになっています。
それと同じ様にネイティブアプリの開発でもサーバーレスのアプリ開発が広がり、
当たり前のような時代になるかも知りません。
- 投稿日:2019-12-03T09:51:31+09:00
GitリポジトリのライブラリをAndroidアプリから参照する
Gitのリポジトリのライブラリを参照する方法
色々調べてトライしたけれど、ブランチ指定した場合にうまく動作するのがこの方法だったのでメモ
他の方法はmaster指定しかうまく動かなかった・・・詳細は以下のソースコードをみてください。
[ライブラリのリポジトリ]
https://github.com/tokuyama-san/MyLibrarybuild.gradle(Module)apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply from: 'maven.gradle' ←ここを追加 android { compileSdkVersion 29 defaultConfig { minSdkVersion 28 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } 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.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }maven.gradledef versionName = "0.9.1beta" def repo = new File(rootDir, "repository") apply plugin: 'maven' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' uploadArchives { repositories { mavenDeployer { repository url: "file://${repo.absolutePath}" pom.version = "${versionName}" // version pom.groupId = 'toku.san.mylibrary' // グループ名 pom.artifactId = 'MyLibrarySDK' // ライブラリ名 } } }[アプリのリポジトリ]
https://github.com/tokuyama-san/MyApplicationbuild.gradle(Project)// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.3.50' repositories { google() jcenter() maven { url "https://github.com/layerhq/releases-gradle/raw/master/releases" } ←これを追加 } dependencies { classpath 'com.android.tools.build:gradle:3.5.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath group: 'com.layer', name: 'git-repo-plugin', version: '1.0.0' ←これを追加 } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }build.gradle(app)apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "toku.san.myapplication" minSdkVersion 28 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' } } } apply plugin: 'git-repo' ←これを追加 repositories { git("git@github.com:tokuyama-san/MyLibrary.git", "toku.san.mylibrary:MyLibrarySDK", "master", "repository") ←これを追加 } 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.1.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' implementation 'toku.san.mylibrary:MyLibrarySDK:0.9.0' ←これを追加 }ライブラリの更新方法
ライブラリのリポジトリですること
1.maven.gradleに記載しているバージョン(VersionName)を変更する(例. "0.9.1beta")
2.ライブラリのルートディレクトリで以下のコマンド
./gradlew uploadArchives
3.repository配下に指定したバージョンのファイルがいくつか出来上がるのでGitにコミット&プッシュする
アプリのリポジトリですること
アプリのbuild.gradleに記載されているdependenciesスコープ内の
ライブラリのバージョン部分を変更する。
implementation 'toku.san.mylibrary:MyLibrarySDK:0.9.0'
↓
implementation 'toku.san.mylibrary:MyLibrarySDK:0.9.1beta'
あとはビルドすればOK
- 投稿日:2019-12-03T08:10:42+09:00
【Android】license-tools-pluginで出力したlicenses.htmlで、URLが長いと改行位置がずれてしまう
はじめに
アプリでライセンス表示を埋め込む際、色々な方法があるかと思います。
今回クックパッドさんが開発されたlicense-tools-pluginを使わせていただくこととなり、
実際に表示まで行なった際に表題の問題が発生してしまいました。
その原因と対応方法を自分用のメモとして残しておきたいと思います。license-tools-pluginの詳細な使い方については、
他の方がわかりやすくまとめられている為、本記事では割愛させていただきます。
※あの素晴らしいlicense-tools-pluginをもう一度どういう現象か
以下のように、横幅(width)より、リンクの文字列が長くなってしまった場合、
少しはみ出た状態での改行になってしまいます。
原因と対応方法
出力したlicenses.htmlを確認してみると、はみ出ていた部分は以下のようになっていました。
<p><a href="https://developer.android.com/topic/libraries/architecture/index.html">https://developer.android.com/topic/libraries/architecture/index.html</a></p>HTMLをあまり詳しく知らなかったため、調査したところ、CSSの設定が足りていないことが原因であることが判明。
※URLが自動改行してくれない問題
<style></style>
の中に、<a>
タグ用の設定として以下を追記することで、ずれなくすることができました。<style> a { word-break: break-all; } </style>補足
尚、出力するたびに上記の設定を追記する必要があるため、面倒であるのと対応するの忘れそう…
流石に他にlicense-tools-pluginを使っている方も同じ現象に陥っているのでは…?と思い、
公式のGitHabリポジトリを確認すると、なんと以下のプルリクエストを発見。
https://github.com/cookpad/license-tools-plugin/pull/124出力時に以下の設定を追加するようにされており、
手動で試してみたところ、こちらでもずれが発生しないことを確認できました。a { overflow-wrap: break-word; }ですので、クックパッドさんがlicense-tools-pluginを次回アップデートされる際には修正されたものが上がってくるかと思います。
修正版が上がるまでは面倒ですが、以下のどちらかを手動で追記して対応するようにしましょう。
word-break: break-all;
overflow-wrap: break-word;
- 投稿日:2019-12-03T08:09:22+09:00
MotionLayoutのサンプルコードから学ぶMotion Editor
この記事はコネヒト Advent Calendar 2019 3日目の記事になります。
はじめに
Android Studio 4.0 Canary 4に新機能として導入されたMotion Editorについて調べてみたので、公開されているMotionLayoutのサンプルコードをベースに、どのように利用されているのか紹介します。
MotionLayoutとは?
- ConstraintLayout 2.0に含まれている機能
- 始まりと終わりのレイアウトをXMLで定義することによって簡単にアニメーションを実装することができる
Motion Editorとは?
- Android Studio 4.0 CanaryからMotion Editor導入された
- IDE内でViewの関係性やアニメーションを確認、設定できる
サンプルコードの紹介
今回利用したサンプルコードはPiotrPrusさんのMotionLayoutPlaygroundリポジトリです。こちらのリポジトリでは、いくつかMotionLayoutのサンプルがありますが、「scene13(RV with ML)」のTinder風アニメーションのMotionLayoutについて紹介します。まずは以下のキャプチャを見て挙動を確認しましょう。
グリッドリスト上に複数のCardViewがあり、そのCardViewを左にスワイプする(like)と、リストから削除されます。反対に右にスワイプする(dislike)と先ほどと同様にリストから削除されるようにMotionLayoutで実装されています。
サンプルコードの解説
対象画面のコード一式は下記の通りです。
- Scene13Fragment.kt Like/DisLikeできるグリッドリスト画面
- MySceneGridItemRecyclerViewAdapter.kt グリッドリスト用のアダプター
- fragment_scene13.xml Fragmentのレイアウト
- item_scene_13_grid.xml グリッドリストのレイアウト
- ml_scene_13.xml MotionSceneのレイアウト
item_scene_13_grid.xmlの中身を見てみると、Viewの要素にCardViewとそれに対応したlike、dislike用のオーバーレイが配置されています。
item_scene_13_grid.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.motion.widget.MotionLayout 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="300dp" android:clipChildren="false" android:clipToPadding="false" app:layoutDescription="@xml/ml_scene_13"> <!-- CardView --> <com.google.android.material.card.MaterialCardView android:id="@+id/cardViewScene13" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorGrey" app:cardBackgroundColor="@color/colorBlue" app:cardCornerRadius="10dp" app:cardElevation="4dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:id="@+id/item_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" tools:text="1" /> <TextView android:id="@+id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" tools:text="test" /> </LinearLayout> </com.google.android.material.card.MaterialCardView> <!-- Like用のオーバーレイ --> <View android:id="@+id/cardScene13LikeOverlay" android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/frame_overlay_like" app:layout_constraintBottom_toBottomOf="@id/cardViewScene13" app:layout_constraintEnd_toEndOf="@id/cardViewScene13" app:layout_constraintStart_toStartOf="@id/cardViewScene13" app:layout_constraintTop_toTopOf="@id/cardViewScene13" /> <!-- Dislike用のオーバーレイ --> <View android:id="@+id/cardScene13DislikeOverlay" android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/frame_overlay_dislike" app:layout_constraintBottom_toBottomOf="@id/cardViewScene13" app:layout_constraintEnd_toEndOf="@id/cardViewScene13" app:layout_constraintStart_toStartOf="@id/cardViewScene13" app:layout_constraintTop_toTopOf="@id/cardViewScene13" /> </androidx.constraintlayout.motion.widget.MotionLayout>Motion Editorで処理の流れを見る
では、MotionLayoutの構成を見てみましょう。
グリッドリスト部分に当たる、item_scene_13_grid.xmlをMotion Editorを開いたキャプチャです。これまではXMLで書いていたものが、GUIで表示、設定できるようになりました!Motion Editorを見てみると、5つのConstraintSetで構成されています。
- rest : 初期状態
- like : いいね
- goneRight : 右に消える
- dislike : 好まない
- goneLeft : 左に消える
ConstraintSetとは、Viewの状態を定義でき、例えば初めのViewの状態と終わりのViewの状態やアニメーションを定義することができます。これら複数のConstraintSetをどのように組み合わせてlike、dislikeの処理を行なっているのでしょうか?それぞれ確認してみたい思います。
like処理
ConstraintSet rest
CardViewを表示する部分に当たるConstraintSet restを選択すると、ConstraintSetの中に3つのConstraintが定義されていて、cardViewScene13、cardScene13LikeOverlay、cardScene13DislikeOverlayのConstraintがあります。さらにConstraint cardViewScene13を選択すると、それぞれプロパティが設定されています。
ConstraintSet restを矢印がlikeとdislikeのConstraintSetに設定されています。まずはlikeの矢印を見てみますが、矢印はTransitionを表します。Transitionを再生すると右に回転していくことがわかります。
ちなみにrestからlikeのTransition上のマークはスワイプやクリックイベントを表すマークとなっています。
ConstraintSet like
次にいいねの処理に当たる、ConstraintSet likeについて見てみます。CardViewが回転されている状態であることがわかります。
Constraint cardViewScene13で右にローテーションしていることがわかります。
次にTransitionがlikeからgoneRightへと定義されており、Transitionを再生してみるとViewが画面外に移動し見えなくなることがわかります。autoTransitionプロパティにanimateToEndが設定されていますが、これによってlikeからgoneRight状態に自動的にアニメーション化されます。
ConstraintSet goneRight
ここまでrestからlike、goneRightまでConstraintSetを組み合わせてアニメーションを実装していることがわかりました。ConstraintSet goneRightの中にはConstraintとして、cardViewScene13などが定義されており、左のプレビューからcardViewScene13が画面でかつalpha0になっていることがわかります。このようにして右スワイプしてCardViewが消えるアニメーションを実装されています。
以上がlike処理になります。restなどのConstraintSet以下に破線がありますが、これはConstraintSetのderiveConstraintsFromプロパティを利用すると表示されます。likeの場合はrestから派生しているという意味になります。リストから削除されている部分については、コード側で実装されているため別途後述します。次にdislike処理についても確認します。
dislike処理
ConstraintSet dislike
likeと同様にConstraintSetでrestからdislikeが定義されています。左スワイプをされるとローテートされた状態がConstraintSet dislikeです。さらにConstraintSet dislikeのcardViewScene13を見てみると左向きにローテーションされているのがわかります。
次にTransitionを見てみるとConstraintSet dislikeからConstraintSet goneLeftに設定されているのがわかります。
ConstraintSet goneLeft
rest -> dislike -> goneLeft ConstraintSetとTransitionが定義されており、goneLeftでは、alpha:0で透過し、rotation:-70、layout_marginEnd:600dpで画面外にViewが移動していることがわかります。ここまでがdislikeのアニメーションになります。
like、dislike後のリスト削除
likeとdislike後にリストから削除されるようになっていて、ここの部分はコード側で実装されているため解説します。
Scene13Fragmentでは、MySceneGridItemRecyclerViewAdapterを使っています。リスト削除についてですが、MotionLayout.TransitionListenerインターフェースが提供されていて、goneRight、goneLeftのトランジッションが完了したタイミングでリストから削除しています。MySceneGridItemRecyclerViewAdapter.ktclass MySceneGridItemRecyclerViewAdapter( private val mValues: MutableList<DummyItem>, private val animationListener: (state: AnimationState) -> Unit ) : RecyclerView.Adapter<MySceneGridItemRecyclerViewAdapter.ViewHolder>() { // 〜省略〜 private val removeItemListener: (Int) -> Unit = { Handler().postDelayed({ mValues.removeAt(it) notifyDataSetChanged() }, 100) } inner class ViewHolder(val mView: View, removeItemListener: (Int) -> Unit) : RecyclerView.ViewHolder(mView) { val mIdView: TextView = mView.item_number val mContentView: TextView = mView.content internal var position = 0 override fun toString(): String { return super.toString() + " '" + mContentView.text + "'" } init { (mView as MotionLayout).setTransitionListener(object : MotionLayout.TransitionListener { override fun onTransitionTrigger( p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float ) { } override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { when { p1 == R.id.rest && p3 > 0f -> animationListener(AnimationState.STARTED) } } override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { animationListener(AnimationState.COMPLETED) if (p1 == R.id.goneRight || p1 == R.id.goneLeft) { Log.d("Adapter", "Remove item at position: $position") removeItemListener(position) } } override fun onTransitionStarted(p0: MotionLayout?, startId: Int, endId: Int) { } }) } } }おわりに
簡単にでしたが、MotionLayoutのサンプルを用いてMotion Editorについて触れてみました。GUIによってConstraintSetとTransitionの関係性がわかりやすくなり、Transitionも手軽に再生できるのでとても便利です。Motion EditorはAndroid Studio 4.0 Canaryの新機能でもっと触っておかしいところがあればIssueを投げていけるように頑張ります。
PR
コネヒトではエンジニアを募集しています!家族向けサービスをつくりたいエンジニアの皆さん、お待ちしています。
家族の課題を解決するサービスのMAUを増やすAndroidエンジニア募集!
- 投稿日:2019-12-03T01:15:23+09:00
Coroutines私的メモ2~Dispatchers, withContext, async/await~
私的メモ1の続きです
前提
前回同様、現在のスレッドがわかるようにloggerメソッドを仕込むことにします
fun logger(msg: String) = println("${Thread.currentThread().name} : $msg")dispatchers
コルーチンを使う際に、どのスレッドもしくはスレッドプールにて実行・制御したいということがあります
その際にdispatcherを指定することで解決できます Dispatcherにもいくつか種類があるので使い分けたいところです
Dispatchers.Main
UIに関する処理をするメインスレッドのためのコルーチン
Dispatchers.Main.immediate
UI更新を即時行いたい際に使用される (e.g. textviewのtext更新とか)
Dispatchers.Default
共有スレッドプールを使用し、CPUのコア数に従って設定される
CPUに負荷をかけるような計算をするような処理に対して使うことが推奨されますDispatchers.IO
その処理に必要なコルーチンの数に応じて必要な分だけ共有スレッドプールを使用する
DBの検索等、I/Oに負荷をかけるような処理に対して使うことが推奨されますDispatchers.Unconfined
特定のスレッドに限定させない
一度停止させて再開した場合は別スレッドに切り替わっている可能性あり試しにこのようなプログラムを組んでみます
fun unconfined() = runBlocking { launch(Dispatchers.Unconfined) { logger("Hello") delay(1000L) logger("World") } }実行結果はこのように、スレッドが異なっていますね
main : Hello kotlinx.coroutines.DefaultExecutor : WorldwithContext
コルーチンの実行スレッドを切り替えたいときに使う
withContextの引数に上述したDispatcherを指定してあげる
そのため、切り替える必要がない単純な場合はlaunch(対象のDispatcher){}
で事足りると思います例としては下記のように、ローディングを走らせ、別のスレッドでデータを引っ張ってきて、そのデータを元にUIを更新するなどです
loading()
とupdateList(data)
はtop-levelで指定しているMainスレッドで処理されますが、データをひっぱってきているfetchData
は別スレッドでの処理になりますfun update() { launch(Dispatchers.Main) { loading() val data = withContext(Dispatchers.Default) { repository.fetchData() } updateList(data) } }async/await
コルーチンからの返り値を使って複数の結果を待ち合わせて処理したい場合に用いられます
例えば上述した updateメソッドのfetchDataが他のデータも必要な場合ですね
fun update() { launch(Dispatchers.Main) { loading() val feedData = async (Dispatchers.Default) { repository.fetchFeed() } val adData = ahync (Dispatchers.Default) { repository.fetchAd() } val data = feedData.await() + adData.await() updateList(data) } }
withTimeout
orwithTimeoutOrNull
でタイムアウトの指定もできます
ネットワーク環境が悪かったり、遅い結果でユーザ体験を損なわせないことを目的として使えそうですね単純な例を示します
fun asyncAwaitWithTimeout() { runBlocking { logger("Hello") val simpleTask = async(Dispatchers.Default) { 2 + 2 } val heavyTask = async(Dispatchers.Default) { logger("World") delay(3000L) 4 + 4 } logger("Calculating...") val total = withTimeoutOrNull(2) { simpleTask.await() + heavyTask.await() } logger("total=$total") } }ただ足し算をしてその結果を返しているだけですね
ここでは、簡単なタイムアウトを2秒と定義し、時間のかからない簡単なタスクと実行に最低3秒はかかってしまう重いタスクを定義して、わざとタイムアウトさせるようにしています結果としては以下の様になります
main : Hello main : Calculating... DefaultDispatcher-worker-1 : World main : total=null今回は
withTimeoutOrNull
を使ったので、合計の12が返るのではなく、タイムアウトしたのでnullが返りました次回はCoroutines Flowを扱う予定です
参考
- 投稿日:2019-12-03T01:05:07+09:00
【Android】世界一わかりやすいRecyclerViewの実装
この記事はFUN Advent Calender 2019の3日目の記事です。
昨日の記事は、【ガチ比較】登校ルートをチャリで往くでした。
前置き
Androidアプリ開発で、長ーいリストや逐次中身のデータ更新があるリストを扱いたいこと、結構あります。
そこでよく使われるのがRecyclerViewというものです。この記事を書こうとしたきっかけとしては、大抵の人がRecyclerViewについて検索してたどり着く記事は大抵RecyclerViewを単体で扱ってくれていないというところからです。
RxJava....? DataBind....? 記事を読む人の学習コストをわざわざ上げていることがよくわかりません。分かっているなら話は別ですが。ということで、今回はRecyclerViewをとりあえず動かせる最低限のコードをかんたんに解説していきます。プロジェクト全体はGitHubにあげていますので参考にしてください。
https://github.com/DaisenKudo/TheMostSimpleRecyclerView/tree/master本題
材料
コード
- MainActivity.kt : 今回はFragmentの入れ物に徹していただきます。
- MainFragment.kt : 今回はRecyclerViewの入れ物に徹していただきます。
- MainViewAdapter.kt : RecyclerViewのアレコレを管理してもらいます
- MainViewHolder.kt : RecyclerViewにいれるアイテム(一つ分)のガワを定義します。
- ItemModel.kt : RecyclerViewにいれるアイテム(一つ分)の中身を定義します。
レイアウト
- main_activity.xml : Activityのレイアウト
- main_fragment.xml : Fragmentのレイアウト
- part_item_model.xml : RecyclerViewに入れたいアイテムのレイアウト
外部ライブラリ
appディレクトリに入っている方のbuild.gradleのdependenciesの中に以下を追記してください。
build.gradleimplementation 'androidx.recyclerview:recyclerview:1.1.0'レイアウト
特にコレといって言うことはないです。
別にRecyclerViewに入れるアイテムはConstraintLayoutを使わなくてもいいです。
findViewByIdしたときの型だけ注意してください。activity_main
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:id="@+id/container_main_fragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".view.ui.main.MainActivity" />fragment_main
fragment_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <androidx.recyclerview.widget.RecyclerView android:id="@+id/container_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </androidx.constraintlayout.widget.ConstraintLayout>part_item_model
part_item_model<?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:orientation="vertical" android:layout_width="match_parent" android:layout_height="30dp"> <TextView android:id="@+id/tv_item_model" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>コード
コードで結構陥りやすいのが、「Unresolved reference: R」というエラーです。
import文内のio.github.qlain.themostsimplerecyclerview
を適宜
(プロジェクトのPackage Name
に読み替えてください。Android Studioなら自動でimportを書いてくれますが、たまーにやってくれないことがあるので。Model
RecyclerView内に入れるデータをまとめておくためのクラスです。data classにしてしまってもいいかもしれません。
今回はStringだけを格納していますが、Bundleのように決められたものしか入れられないってわけではないので、柔軟なリストが作れます。ItemModel.ktpackage io.github.qlain.themostsimplerecyclerview.model class ItemModel { var text: String = "" }MainViewHolder
RecyclerViewで使いたいパーツを定義しておきます。
RecyclerViewのアイテム1つごとにViewAdapterによってインスタンス化(アイテムごとにMainViewHolderが1対1で紐付けられる)されます。
ここらへんかどうなっているかを知りたければ、公式リファレンスとか読むといいかもです。
道具として使うだけなら分からなくてもOKです。MainViewHolder.ktpackage io.github.qlain.themostsimplerecyclerview.model import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import io.github.qlain.themostsimplerecyclerview.R class MainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val textView: TextView = itemView.findViewById(R.id.tv_item_model) }MainViewAdapter
RecyclerViewのアレコレを管理します。
RecyclerView内のアイテム更新とかはここでやりましょう。
Fragmentからメソッドを呼び出す形でもいいと思います。MainViewAdapter.ktpackage io.github.qlain.themostsimplerecyclerview.view.ui.main import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import io.github.qlain.themostsimplerecyclerview.R import io.github.qlain.themostsimplerecyclerview.model.ItemModel import io.github.qlain.themostsimplerecyclerview.model.MainViewHolder class MainViewAdapter( private val list: List<ItemModel>, private val listener: ListListener ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { interface ListListener { fun onClickItem(tappedView: View, itemModel: ItemModel) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.part_item_model, parent, false) return MainViewHolder(itemView) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { holder.itemView.findViewById<TextView>(R.id.tv_item_model).text = list[position].text holder.itemView.setOnClickListener { listener.onClickItem(it, list[position]) } } override fun getItemCount(): Int = list.size }MainActivity
MainFragmentの表示だけです。
MainActivity.ktpackage io.github.qlain.themostsimplerecyclerview.view.ui.main import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import io.github.qlain.themostsimplerecyclerview.R class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) supportFragmentManager.beginTransaction().replace( R.id.container_main_activity, MainFragment() ).commit() } }MainFragment
定義したRecyclerViewを表示させます。
今回はgenerateItemList()で花丸ちゃん、おまんじゅうn個目だよというテキストを生成しています。
val recyclerAdapter = MainViewAdapter(generateItemList(), object : MainViewAdapter.ListListener {
の行のgenerateItemList()
を差し替えると動的にリストを変更することもできます。ConstraintLayoutなのにLinearLayoutManager...????と思うかもしれませんが気にしないでください。大丈夫です。
ちなみに、onDestroyViewの処理を実行しないと、メモリリークの原因になります。(ActivityよりRecyclerAdapterが長生きしてしまう恐れがある)
MainFragment.ktpackage io.github.qlain.themostsimplerecyclerview.view.ui.main import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.github.qlain.themostsimplerecyclerview.R import io.github.qlain.themostsimplerecyclerview.model.ItemModel class MainFragment : Fragment(){ private var recyclerView: RecyclerView? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { super.onCreateView(inflater, container, savedInstanceState) return inflater.inflate(R.layout.fragment_main, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) this.recyclerView = view.findViewById(R.id.container_recycler_view) this.recyclerView?.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) itemAnimator = DefaultItemAnimator() adapter = MainViewAdapter( generateItemList(), object : MainViewAdapter.ListListener { override fun onClickItem(tappedView: View, itemModel: ItemModel) { this@MainFragment.onClickItem(tappedView, itemModel) } } ) } } override fun onDestroyView() { super.onDestroyView() this.recyclerView?.adapter = null this.recyclerView = null } //RecyclerViewの生成時に一度だけ動く private fun generateItemList(): List<ItemModel> { val itemList = mutableListOf<ItemModel>() for (i in 0..100) { val item: ItemModel = ItemModel().apply { text = "花丸ちゃん、おまんじゅう${i}個目だよ" } itemList.add(item) } return itemList } //RecyclerView内のアイテムがクリックされたときに動く private fun onClickItem(tappedView: View, itemModel: ItemModel) { } }できるもの
まとめ
再掲ですが、今回作ったコードはここに上げておきます。
わからないことはIssueかTwitterで聞いてください。ViewHolderはViewAdapterのインナークラスにしてしまってもいいと思います。
MIT Licenseとはいっていますが、誰が書いても似たようなコードになると思うので、アレコレしても怒りません。なんなら写経でも
DataBind絶対に許さんAdvent Calender、明日はnao(ki)さんです。
- 投稿日:2019-12-03T00:23:59+09:00
[Android / Kotlin / OkHttp] JSONをPOSTする話と拡張関数を触った話。
どうもこんばんわ
本題
インターネットで何か情報をとってくるアプリを作るのは楽しいものです。
そこでWebAPIをよくたたきますが、その時に便利なライブラリが「OkHttp」です。
HttpURLConnectionよりわかりやすく書くことができます。なんのAPIを叩くのか
調べるの面倒なので身近なMastodonの投稿するAPIを叩こうと思います。
青い鳥と違いアクセストークンは何もしなくても設定画面の開発から生成することができます。すごい。OkHttp導入
implementation("com.squareup.okhttp3:okhttp:4.2.2")POSTまで書く。
これ書く前にAndroidManifestにインターネットのパーミッション書き足してね。忘れがち
AndroidManifest.xml<uses-permission android:name="android.permission.INTERNET"/>MainActivity.ktval accessToken = "アクセストークン" val instance = "インスタンス名" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //特にWebAPIが思いつかなかったのでMastodonへ投稿するAPIをたたく。POST val url = "https://${instance}/api/v1/statuses" //POSTするときに送るJSON val postJSON = JSONObject().apply { put("access_token",accessToken) put("status","ねむい") put("visibility","direct") } //JSONをRequestBodyにする val requestBody = RequestBody.create("application/json".toMediaTypeOrNull(),postJSON.toString()) val request = Request.Builder() .url(url) .post(requestBody) .build() val okHttpClient = OkHttpClient() okHttpClient.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { runOnUiThread { //UIスレッド //しっぱい Toast.makeText(this@MainActivity,"問題が発生しました",Toast.LENGTH_SHORT).show() } } override fun onResponse(call: Call, response: Response) { runOnUiThread { //UIスレッド if(response.isSuccessful){ //成功 Toast.makeText(this@MainActivity,"成功しました ${response.code}",Toast.LENGTH_SHORT).show() }else{ //しっぱい Toast.makeText(this@MainActivity,"問題が発生しました ${response.code}",Toast.LENGTH_SHORT).show() } } } }) }非同期処理です。onResponseの中UIスレッドではないのでUIいじる(TextViewに値入れるなど)すると落ちます。
UIいじるときはrunOnUiThread{}
などを使っていきましょう。
Android 11でAsyncTaskが非推奨になるとか。ソースはXDA DevelopersさんですRequestBody.
createが非推奨になっている。このままだとRequestBody.
createに線が入っています。非推奨に。解決
Alt+Enterを押して置き換えましょう。
するとこうなるんですね。
val requestBody = postJSON.toString().toRequestBody("application/json".toMediaTypeOrNull())toRequestBodyどっから出てきたの
Stringにそんなのあった?
Kotlinの拡張関数という機能を使っているみたい。
プログラミングはアプリを作るためにやっているので詳しいわけではありませんが、
こんな関数あればいいのになーって時に追加できるみたいです。拡張関数を触ってみる
消費税込みの値段を返す拡張関数を書いてみた。シンプル。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) supportActionBar?.title="拡張関数" //消費税をかける拡張関数。 val nedan = 500 textview.text =nedan.addTax().toString() } /* * 消費税をかける拡張関数。 * */ fun Int.addTax(): Int { return (this*1.10).toInt() }名前は適当。
return (this*1.10).toInt()この例ではthisの部分には500が入ります。
終わりです。お疲れ様でした。
参考にしました。
https://square.github.io/okhttp/recipes/#posting-a-string-kt-java
https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/extensions.html