見出し画像

【Jetpack Compose】Clean architectureで作るUnsplash画像検索アプリ

こんにちは、まっこりです。
今回の記事はClean ArchitecutureでAndroidアプリを作成するチュートリアルとなっており、Clean Architechtureの実装方法を学ぶことができる内容となっています。

このチュートリアルは、初心者向けではなく、少なくとも一つはAndroidアプリの開発経験があるような初級者、初中級者向けのものとなっています。
Udemyの方で、初心者の方向けのコースを出していますので、初心者の方はこちらからみていただけると幸いです。

このコースのソースコードはGitHubの方に公開していますので、途中でわからなくなった場合は参照ください。

Clean Architectureとは(あくまで私の認識)

Clean Architectureは私の認識では、依存関係の方向の制御コードの細分化によって、機能追加や修正に強いアプリを作れるアーキテクチャです。

依存関係の方向の制御

Clean Architectureでは、アプリが提供する価値をドメインと呼び、そのドメインを実装しているクラスに他のクラスが依存するように依存関係を構築していきます。
アプリが提供する価値が変わる頻度は少ないので、仕様変更があったとしても影響を受けずらいドメインに他のクラスが依存するようにすることによって、仕様変更に強いコードベースにすることができます。

依存関係の方向を制御するために、Clean Architecutureの実装ではインターフェースを使用していきます。

コードの細分化

システムを一つの大きなクラスによって実現するのではなく、機能や関心といったものによって細かく分割し、それを適切にパッケージへと配置していくことによって、機能追加などでのコードの変更による影響箇所を限定し、コードの変更をしやすくすることができます。

今回作成するアプリについて

Unsplashという商用利用可能な画像をダウンロードできるサイトのAPIを使って画像検索アプリを作っていきます。

アプリの構成としては、画像検索画面と画像詳細画面の二画面構成になっています。

画像検索画面

画像検索画面で画像を検索し、その検索結果から画像を選択すると、画像詳細画面に遷移します。

画像の検索結果として、「画像の説明、likeの数、撮影者の名前」を持ったUIコンポーネントを一覧表示していきます。

画像検索画面

画像詳細画面

画像詳細画面では、画像検索結果で表示していた、画像の説明、likeの数、撮影者の名前に加えて、ダウンロード数、撮影したカメラ、撮影場所といった情報を表示していきます。

画像詳細画面

1. プロジェクトの新規作成

それではアプリを作っていきましょう。まずは、プロジェクトの新規作成からしていきます。

テンプレートはEmptyComposeActivityを選択してください。

プロジェクト名は「UnsplashClient」として、プロジェクトを作成してください。

選択したテンプレートに沿って、プロジェクトが作成されたら不要なテンプレートコードを削除してしまいましょう。

MainActivityのGreeting関数とDefaultPreview関数を削除し、Greetingを呼び出している箇所も削除してしまいましょう。以下の二つの関数とMainActivity内のGreeting関数を呼び出している箇所を削除してください。

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    UnsplashClientTheme {
        Greeting("Android")
    }
}

2. パッケージ構造の整理

clean architectureに沿った形で、クラスやファイルを整理するために、まずはパッケージ構造を作ってしまいます。以下のような構造になるよう、ルートパッケージにパッケージを作成していってください。(わかりずらい場合はセクションの最後にAndroid Stuidoのキャプチャがあるのでそちらを見てください。)

data -> remote & repository
di
common
domain -> model & repository & use_case
presentation

diパッケージはHiltによる依存関係注入定義クラスを配置するためのパッケージです。commonはアプリで広く使用する共通系のクラスを配置するためのパッケージです。そのほかのパッケージには以下のような形でファイルを追加していきます。

Presentation:
FragmentやActivityといったUI関連のクラスを追加していきます。

domain:
UseCas, Entity, Repositoryのインターフェース、に該当するクラスやインターフェースを追加していきます。

data:
Repository, DataSourceに該当するクラスを追加していきます。

以上のようにパッケージを作ったら、ui.themeパッケージと、MainActivityをpresentationパッケージの中に移動させてください。

以下のようなパッケージ構造になっていればOKです。

3. 共通クラスの作成

では、コードの方をかいていきましょう。まずは、アプリ全体で使用する共通クラスを追加していきます。

