diff --git a/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt b/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt index f1dec62..130ee9f 100644 --- a/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt @@ -1,5 +1,7 @@ package com.wei.picquest.core.data.di +import com.wei.picquest.core.data.repository.DefaultSearchImagesRepository +import com.wei.picquest.core.data.repository.SearchImagesRepository import com.wei.picquest.core.data.utils.ConnectivityManagerNetworkMonitor import com.wei.picquest.core.data.utils.NetworkMonitor import dagger.Binds @@ -11,6 +13,11 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) interface DataModule { + @Binds + fun bindsSearchImagesRepository( + searchImagesRepository: DefaultSearchImagesRepository, + ): SearchImagesRepository + @Binds fun bindsNetworkMonitor( networkMonitor: ConnectivityManagerNetworkMonitor, diff --git a/core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt b/core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt new file mode 100644 index 0000000..df45fee --- /dev/null +++ b/core/data/src/main/java/com/wei/picquest/core/data/model/SearchImages.kt @@ -0,0 +1,63 @@ +package com.wei.picquest.core.data.model + +import com.wei.picquest.core.network.model.NetworkSearchImages + +data class SearchImages( + val total: Int, + val totalHits: Int, + val images: List, +) + +data class ImageDetail( + val id: Int, + val pageURL: String, + val type: String, + val tags: String, + val previewURL: String, + val previewWidth: Int, + val previewHeight: Int, + val webformatURL: String, + val webformatWidth: Int, + val webformatHeight: Int, + val largeImageURL: String, + val imageWidth: Int, + val imageHeight: Int, + val imageSize: Long, + val views: Int, + val downloads: Int, + val likes: Int, + val comments: Int, + val userId: Int, + val user: String, + val userImageURL: String, +) + +fun NetworkSearchImages.asExternalModel() = SearchImages( + total = this.total, + totalHits = this.totalHits, + images = this.hits.map { networkImageDetail -> + ImageDetail( + id = networkImageDetail.id, + pageURL = networkImageDetail.pageURL, + type = networkImageDetail.type, + tags = networkImageDetail.tags, + previewURL = networkImageDetail.previewURL, + previewWidth = networkImageDetail.previewWidth, + previewHeight = networkImageDetail.previewHeight, + webformatURL = networkImageDetail.webformatURL, + webformatWidth = networkImageDetail.webformatWidth, + webformatHeight = networkImageDetail.webformatHeight, + largeImageURL = networkImageDetail.largeImageURL, + imageWidth = networkImageDetail.imageWidth, + imageHeight = networkImageDetail.imageHeight, + imageSize = networkImageDetail.imageSize, + views = networkImageDetail.views, + downloads = networkImageDetail.downloads, + likes = networkImageDetail.likes, + comments = networkImageDetail.comments, + userId = networkImageDetail.userId, + user = networkImageDetail.user, + userImageURL = networkImageDetail.userImageURL, + ) + }, +) diff --git a/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt b/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt new file mode 100644 index 0000000..f20cfdf --- /dev/null +++ b/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchImagesRepository.kt @@ -0,0 +1,35 @@ +package com.wei.picquest.core.data.repository + +import com.wei.picquest.core.network.Dispatcher +import com.wei.picquest.core.network.PqDispatchers +import com.wei.picquest.core.network.PqNetworkDataSource +import com.wei.picquest.core.network.model.NetworkSearchImages +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * Implementation of the [SearchImagesRepository]. + * @param ioDispatcher 用於執行 IO 相關操作的 CoroutineDispatcher。 + * @param network 數據源的網路接口。 + */ +class DefaultSearchImagesRepository @Inject constructor( + @Dispatcher(PqDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, + private val network: PqNetworkDataSource, +) : SearchImagesRepository { + + /** + * @param query。A URL encoded search term. If omitted, all images are returned. This value may not exceed 100 characters. + * Example: "yellow+flower" + * @return 一個 Flow,內容為 Search Images 的數據。 + */ + override suspend fun getSearchImages( + query: String, + ): Flow = withContext(ioDispatcher) { + flow { + emit(network.searchImages(query)) + } + } +} diff --git a/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt b/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt new file mode 100644 index 0000000..5e1a293 --- /dev/null +++ b/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchImagesRepository.kt @@ -0,0 +1,9 @@ +package com.wei.picquest.core.data.repository + +import com.wei.picquest.core.network.model.NetworkSearchImages +import kotlinx.coroutines.flow.Flow + +interface SearchImagesRepository { + + suspend fun getSearchImages(query: String): Flow +} diff --git a/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt b/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt new file mode 100644 index 0000000..16dc2ff --- /dev/null +++ b/core/network/src/main/java/com/wei/picquest/core/network/PqNetworkDataSource.kt @@ -0,0 +1,11 @@ +package com.wei.picquest.core.network + +import com.wei.picquest.core.network.model.NetworkSearchImages + +/** + * Interface representing network calls to the PicQuest backend + */ +interface PqNetworkDataSource { + + suspend fun searchImages(query: String): NetworkSearchImages +} diff --git a/core/network/src/main/java/com/wei/picquest/core/network/di/FlavoredNetworkModule.kt b/core/network/src/main/java/com/wei/picquest/core/network/di/FlavoredNetworkModule.kt new file mode 100644 index 0000000..a0d5333 --- /dev/null +++ b/core/network/src/main/java/com/wei/picquest/core/network/di/FlavoredNetworkModule.kt @@ -0,0 +1,19 @@ +package com.wei.picquest.core.network.di + +import com.wei.picquest.core.network.PqNetworkDataSource +import com.wei.picquest.core.network.retrofit.RetrofitPqNetwork +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +/** + * TODO Wei 移動至 build variants 下產出資料夾 + */ +@Module +@InstallIn(SingletonComponent::class) +interface FlavoredNetworkModule { + + @Binds + fun binds(implementation: RetrofitPqNetwork): PqNetworkDataSource +} diff --git a/core/network/src/main/java/com/wei/picquest/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/wei/picquest/core/network/di/NetworkModule.kt new file mode 100644 index 0000000..57ec455 --- /dev/null +++ b/core/network/src/main/java/com/wei/picquest/core/network/di/NetworkModule.kt @@ -0,0 +1,36 @@ +package com.wei.picquest.core.network.di + +import com.wei.core.network.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun providesNetworkJson(): Json = Json { + ignoreUnknownKeys = true + } + + @Provides + @Singleton + fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .apply { + if (BuildConfig.DEBUG) { + setLevel(HttpLoggingInterceptor.Level.BODY) + } + }, + ) + .build() +} diff --git a/core/network/src/main/java/com/wei/picquest/core/network/model/NetworkSearchImages.kt b/core/network/src/main/java/com/wei/picquest/core/network/model/NetworkSearchImages.kt new file mode 100644 index 0000000..3394a5b --- /dev/null +++ b/core/network/src/main/java/com/wei/picquest/core/network/model/NetworkSearchImages.kt @@ -0,0 +1,63 @@ +package com.wei.picquest.core.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Network representation of [SearchImages] when fetched from / + */ +@Serializable +data class NetworkSearchImages( + @SerialName("total") + val total: Int, + @SerialName("totalHits") + val totalHits: Int, + @SerialName("hits") + val hits: List, +) + +@Serializable +data class NetworkImageDetail( + @SerialName("id") + val id: Int, + @SerialName("pageURL") + val pageURL: String, + @SerialName("type") + val type: String, + @SerialName("tags") + val tags: String, + @SerialName("previewURL") + val previewURL: String, + @SerialName("previewWidth") + val previewWidth: Int, + @SerialName("previewHeight") + val previewHeight: Int, + @SerialName("webformatURL") + val webformatURL: String, + @SerialName("webformatWidth") + val webformatWidth: Int, + @SerialName("webformatHeight") + val webformatHeight: Int, + @SerialName("largeImageURL") + val largeImageURL: String, + @SerialName("imageWidth") + val imageWidth: Int, + @SerialName("imageHeight") + val imageHeight: Int, + @SerialName("imageSize") + val imageSize: Long, + @SerialName("views") + val views: Int, + @SerialName("downloads") + val downloads: Int, + @SerialName("likes") + val likes: Int, + @SerialName("comments") + val comments: Int, + @SerialName("user_id") + val userId: Int, + @SerialName("user") + val user: String, + @SerialName("userImageURL") + val userImageURL: String, +) diff --git a/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt b/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt new file mode 100644 index 0000000..e18a633 --- /dev/null +++ b/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt @@ -0,0 +1,56 @@ +package com.wei.picquest.core.network.retrofit + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.wei.core.network.BuildConfig +import com.wei.picquest.core.network.PqNetworkDataSource +import com.wei.picquest.core.network.model.NetworkSearchImages +import kotlinx.serialization.json.Json +import okhttp3.Call +import okhttp3.MediaType.Companion.toMediaType +import retrofit2.Retrofit +import retrofit2.http.GET +import retrofit2.http.Query +import javax.inject.Inject +import javax.inject.Singleton + +private const val PIXABAY_BASE_URL = BuildConfig.BACKEND_URL +private const val API_KEY = BuildConfig.API_KEY + +/** + * Retrofit API declaration for Pixabay Network API + */ +interface RetrofitPixabayApi { + /** + * https://pixabay.com/api/?key=${api key}&q=yellow+flowers&image_type=photo + */ + @GET(".") + suspend fun searchImages( + @Query("key") apiKey: String = API_KEY, + @Query("q") query: String, + @Query("image_type") imageType: String = "photo", + // Add more parameters as needed + ): NetworkSearchImages +} + +/** + * [Retrofit] backed [PqNetworkDataSource] + */ +@Singleton +class RetrofitPqNetwork @Inject constructor( + networkJson: Json, + okhttpCallFactory: Call.Factory, +) : PqNetworkDataSource { + + private val pixabayApi = Retrofit.Builder() + .baseUrl(PIXABAY_BASE_URL) + .callFactory(okhttpCallFactory) + .addConverterFactory( + networkJson.asConverterFactory("application/json".toMediaType()), + ) + .build() + .create(RetrofitPixabayApi::class.java) + + override suspend fun searchImages(query: String): NetworkSearchImages { + return pixabayApi.searchImages(query = query) + } +} diff --git a/secrets.defaults.properties b/secrets.defaults.properties index a7f254e..a8f4b44 100644 --- a/secrets.defaults.properties +++ b/secrets.defaults.properties @@ -1,4 +1,5 @@ ## This file provides default values to modules using the secrets-gradle-plugin. It is necessary # because the secrets properties file is not under source control so CI builds will fail without # default values. -BACKEND_URL="https://pixabay.com/api/" \ No newline at end of file +BACKEND_URL="https://pixabay.com/api/" +API_KEY="fake_api_key" \ No newline at end of file