ベータコンピューティングの活動や技術、開発のこだわりなどを紹介するブログです。



Androidアプリケーション開発を経験して学んだこと#フレームワーク&ライブラリ編

この記事について

こんにちは、Beta Computing株式会社でアルバイトをしていますAndroidアプリケーション開発担当です。
前回の「Androidアプリケーション開発を経験して学んだこと#ツール編」に引き続き、学んだことを書いていきます。今回はフレームワークやライブラリについてです。

学んだこと

Gitにおけるブランチ名

私はアプリケーション開発に関わる以前、Gitに関する知識が浅く、ブランチを分けることすらしていませんでした。
しかし開発となるとそうはいかず、ブランチを適切に作成し進める必要があります。
そこで問題になったのがブランチの命名方法です。複数人で開発を行う際はブランチの命名は適当では無く、社内で共通ルールを決めて命名する場合があります。
例えば以下の様な命名方法があります。

  • git-flowモデル
    • masterブランチ - メインブランチ
    • developブランチ - 次回リリースする際にmasterマージするブランチ
    • featureブランチ - 新しい機能を開発する際のブランチ

その他については以下のサイトが勉強になりました。

参照:Gitのブランチモデル(git-flow, GitHub Flow, GitLab Flow)のブランチ名まとめ


ドキュメンテーションコメント

過去の私はとにかくいろいろな場所にドキュメンテーションコメントと呼ばれるものを書いていましたが、その役割についてはあまり理解していませんでした。今回、先輩社員の方からアドバイスをいただいたので、それについてまとめたいと思います。

まず、ドキュメンテーションコメントとはクラスや変数・メソッドなどにそれらの概要や目的を説明するために書くコメントのことです。 Javaなどではドキュメンテーションコメントを書いておくことによって、クラスの仕様書のようなもの(Javadocなど)を簡単に作成できたりします。
例えば以下の様に書くことが出来ます。

/**
 * クラスの説明
 **/
class TestClass {
  /**
   * メンバの説明
   **/
  int value = 0;
}

ドキュメンテーションコメントはとても便利なのですが、常にどこでも使えばよいというわけではないようです。
privateメソッドなどにはドキュメンテーションコメントではなく、普通のコメントを使用します。

/**
 * クラスの説明
 **/
class TestClass {
  // privateメソッドの実装内容(通常コメントを使用する)
  private void function(int value) {
    ...
  }
}

ドキュメンテーションコメントについては以下のサイトが勉強になりました。

参照:Javadoc ドキュメンテーションコメントの書き方


ktlint(コードスタイルのチェック)

ktlintとはKotlinのコードスタイルをチャックしてくれるライブラリです。
今回はそのktlintをGradleプラグインとして導入できる「org.jlleitschuh.gradle.ktlint」を使用しました。
このライブラリを導入した後に、コマンドラインで「./gradlew ktlintCheck」を実行するだけで、プロジェクト内のコードをチェックしてくれます。下の画面をご覧ください。

画像のように「BUILD SUCCESSFUL」と表示されればコードスタイルに問題が無かったということになります。
逆にビルドに失敗すると、コードスタイルが間違っているところを提示してくれます。
ビルドが成功するまでコードを修正することでコードを綺麗に保てます。

ktlintについては以下のサイトが勉強になりました。

参考:【Android】コードスタイルをチェックする ktlint の導入方法


Timber(ログの出力方法)

TimberとはAndroidアプリケーションにおけるログ出力を便利にするライブラリです。
まず、普通にAndroid studioでログを出力させるには、以下のようにします。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.d("MainActivity","ログ出力テスト")
    }
}

すると以下の様にログが出力されます。

ここで先ほどのコードをみていただきたいのですが、Log.d() 内には二つの引数を渡す必要があります。
左側にはタグを、右側にはメッセージを渡しています。そして出力では左側の方にタグが、右側の方にメッセージが出力されています。

それに対して、Timberを使用すると以下の様に書くことが出来ます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Timber.d("ログ出力テスト")
    }
}

Timberを使うと、ログ出力時にフォーマット書式を組み立ててくれたり、ビルドバリアントごとにログ出力の設定 (Tree) をカスタマイズできたり、ログ出力の書式に関してLintによる警告を追加してくれたりします。
特に、デバッグ用途でログを使用する際は、ライブラリ標準のDebugTreeというTree (ログの出力制御ロジック) を使用するのが一般的ですが、このDebugTreeを使用している場合、わざわざ自分でログのタグ名を指定せずとも、呼び出し元のクラス名を元に勝手にタグ名を設定してくれたりします。
ただし、このDebugTreeはパフォーマンスの関係上リリースビルドでは使用しないことが多く、注意が必要です。