commonパッケージの中に以下のようにNetworkResponseという名前のsealed classを追加してください。Http通信の状態を管理するのに使用します。

sealed class NetworkResponse<T>(
    val data : T? = null,
    val error : String? = null,
){
    class Success<T> (data : T) : NetworkResponse<T>(data = data)
    class Failure<T> (error : String) : NetworkResponse<T>(error = error)
    class Loading<T> : NetworkResponse<T>()
}

4. 画像検索HTTP通信インターフェースの作成

この章では、UnsplashのWeb APIのsearch photosエンドポイントを使用するためのインターフェースをRetrofitでを作っていきます。

使用していくエンドポイントのドキュメントはこちら

手順としては、以下のようになります。

1. DTO(Data Transfer Object)の作成
2. Retrofitサービスの作成
3. Repositoryの作成

まず、UnsplashのWebAPIから返されるJsonをKotlinのオブジェクトとして扱うための、DTOを作成します。その後、作成したDTOとRetrofitを使って、APIと通信するためのインターフェース(Retrofitサービス)を作成します。
その後、APIから取得したデータをアプリ側で扱いやすくするためにRepositoryを作成します。

4-1. DTOの作成

それでは、DTOを作っていきましょう。
DTOの作成にはAndroid Studioに「Kotlin data class from JSON」というプラグインを入れて作成します。プラグインがAndroid Studioに入っていない方はインストールしてください。このプラグインにJSONサンプルを食わせると、一瞬でDTOが作成されます。

インストール方法はこちらのQiitaの記事を参考にするとわかりやすいです。(記事ではIntelliJでのPluginのインストール方法を紹介していますが、Android Studioでも全く同じ方法でインストールすることができます。)

プラグインをインストールできたら、DTOを作っていきます。
data/remoteパッケージの中に、dtoという名前のパッケージを新規作成してください。

このパッケージの中にDTOを作っていきます。ファイル新規作成メニューを開くと、以下のようにKotlin data class File from JSONというオプションあるはずなので、こちらを選択してください。

Kotlin data class File from JSONを選択すると、以下のようなwindowが表示されます。ここに、JSONサンプルとクラス名を入力すると、Jsonサンプルと同じデータ構造を持ったdata classが作成されます。

エンドポイントのドキュメントを参照すると、Jsonのサンプルがあるので、それをコピペしてください。

Class Nameのところは、「SearchPhotosResultDto」としましょう。

これで、「Generate」を押すと、DTOが作成されるのですが、少し細かい設定をしたいので、windowの左下にある「Advanced」というボタンをクリックしてください。
使用するライブラリやアノテーションの詳細設定を追加します。

まずはPropertyタブを開いて、TypeセクションのNullableというところにチェックをしてください。

次は、Annotationというタブを開いて、Moshi(Codegen)というオプションを選択してください。

Otherというタブを開いて、アノテーションの振り方を変更します。

「Only create annotations when needed」という箇所にチェックをつけてください。これをつけないと、無駄なアノテーションが大量に付与されてしまいます。

ここまで設定できたら、Advancedウィンドウで「OK」ボタンを押し、元のウィンドウの方でも「Generate」を押してDTOを生成しましょう。

7つのファイルが生成されて、上の画像のようにエラーが複数箇所出ているかと思います。これは、先ほど設定したMoshiというライブラリがインストールされていないことが原因なので、Moshiをインストールしてしまいましょう。ついでに、Retrofit周りのインストールも一緒にやってしまいます。ちなみに、MoshiはJsonとKotlinの変換を行うのに使用するライブラリです。

build.gradle(app)を開いて、dependenciesブロックの中に以下のようにretrofitとconvertr-moshiというライブラリの依存関係を追加して、Syncしてください。

dependencies {
    ...
    def retrofit_version = '2.9.0'
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"

    implementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
}

moshiライブラリは直接的にインストールしていないですが、converter-moshiの中に含まれているので、先ほどまで、SearchPhotosResultDto.ktなどで発生していたエラーが消えたかと思います。

以上で画像検索エンドポイント用のDTOの作成は完了です。

4-2. Retrofitサービスの作成

エンドポイントとのインターフェースを定義していきます。
ただ、その前にUnsplashのAPIキーを取得する必要があるので、APIキーを取得しにいきます。(UnsplashのAPIは無料で使用できます)

