RecyclerViewにListAdapterを使って差分更新を実現する
TL;DR
RecyclerViewを用いた画面で、ListAdapterを使用して差分更新を実現したので方法をまとめます。
ListAdapterとはなにか?
下記のリンクがListAdapter
リファレンスです。
https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter
パッケージはandroid.support.v7.recyclerview.extensions
となり、Support Library 27.1.0から実装されています。
ListAdapter
はAsyncListDiffer
をラップしたRecyclerView.Adapter
の実装です。
AsyncListDiffer
は与えられたList
の差分更新をバックグラウンドスレッドで実行し、差分が存在する場合、RecyclerView.Adapter
の更新通知(notifyItemRangeInserted
など)の呼び出しを行います。
なにが嬉しいのか?
AACのViewModelとLiveDataを持ちいたMVVMのアーキテクチャを採用した場合、ViewModel側にLiveData<List<Hoge>>
のような形でリスト要素をプロパティとして持たせた上でView側(Activity/Fragment)でLiveDataを監視、変更があれば変更されたListを用いてRecyclerViewを更新という実装をするかと思います。
このときに、単純にRecyclerView.Adapter
に対して更新されたListを渡した場合、すべての要素が更新されますが、ListAdapter
を使用した場合は新しく取得したListと前回ListAdapter
に設定したListの差分となる部分のみRecyclerViewの更新が行われる形となり、更新処理の高速化が見込まれます。
DiffUtil.ItemCallbackの実装
まず、ListAdapter
を使用するためにはListに含まれる要素を比較する為のDiffUtil.ItemCallback<ITEM>
の実装が必要なので、そちらを実装します。
DiffUtil.ItemCallback<ITEM>
の定義は以下を参照ください。
https://developer.android.com/reference/android/support/v7/util/DiffUtil.ItemCallback
実装すべきメソッドは以下の2つになります。
areContentsTheSame(T oldItem, T newItem)
oldItemとnewItemの値が同じ場合にtrue
areItemsTheSame(T oldItem, T newItem)
oldItemとnewItemが同じ要素(DBのカラムなど)を参照している場合にtrue
実装例は以下のようになります。
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Book>() { override fun areContentsTheSame(oldItem: Book?, newItem: Book?) = oldItem == newItem override fun areItemsTheSame(oldItem: Book?, newItem: Book?) = oldItem?.id == newItem?.id }
ListAdapterの実装
次にListAdapter
自体の実装ですが、こちらはコンストラクタに実装済みのDiffUtil.ItemCallback
インスタンスを渡すようにすればOKです。
class MyAdapter(val inflater: LayoutInflater) : ListAdapter<MyItem, MyItemViewHolder>(DIFF_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) { … } override fun onBindViewHolder(holder: MyItemViewHolder, position: Int) { holder.bindTo(getItem(position)); } }
Activity/Fragmentからの呼び出し
最後にActivity/FragmentからAdapterに値を反映させる場合はsubmitList()
の呼び出しを行います。
viewModel.itemList.observe(this, Observer{ adapter.submitList(it) })
Android ThingsをRaspberry Piにセットアップする
はじめに
Raspberry Piを使用したAndroid Thingsアプリ開発の為の環境構築部分についてのまとめです。 基本的にhttps://developer.android.com/things/hardware/raspberrypiの記載内容に従って行います。
実行環境
- 確認日:2018/09/30
- Android Thingsのバージョン:1.0.4(Android 8.1.0)
- Android Studioのバージョン:3.2
- Android Things Setup Utilityのバージョン: 1.0.21
- 環境はMacbook Proを使用しました(MacOS 10.13.6)
- 初回の接続はEthernetケーブル接続で行います
※ USB to TTL Serial Cableを使用した方法については前述のリンク先に記載がありますので、そちらをご覧ください
機材の準備
買い物リストです。
- Raspberry Pi 3 Model B
※ 2018/09時点の最新モデルはModel B+となりますが、現時点ではAndroid Thingsは前モデルのModel Bにのみ対応しています。 - 8GB以上のmicroSD CardおよびSDカードアダプタ
- Micro-USBケーブル(5V2.5Aの給電が可能なUSBポートから給電してください)
- (MacにSDカードスロットがない場合は)SDカードライタ
- HDMI接続可能なディスプレイ&HDMIケーブル
- Ethernetケーブル
※ 使用するMacと有線接続によるLANを構築する必要があります、無線接続環境しか無い場合は別途thunderbolt to Ethernetアダプタなどを購入してください。
micro SDにAndroid Thingsのイメージを書き込む
Android Things ConsoleからAndroid Things Setup Utilityをダウンロードします。 https://partner.android.com/things/console/u/0/#/tools
ダウンロードしたzipファイルを回答し、Android Things Setup Utilityを起動します
$ sudo ~/Downloads/android-things-setup-utility/android-things-setup-utility-macos
- 次の画面が表示されるので「1」を選択します。
What do you want to do? 1 - Install Android Things and optionally set up Wi-Fi 2 - Set up Wi-Fi on an existing Android Things device
- 次の画面が表示されるので「1」を選択します。
What do you want to do? What hardware are you using? 1 - Raspberry Pi 3 2 - NXP Pico i.MX7D
- Platform Toolsのダウンロードが行われ、問題なければ次の画面が表示されるので「1」を選択します。
Do you want to use the default image or a custom image? 1 - Default image: Used for development purposes. No access to the Android Things Console features such as metrics, crash reports, and OTA updates. 2 - Custom image: Upload your custom image for full device development and management with all Android Things Console features.
- 書き込むAndroid Thingsイメージのダウンロードが行われます。成功した場合以下のメッセージが表示されますので、MacのSDカードスロットもしくは、接続したSDカードライタにmicro SDカードをセットしたSDカードアダプタをセットしてEnterキーを押してください。 ※ 当然現在micro SDカードに入っているデータは消えますので、必要なデータは事前にコピーなどしておいてください
Plug the SD card into your computer. Press [Enter] when ready
- 書き込みに成功した場合以下のメッセージが表示されます。
If you have successfully installed Android Things on your SD card, you can now put the SD card into the Raspberry Pi and power it up. Otherwise you can abort and run the tool again.
- このあと次のメッセージが表示されますが、ここでは一旦「n」を選択し、Android Things Setup Utilityを終了します。
Would you like to set up Wi-Fi on this device? (y/n)
書き込んだAndroid Thingsの起動確認を行う
micro SDを取り外し、Raspberry Piにセットした後でRaspberry Piに通電します。接続したディスプレイの画面に「Android Things」と表示された画面が表示されれば成功です。
正方形のレインボー画面(伝わるかな?)が表示されたままの場合はAndroid Thingsイメージの書き込みが正しく行えていない場合があるので、上記の手順を再度確認してください。
また、Raspberry Pi 3 Model B+を購入した場合もそうなりますので、Amazonなどで再度Raspberry Pi 3 Model Bを購入してください(僕はそうしました)
Raspberry Piのネットワーク接続(有線LAN)
Raspberry Pi 3 Model BにはWiFiモジュールが搭載されてるため、WiFiを用いてAndroid Studioとの接続が可能ですが、最初にWiFiのSSIDおよびパスワードを設定する必要があるため、一度有線接続する必要があります。
ご利用のルーターにEthernetのポートがあれば、そちらに接続すれば良いですが、存在しない場合や物理的に届かないなどの場合は、Macと直接Ethernet接続を行い、「インターネット共有」等を機能を用いてRaspberry Pi にIPアドレスの割当を行ってください。
※MacにEthernetポートが存在しないモデルの場合、別途thunderbolt to Ethernetアダプタなどを購入してください。
参考:Macで「インターネット共有」を行う方法
https://support.apple.com/ja-jp/HT203819
Raspberry PiにIPアドレスが割り当てられた場合、次のようにadbコマンドを用いて接続することが可能です。
$ adb connect <インターネット共有などのDHCP機能で割り当てられたIPアドレス>
この時使用するIPアドレスについては以前はAndroid Thingsの起動画面に表示されていたようなのですが、ちょっと見当たらなかったので…なんとか…頑張っていただくしか… また、MDNSをサーポートする環境では以下のコマンドで接続することが可能なようですので、とりあえずこちらで試してみてください。
$ adb connect Android.local
無事、接続が成功すれば有線接続はOKです。
Raspberry PiのWiFiを有効にする
次にRaspberry Piに使用するWiFiのSSIDおよびパスワードを入力してWiFi経由で接続できるようにします。
こちらもAndroid Things Setup Utilityを使用して設定を行います。
- Android Things Setup Utility実行後の次の画面で今度は「2」を選択します。
What do you want to do? 1 - Install Android Things and optionally set up Wi-Fi 2 - Set up Wi-Fi on an existing Android Things device
- 次の画面では「1」を選択します
What hardware are you using? 1 - Raspberry Pi 3 2 - NXP Pico i.MX7D
- Platform Toolsのダウンロードが行われ次の画面が表示されたら、Enterを選択します。
※この時有線Ethernet接続されている必要があります。
Please plug your Raspberry Pi to your router with an Ethernet cable, then press [Enter].
- この際にMDNSに対応した環境であれば、自動で接続されますが、そうでない場合は以下のメッセージが表示されますので、先程調べたIPアドレスを入力してください。
Once you are ready, enter the IP of your Raspberry Pi:
- 問題無く接続できたら、次はSSIDの入力を求められますので、入力してください。 なお、Raspberry Pi 3 Model Bでは2.4GHzのWiFiのみ対応しているようです
Enter the Wi-Fi network name:
- 次にWiFiのパスワードを入力します。
Enter the Wi-Fi network password (leave empty if no password):
- 最後に次のメッセージが表示されればWiFi設定は完了です。
Successfully connected to Wifi Stopping adb server... Stopped adb server... Now that you’re set up, try sample projects in Android Studio or in the sample repository here: https://developer.android.com/things/sdk/samples.html To learn more about features like over-the-air updates, visit the Android Things Console: https://partner.android.com/things/console Press [Enter] to quit.
Wifi経由でAndroid Thingsに接続する
Raspberry PiにWiFi設定を行ったので、WiFi経由でのadb接続を行います。
adb connect <WiFiルーターなどで割り当てられたIPアドレス>
この時使用するIPアドレスはWiFiルーターの管理画面などで確認するか、有線接続した状態で以下のコマンドを実行し、Raspberry Pi側で確認することも可能です。
$ adb connect Android.local $ adb shell rpi3:/ $ ifconfig
この時表示されるwlan0のinet addr:192.168.0.153
の部分が無線LAN側に割り当てられたIPアドレスとなります。
wlan0 Link encap:Ethernet HWaddr b8:27:eb:8a:88:51 Driver brcmfmac_sdio inet addr:192.168.0.153 Bcast:192.168.0.255 Mask:255.255.255.0 inet6 addr: fe80::7ede:1d55:c0cc:f34b/64 Scope: Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:19078 errors:0 dropped:35 overruns:0 frame:0 TX packets:8859 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:2709805 TX bytes:2565208 lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 inet6 addr: ::1/128 Scope: Host UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:27 errors:0 dropped:0 overruns:0 frame:0 TX packets:27 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1 RX bytes:3258 TX bytes:3258 eth0 Link encap:Ethernet HWaddr b8:27:eb:df:37:65 Driver smsc95xx UP BROADCAST MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 TX bytes:0
Android Studioでプロジェクトを作成する
最後にAndroid Studioを起動し、実際にアプリを実行できるか確認します。
- File > New > New Project から新規アプリウィザードを起動します。
- 「Target Android Device」の画面にて、「Android Things」にチェックを入れます。 *※プロジェクトを簡単にする為、デフォルトでチェックが入っている「Phone and Tablet」のチェックは外します
- 「Add Activity to Things」の画面で「Android Things Empty Activity」を選択します。
- 最後にお好みのActivity名を入力してプロジェクトを作成します。
上記の手順でプロジェクトを作成した後は通常のAndroidプロジェクトと同等にRun > Run 'app'などから先程adb connectしたRaspberry Pi上でビルドしたアプリを転送する事が可能です。
デフォルトのHello Worldメッセージが表示されるかと思いますので、ご確認ください。
dagger-androidでDialogFragmentにInjectする
仕事で担当しているプロダクトではDI用のライブラリとしてDagger2のAndroid拡張(https://google.github.io/dagger/android)を使用しています。 先日DialogFragmentに対してDIを行う処理を書いたのですが、その際のやり方をまとめておきます。
Scopeの定義
このプロジェクトではDaggerのScopeとしてActivityScope
やFragmentScope
のようなカスタムScopeを定義しています。
同じようにDialogFragment用のScopeとしてDialogFragmentScope
を定義します。
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface DialogFragmentScope { }
Component, Module定義
dagger-androidでは@ContributesAndroidInjector
アノテーションを用いてActivity, Fragmentなどのコンポーネント定義を行うことができます。
また、@ContributesAndroidInjector
アノテーションにmoduleパラメータを指定し、該当モジュール内で更に@ContributesAndroidInjector
の定義を行う事で、サブコンポーネントの定義を行うことができます。
これを利用して今回は以下のようなコンポーネント関係を構築します。
ActivityComponent → FragmentComponent → DialogFragmentComopnent
- Application用のModule(Activity用のInjectorを定義)
@Module @Suppress("unused") interface ApplicationModule { @ActivityScope @ContributesAndroidInjector(modules = [MyActivityModule::class]) fun contributeMyActivity(): MyActivity }
- Activity用のModule(Fragment用のInjectorを定義)
@Module @Suppress("unused") interface MyActivityModule { @FragmentScope @ContributesAndroidInjector(modules = [MyFragmentModule::class]) fun contributeMyFragment(): MyFragment }
- Fragment用のModule(DialogFragment用のInjectorを定義)
@Module @Suppress("unused") interface MyFragmentModule { @DialogFragmentScope @ContributesAndroidInjector fun contributeMyDialogFragment(): MyDialogFragment }
Inject
DialogFragment側の実装ではDaggerAppCompatDialogFragment
もしくはDaggerDialogFragment
を利用します。
あとは普通に@Inject
アノテーションを用いて使用したいクラスを注入すればOKです。
class MyDialogFragment : DaggerAppCompatDialogFragment() { @Inject lateinit var presenter: MyPresenter }
ここでInjectされているMyPresenter
がFragmentScopeやActivityScopeとなっている場合、上位のFragment/Activityとインスタンスが共有されますのでPresenter経由でFragmentを操作する事などが可能になります。
AACのViewModelをKoinでInjectする
Kotlinを利用したプロジェクトで利用できるDIライブラリであるKoin(https://github.com/InsertKoinIO/koin) ですが、Android開発用の拡張ライブラリであるkoin-androidを使ってAndroid Architecture ComponentsのViewModelを生成する方法についてまとめます。
基本的には下記のサイトに記載されている内容です。 https://beta.insert-koin.io/docs/1.0/quick-references/koin-android/#android-architecture-viewmodel
Koinの基本的な使い方については上記githubのREADME等をご参照ください。
Koinを用いない場合のViewModelの生成
Koinを用いない場合のAndroid Architecture ComponentsのViewModelクラスの生成方法は次のようにViewModelProviders
を使用します。
public class MyActivity extends AppCompatActivity { public fun onCreate(savedInstanceState: Bundle) { val model = ViewModelProviders.of(this).get(MyViewModel::class.java); } }
参考:https://developer.android.com/topic/libraries/architecture/viewmodel
Koinを用いる場合のViewModelの生成
Koinを用いる場合は以下のようにby viewModel()
によりインスタンス生成を行います。
通常のクラスの場合はby inject()
としますが、ViewModel
の場合はby viewModel()
を指定してください。
class LoginActivity : AppCompatActivity() { private val viewModel: MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) }
また、プロパティなどにインジェクションしない場合はgetViewModel()
関数を使用することも可能です。
override fun onCreate() { super.onCreate() val model : MyViewModel = getViewModel() }
ViewModelの為のモジュール定義
この場合のViewModel生成の為のモジュール定義は通常の場合のsingle
やfactory
ではなく次のようにviewModel
を指定します。
val viewModelModule = module { viewModel<MyViewModel>() viewModel { ParameterNeedViewModel(get()) } // パラメータが必要な場合 }
Koinでインスタンス生成時にActivityをinjectする方法
Koinを使ったをDI行った際に、Activity/Fragmentにinjectしたクラス(Presenterなど)に対してActivity/Fragmentなどをコンストラクタインジェクションする方法を調べました。
対象となるクラスの定義
今回のサンプルとしてコンストラクタパラメータとしてActivityおよびUseCaseを必要とするPresenterを定義します。 (UseCaseクラスのインスタンス生成に関しては別途モジュール定義がされているものとします)
class LoginPresenter(private val activity: Activity, private val loginUseCase: LoginUseCase) { … }
モジュールの定義
上記のLoginPresenter
を生成するためのモジュール定義は以下のようになります。
通常のモジュール定義と異なり、LoginUseCase
に関してはget()
を用いたKoinによるインスタンス生成を行いますが、Activity
に関してはfactoryブロックのパラメータとして定義します。
val presenterModule = module { factory { (activity: Activity) -> LoginPresenter(activity, get()) } }
Activity側の実装
次のようにInjectする際にパラメータとしてfactoryブロックのパラメータとして定義したActivity
のインスタンス(this)を指定する事が可能です。
class LoginActivity : AppCompatActivity() { private val loginPresenter: LoginPresenter by inject { parametersOf(this) } override fun onCreate(savedInstanceState: Bundle?) { … } … }
参考
Firebase Authenticationの実装(Android, Google認証)
Firebase Authenticationの実装を行ったので実装内容をまとめておきます。
この記事では以下の内容についてまとめています。
基本的には公式ドキュメント(https://firebase.google.com/docs/auth/android/google-signin)の内容通りの実装です。
Googleログインの統合まわりで別のページ(https://developers.google.com/identity/sign-in/android/sign-in)を案内されるのが少し分かりにくかったので自分用にまとめ直しています。
(ちょっとガーっと書いちゃったのであとで見直します。見直すべき。見直したい人生だった。)
build.gradleへの追記内容
Firebase Authentication用のモジュールとGoogle Sign-In用のモジュール(Google Play Service)が必要となるので 以下のようになります。
apply plugin: 'com.google.gms.google-services' android { … } dependencies { // Google Sign-In implementation 'com.google.android.gms:play-services-auth:16.0.0' // Firebase Authentication implementation 'com.google.firebase:firebase-core:16.0.3' implementation 'com.google.firebase:firebase-auth:16.0.3'
Firebaseのプロジェクト側での設定とgoogle-services.jsonの生成
こちらは公式ドキュメントの「準備」に手順が書いてあるので詳しくはそちらを参照してください。
Firebase Consoleにアクセスし、対象となるFirebaseプロジェクト(なければ新規作成する)にAndroidアプリを追加(パッケージ名の登録や、key storeのフィンガープリントの登録が必要)を行うとgoogle-service.json
ファイルがダウンロード可能になるので、そちらをアプリに配置します。
現在の認証状況を確認する
通常アプリで認証を要求する前に、現在の認証状況を確認し、認証済みの場合は通常時の処理、未認証の場合はログイン画面などへの遷移を行うかと思います。
Firebase Authenticationを用いて認証状況を確認する場合はFirebaseAuth
オブジェクトのcurrentUser
プロパティを取得する事で、現在認証済みの状態であるか判断できます。
currentUser
プロパティはFirebaseUser
クラスのインスタンスとなっており、ユーザー名などの情報を取得することも可能です。
なお、こちらはFirebase Authenticationの状態が認証状態にあるか否かを確認するため、Googleアカウントによる認証かどうかは問いません。
val firebaseAuth = FirebaseAuth.getInstance() if (firebaseAuth.currentUser != null) { // 認証済み } else { // 未認証 }
Google Sign-Inを要求する
Googleアカウントを用いてFirebase Authenticationを行う場合
- Google Sign-Inを使用してGoogleアカウントの認証を行い、Credentialを取得する
- 取得したCredentialをFirebaseに送信し、Firebase Authenticationを行う。
となります。その為のGoogle Sign-In要求のコードは以下のようなものになります。
(R.string.default_web_client_id
はgoogle-services.json
の配置とbuild.gradle
の記述が間違っていなければ自動生成されているはずです)
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build() val googleSignInClient = GoogleSignIn.getClient(this, gso) startActivityForResult(googleSignInClient.signInIntent, RC_SIGN_IN)
Google Sign-In結果の取得
Google Sign-Inの結果はActivityResult()
を通じて取得できます。
取得したGoogle Sign-Inの結果を用いてFirebaseAuth. signInWithCredential()
の呼び出しを行うことで、Firebase Authenticationに対してGoogleアカウントでの認証を要求します。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == RC_SIGN_IN && data != null) { try { // Google Sign In was successful, authenticate with Firebase val account = GoogleSignIn.getSignedInAccountFromIntent(data) .getResult(ApiException::class.java) val credential = GoogleAuthProvider.getCredential(account.idToken, null) firebaseAuth.signInWithCredential(credential) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { // TODO Firebase Authentication成功時の処理 } else { // TODO Firebase Authentication失敗の処理 } } } catch (e: ApiException) { // TODO Google Sign-In失敗時の処理 } } else { super.onActivityResult(requestCode, resultCode, data) } }
Resources.getSystem()知らんかった
私、Android開発に7年くらい携わっているんですが、さっきResources.getSystem()
というのでContextが取得できるのを知りました。
Android開発やってるとContextの取り回しに苦労する事が多いので、楽が出来るのかもなと思いました。
リファレンスによると
Return a global shared Resources object that provides access to only system resources (no application resources), and is not configured for the current screen (can not use dimension units, does not change based on orientation, etc).
https://developer.android.com/reference/android/content/res/Resources.html#getSystem()
と説明されているのでandroid.Rで参照できるリソースはOKだけど、アプリ内で定義したリソースは取得できない他、いろいろ出来ることも限られているということかな。 まぁ、覚えとくとそのうち役に立つかもなー。