また、Timberを導入すると、元々のLogを使ってログを出力していた箇所を指摘してくれます。以下の画像をご覧ください。

ここからTimberへの書き換えはワンクリックで可能です。

大変便利なTimberですが、導入は以下の手順で行います。 まず、build.gradleファイルに次の記載が必要です。

dependencies {

    implementation("com.jakewharton.timber:timber:5.0.1")
}

追記した後は右上の「Snyc Now」を押して同期します。
同期が完了した次は、Applicationクラスの設定します。
以下の様なクラスを追加で作成します。「MyApplication」は適宜命名してください。重要なのはApplicationクラスを継承していることです。

import android.app.Application
import timber.log.Timber

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
    }
}

こちらでTreeにDebugTreeを設定しています。
ここで自作したTreeを設定することも可能です。
Treeを自作することで、出力時のルールの変更、ログの外部出力といったカスタマイズを追加することが出来ます。
※Applicationクラスについては以下のサイトが簡潔に説明されていました。

参照:Applicationクラスとは何か?

Applicationクラスを継承したクラスを作成した後は、プロジェクトの「AndroidManifest.xml」ファイルに以下の記述を追加します。

    <application
        android:name = ".MyApplication"

以上がTimberの使い方および導入方法です。 Timberについては以下のサイトが勉強になりました。

参照:AndroidのLogとTimberについて


Hilt(DIコンテナ)

Hiltは依存性(オブジェクト)の注入を簡素化することが出来るライブラリです。
依存性の注入とは、あるオブジェクトや関数が、依存する他のオブジェクトや関数を受け取るデザインパターンです。
これにより、プログラムの柔軟性が向上し、テストが容易になります。 導入方法や使い方は説明するとそれだけで記事が書けてしまいそうなので、今回は割愛します。

Hiltについては以下のサイトが勉強になりました。

参照:Hiltを使ってみよう


Ktor(Http通信)と Kotlinx.serialization.json

Ktorとは、Kotlinで動くWebアプリケーションフレームワークです。
KtorのHttpクライアントを使うことで、Http通信を簡単に行うことができます。 さらに、後述する「Kotlinx.serialization.json」と組み合わせることで、パースも簡単かつ高速に行うことが出来ます。

Kotlinx.serialization.jsonはJsonのパースを簡単かつ高速に行うことが出来るライブラリです。
Httpクライアントと一緒に使うことで、Http通信を簡単かつ高速に行えます。

KtorのHttpクライアントとKotlinx.serialization.jsonを使うには、モジュールのbuild.gradleファイルに次の記載が必要です。

plugins {
    // Kotlin Serialization関連
    id 'org.jetbrains.kotlin.plugin.serialization'
}

dependencies {

    // Ktor(ケイター)関連
    implementation "io.ktor:ktor-client-core:2.3.2"
    implementation "io.ktor:ktor-client-cio:2.3.2"
    implementation "io.ktor:ktor-client-json:2.3.2"
    implementation "io.ktor:ktor-client-serialization:2.3.2"
    implementation "io.ktor:ktor-client-content-negotiation:2.3.2"
    implementation "io.ktor:ktor-serialization-kotlinx-json:2.3.2"

    // Kotlin Serialization関連
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
}

また、プロジェクトのbuild.gradleファイルにも以下の記載が必要です。

plugins {
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.20' apply false
}

Kotlinx.serialization.jsonにいては、詳しい設定や使い方は以下のサイトが大変わかりやすく説明しています。

参照:【Kotlin】Kotlin Serialization で JSON をパースする

設定は以上です。
次に使い方ですが、APIコールしてデータを受け取りログに出力するアプリケーションを例に考えてみます。 まずAPIコールには以下のユーザー情報をランダムに作成するAPIを使用します。

参照:RANDOM USER GENERATOR

このAPIを使うと以下の様なレスポンスが返ってきます。