まずは、Unsplashにアクセスしてアカウントを作成します。

必要な項目を入力してアカウントを作成してください。

https://unsplash.com/oauth/applications
アカウントを作成したら、APIキーの発行をします、上のリンクにアクセスしてください。

「New Application」というボタンを押してください。すると、APIの利用規約ページが表示されるので、利用規約を読んでチェックをつけていき、最後に「Accept terms」というボタンを押してください。

すると、以下のようなダイアログが表示されますので、Application nameとdescriptionに何かしら入力して、「Create Application」ボタンを押してください。これでAPIキーが発行されます。

applicationの作成が完了したので、APIキー(アクセスキー)を確認しましょう。作成後に遷移した画面で下にスクロールすると下の画像のような「Keys」というセクションがあります。ここのAccessKeyがAPIキーになります。

APIキーの作成までできたので、Android Studioの方に戻って、取得したキーを定数として定義しておきます。

commonパッケージの中に、Constantsという名前でobject classを追加して、以下のようにAPIキーの定数を追加してください。

object Constants {
    const val API_KEY = "あなたのAPIキーを入れてください"
}

では、Retrofitサービスの方を作っていきましょう。
下のリンクのsearch/photosという画像検索を行えるエンドポイントにアクセスできるようなインターフェースを作成します。

remoteパッケージの中に、UnsplashApiという名前でinterfaceを追加してください。

interface UnsplashApi {

    @Headers("Authorization: Client-ID ${Constants.API_KEY}")
    @GET("search/photos")
    suspend fun searchPhotos(@Query("query") query: String): SearchPhotosResultDto
}

@Headersというアノテーションを使用して、先ほど取得したAPIキーをヘッダーに入れて送信できるようにしています。
coroutine scopeの中で非同期に通信処理を実行するようにしたいので、suspend修飾子を付与しています。
queryという引数のところに画像を検索するキーワードを渡せる形にしています。

以上で画像検索通信インターフェースの作成完了です。

4-3 Repositoryの作成

写真データの取得を担当するrepositoryを作成します。
domainパッケージのrepositoryパッケージにインターフェースを定義して、その後、そのインターフェースの実装クラスをdata->repositoryパッケージに定義していきます。

では、domain->repositoryパッケージに以下のようにインターフェースを定義してください。

interface PhotoRepository {

    suspend fun searchPhotos(query: String): SearchPhotosResultDto
}

画像検索用の関数を提供するRepositoryの実装を強制するインターフェースの定義ができました。

次は、Repositoryの実装をdata->repositoryパッケージ内に追加していきます。HiltによるDIの設定もしながら進めていきたいので、まずはHIltの依存関係を追加します。

プロジェクトレベルのbuild.gradleを開いて以下のように変更してください。

uildscript {
    ext {
        ...
        hilt_version = '2.44' // new
    }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    ...
    id 'com.google.dagger.hilt.android' version "${hilt_version}" apply false // new
}

次は、アプリレベルのbuild.gradleを変更します。

implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"

Hiltの依存関係は追加できました。Repositoryの実装を追加します。data->repositoryパッケージにPhotoRepositoryImplという名前のクラスを追加してください。

class PhotoRepositoryImpl @Inject constructor(
    private val api: UnsplashApi,
) : PhotoRepository {

    override suspend fun searchPhotos(query: String): SearchPhotosResultDto {
        return api.searchPhotos(query)
    }
}

UnsplashApiを使用して、画像を検索するための関数を定義できました。

以上で、画像検索HTTP通信インターフェースの作成は完了です!

5. 画像詳細情報取得HTTP通信インターフェースの作成

この章では、UnsplashAPIのもう一つのエンドポイントを使った画像詳細情報取得HTTP通信インターフェースの作成をしていきます。

使っていくエンドポイントはこちらです。

画像の詳細情報を取得するのに使用します。一つ前の章で作成した画像検索インターフェースで取得できる画像のID使って、詳細情報を取得する画像を指定できます。

では、画像検索の方と同じ要領で進めていきます。

5-1. DTOの作成

