From 7307abf6f2d04ebf187be461309bc4a82a0e3bfd Mon Sep 17 00:00:00 2001 From: Leonel Zalegas Date: Sun, 21 Jul 2024 21:22:49 -0300 Subject: [PATCH] finished Data layer of Intraday and CompanyInfo --- app/build.gradle.kts | 7 +-- .../example/stockmarketcheck/di/AppModule.kt | 2 + .../stockmarketcheck/di/RepositoryModule.kt | 6 ++ .../data/csv/IntradayInfoParser.kt | 56 +++++++++++++++++++ .../mainFeature/data/mapper/CompanyMapper.kt | 15 +++++ .../mainFeature/data/remote/StockClient.kt | 13 +++++ .../data/remote/dto/CompanyInfoDto.kt | 12 ++++ .../data/repository/StockRepositoryImpl.kt | 44 +++++++++++++++ .../mainFeature/domain/model/CompanyInfo.kt | 9 +++ .../mainFeature/domain/model/IntradayInfo.kt | 8 +++ .../domain/repository/StockRepository.kt | 6 ++ gradle/libs.versions.toml | 9 +-- 12 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/example/stockmarketcheck/mainFeature/data/csv/IntradayInfoParser.kt create mode 100644 app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/dto/CompanyInfoDto.kt create mode 100644 app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/CompanyInfo.kt create mode 100644 app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/IntradayInfo.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3bc8a53..4e67b76 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,11 +142,7 @@ dependencies { // Retrofit implementation(libs.retrofit2.retrofit) - implementation(libs.retrofit2.converter.moshi) - - // Moshi - implementation(libs.moshi.kotlin) - ksp(libs.moshi.kotlin.codegen) + implementation(libs.converter.gson) // OkHttp implementation(libs.okhttp3.okhttp) @@ -161,5 +157,6 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) + // System Bar control implementation(libs.accompanist.systemuicontroller) } diff --git a/app/src/main/java/com/example/stockmarketcheck/di/AppModule.kt b/app/src/main/java/com/example/stockmarketcheck/di/AppModule.kt index 3c623cb..6144de8 100644 --- a/app/src/main/java/com/example/stockmarketcheck/di/AppModule.kt +++ b/app/src/main/java/com/example/stockmarketcheck/di/AppModule.kt @@ -9,6 +9,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create import javax.inject.Singleton @@ -20,6 +21,7 @@ object AppModule { fun provideStockApi(): StockClient { return Retrofit.Builder() .baseUrl("https://alphavantage.co") + .addConverterFactory(GsonConverterFactory.create()) .build() .create() } diff --git a/app/src/main/java/com/example/stockmarketcheck/di/RepositoryModule.kt b/app/src/main/java/com/example/stockmarketcheck/di/RepositoryModule.kt index e799b5c..776ff9f 100644 --- a/app/src/main/java/com/example/stockmarketcheck/di/RepositoryModule.kt +++ b/app/src/main/java/com/example/stockmarketcheck/di/RepositoryModule.kt @@ -2,8 +2,10 @@ package com.example.stockmarketcheck.di import com.example.stockmarketcheck.mainFeature.data.csv.CSVParser import com.example.stockmarketcheck.mainFeature.data.csv.CompanyListingsParser +import com.example.stockmarketcheck.mainFeature.data.csv.IntradayInfoParser import com.example.stockmarketcheck.mainFeature.data.repository.StockRepositoryImpl import com.example.stockmarketcheck.mainFeature.domain.model.CompanyListing +import com.example.stockmarketcheck.mainFeature.domain.model.IntradayInfo import com.example.stockmarketcheck.mainFeature.domain.repository.StockRepository import dagger.Binds import dagger.Module @@ -18,6 +20,10 @@ abstract class RepositoryModule { @Singleton abstract fun bindCompanyListingsParser(companyListingsParser: CompanyListingsParser): CSVParser + @Binds + @Singleton + abstract fun bindIntradayInfoParser(intradayInfoParser: IntradayInfoParser): CSVParser + @Binds @Singleton abstract fun bindStockRepository(stockRepositoryImpl: StockRepositoryImpl): StockRepository diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/csv/IntradayInfoParser.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/csv/IntradayInfoParser.kt new file mode 100644 index 0000000..ef32e11 --- /dev/null +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/csv/IntradayInfoParser.kt @@ -0,0 +1,56 @@ +package com.example.stockmarketcheck.mainFeature.data.csv + +import android.os.Build +import androidx.annotation.RequiresApi +import com.example.stockmarketcheck.mainFeature.domain.model.IntradayInfo +import com.opencsv.CSVReader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.io.InputStreamReader +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntradayInfoParser + @Inject + constructor() : CSVParser { + @RequiresApi(Build.VERSION_CODES.O) + override suspend fun parse(stream: InputStream): List { + val csvReader = CSVReader(InputStreamReader(stream)) + return withContext(Dispatchers.IO) { + csvReader + .readAll() + .drop(1) + .mapNotNull { line -> + val timestamp = line.getOrNull(0) ?: return@mapNotNull null + val close = line.getOrNull(4) ?: return@mapNotNull null + IntradayInfo( + date = convertToLocalDateTime(timestamp), + close = close.toDouble(), + ) + } // Aca ya tenemos la lista de IntradayInfo pero a esto filtramos y Guardamos solo + .filter { // los IntradayInfo con campo date = a la fecha de ayer de nuetra maquina + it.date.dayOfMonth == LocalDate.now().minusDays(4).dayOfMonth + } + .sortedBy { + it.date.hour + } + .also { + csvReader.close() + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun convertToLocalDateTime(timestamp: String): LocalDateTime { + val pattern = "yyyy-MM-dd HH:mm:ss" + val formatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault()) + val localDateTime = LocalDateTime.parse(timestamp, formatter) + return localDateTime + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/mapper/CompanyMapper.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/mapper/CompanyMapper.kt index ada5771..bc2c5fe 100644 --- a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/mapper/CompanyMapper.kt +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/mapper/CompanyMapper.kt @@ -1,6 +1,8 @@ package com.example.stockmarketcheck.mainFeature.data.mapper import com.example.stockmarketcheck.mainFeature.data.local.CompanyListingEntity +import com.example.stockmarketcheck.mainFeature.data.remote.dto.CompanyInfoDto +import com.example.stockmarketcheck.mainFeature.domain.model.CompanyInfo import com.example.stockmarketcheck.mainFeature.domain.model.CompanyListing fun CompanyListingEntity.toCompanyListing(): CompanyListing { @@ -17,4 +19,17 @@ fun CompanyListing.toCompanyListingEntity(): CompanyListingEntity { symbol = symbol, exchange = exchange, ) +} + +fun CompanyInfoDto.toCompanyInfo(): CompanyInfo { + // ponemos ?: "" xq puede ser q te pases de llamadas gratuitas y la API te devuelva + // otro JSON q no corresponde con los fields de CompanyInfoDto (no existen) x ende + // mejor que devuelva "" a q crashee + return CompanyInfo( + symbol = symbol ?: "", + description = description ?: "", + name = name ?: "", + country = country ?: "", + industry = industry ?: "", + ) } \ No newline at end of file diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/StockClient.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/StockClient.kt index 2221668..a5c1683 100644 --- a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/StockClient.kt +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/StockClient.kt @@ -1,6 +1,7 @@ package com.example.stockmarketcheck.mainFeature.data.remote import com.example.stockmarketcheck.BuildConfig +import com.example.stockmarketcheck.mainFeature.data.remote.dto.CompanyInfoDto import okhttp3.ResponseBody import retrofit2.http.GET import retrofit2.http.Query @@ -10,4 +11,16 @@ interface StockClient { suspend fun getListings( @Query("apikey") apikey: String = BuildConfig.API_KEY, ): ResponseBody + + @GET("query?function=TIME_SERIES_INTRADAY&interval=60min&datatype=csv") + suspend fun getIntradayInfo( + @Query("symbol") symbol: String, + @Query("apikey") apiKey: String = BuildConfig.API_KEY, + ): ResponseBody + + @GET("query?function=OVERVIEW") + suspend fun getCompanyInfo( + @Query("symbol") symbol: String, + @Query("apikey") apiKey: String = BuildConfig.API_KEY, + ): CompanyInfoDto } \ No newline at end of file diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/dto/CompanyInfoDto.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/dto/CompanyInfoDto.kt new file mode 100644 index 0000000..899c0bb --- /dev/null +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/remote/dto/CompanyInfoDto.kt @@ -0,0 +1,12 @@ +package com.example.stockmarketcheck.mainFeature.data.remote.dto + +import com.google.gson.annotations.SerializedName + +// Seria el "Response" que llama a aristidevs y es el que sera rellenado x ConverterFactoryJson +data class CompanyInfoDto( + @SerializedName("Symbol") val symbol: String?, + @SerializedName("Description") val description: String?, + @SerializedName("Name") val name: String?, + @SerializedName("Country") val country: String?, + @SerializedName("Industry") val industry: String?, +) diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/repository/StockRepositoryImpl.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/repository/StockRepositoryImpl.kt index 42775a5..f3b74d7 100644 --- a/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/repository/StockRepositoryImpl.kt +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/data/repository/StockRepositoryImpl.kt @@ -2,10 +2,13 @@ package com.example.stockmarketcheck.mainFeature.data.repository import com.example.stockmarketcheck.mainFeature.data.csv.CSVParser import com.example.stockmarketcheck.mainFeature.data.local.StockDatabase +import com.example.stockmarketcheck.mainFeature.data.mapper.toCompanyInfo import com.example.stockmarketcheck.mainFeature.data.mapper.toCompanyListing import com.example.stockmarketcheck.mainFeature.data.mapper.toCompanyListingEntity import com.example.stockmarketcheck.mainFeature.data.remote.StockClient +import com.example.stockmarketcheck.mainFeature.domain.model.CompanyInfo import com.example.stockmarketcheck.mainFeature.domain.model.CompanyListing +import com.example.stockmarketcheck.mainFeature.domain.model.IntradayInfo import com.example.stockmarketcheck.mainFeature.domain.repository.StockRepository import com.example.stockmarketcheck.util.Resource import kotlinx.coroutines.flow.Flow @@ -22,6 +25,7 @@ class StockRepositoryImpl private val api: StockClient, db: StockDatabase, private val companyListingsParser: CSVParser, + private val intradayInfoParser: CSVParser, ) : StockRepository { private val dao = db.dao @@ -81,4 +85,44 @@ class StockRepositoryImpl } } } + + override suspend fun getIntradayInfo(symbol: String): Resource> { + return try { + val response = api.getIntradayInfo(symbol) + val results = intradayInfoParser.parse(response.byteStream()) + Resource.Success(results) + } catch (e: IOException) { + e.printStackTrace() + Resource.Error( + message = "Couldn't load intraday info", + null, + ) + } catch (e: HttpException) { + e.printStackTrace() + Resource.Error( + message = "Couldn't load intraday info", + null, + ) + } + } + + // basicamente el unico donde hacemos el parseo Json posta + override suspend fun getCompanyInfo(symbol: String): Resource { + return try { + val result = api.getCompanyInfo(symbol) + Resource.Success(result.toCompanyInfo()) + } catch (e: IOException) { + e.printStackTrace() + Resource.Error( + message = "Couldn't load company info", + null, + ) + } catch (e: HttpException) { + e.printStackTrace() + Resource.Error( + message = "Couldn't load company info", + null, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/CompanyInfo.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/CompanyInfo.kt new file mode 100644 index 0000000..e95bd18 --- /dev/null +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/CompanyInfo.kt @@ -0,0 +1,9 @@ +package com.example.stockmarketcheck.mainFeature.domain.model + +data class CompanyInfo( + val symbol: String?, + val description: String?, + val name: String?, + val country: String?, + val industry: String?, +) diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/IntradayInfo.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/IntradayInfo.kt new file mode 100644 index 0000000..d177816 --- /dev/null +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/model/IntradayInfo.kt @@ -0,0 +1,8 @@ +package com.example.stockmarketcheck.mainFeature.domain.model + +import java.time.LocalDateTime + +data class IntradayInfo( + val date: LocalDateTime, + val close: Double, +) diff --git a/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/repository/StockRepository.kt b/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/repository/StockRepository.kt index 91a2eff..bbc470f 100644 --- a/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/repository/StockRepository.kt +++ b/app/src/main/java/com/example/stockmarketcheck/mainFeature/domain/repository/StockRepository.kt @@ -1,6 +1,8 @@ package com.example.stockmarketcheck.mainFeature.domain.repository +import com.example.stockmarketcheck.mainFeature.domain.model.CompanyInfo import com.example.stockmarketcheck.mainFeature.domain.model.CompanyListing +import com.example.stockmarketcheck.mainFeature.domain.model.IntradayInfo import com.example.stockmarketcheck.util.Resource import kotlinx.coroutines.flow.Flow @@ -9,4 +11,8 @@ interface StockRepository { fetchFromRemote: Boolean, query: String, ): Flow>> + + suspend fun getIntradayInfo(symbol: String): Resource> + + suspend fun getCompanyInfo(symbol: String): Resource } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8eb740d..c968abf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] accompanistSystemuicontroller = "0.28.0" agp = "8.5.1" +converterGson = "2.11.0" kotlin = "1.9.0" coreKtx = "1.13.1" junit = "4.13.2" @@ -14,8 +15,6 @@ composeBom = "2024.06.00" composeNavigation = "2.8.0-beta05" material = "1.6.8" materialIconsExtended = "" -moshiKotlin = "1.15.0" -moshiKotlinCodegen = "1.15.0" okHttpVersion = "4.12.0" opencsv = "5.7.1" retrofitVersion = "2.10.0" @@ -24,7 +23,6 @@ serialization = "1.6.1" ksp = "1.9.0-1.0.13" hilt = "2.51" hiltNavigationCompose = "1.2.0" -hiltLifecycleViewmodel = "1.0.0-alpha03" [libraries] accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } @@ -36,6 +34,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomRuntime" } +converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -51,8 +50,6 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } -moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" } -moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshiKotlinCodegen" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "composeNavigation" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization"} okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okHttpVersion" } @@ -61,8 +58,6 @@ opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } -hilt-lifecycle-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel", version.ref = "hiltLifecycleViewmodel" } -retrofit2-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitVersion" } retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" } [plugins]