From ab6ee6166b5c34bae71a382b55e0513960b7000e Mon Sep 17 00:00:00 2001 From: ibrahimcanaydogan Date: Mon, 11 Nov 2024 02:12:19 +0300 Subject: [PATCH] Project latest version updated. --- .idea/.name | 1 + .idea/compiler.xml | 2 +- .idea/gradle.xml | 7 +- .idea/misc.xml | 2 +- .idea/runConfigurations.xml | 17 ++ .idea/vcs.xml | 6 + app/build.gradle | 115 ------- app/build.gradle.kts | 86 ++++++ .../stockmarket}/ExampleInstrumentedTest.kt | 4 +- app/src/main/AndroidManifest.xml | 12 +- .../stockmarket/MainActivity.kt | 32 ++ .../stockmarket/StockApplication.kt | 7 + .../data/mapper/CompanyDetailMapper.kt | 14 + .../data/mapper/IntradayInfoMapper.kt | 17 ++ .../stockmarket/data/parser/CSVParser.kt | 7 + .../data/parser/CompanyListParser.kt | 36 +++ .../data/parser/IntradayInfoParser.kt | 41 +++ .../stockmarket/data/remote/StockAPI.kt | 31 ++ .../data/remote/dto/CompanyDetailDto.kt | 11 + .../data/remote/dto/IntradayInfoDto.kt | 6 + .../data/repository/StockRepositoryImpl.kt | 77 +++++ .../dependencyinjection/ParserRepository.kt | 30 ++ .../dependencyinjection/RepositoryModule.kt | 20 ++ .../dependencyinjection/RetrofitModule.kt | 33 ++ .../stockmarket/domain/model/CompanyDetail.kt | 9 + .../stockmarket/domain/model/CompanyList.kt | 7 + .../stockmarket/domain/model/IntradayInfo.kt | 8 + .../domain/repository/StockRepository.kt | 22 ++ .../stockmarket/ui/component/StockChart.kt | 114 +++++++ .../ui/screen/detail/CompanyDetailScreen.kt | 117 +++++++ .../ui/screen/detail/CompanyDetailState.kt | 11 + .../screen/detail/CompanyDetailViewModel.kt | 66 ++++ .../ui/screen/list/CompanyListEvent.kt | 6 + .../ui/screen/list/CompanyListScreen.kt | 124 ++++++++ .../ui/screen/list/CompanyListState.kt | 10 + .../ui/screen/list/CompanyListViewModel.kt | 65 ++++ .../stockmarket/ui/theme/Color.kt | 11 + .../stockmarket/ui/theme/Theme.kt | 58 ++++ .../stockmarket}/ui/theme/Type.kt | 26 +- .../stockmarket/util/Resource.kt | 7 + .../plcoding/stockmarketapp/MainActivity.kt | 29 -- .../plcoding/stockmarketapp/ui/theme/Color.kt | 8 - .../plcoding/stockmarketapp/ui/theme/Shape.kt | 11 - .../plcoding/stockmarketapp/ui/theme/Theme.kt | 47 --- .../ic_launcher_foreground.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/themes.xml | 4 +- app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../stockmarket}/ExampleUnitTest.kt | 2 +- build.gradle | 17 -- build.gradle.kts | 8 + gradle.properties | 6 +- gradle/libs.versions.toml | 32 ++ gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 7 +- gradlew | 285 +++++++++++------- gradlew.bat | 37 ++- settings.gradle | 16 - settings.gradle.kts | 24 ++ 62 files changed, 1445 insertions(+), 399 deletions(-) create mode 100644 .idea/.name create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts rename app/src/androidTest/java/com/{plcoding/stockmarketapp => ibrahimcanerdogan/stockmarket}/ExampleInstrumentedTest.kt (82%) create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/MainActivity.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/StockApplication.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/CompanyDetailMapper.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/IntradayInfoMapper.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CSVParser.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CompanyListParser.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/IntradayInfoParser.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/StockAPI.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/CompanyDetailDto.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/IntradayInfoDto.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/repository/StockRepositoryImpl.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/ParserRepository.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RepositoryModule.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RetrofitModule.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyDetail.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyList.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/IntradayInfo.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/repository/StockRepository.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/component/StockChart.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailScreen.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailState.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailViewModel.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListEvent.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListScreen.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListState.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListViewModel.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Color.kt create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Theme.kt rename app/src/main/java/com/{plcoding/stockmarketapp => ibrahimcanerdogan/stockmarket}/ui/theme/Type.kt (52%) create mode 100644 app/src/main/java/com/ibrahimcanerdogan/stockmarket/util/Resource.kt delete mode 100644 app/src/main/java/com/plcoding/stockmarketapp/MainActivity.kt delete mode 100644 app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Color.kt delete mode 100644 app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Shape.kt delete mode 100644 app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Theme.kt rename app/src/main/res/{drawable-v24 => drawable}/ic_launcher_foreground.xml (100%) create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml rename app/src/test/java/com/{plcoding/stockmarketapp => ibrahimcanerdogan/stockmarket}/ExampleUnitTest.kt (88%) delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..20f2cc4 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +StockMarket \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 4e3844e..efacb99 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,18 +1,19 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index 2a4d5b5..6edda60 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index ea83672..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,115 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'kotlin-kapt' - id 'dagger.hilt.android.plugin' - id 'kotlin-parcelize' - id 'com.google.devtools.ksp' version '1.6.10-1.0.2' -} - -kotlin { - sourceSets { - debug { - kotlin.srcDir("build/generated/ksp/debug/kotlin") - } - release { - kotlin.srcDir("build/generated/ksp/release/kotlin") - } - } -} - -android { - compileSdk 32 - - defaultConfig { - applicationId "com.plcoding.stockmarketapp" - minSdk 21 - targetSdk 32 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion compose_version - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -dependencies { - implementation 'androidx.core:core-ktx:1.7.0' - implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.material:material:$compose_version" - implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.3.1' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" - debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - - // OpenCSV - implementation 'com.opencsv:opencsv:5.5.2' - - // Compose dependencies - implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1" - implementation "androidx.compose.material:material-icons-extended:$compose_version" - implementation "com.google.accompanist:accompanist-flowlayout:0.17.0" - implementation 'androidx.paging:paging-compose:1.0.0-alpha14' - implementation "androidx.activity:activity-compose:1.6.0-alpha01" - implementation "com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha" - - // Compose Nav Destinations - implementation 'io.github.raamcosta.compose-destinations:core:1.1.2-beta' - ksp 'io.github.raamcosta.compose-destinations:ksp:1.1.2-beta' - - // Coil - implementation "io.coil-kt:coil-compose:1.4.0" - - //Dagger - Hilt - implementation "com.google.dagger:hilt-android:2.40.5" - kapt "com.google.dagger:hilt-android-compiler:2.40.5" - implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03" - kapt "androidx.hilt:hilt-compiler:1.0.0" - implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' - - // Retrofit - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' - implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.3" - implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3" - - // Room - implementation "androidx.room:room-runtime:2.4.2" - kapt "androidx.room:room-compiler:2.4.2" - - // Kotlin Extensions and Coroutines support for Room - implementation "androidx.room:room-ktx:2.4.2" -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..fdb13b0 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,86 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + id("com.google.devtools.ksp") + id("com.google.dagger.hilt.android") +} + +android { + namespace = "com.ibrahimcanerdogan.stockmarket" + compileSdk = 34 + + defaultConfig { + applicationId = "com.ibrahimcanerdogan.stockmarket" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // Compose Navigation - https://github.com/raamcosta/compose-destinations + implementation("io.github.raamcosta.compose-destinations:core:1.1.2-beta") + ksp("io.github.raamcosta.compose-destinations:ksp:1.1.2-beta") + implementation("androidx.navigation:navigation-compose:2.8.3") + + // Dagger-Hilt - https://developer.android.com/training/dependency-injection/hilt-android + implementation("com.google.dagger:hilt-android:2.51.1") + ksp ("com.google.dagger:hilt-android-compiler:2.51.1") + ksp("androidx.hilt:hilt-compiler:1.2.0") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") + + // Retrofit + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3") + implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.google.code.gson:gson:2.10.1") + + // Swipe Refresh + implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") + + // OpenCSV + implementation("com.opencsv:opencsv:5.5.2") +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/plcoding/stockmarketapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/ibrahimcanerdogan/stockmarket/ExampleInstrumentedTest.kt similarity index 82% rename from app/src/androidTest/java/com/plcoding/stockmarketapp/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/com/ibrahimcanerdogan/stockmarket/ExampleInstrumentedTest.kt index fd8370e..321593f 100644 --- a/app/src/androidTest/java/com/plcoding/stockmarketapp/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/ibrahimcanerdogan/stockmarket/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.plcoding.stockmarketapp +package com.ibrahimcanerdogan.stockmarket import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.plcoding.stockmarketapp", appContext.packageName) + assertEquals("com.ibrahimcanerdogan.stockmarket", appContext.packageName) } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 620f62f..51e3911 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,19 +1,25 @@ + xmlns:tools="http://schemas.android.com/tools"> + + + android:theme="@style/Theme.StockMarket" + tools:targetApi="31"> + android:theme="@style/Theme.StockMarket"> diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/MainActivity.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/MainActivity.kt new file mode 100644 index 0000000..07beee9 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/MainActivity.kt @@ -0,0 +1,32 @@ +package com.ibrahimcanerdogan.stockmarket + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import com.ibrahimcanerdogan.stockmarket.ui.screen.NavGraphs +import com.ibrahimcanerdogan.stockmarket.ui.theme.StockMarketTheme +import com.ramcosta.composedestinations.DestinationsNavHost +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + StockMarketTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + DestinationsNavHost(navGraph = NavGraphs.root) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/StockApplication.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/StockApplication.kt new file mode 100644 index 0000000..673ced6 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/StockApplication.kt @@ -0,0 +1,7 @@ +package com.ibrahimcanerdogan.stockmarket + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class StockApplication: Application() \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/CompanyDetailMapper.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/CompanyDetailMapper.kt new file mode 100644 index 0000000..3d5a0db --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/CompanyDetailMapper.kt @@ -0,0 +1,14 @@ +package com.ibrahimcanerdogan.stockmarket.data.mapper + +import com.ibrahimcanerdogan.stockmarket.data.remote.dto.CompanyDetailDto +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyDetail + +fun CompanyDetailDto.toCompanyInfo(): CompanyDetail { + return CompanyDetail( + companyDetailSymbol = symbol ?: "", + companyDetailDescription = description ?: "", + companyDetailName = name ?: "", + companyDetailCountry = country ?: "", + companyDetailIndustry = industry ?: "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/IntradayInfoMapper.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/IntradayInfoMapper.kt new file mode 100644 index 0000000..9927ea2 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/mapper/IntradayInfoMapper.kt @@ -0,0 +1,17 @@ +package com.ibrahimcanerdogan.stockmarket.data.mapper + +import com.ibrahimcanerdogan.stockmarket.data.remote.dto.IntradayInfoDto +import com.ibrahimcanerdogan.stockmarket.domain.model.IntradayInfo +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +fun IntradayInfoDto.toIntradayInfo(): IntradayInfo { + val pattern = "yyyy-MM-dd HH:mm:ss" + val formatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault()) + val localDateTime = LocalDateTime.parse(timestamp, formatter) + return IntradayInfo( + intradayDate = localDateTime, + intradayClose = close + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CSVParser.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CSVParser.kt new file mode 100644 index 0000000..bc5a1e6 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CSVParser.kt @@ -0,0 +1,7 @@ +package com.ibrahimcanerdogan.stockmarket.data.parser + +import java.io.InputStream + +interface CSVParser { + suspend fun parse(stream: InputStream): List +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CompanyListParser.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CompanyListParser.kt new file mode 100644 index 0000000..c6bb697 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/CompanyListParser.kt @@ -0,0 +1,36 @@ +package com.ibrahimcanerdogan.stockmarket.data.parser + +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyList +import com.opencsv.CSVReader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.io.InputStreamReader +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CompanyListParser @Inject constructor(): CSVParser { + + override suspend fun parse(stream: InputStream): List { + val csvReader = CSVReader(InputStreamReader(stream)) + return withContext(Dispatchers.IO) { + csvReader + .readAll() + .drop(1) + .mapNotNull { line -> + val symbol = line.getOrNull(0) + val name = line.getOrNull(1) + val exchange = line.getOrNull(2) + CompanyList( + companyListName = name ?: return@mapNotNull null, + companyListSymbol = symbol ?: return@mapNotNull null, + companyListExchange = exchange ?: return@mapNotNull null + ) + } + .also { + csvReader.close() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/IntradayInfoParser.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/IntradayInfoParser.kt new file mode 100644 index 0000000..ba17920 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/parser/IntradayInfoParser.kt @@ -0,0 +1,41 @@ +package com.ibrahimcanerdogan.stockmarket.data.parser + +import com.ibrahimcanerdogan.stockmarket.data.mapper.toIntradayInfo +import com.ibrahimcanerdogan.stockmarket.data.remote.dto.IntradayInfoDto +import com.ibrahimcanerdogan.stockmarket.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 javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntradayInfoParser @Inject constructor(): CSVParser { + + 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 + val dto = IntradayInfoDto(timestamp, close.toDouble()) + dto.toIntradayInfo() + } + .filter { + it.intradayDate.dayOfMonth == LocalDate.now().minusDays(4).dayOfMonth + } + .sortedBy { + it.intradayDate.hour + } + .also { + csvReader.close() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/StockAPI.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/StockAPI.kt new file mode 100644 index 0000000..a62c5c2 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/StockAPI.kt @@ -0,0 +1,31 @@ +package com.ibrahimcanerdogan.stockmarket.data.remote + +import com.ibrahimcanerdogan.stockmarket.data.remote.dto.CompanyDetailDto +import okhttp3.ResponseBody +import retrofit2.http.GET +import retrofit2.http.Query + +interface StockAPI { + + @GET("query?function=LISTING_STATUS") + suspend fun getListings( + @Query("apikey") apiKey: String = API_KEY + ): ResponseBody + + @GET("query?function=TIME_SERIES_INTRADAY&interval=60min&datatype=csv") + suspend fun getIntradayInfo( + @Query("symbol") symbol: String, + @Query("apikey") apiKey: String = API_KEY + ): ResponseBody + + @GET("query?function=OVERVIEW") + suspend fun getCompanyInfo( + @Query("symbol") symbol: String, + @Query("apikey") apiKey: String = API_KEY + ): CompanyDetailDto + + companion object { + const val API_KEY = "demo" + const val BASE_URL = "https://alphavantage.co" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/CompanyDetailDto.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/CompanyDetailDto.kt new file mode 100644 index 0000000..e531e09 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/CompanyDetailDto.kt @@ -0,0 +1,11 @@ +package com.ibrahimcanerdogan.stockmarket.data.remote.dto + +import com.squareup.moshi.Json + +data class CompanyDetailDto( + @field:Json(name = "Symbol") val symbol: String?, + @field:Json(name = "Description") val description: String?, + @field:Json(name = "Name") val name: String?, + @field:Json(name = "Country") val country: String?, + @field:Json(name = "Industry") val industry: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/IntradayInfoDto.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/IntradayInfoDto.kt new file mode 100644 index 0000000..b23ab56 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/remote/dto/IntradayInfoDto.kt @@ -0,0 +1,6 @@ +package com.ibrahimcanerdogan.stockmarket.data.remote.dto + +data class IntradayInfoDto( + val timestamp: String, + val close: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/repository/StockRepositoryImpl.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/repository/StockRepositoryImpl.kt new file mode 100644 index 0000000..d39fc69 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/data/repository/StockRepositoryImpl.kt @@ -0,0 +1,77 @@ +package com.ibrahimcanerdogan.stockmarket.data.repository + +import com.ibrahimcanerdogan.stockmarket.data.mapper.toCompanyInfo +import com.ibrahimcanerdogan.stockmarket.data.parser.CSVParser +import com.ibrahimcanerdogan.stockmarket.data.remote.StockAPI +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyDetail +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyList +import com.ibrahimcanerdogan.stockmarket.domain.model.IntradayInfo +import com.ibrahimcanerdogan.stockmarket.domain.repository.StockRepository +import com.ibrahimcanerdogan.stockmarket.util.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import retrofit2.HttpException +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StockRepositoryImpl @Inject constructor( + private val stockAPI: StockAPI, + private val companyListingsParser: CSVParser, + private val intradayInfoParser: CSVParser, +): StockRepository { + + override suspend fun getCompanyListings( + query: String + ): Flow>> { + return flow { + emit(Resource.Loading(true)) + try { + val response = stockAPI.getListings() + if (query.isNotEmpty()) { + emit(Resource.Success( + companyListingsParser.parse(response.byteStream()) + .filter { it.companyListName.contains(query, ignoreCase = true) } + )) + } else{ + emit(Resource.Success(companyListingsParser.parse(response.byteStream()))) + } + } catch(e: IOException) { + e.printStackTrace() + emit(Resource.Error("Couldn't load data")) + } catch (e: HttpException) { + e.printStackTrace() + emit(Resource.Error("Couldn't load data")) + } + emit(Resource.Loading(false)) + } + } + + override suspend fun getIntradayInfo(symbol: String): Resource> { + return try { + val response = stockAPI.getIntradayInfo(symbol) + val results = intradayInfoParser.parse(response.byteStream()) + Resource.Success(results) + } catch(e: IOException) { + e.printStackTrace() + Resource.Error(message = "Couldn't load intraday info") + } catch(e: HttpException) { + e.printStackTrace() + Resource.Error(message = "Couldn't load intraday info") + } + } + + override suspend fun getCompanyInfo(symbol: String): Resource { + return try { + val result = stockAPI.getCompanyInfo(symbol) + Resource.Success(result.toCompanyInfo()) + } catch(e: IOException) { + e.printStackTrace() + Resource.Error(message = "Couldn't load company info") + } catch(e: HttpException) { + e.printStackTrace() + Resource.Error(message = "Couldn't load company info") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/ParserRepository.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/ParserRepository.kt new file mode 100644 index 0000000..dfc0946 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/ParserRepository.kt @@ -0,0 +1,30 @@ +package com.ibrahimcanerdogan.stockmarket.dependencyinjection + +import com.ibrahimcanerdogan.stockmarket.data.parser.CSVParser +import com.ibrahimcanerdogan.stockmarket.data.parser.CompanyListParser +import com.ibrahimcanerdogan.stockmarket.data.parser.IntradayInfoParser +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyList +import com.ibrahimcanerdogan.stockmarket.domain.model.IntradayInfo +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class ParserRepository { + + @Binds + @Singleton + abstract fun bindCompanyListingsParser( + companyListParser: CompanyListParser + ): CSVParser + + @Binds + @Singleton + abstract fun bindIntradayInfoParser( + intradayInfoParser: IntradayInfoParser + ): CSVParser + +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RepositoryModule.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RepositoryModule.kt new file mode 100644 index 0000000..41202dc --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RepositoryModule.kt @@ -0,0 +1,20 @@ +package com.ibrahimcanerdogan.stockmarket.dependencyinjection + +import com.ibrahimcanerdogan.stockmarket.data.repository.StockRepositoryImpl +import com.ibrahimcanerdogan.stockmarket.domain.repository.StockRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindStockRepository( + stockRepositoryImpl: StockRepositoryImpl + ): StockRepository +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RetrofitModule.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RetrofitModule.kt new file mode 100644 index 0000000..ad7a02d --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/dependencyinjection/RetrofitModule.kt @@ -0,0 +1,33 @@ +package com.ibrahimcanerdogan.stockmarket.dependencyinjection + +import com.ibrahimcanerdogan.stockmarket.data.remote.StockAPI +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.create +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + + @Provides + @Singleton + fun provideStockAPI(): StockAPI { + return Retrofit.Builder() + .baseUrl(StockAPI.BASE_URL) + .addConverterFactory(MoshiConverterFactory.create()) + .client(OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + }).build() + ) + .build() + .create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyDetail.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyDetail.kt new file mode 100644 index 0000000..c740edb --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyDetail.kt @@ -0,0 +1,9 @@ +package com.ibrahimcanerdogan.stockmarket.domain.model + +data class CompanyDetail( + val companyDetailSymbol: String, + val companyDetailDescription: String, + val companyDetailName: String, + val companyDetailCountry: String, + val companyDetailIndustry: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyList.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyList.kt new file mode 100644 index 0000000..ccc77cf --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/CompanyList.kt @@ -0,0 +1,7 @@ +package com.ibrahimcanerdogan.stockmarket.domain.model + +data class CompanyList( + val companyListName: String, + val companyListSymbol: String, + val companyListExchange: String, +) diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/IntradayInfo.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/IntradayInfo.kt new file mode 100644 index 0000000..7d2cc12 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/model/IntradayInfo.kt @@ -0,0 +1,8 @@ +package com.ibrahimcanerdogan.stockmarket.domain.model + +import java.time.LocalDateTime + +data class IntradayInfo( + val intradayDate: LocalDateTime, + val intradayClose: Double +) diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/repository/StockRepository.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/repository/StockRepository.kt new file mode 100644 index 0000000..23504fb --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/domain/repository/StockRepository.kt @@ -0,0 +1,22 @@ +package com.ibrahimcanerdogan.stockmarket.domain.repository + +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyDetail +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyList +import com.ibrahimcanerdogan.stockmarket.domain.model.IntradayInfo +import com.ibrahimcanerdogan.stockmarket.util.Resource +import kotlinx.coroutines.flow.Flow + +interface StockRepository { + + suspend fun getCompanyListings( + query: String + ): Flow>> + + suspend fun getIntradayInfo( + symbol: String + ): Resource> + + suspend fun getCompanyInfo( + symbol: String + ): Resource +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/component/StockChart.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/component/StockChart.kt new file mode 100644 index 0000000..0638861 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/component/StockChart.kt @@ -0,0 +1,114 @@ +package com.ibrahimcanerdogan.stockmarket.ui.component + +import android.graphics.Paint +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.ibrahimcanerdogan.stockmarket.domain.model.IntradayInfo +import kotlin.math.round +import kotlin.math.roundToInt + +@Composable +fun StockChart( + infos: List = emptyList(), + modifier: Modifier = Modifier, + graphColor: Color = Color.Green +) { + val spacing = 100f + val transparentGraphColor = remember { + graphColor.copy(alpha = 0.5f) + } + val upperValue = remember(infos) { + (infos.maxOfOrNull { it.intradayClose }?.plus(1))?.roundToInt() ?: 0 + } + val lowerValue = remember(infos) { + infos.minOfOrNull { it.intradayClose }?.toInt() ?: 0 + } + val density = LocalDensity.current + val textPaint = remember(density) { + Paint().apply { + color = android.graphics.Color.WHITE + textAlign = Paint.Align.CENTER + textSize = density.run { 12.sp.toPx() } + } + } + Canvas(modifier = modifier) { + val spacePerHour = (size.width - spacing) / infos.size + (0 until infos.size - 1 step 2).forEach { i -> + val info = infos[i] + val hour = info.intradayDate.hour + drawContext.canvas.nativeCanvas.apply { + drawText( + hour.toString(), + spacing + i * spacePerHour, + size.height - 5, + textPaint + ) + } + } + val priceStep = (upperValue - lowerValue) / 5f + (0..4).forEach { i -> + drawContext.canvas.nativeCanvas.apply { + drawText( + round(lowerValue + priceStep * i).toString(), + 30f, + size.height - spacing - i * size.height / 5f, + textPaint + ) + } + } + var lastX = 0f + val strokePath = Path().apply { + val height = size.height + for(i in infos.indices) { + val info = infos[i] + val nextInfo = infos.getOrNull(i + 1) ?: infos.last() + val leftRatio = (info.intradayClose - lowerValue) / (upperValue - lowerValue) + val rightRatio = (nextInfo.intradayClose - lowerValue) / (upperValue - lowerValue) + + val x1 = spacing + i * spacePerHour + val y1 = height - spacing - (leftRatio * height).toFloat() + val x2 = spacing + (i + 1) * spacePerHour + val y2 = height - spacing - (rightRatio * height).toFloat() + if(i == 0) { + moveTo(x1, y1) + } + lastX = (x1 + x2) / 2f + quadraticTo( + x1, y1, lastX, (y1 + y2) / 2f + ) + } + } + val fillPath = android.graphics.Path(strokePath.asAndroidPath()) + .asComposePath() + .apply { + lineTo(lastX, size.height - spacing) + lineTo(spacing, size.height - spacing) + close() + } + drawPath( + path = fillPath, + brush = Brush.verticalGradient( + colors = listOf( + transparentGraphColor, + Color.Transparent + ), + endY = size.height - spacing + ) + ) + drawPath( + path = strokePath, + color = graphColor, + style = Stroke( + width = 3.dp.toPx(), + cap = StrokeCap.Round + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailScreen.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailScreen.kt new file mode 100644 index 0000000..1036997 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailScreen.kt @@ -0,0 +1,117 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.detail + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ibrahimcanerdogan.stockmarket.ui.component.StockChart +import com.ramcosta.composedestinations.annotation.Destination + +@Composable +@Destination +fun CompanyInfoScreen( + symbol: String, + viewModel: CompanyDetailViewModel = hiltViewModel() +) { + val state = viewModel.state + + Scaffold { paddingValues -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + if (state.detailError == null) { + + Column { + state.detailCompany?.let { company -> + Text( + text = company.companyDetailName, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = company.companyDetailSymbol, + fontStyle = FontStyle.Italic, + fontSize = 14.sp, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Industry: ${company.companyDetailIndustry}", + fontSize = 14.sp, + modifier = Modifier.fillMaxWidth(), + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Country: ${company.companyDetailCountry}", + fontSize = 14.sp, + modifier = Modifier.fillMaxWidth(), + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = company.companyDetailDescription, + fontSize = 12.sp, + modifier = Modifier.fillMaxWidth(), + ) + if (state.detailIntradayInfo.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Market Summary") + Spacer(modifier = Modifier.height(32.dp)) + StockChart( + infos = state.detailIntradayInfo, + modifier = Modifier + .fillMaxWidth() + .height(250.dp) + .align(CenterHorizontally) + ) + } + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Center + ) { + if (state.detailIsLoading) { + CircularProgressIndicator() + } else if (state.detailError != null) { + Text( + text = state.detailError, + color = MaterialTheme.colorScheme.error + ) + } + } + } + } +} diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailState.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailState.kt new file mode 100644 index 0000000..207cf2d --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailState.kt @@ -0,0 +1,11 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.detail + +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyDetail +import com.ibrahimcanerdogan.stockmarket.domain.model.IntradayInfo + +data class CompanyDetailState( + val detailIntradayInfo: List = emptyList(), + val detailCompany: CompanyDetail? = null, + val detailIsLoading: Boolean = false, + val detailError: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailViewModel.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailViewModel.kt new file mode 100644 index 0000000..22e2085 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/detail/CompanyDetailViewModel.kt @@ -0,0 +1,66 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.detail + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ibrahimcanerdogan.stockmarket.domain.repository.StockRepository +import com.ibrahimcanerdogan.stockmarket.util.Resource +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CompanyDetailViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val repository: StockRepository +): ViewModel() { + + var state by mutableStateOf(CompanyDetailState()) + + init { + viewModelScope.launch { + val symbol = savedStateHandle.get("symbol") ?: return@launch + state = state.copy(detailIsLoading = true) + val companyInfoResult = async { repository.getCompanyInfo(symbol) } + val intradayInfoResult = async { repository.getIntradayInfo(symbol) } + when(val result = companyInfoResult.await()) { + is Resource.Success -> { + state = state.copy( + detailCompany = result.data, + detailIsLoading = false, + detailError = null + ) + } + is Resource.Error -> { + state = state.copy( + detailIsLoading = false, + detailError = result.message, + detailCompany = null + ) + } + else -> Unit + } + when(val result = intradayInfoResult.await()) { + is Resource.Success -> { + state = state.copy( + detailIntradayInfo = result.data ?: emptyList(), + detailIsLoading = false, + detailError = null + ) + } + is Resource.Error -> { + state = state.copy( + detailIsLoading = false, + detailError = result.message, + detailCompany = null + ) + } + else -> Unit + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListEvent.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListEvent.kt new file mode 100644 index 0000000..f0f1a60 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListEvent.kt @@ -0,0 +1,6 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.list + +sealed class CompanyListEvent { + data object Refresh: CompanyListEvent() + data class OnSearchQueryChange(val query: String): CompanyListEvent() +} diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListScreen.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListScreen.kt new file mode 100644 index 0000000..f9f08a3 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListScreen.kt @@ -0,0 +1,124 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyList +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import androidx.compose.foundation.layout.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.ui.Alignment +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import com.ibrahimcanerdogan.stockmarket.ui.screen.destinations.CompanyInfoScreenDestination + +@Composable +@Destination(start = true) +fun CompanyListingsScreen( + navigator: DestinationsNavigator, + viewModel: CompanyListViewModel = hiltViewModel() +) { + val state = viewModel.state + val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = viewModel.state.listIsRefreshing) + + Scaffold { paddingValues -> + Surface(Modifier.fillMaxSize().padding(paddingValues)) { + Column { + OutlinedTextField( + value = state.listSearchQuery, + onValueChange = { + viewModel.onEvent(CompanyListEvent.OnSearchQueryChange(it)) + }, + modifier = Modifier.padding(16.dp).fillMaxWidth(), + placeholder = { Text(text = "Search...") }, + maxLines = 1, + singleLine = true + ) + SwipeRefresh( + state = swipeRefreshState, + onRefresh = { + viewModel.onEvent(CompanyListEvent.Refresh) + } + ) { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(state.listCompany.size) { i -> + val company = state.listCompany[i] + CompanyListItem( + company = company, + modifier = Modifier + .fillMaxWidth() + .clickable { + navigator.navigate(CompanyInfoScreenDestination(company.companyListSymbol)) + } + .padding(16.dp) + ) + if(i < state.listCompany.size) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + } + } + } + } + } + } + } +} + +@Composable +fun CompanyListItem( + company: CompanyList, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = company.companyListName, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onBackground, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = company.companyListExchange, + fontWeight = FontWeight.Light, + color = MaterialTheme.colorScheme.onBackground + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "(${company.companyListSymbol})", + fontStyle = FontStyle.Italic, + color = MaterialTheme.colorScheme.onBackground + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListState.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListState.kt new file mode 100644 index 0000000..7849a4c --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListState.kt @@ -0,0 +1,10 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.list + +import com.ibrahimcanerdogan.stockmarket.domain.model.CompanyList + +data class CompanyListState( + val listCompany: List = emptyList(), + val listIsLoading: Boolean = false, + val listIsRefreshing: Boolean = false, + val listSearchQuery: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListViewModel.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListViewModel.kt new file mode 100644 index 0000000..fb69c2e --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/screen/list/CompanyListViewModel.kt @@ -0,0 +1,65 @@ +package com.ibrahimcanerdogan.stockmarket.ui.screen.list + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ibrahimcanerdogan.stockmarket.domain.repository.StockRepository +import com.ibrahimcanerdogan.stockmarket.util.Resource +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CompanyListViewModel @Inject constructor( + private val repository: StockRepository +): ViewModel() { + + var state by mutableStateOf(CompanyListState()) + private var searchJob: Job? = null + + init { + getCompanyListings() + } + + fun onEvent(event: CompanyListEvent) { + when(event) { + is CompanyListEvent.Refresh -> { + getCompanyListings() + } + is CompanyListEvent.OnSearchQueryChange -> { + state = state.copy(listSearchQuery = event.query) + searchJob?.cancel() + searchJob = viewModelScope.launch { + delay(500L) + getCompanyListings() + } + } + } + } + + private fun getCompanyListings( + query: String = state.listSearchQuery + ) { + viewModelScope.launch { + repository + .getCompanyListings(query) + .collect { result -> + when(result) { + is Resource.Success -> { + result.data?.let { listings -> + state = state.copy(listCompany = listings) + } + } + is Resource.Error -> Unit + is Resource.Loading -> { + state = state.copy(listIsLoading = result.isLoading) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Color.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Color.kt new file mode 100644 index 0000000..7d65ec9 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.ibrahimcanerdogan.stockmarket.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Theme.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Theme.kt new file mode 100644 index 0000000..c988640 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.ibrahimcanerdogan.stockmarket.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun StockMarketTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Type.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Type.kt similarity index 52% rename from app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Type.kt rename to app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Type.kt index f433b13..426b359 100644 --- a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Type.kt +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/ui/theme/Type.kt @@ -1,6 +1,6 @@ -package com.plcoding.stockmarketapp.ui.theme +package com.ibrahimcanerdogan.stockmarket.ui.theme -import androidx.compose.material.Typography +import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -8,21 +8,27 @@ import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( - body1 = TextStyle( + bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 16.sp + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp ) /* Other default text styles to override - button = TextStyle( + titleLarge = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.W500, - fontSize = 14.sp + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp ), - caption = TextStyle( + labelSmall = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp ) */ ) \ No newline at end of file diff --git a/app/src/main/java/com/ibrahimcanerdogan/stockmarket/util/Resource.kt b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/util/Resource.kt new file mode 100644 index 0000000..622a9f1 --- /dev/null +++ b/app/src/main/java/com/ibrahimcanerdogan/stockmarket/util/Resource.kt @@ -0,0 +1,7 @@ +package com.ibrahimcanerdogan.stockmarket.util + +sealed class Resource(val data: T? = null, val message: String? = null) { + class Success(data: T?): Resource(data) + class Error(message: String, data: T? = null): Resource(data, message) + class Loading(val isLoading: Boolean = true): Resource(null) +} diff --git a/app/src/main/java/com/plcoding/stockmarketapp/MainActivity.kt b/app/src/main/java/com/plcoding/stockmarketapp/MainActivity.kt deleted file mode 100644 index 58b80a0..0000000 --- a/app/src/main/java/com/plcoding/stockmarketapp/MainActivity.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.plcoding.stockmarketapp - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.plcoding.stockmarketapp.ui.theme.StockMarketAppTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - StockMarketAppTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colors.background - ) { - - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Color.kt b/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Color.kt deleted file mode 100644 index 6ac138a..0000000 --- a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Color.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.plcoding.stockmarketapp.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Shape.kt b/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Shape.kt deleted file mode 100644 index 6fad842..0000000 --- a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Shape.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.plcoding.stockmarketapp.ui.theme - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Shapes -import androidx.compose.ui.unit.dp - -val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(4.dp), - large = RoundedCornerShape(0.dp) -) \ No newline at end of file diff --git a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Theme.kt b/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Theme.kt deleted file mode 100644 index e945dfc..0000000 --- a/app/src/main/java/com/plcoding/stockmarketapp/ui/theme/Theme.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.plcoding.stockmarketapp.ui.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors -import androidx.compose.runtime.Composable - -private val DarkColorPalette = darkColors( - primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200 -) - -private val LightColorPalette = lightColors( - primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200 - - /* Other default colors to override - background = Color.White, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, - onBackground = Color.Black, - onSurface = Color.Black, - */ -) - -@Composable -fun StockMarketAppTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit -) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette - } - - MaterialTheme( - colors = colors, - typography = Typography, - shapes = Shapes, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cf..6f3b755 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cf..6f3b755 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49ec0f9..0df33ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - StockMarketApp + StockMarket \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2768a27..89e55fd 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,7 +1,5 @@ - +