UnsplashAPIのドキュメントのサンプルJSONをKotlin data class File from JSONに食わせてDTOを作成してください。(コメントを削除しないとうまくDTOを生成できません)
もし、サンプルのJSONを食わせても、DTOが作れない場合は、https://github.com/masato1230/UnsplashClient/blob/main/photo_detail_sample.json こちらのJSONを使用してください。

Class NameはPhotoDetailDtoとしくてください。

これでDTOの作成は完了です。大量のdata classが生成されるかと思います。

5-2. Retrofit関数の追加

UnsplashApiインターフェースに以下の関数を追加してください。

@Headers("Authorization: Client-ID ${Constants.API_KEY}")
@GET("photos/{id}")
suspend fun getPhotoById(@Path("id") photoId: String): PhotoDetailDto

5-3. Repositoryへの関数の追加

次は、Repositoryの方に画像詳細情報取得用の関数を追加していきます。
まずは、PhotoRepositoryインターフェースの方から変更していきます。
PhotoRepositoryインターフェースを開いて、以下の関数を追加してください。

suspend fun getPhotoById(photoId: String): PhotoDetailDto

インターフェイスに関数を追加したので、PhotoRepositoryImplに実装を追加していきます。

PhotoRepositoryImplを開いて、以下の関数を追加してください。

override suspend fun getPhotoById(photoId: String): PhotoDetailDto {
    return api.getPhotoById(photoId)
}

以上で、画像詳細情報取得のインターフェイス部分の追加も完了です!

6. Modelの作成

ここまでで、dataレイヤーの作成は完了です。ここからはdomainレイヤーの作成に入っていきます。

まずは、アプリで使用するデータを管理するためのモデルクラスを作ります。
今回のアプリでは、画像検索結果を管理するモデルと、画像詳細情報を表すモデルが必要になってきます。

6-1. Photoモデルの作成

まずは、画像検索結果を管理するモデルを作成します。
モデルで保持するべき項目は以下のようなものになります。

・photoId: UnsplashAPIでの画像のIDを保持するのに使用
・description: 画像の説明
・likes: 画像へのlikeの数
・imgeUrl: 画像のリンク
・photographer: 撮影者のユーザー名

domain->modelパッケージの中に、以下のようにPhotoという名前でdata classを作成してください。

data class Photo(
    val photoId: String,
    val description: String?,
    val likes: Int?,
    val imageUrl: String,
    val photographer: String?,
)

6-2. PhotoDetailモデルの作成

次は、画像詳細情報を保持するためのモデルを作成します。
モデルで保持するべき項目としては以下のようになります。

・description: 画像の説明
・likes: 画像へのlikeの数
・imageUrl: 画像のurl
・photographer: 撮影者のユーザー名
・camera: 撮影したカメラの名前
・location: 撮影場所
・downloads: Unsplashでダウンロードされた回数

domain->modelパッケージの中に、以下のようにPhotoDetailという名前でdata classを作成してください。

data class PhotoDetail(
    val description: String?,
    val likes: Int?,
    val imageUrl: String,
    val photographer: String?,
    val camera: String?,
    val location: String?,
    val downloads: Int?,
)

以上でモデルの作成は完了です。

次は、DTOを今作成したモデルに変換するためのエクステンションを作成していきます。

まずは、SearchPhotosResultDtoをList<Photo>に変換するためのエクステンションを作ります。SearchPhotosResultDtoファイルを開いて、ファイルの一番下に以下のようなエクステンションを追加してください。

fun SearchPhotosResultDto.toPhotos(): List<Photo> {
    return results!!.map {
        Photo(
            photoId = it.id!!,
            description = it.description,
            likes = it.likes,
            imageUrl = it.urls!!.raw!!,
            photographer = it.user?.username,
        )
    }
}

次はPhotoDetailDtoからPhotoDetailへの変換をするためのエクステンションを作ります。

fun PhotoDetailDto.toPhotoDetail(): PhotoDetail {
    return  PhotoDetail(
        description = description,
        likes = likes,
        imageUrl = urls!!.raw!!,
        photographer = user?.username,
        camera = exif?.name,
        location = "${location?.city}, ${location?.country}",
        downloads = downloads,
    )
}

7. 画像検索UseCaseの作成

ここから先は

29,129字 / 11画像
この記事のみ ¥ 480
期間限定!PayPayで支払うと抽選でお得

この記事が気に入ったらチップで応援してみませんか?