{
    "results": [
        {
            "gender": "female",
            "name": { // 名前
                "title": "Ms",
                "first": "Rena",
                "last": "Großmann"
            },
            "location": { // 住所
                "street": {
                    "number": 7366,
                    "name": "Parkstraße"
                },
                "city": "Waghäusel",
                "state": "Bayern",
                "country": "Germany",
                "postcode": 11878,
                "coordinates": {
                    "latitude": "-54.8818",
                    "longitude": "149.4619"
                },
                "timezone": {
                    "offset": "-3:00",
                    "description": "Brazil, Buenos Aires, Georgetown"
                }
            },
            "email": "rena.grossmann@example.com",

// 以下情報が続く...

このレスポンスをパースしてログに出力してみます。 まずはこのJsonをパースするためのデータクラスを作成します。
「Kotlinx.serialization.json」を使ったパースでは、受け取るJsonの構造と同じ構造をしたデータクラスを作るだけでその型としてJsonデータをパースして受け取ってくれます。
さらに、Jsonの必要な一部のデータのみを受け取ることも可能です。

では、先ほどのレスポンスから名前と住所のみを受け取るデータクラスを定義してみましょう。 以下が受け取る対象です。

{
    "results": [
        {
            //...
            "name": {
                "title": "Miss",
                "first": "Naja",
                "last": "Andersen"
            },
            "location": {
                //...
                "city": "Stenderup",
                "state": "Syddanmark",
                "country": "Denmark",
                //...
            },
            //...
        }
    ],
    //...
}

以上の内容をデータクラス化します。まずは以下のコードをご覧ください。

package com.example.myapplication

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UserJson (
    @SerialName("results") val results : List<Result>?
)

@Serializable
data class Result (
    @SerialName("name") val name: Name?,
    @SerialName("location") val location: Location?
)

@Serializable
data class Name (
    @SerialName("title") val title: String?,
    @SerialName("first") val first: String?,
    @SerialName("last") val last: String?
)

@Serializable
data class Location(
    @SerialName("city") val city: String?,
    @SerialName("state") val state: String?,
    @SerialName("country") val country: String?
)

Jsonの階層構造をそのままデータクラスで表現しています。
Jsonとデータクラスを紐付けるには@Serializable アノテーションをそれぞれのクラスに付与します。
@SerialName アノテーションは、変数名とJsonにおけるキーの値が異なる場合に必要になります。

次に以下の様なクラスを用意します。
※サンプルコードのため細かなエラーハンドリングは割愛しています。

package com.example.myapplication

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import timber.log.Timber

class UserQueryService {
    // HttpClientおよびJsonパーサーを使い回します。
    private val _client: HttpClient by lazy {
        HttpClient(CIO) {
            install(ContentNegotiation) {
                json(
                    Json {
                        ignoreUnknownKeys = true
                        coerceInputValues = true
                    }
                )
            }
        }
    }

    suspend fun getRandomUser() {
        val request = "https://randomuser.me/api/"

        val result: UserJson
        try {
            val response: HttpResponse = _client.get(request)
            //  TODO: 今回は割愛しますが、エラーハンドリングを行います。

            result = response.body()
        } catch (ex: IOException) {
            Timber.e("通信エラー")
            //  TODO: 今回は割愛しますが、エラーハンドリングを行います。
        }
        Timber.d("パース成功")
        Timber.d(result.toString())
    }
}

このクラスにはメンバーに変数の_client と、メソッドのgetTandomUser() があります。

_client はHttpクライアントとJsonパーサーの役割を持っています。
この変数は通信処理が起こるたびにインスタンスを作り直すのでは無く、メンバーとして宣言し使い回す方が効率的です。

getTandomUser()_client を用いて実際にHttp通信とJsonのパースを実行します。
このように記述することで、APIコールの結果のレスポンスボディがUserJson型にパースされ、APIの処理結果を取り扱うことが出来ます。 この関数をコルーチンを用いて実行すると以下の結果が得られました。

  UserJson(result=[Result(name=Name(title=Ms, first=Namratha, last=Uchil), location=Location(city=Thoothukudi, state=Tripura, country=India))])

ちゃんとパース出来ているようです。

以上がHttpクライアントとKotlinx.serialization.jsonを使ったHttp通信とJsonパースです。
このコードを動かすにはコルーチンを使った非同期処理の実装が必要ですが、コルーチンについては次の設計編で紹介します。

まとめ

今回はAndroidアプリケーションの開発を通して、学んだことのうちフレームワークとライブラリに関するものを集めました。フレームワークなどを扱うにはフレームワークそのもの以外の知識も要求され、使いこなすのことは簡単ではありませんでした。また、まだまだ知らない部分や理解していないことも多く、さらなる勉強が必要であると感じています。