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から実装されています。

ListAdapterAsyncListDifferをラップした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のイメージを書き込む

$ 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との接続が可能ですが、最初にWiFiSSIDおよびパスワードを設定する必要があるため、一度有線接続する必要があります。

ご利用のルーターEthernetのポートがあれば、そちらに接続すれば良いですが、存在しない場合や物理的に届かないなどの場合は、Macと直接Ethernet接続を行い、「インターネット共有」等を機能を用いてRaspberry PiIPアドレスの割当を行ってください。
MacEthernetポートが存在しないモデルの場合、別途thunderbolt to Ethernetアダプタなどを購入してください。

参考:Macで「インターネット共有」を行う方法
https://support.apple.com/ja-jp/HT203819

Raspberry PiIPアドレスが割り当てられた場合、次のようにadbコマンドを用いて接続することが可能です。

$ adb connect <インターネット共有などのDHCP機能で割り当てられたIPアドレス>

この時使用するIPアドレスについては以前はAndroid Thingsの起動画面に表示されていたようなのですが、ちょっと見当たらなかったので…なんとか…頑張っていただくしか… また、MDNSをサーポートする環境では以下のコマンドで接続することが可能なようですので、とりあえずこちらで試してみてください。

$ adb connect Android.local 

無事、接続が成功すれば有線接続はOKです。

Raspberry PiWiFiを有効にする

次にRaspberry Piに使用するWiFiSSIDおよびパスワードを入力して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 PiWiFi設定を行ったので、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を起動し、実際にアプリを実行できるか確認します。

  1. File > New > New Project から新規アプリウィザードを起動します。
  2. 「Target Android Device」の画面にて、「Android Things」にチェックを入れます。 *※プロジェクトを簡単にする為、デフォルトでチェックが入っている「Phone and Tablet」のチェックは外します
  3. 「Add Activity to Things」の画面で「Android Things Empty Activity」を選択します。
  4. 最後にお好みの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としてActivityScopeFragmentScopeのようなカスタム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生成の為のモジュール定義は通常の場合のsinglefactoryではなく次のように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?) {
        …
    }
    
    …
}

参考

https://beta.insert-koin.io/docs/1.0/documentation/koin-core/index.html#_defining_an_injection_parameter

Firebase Authenticationの実装(Android, Google認証)

Firebase Authenticationの実装を行ったので実装内容をまとめておきます。

この記事では以下の内容についてまとめています。

  • Android
  • Kotlinで実装
  • Googleアカウントを用いた認証

基本的には公式ドキュメント(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を行う場合

  1. Google Sign-Inを使用してGoogleアカウントの認証を行い、Credentialを取得する
  2. 取得したCredentialをFirebaseに送信し、Firebase Authenticationを行う。

となります。その為のGoogle Sign-In要求のコードは以下のようなものになります。
(R.string.default_web_client_idgoogle-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だけど、アプリ内で定義したリソースは取得できない他、いろいろ出来ることも限られているということかな。 まぁ、覚えとくとそのうち役に立つかもなー。