はじめに
iOS みたいに Android のおすすめライブラリを紹介したいと思います。しかし、iOSと違って Android は実践したことはないです。。。(たぶんこれいいんじゃないかなぁ?って感じで書いてます)
リソース管理とリントツールは iOS では書いてましたが Android は標準のやつでいいと思います。
- oss-licenses-plugin (ライセンス表示)
- JakeWharton/timber (ログ出力)
- square/retrofit (通信用)
- Room (DB)
- google/dagger (DI用)
リソース管理
ビルド時に aapt ツールが R
クラスを自動で生成してくれるためリソース管理はこの標準のやつでいいと思います。(R.swift みたいなもんだと思えば)
詳細は下記
Android デベロッパー:リソースへのアクセス
リントツール
Android Studio 標準の [Analyze] > [Inspect Code] でいいと思います。
使い方詳細は下記
Android Studioには標準のやつ
こういうのもあるみたいです。detekt/detekt
oss-licenses-plugin(v0.10.2)
oss-licenses-plugin
ライブラリを使うとどうしてもしないといけないのがライセンス表示!!ライセンス表示をサクッとやってくれるのが oss-licenses-plugin !!
使い方は簡単
- プロジェクトの build.gradle に追加
- モジュールの build.gradle に追加
- Activity 表示
これだけ!!
1. プロジェクトの build.gradle に下記追加
1 2 3 4 5 6 |
dependencies { ・ ・ ・ classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2' } |
2. モジュールの build.gradle に追加
1 2 3 4 5 6 7 8 |
apply plugin: 'com.google.android.gms.oss-licenses-plugin' dependencies { ・ ・ ・ implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' } |
3. Activity 表示
1 2 3 |
// これで画面タイトルを設定できる // OssLicensesMenuActivity.setActivityTitle("ライセンス") startActivity(Intent(this, OssLicensesMenuActivity::class.java)) |
こんな感じで表示される
一覧 | ライセンス |
---|---|
なんかめっちゃ細かく表示されるけどないよりはいいはず(たぶん)
詳細は下記参考
- Android 用 Google API:Including Open Source Notices
- com.google.gms:oss-licenses を使ってオープンソースライセンスを表示する
他にもライセンス表示できるライブラリは色々あるみたいです
JakeWharton/timber(4.7.1)
JakeWharton/timber
ログ出力のためのやつ。ログ出力は標準のやつを使えばいいけどこれをそのまま使うとリリースビルド時もログ出力されるのでそのへんを簡単に制御するために使います。
使い方は簡単
1. モジュールの build.gradle に追加
モジュールの build.gradle に下記追加
1 2 3 4 5 6 |
dependencies { ・ ・ ・ implementation 'com.jakewharton.timber:timber:4.7.1' } |
2. Treeの設定
Application
クラスの onCreate
などで下記設定を行う。これでDebugビルド時のみログ出力される
1 2 3 4 5 6 7 8 |
class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(DebugTree()) } } } |
Application
クラスを追加する場合はマニフェストに下記を追記する。
1 2 3 4 5 6 |
<application android:name=".MyApplication" ・ ・ ・ </application> |
3. ログ出力
使い方は表示の Log
と同じ。tag
を付けなくてもいいので楽!!
1 2 3 4 5 6 |
Timber.v("Verbose") Timber.d("Debug") Timber.i("Info") Timber.w("Warn") Timber.e("Error") Timber.tag("TAG").d("Debug") |
こんな感じで出力される
1 2 3 4 5 6 |
V/MainActivity$onCreate: Verbose D/MainActivity$onCreate: Debug I/MainActivity$onCreate: Info W/MainActivity$onCreate: Warn E/MainActivity$onCreate: Error D/TAG: Debug |
タグを設定しない場合はクラス名とメソッド名がタグになるみたいです。
square/retrofi(2.9.0)
square/retrofit
通信するならこれ!
使い方は基本的にこの記事 Android Retrofit2 with Kotlin に丁寧に書かれていました。
- モジュールの build.gradle に追加
- HTTP API の interface 定義
- 呼び出し処理
1. モジュールの build.gradle に追加
1 2 3 4 5 6 |
dependencies { def retrofit_version = "2.9.0" implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" } |
コンバーターは GSON 以外も色々あるみたいです。(GSON は Swift でいう Codable のようなものです)
- Gson: com.squareup.retrofit2:converter-gson
- Jackson: com.squareup.retrofit2:converter-jackson
- Moshi: com.squareup.retrofit2:converter-moshi
- Protobuf: com.squareup.retrofit2:converter-protobuf
- Wire: com.squareup.retrofit2:converter-wire
- Simple XML: com.squareup.retrofit2:converter-simplexml
- JAXB: com.squareup.retrofit2:converter-jaxb
- Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
2. HTTP API の interface 定義
一番単純な設定が下記。これで指定して Retrofit
インスタンス生成時に指定した baseURL
に対して user/list
の GET 通信を行うことができます。
1 2 3 4 |
interface HogeService { @GET("users/list") fun listRepos(@Path("user") user: String): Call<List<Repo>> } |
クエリ設定
アノテーションを使ってパスとクエリを下記のように指定することもできる。@Query は自動でエンコードされる(。。。たぶん)
1 2 3 4 5 6 7 8 9 10 11 12 |
@GET("group/{id}/users") fun groupList( @Path("id") groupId: Int, @Query("sort") sort: String ): Call<List<User>> // Mapも使える @GET("group/{id}/users") fun groupList( @Path("id") groupId: Int, @QueryMap options: Map<String, String> ): Call<List<User>> |
POST
POST する場合は @POST アノテーションを使う。@Body アノテーションは Retrofit
インスタンス生成時に指定した addConverterFactory
に適したやつで変換してくれる。
変換したくない場合は @RequestBody アノテーションを指定する。
1 2 3 4 |
@POST("users/new") fun createUser(@Body user: User): Call<User> data class User(val id: Int, val name: String) |
他にも @FormUrlEncoded, @Field, @Multipart アノテーションとかがあるみたいです。
ヘッダー設定
@Headers アノテーションを利用すればヘッダーの設定もできる
1 2 3 4 5 6 7 |
@Headers("Cache-Control: max-age=640000") @GET("widget/list") fun widgetList(): Call<List<Widget>> @Headers(["Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-Sample-App"]) @GET("users/{username}") fun getUser(@Path("username") username: String): Call<User> |
動的に設定する場合は下記
1 2 3 4 5 6 |
@GET("user") fun getUser(@Header("Authorization") authorization: String): Call<User> // Mapも使える @GET("user") fun getUser(@HeaderMap headers: Map<String, String>): Call<User> |
3. 呼び出し処理
単純に呼び出し処理を書くとこんな感じ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
interface HogeService { @GET("users/list") fun listRepos(@Path("user") user: String): Call<List<Repo>> } data class Repo(val id: Int, val name: String) // 呼び出し val retrofit = Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .baseUrl("http://example.com/") // Put your base URL .build() val service = retrofit.create(HogeService::class.java) service.listRepos("hoge").enqueue(object : retrofit2.Callback<List<Repo>> { override fun onFailure(call: retrofit2.Call<List<Repo>>?, t: Throwable?) { } override fun onResponse(call: retrofit2.Call<List<Repo>>?, response: retrofit2.Response<List<Repo>>) { if (response.isSuccessful) { response.body()?.let { } } } }) |
これをもうちょっと使いやすくしたい。。。
参考
- Retrofit
- Android Retrofit2 with Kotlin
- 【Android】【Retrofit】Retrofit 2.0.1使い方メモとハマりどころメモ - Tumbling Dice
Room(2.2.5)
Android JetPack にある Room
DBならこれ!!もしくは iOS と合わせて Realm でもいいかもしれない。
- モジュールの build.gradle に追加
- Entity定義
- Dao定義
- RoomDatabaseクラス定義
各コンポーネントはこんな感じ
コンポーネント | 説明 |
---|---|
Entity | DBのテーブルを表すクラス |
Dao | DBのデータ操作を行うクラス |
RoomDatabase | DAOを生成するクラス |
引用:Android デベロッパー:Room を使用してローカル データベースにデータを保存する
1. モジュールの build.gradle に追加
1 2 3 4 5 6 7 8 9 |
apply plugin: 'kotlin-kapt' dependencies { def room_version = "2.2.5" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" } |
他にもこんなのがあるらしいけど今回は割愛
1 2 3 4 5 6 |
// optional - RxJava support for Room implementation "androidx.room:room-rxjava2:$room_version" // optional - Guava support for Room, including Optional and ListenableFuture implementation "androidx.room:room-guava:$room_version" // Test helpers testImplementation "androidx.room:room-testing:$room_version" |
2. Entity定義
Entity が DB のテーブルに相当する。定義は下記のように @Entity アノテーションをつける。
1 2 3 4 5 6 |
@Entity data class User( @PrimaryKey val id: Int, val firstName: String?, val lastName: String? ) |
@Entity は少なくとも1つの主キーが必要であり単純なものは上記のような実装になる。DB のテーブル名はそのままクラス名になりカラム名はフィールド名になる。
テーブル
テーブル名をクラス名と別にしたい場合は下記のように tableName
プロパティを利用する。(SQLite のテーブル名は大文字小文字の区別はないらしいです)
1 2 3 4 |
@Entity(tableName = "users") data class User ( @PrimaryKey val id: Int, ) |
@Entity には tableName
プロパティ以外にも foreignKeys
, ignoredColumns
, indices
, inheritSuperIndices
, primaryKeys
があるみたいです。
詳細は下記
Android デベロッパー:Entity
主キー
主キーを自動生成したい場合は下記のようにできる
1 2 3 4 |
@Entity data class User ( @PrimaryKey(autoGenerate = true) val id: Int, ) |
複合主キーの場合は下記のようにする
1 2 3 4 5 |
@Entity(primaryKeys = arrayOf("firstName", "lastName")) data class User( val firstName: String?, val lastName: String? ) |
カラム
カラム名をフィールド名と別で付けたい場合は @ColumnInfo アノテーションを利用する。
1 2 3 4 5 6 |
@Entity data class User ( @PrimaryKey val id: Int, @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? ) |
DB に特定フィールドを保存したくない場合は @Ignore を利用する。
1 2 3 4 5 6 7 |
@Entity data class User( @PrimaryKey val id: Int, val firstName: String?, val lastName: String?, @Ignore val picture: Bitmap? ) |
@ColumnInfo には name
以外にも collate
, defaultValue
, index
, typeAffinity
があるみたいです。
詳細は下記
Android デベロッパー:ColumnInfo
埋め込みオブジェクト
@Embedded を利用すればオブジェクトの埋め込みができる。下記のように User
を定義すると DB の user テーブルには id、firstName、street、state、city、post_code というカラムができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
data class Address( val street: String?, val state: String?, val city: String?, @ColumnInfo(name = "post_code") val postCode: Int ) @Entity data class User( @PrimaryKey val id: Int, val firstName: String?, @Embedded val address: Address? ) |
同じ型で2つ以上埋め込む場合は @Embedded(prefix = "loc_")
のようにプレフィックス設定で複数埋め込みも可能らしい。
1対1のリレーション
User と Library に1対 1のリレーションがある場合下記のように User
と Library
を定義し @Relation を使って UserAndLibrary
を定義する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int ) @Entity data class Library( @PrimaryKey val libraryId: Long, val userOwnerId: Long ) data class UserAndLibrary( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userOwnerId" ) val library: Library ) |
データ取得は @Transaction を使い Dao に追加する
1 2 3 |
@Transaction @Query("SELECT * FROM User") fun getUsersAndLibraries(): List<UserAndLibrary> |
1対多のリレーション/多対多のリレーション
1対多のリレーションの場合は下記のように定義する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@Entity data class User( @PrimaryKey val userId: Long, val name: String, val age: Int ) @Entity data class Playlist( @PrimaryKey val playlistId: Long, val userCreatorId: Long, val playlistName: String ) data class UserWithPlaylists( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List<Playlist> ) /// Dao data class UserWithPlaylists( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List<Playlist> ) |
多対多のリレーションの場合は下記のように定義する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
@Entity data class Playlist( @PrimaryKey val playlistId: Long, val playlistName: String ) @Entity data class Song( @PrimaryKey val songId: Long, val songName: String, val artist: String ) @Entity(primaryKeys = ["playlistId", "songId"]) data class PlaylistSongCrossRef( val playlistId: Long, val songId: Long ) data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = @Junction(PlaylistSongCrossRef::class) ) val songs: List<Song> ) data class SongWithPlaylists( @Embedded val song: Song, @Relation( parentColumn = "songId", entityColumn = "playlistId", associateBy = @Junction(PlaylistSongCrossRef::class) ) val playlists: List<Playlist> ) // Dao @Transaction @Query("SELECT * FROM Playlist") fun getPlaylistsWithSongs(): List<PlaylistWithSongs> @Transaction @Query("SELECT * FROM Song") fun getSongsWithPlaylists(): List<SongWithPlaylists> |
リレーションの詳細は下記参考
Android デベロッパー:オブジェクト間のリレーションを定義する
ビューの作成
Room ではビューも使えるらしい。下記のように @DatabaseView アノテーションを使う。
1 2 3 4 5 6 7 8 9 |
@DatabaseView("SELECT user.id, user.name, user.departmentId," + "department.name AS departmentName FROM user " + "INNER JOIN department ON user.departmentId = department.id") data class UserDetail( val id: Long, val name: String?, val departmentId: Long, val departmentName: String? ) |
DB に関連付けるため RoomDatabase
に views
プロパティを追加する
1 2 3 4 5 |
@Database(entities = arrayOf(User::class), views = arrayOf(UserDetail::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } |
@DatabaseView には value
, viewName
プロパティがあるみたいです。
詳細は下記
Android デベロッパー:DatabaseView
3. Dao定義
DB へのアクセスはすべて Dao (Data access objects) を使用する。Dao は下記のようにインターフェースにアノテーションを付けて実装してやると Room が自動で処理を実装してくれるみたいです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> @Query("SELECT * FROM user WHERE uid IN (:userIds)") fun loadAllByIds(userIds: IntArray): List<User> @Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1") fun findByName(first: String, last: String): User @Insert fun insertAll(vararg users: User) @Delete fun delete(user: User) } |
挿入
データ挿入には @Insert アノテーションを利用する。
1 2 3 4 5 6 7 8 9 10 11 |
@Dao interface MyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUsers(vararg users: User) @Insert fun insertBothUsers(user1: User, user2: User) @Insert fun insertUsersAndFriends(user: User, friends: List<User>) } |
データを挿入すると rowId を返却することもできる。データが1つの場合は Long
が返却され、複数挿入の場合は List<Long>
で返却される。
挿入データがコンフリクトした場合の設定は onConflict
プロパティで設定できる。下記3種類。
- OnConflictStrategy.ABORT
デフォルトの設定。ロールバックする。 - OnConflictStrategy.REPLACE
新規データと入れ替える。 - OnConflictStrategy.IGNORE
既存データを残す。
entity
プロパティもあるみたいだけど使い道がよくわからない。。。
更新
データ更新には @Update アノテーションを利用する。Entity の主キーをみて値が更新される。
1 2 3 4 5 |
@Dao interface MyDao { @Update fun updateUsers(vararg users: User) } |
更新された行数を Int
で返すこともできるようです。Insert
同様 onConflict
、entity
プロパティが設定できるようです。
削除
データ削除には @Delete アノテーションを利用する。Entity の主キーをみて値が削除される。
1 2 3 4 5 |
@Dao interface MyDao { @Delete fun deleteUsers(vararg users: User) } |
削除された行数を Int
で返すこともできるようです。entity
プロパティが設定できるようです。
クエリ
@Query アノテーションを使うと DB の読み書き処理ができます。クエリに問題がある場合はコンパイル時にエラーチェックされるので安心!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Dao interface MyDao { @Query("SELECT * FROM user") fun loadAllUsers(): List <User> @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): List <User> @Query("SELECT * FROM user WHERE first_name LIKE :search " + "OR last_name LIKE :search") fun findUserWithName(search: String): List<User> @Query("SELECT * FROM user WHERE region IN (:regions)") fun loadUsersFromRegions(regions: List<String>): List<User> } |
: minAge
のように引数を渡すこともできる。
下記のようにすると特定カラムのみ返すこともできる。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 返すカラムのクラスを定義する data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? ) @Dao interface MyDao { // NameTupleを返すようにする @Query("SELECT first_name, last_name FROM user") fun loadFullName(): List<NameTuple> } |
4. RoomDatabaseクラス定義
Entity
と Dao
を定義した後は下記のように RoomDatabase
を継承した抽象クラスを作成する。
1 2 3 4 |
@Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } |
@Database のプロパティには entities
, exportSchema
, version
, views
があるみたいです。
あとは下記のように Dao
を生成して DB 操作を行う。
1 2 3 4 5 6 7 8 |
val db = Room.databaseBuilder( applicationContext, AppDatabase::class.java, "database-name" // なにか適当にDB名 ).build() val userDao = db.userDao() val newUser = User(0, Date().time.toString(), Date().time.toString(), null) userDao.insert(newUser) |
Room の処理は UI スレッドで呼んじゃダメらしいので Coroutines
とか使わないといけないんだとか。(Coroutines
についても調べないと。。。)
DBの中身を確認する
- [View] > [Tool Windows] > [Device File Explorer] から Device File Explorer を開く
- data > data > アプリ名 > databases を開く
- フォルダ内のdatabase_name, database_name-shm, database_name-wal を選択
- 右クリックのSave As... で PC に保存する
あとはターミナルで sqlite コマンドを使うか DB Browser for SQLite で開く
参考
- Android デベロッパー:Room 永続ライブラリ
- Android デベロッパー:Room を使用してローカル データベースにデータを保存する
- [Android]Room を使ったサンプルと解説
- Roomを使用した際に詰まったこと
Dagger(2.28.3)
google/dagger
DI 用!! Dagger2 と呼ばれてるやつらしい。元々 Square 社が開発してたのを Google が fork して開発してるらしい。(参考:Dagger Android Hiltが神)
鋭意作成中。。。わからなすぎて草
参考
おまけ〜build.gradleのバージョン指定〜
ライブラリのバージョンは特に何も考えずにこんな感じで書いてました
1 2 3 4 5 6 |
dependencies { def retrofit_version = "2.9.0" implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" } |
別にこの書き方でもいいですがいい書き方を見つけたのでそれを採用したいと思います。
プロジェクトの build,gradle にバージョンはまとめます!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
buildscript { ・ ・ ・ ext.buildConfig = [ 'compileSdk': 29, 'minSdk': 23, 'targetSdk': 29, 'buildTools': '29.0.3', 'versionCode': 1, 'versionName': '1.0', ] ext.versions = [ 'kotlin': '1.3.72', 'retrofit': '2.9.0', 'room': '2.2.5', ] |
モジュールの build,gradle はこんな感じ!一箇所にまとまってるのでスッキリした気がする。(buildConfig もいるかはわからないけどとりあえずこれでいきます)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
android { compileSdkVersion buildConfig.compileSdk buildToolsVersion buildConfig.buildTools defaultConfig { applicationId "com.example.samplelib" minSdkVersion buildConfig.minSdk targetSdkVersion buildConfig.targetSdk versionCode buildConfig.versionCode versionName buildConfig.versionName } ・ ・ ・ } dependencies { ・ ・ ・ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" implementation "com.squareup.retrofit2:retrofit:$versions.retrofit" implementation "com.squareup.retrofit2:converter-gson:$versions.retrofit" implementation "androidx.room:room-runtime:$versions.room" implementation "androidx.room:room-ktx:$versions.room" kapt "androidx.room:room-compiler:$versions.room" } |
参考:あなたの build.gradle バージョン記述、きもいです。
おわりに
iOS と違って Carthage, CocoaPods, Swift Package Manager とか混在しないのはいい!!
とりあえず iOS 参考に同じような機能のライブラリを書いてみたけど Android の開発経験があまりないのでまたのちのち更新するかも。
他に気になったライブラリ
- permissions-dispatcher/PermissionsDispatcher
パーミッションの処理をいい感じにしてくれる - square/picasso OR bumptech/glide
URL 指定で ImageView に画像表示できる
コメント