diff --git a/README.md b/README.md
index fa0331d..3a394ef 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,148 @@
-This is a Kotlin Multiplatform project targeting Android, iOS.
+# Weather
-* `/composeApp` is for code that will be shared across your Compose Multiplatform applications.
- It contains several subfolders:
- - `commonMain` is for code that’s common for all targets.
- - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
- For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
- `iosMain` would be the right folder for such calls.
+Weather is a simple Kotlin Multiplatform application built with Compose Multiplatform that gets
+weather information data for a specific city.
-* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform,
- you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
+## Features
+- [X] user can get today's weather data
+- [X] user can get weather data for the next 7 days
+- [X] user can get the last 14 days weather data
-Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)…
+## Design
+
+### android
+
+| Loading | Today | More |
+|:-----------------------------------------------------------------------------:|:---------------------------------------------------------------------------:|:--------------------------------------------------------------------------:|
+|
|
|
|
+
+### iOS
+
+| Loading | Today | More |
+|:-------------------------------------------------------------------------:|:-----------------------------------------------------------------------:|:----------------------------------------------------------------------:|
+|
|
|
|
+
+## Architecture
+
+This project uses the MVI(Model - View - Intent) architecture based on UDF(Unidirectional Data Flow)
+and Reactive programming.
+
+Why?
+
+- more clear and intentional separation of concerns
+- single source of truth for our UI state which can only be mutated by intent/actions
+- simpler and more direct UI testability, since we can define how the UI should look like with our
+ state objects
+
+### Packaging Structure
+
+- `sources`
+ - `remotesource`
+ - handles getting data from any server/remote source
+ - `localsource`
+ - handles getting cached device data
+- `data`
+ - handles getting and mutating data from needed sources
+- `domain`
+ - handles encasing business logic for reuse
+- `ui`
+ - handles displaying data on device
+
+### Testing
+
+The app includes both unit and instrumented tests.
+#### Sources
+---
+
+- Remote
+
+| WeatherRemoteSource |
+|:----------------------------------------------------------------------------:|
+|
|
+
+- Local
+
+| WeatherLocalSource |
+|:-------------------------------------------:|
+|
|
+
+#### Data
+---
+
+- Repositories
+
+| WeatherRepository |
+|:--------------------------------------------------------------------------:|
+|
|
+
+- Extensions
+
+| DateTime |
+|:-----------------------------------------------------------------------------------------:|
+|
|
+
+| String |
+|:---------------------------------------------------------------------------------------:|
+|
|
+
+| Int |
+|:------------------------------------------------------------------------------------:|
+|
|
+
+#### Domain
+---
+
+| GetCurrentWeatherDataUseCase |
+|:------------------------------------------------------------------------------------:|
+|
|
+
+| GetHistoryWeatherDataUseCase |
+|:------------------------------------------------------------------------------------:|
+|
|
+
+#### ui
+---
+
+- screen-model
+
+| WeatherDetailScreenModel |
+|:---------------------------------------------------------------------------------------:|
+|
|
+
+| WeatherListScreenModel |
+|:------------------------------------------------------------------------------------:|
+|
|
+
+- screens
+
+| WeatherDetailScreen |
+|:---------------------------------------------------------------------------------------:|
+|
|
+
+| WeatherListScreen |
+|:-------------------------------------------:|
+| |
+
+## Stack
+
+### Language & Framework
+
+| Title | Description |
+ |:-----------------------------------------------------------------------------------|:-----------------------------------|
+| [Kotlin](https://kotlinlang.org/) | `fun` programming language |
+| [KMP - Kotlin Multiplatform](https://www.jetbrains.com/kotlin-multiplatform/) | cross platform framework |
+| [CMP - Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) | declarative UI rendering framework |
+| [Ktor](https://github.com/ktorio/ktor) | networking client framework |
+
+### Libraries
+
+| Title | Description |
+|:-------------------------------------------------------------------------|:------------------|
+| [Kotlinx-DateTime](https://github.com/Kotlin/kotlinx-datetime) | date/time library |
+| [Kotlinx-Coroutines](https://github.com/Kotlin/kotlinx.coroutines) | async programming |
+| [Kotlinx-Serialization](https://github.com/Kotlin/kotlinx.serialization) | serialization |
+| [Kamel](https://github.com/Kamel-Media/Kamel) | image loading |
+| [Voyager](https://github.com/adrielcafe/voyager) | navigation |
+
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/composables/WeatherHourItem.kt b/composeApp/src/commonMain/kotlin/ui/composables/WeatherHourItem.kt
index 5a689f3..0b14633 100644
--- a/composeApp/src/commonMain/kotlin/ui/composables/WeatherHourItem.kt
+++ b/composeApp/src/commonMain/kotlin/ui/composables/WeatherHourItem.kt
@@ -42,7 +42,7 @@ fun WeatherHourItem(
) {
Text(
text = buildAnnotatedString {
- append("${item.temperatureInCelsius}")
+ append("${item.temperatureInCelsius.toInt()}")
withStyle(
style = SpanStyle(
fontSize = 12.sp,
diff --git a/composeApp/src/commonMain/kotlin/ui/screens/detail/WeatherDetailScreenModel.kt b/composeApp/src/commonMain/kotlin/ui/screens/detail/WeatherDetailScreenModel.kt
index 14cb708..b68bfba 100644
--- a/composeApp/src/commonMain/kotlin/ui/screens/detail/WeatherDetailScreenModel.kt
+++ b/composeApp/src/commonMain/kotlin/ui/screens/detail/WeatherDetailScreenModel.kt
@@ -25,6 +25,7 @@ class WeatherDetailScreenModel(
) : StatefulScreenModel(initial = WeatherDetailScreenState()) {
fun getCurrentWeatherForecast() {
+ if (state.value.weatherData !is LoadState.Success)
screenModelScope.launch {
updateState { copy(weatherData = LoadState.Loading) }
when (val result = getCurrentWeatherDataUseCase()) {
diff --git a/composeApp/src/commonTest/kotlin/data/repositories/WeatherRepositoryTest.kt b/composeApp/src/commonTest/kotlin/data/repositories/WeatherRepositoryTest.kt
index b6fea65..90b6957 100644
--- a/composeApp/src/commonTest/kotlin/data/repositories/WeatherRepositoryTest.kt
+++ b/composeApp/src/commonTest/kotlin/data/repositories/WeatherRepositoryTest.kt
@@ -29,35 +29,34 @@ import kotlin.test.assertTrue
class FakeWeatherRepository : WeatherRepository {
private val data: MutableStateFlow?> = MutableStateFlow(emptyList())
+ private val fake = WeatherForecastData(
+ date = LocalDate(2024, 1, 1),
+ day = WeatherDayData(
+ maxTemperatureInCelsius = 35.0,
+ minTemperatureInCelsius = 24.0,
+ averageTemperatureInCelsius = 29.5,
+ averageHumidity = 24.0,
+ maxWindInKilometersPerHour = 40.0,
+ willRain = false,
+ chancesOfRainInPercentage = 25.0,
+ condition = WeatherConditionData("Sunny", "")
+ ),
+ hours = listOf(
+ WeatherHourData(
+ time = LocalDateTime(2024, 2, 3, 6, 30, 0, 0),
+ temperatureInCelsius = 25.0,
+ isDaylight = true,
+ condition = WeatherConditionData("Sunny", "")
+ )
+ )
+ )
fun simulateError() {
data.value = null
}
fun simulateSuccess() {
- data.value = listOf(
- WeatherForecastData(
- date = LocalDate(2024, 1, 1),
- day = WeatherDayData(
- maxTemperatureInCelsius = 35.0,
- minTemperatureInCelsius = 24.0,
- averageTemperatureInCelsius = 29.5,
- averageHumidity = 24.0,
- maxWindInKilometersPerHour = 40.0,
- willRain = false,
- chancesOfRainInPercentage = 25.0,
- condition = WeatherConditionData("Sunny", "")
- ),
- hours = listOf(
- WeatherHourData(
- time = LocalDateTime(2024, 2, 3, 6, 30, 0, 0),
- temperatureInCelsius = 25.0,
- isDaylight = true,
- condition = WeatherConditionData("Sunny", "")
- )
- )
- )
- )
+ data.value = listOf(fake, fake)
}
override suspend fun getCurrentWeatherData(): DataResult> {
@@ -92,7 +91,7 @@ class WeatherRepositoryTest {
}
@Test
- fun `given WeatherRepository - when fetching weather data - should return DataResult Error`() =
+ fun `given WeatherRepository - when fetching weather data - on failure should return DataResult Error`() =
runTest {
repository.simulateError()
val data = repository.getCurrentWeatherData()
diff --git a/composeApp/src/commonTest/kotlin/ui/screens/WeatherListScreenModelTest.kt b/composeApp/src/commonTest/kotlin/ui/screens/WeatherListScreenModelTest.kt
index 944e8be..e6918ef 100644
--- a/composeApp/src/commonTest/kotlin/ui/screens/WeatherListScreenModelTest.kt
+++ b/composeApp/src/commonTest/kotlin/ui/screens/WeatherListScreenModelTest.kt
@@ -18,6 +18,7 @@ import ui.screens.list.WeatherListScreenModel
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
@@ -79,7 +80,7 @@ class WeatherListScreenModelTest {
@Test
fun `given WeatherListScreenModel - when updating futureState - should be ListState Success`() =
runTest {
- weatherListScreenModel.updateFutureList(listOf())
+ weatherListScreenModel.updateFutureList(listOf(data))
val data = weatherListScreenModel.state.value.futureState
assertTrue { data is LoadState.Success }
}
@@ -87,10 +88,11 @@ class WeatherListScreenModelTest {
@Test
fun `given WeatherListScreenModel - when updating futureState with data - state should contain data`() =
runTest {
- weatherListScreenModel.updateFutureList(listOf(data))
- val data = weatherListScreenModel.state.value.futureState
- val value = (data as LoadState.Success).data
- assertTrue { value.isNotEmpty() }
+ val list = listOf(data, data)
+ weatherListScreenModel.updateFutureList(list)
+ val state = weatherListScreenModel.state.value.futureState
+ val value = (state as LoadState.Success).data
+ assertEquals(listOf(data), value)
}
@Test
diff --git a/images/android/loading.png b/images/android/loading.png
new file mode 100644
index 0000000..b9ed94c
Binary files /dev/null and b/images/android/loading.png differ
diff --git a/images/android/more.png b/images/android/more.png
new file mode 100644
index 0000000..e3b27a6
Binary files /dev/null and b/images/android/more.png differ
diff --git a/images/android/today.png b/images/android/today.png
new file mode 100644
index 0000000..cc4643c
Binary files /dev/null and b/images/android/today.png differ
diff --git a/images/ios/loading.png b/images/ios/loading.png
new file mode 100644
index 0000000..ce1a487
Binary files /dev/null and b/images/ios/loading.png differ
diff --git a/images/ios/more.png b/images/ios/more.png
new file mode 100644
index 0000000..4983f19
Binary files /dev/null and b/images/ios/more.png differ
diff --git a/images/ios/today.png b/images/ios/today.png
new file mode 100644
index 0000000..c1fccdf
Binary files /dev/null and b/images/ios/today.png differ
diff --git a/images/testing/domain/current.png b/images/testing/domain/current.png
new file mode 100644
index 0000000..f41ec94
Binary files /dev/null and b/images/testing/domain/current.png differ
diff --git a/images/testing/domain/history.png b/images/testing/domain/history.png
new file mode 100644
index 0000000..6b4d999
Binary files /dev/null and b/images/testing/domain/history.png differ
diff --git a/images/testing/extensions/datetime.png b/images/testing/extensions/datetime.png
new file mode 100644
index 0000000..ded4542
Binary files /dev/null and b/images/testing/extensions/datetime.png differ
diff --git a/images/testing/extensions/int.png b/images/testing/extensions/int.png
new file mode 100644
index 0000000..fdce53f
Binary files /dev/null and b/images/testing/extensions/int.png differ
diff --git a/images/testing/extensions/string.png b/images/testing/extensions/string.png
new file mode 100644
index 0000000..b72b445
Binary files /dev/null and b/images/testing/extensions/string.png differ
diff --git a/images/testing/remote.png b/images/testing/remote.png
new file mode 100644
index 0000000..54223a9
Binary files /dev/null and b/images/testing/remote.png differ
diff --git a/images/testing/repo.png b/images/testing/repo.png
new file mode 100644
index 0000000..90901dc
Binary files /dev/null and b/images/testing/repo.png differ
diff --git a/images/testing/ui/models/details.png b/images/testing/ui/models/details.png
new file mode 100644
index 0000000..25b50d6
Binary files /dev/null and b/images/testing/ui/models/details.png differ
diff --git a/images/testing/ui/models/list.png b/images/testing/ui/models/list.png
new file mode 100644
index 0000000..8af6184
Binary files /dev/null and b/images/testing/ui/models/list.png differ
diff --git a/images/testing/ui/screens/detail.png b/images/testing/ui/screens/detail.png
new file mode 100644
index 0000000..9a1fd89
Binary files /dev/null and b/images/testing/ui/screens/detail.png differ
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 66c59c0..5667b42 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -39,6 +39,30 @@
name = Frameworks;
sourceTree = "";
};
+ 62DBF4183AB37B668A72A8E5 /* xcschemes */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = xcschemes;
+ sourceTree = "";
+ };
+ 62DBF5F15D2B2366E4BAFF9A /* mambo.xcuserdatad */ = {
+ isa = PBXGroup;
+ children = (
+ 62DBF4183AB37B668A72A8E5 /* xcschemes */,
+ );
+ path = mambo.xcuserdatad;
+ sourceTree = "";
+ };
+ 62DBF7D64596272A11ECB760 /* xcuserdata */ = {
+ isa = PBXGroup;
+ children = (
+ 62DBF5F15D2B2366E4BAFF9A /* mambo.xcuserdatad */,
+ );
+ name = xcuserdata;
+ path = iosApp.xcodeproj/xcuserdata;
+ sourceTree = "";
+ };
7555FF72242A565900829871 = {
isa = PBXGroup;
children = (
@@ -46,6 +70,7 @@
7555FF7D242A565900829871 /* iosApp */,
7555FF7C242A565900829871 /* Products */,
42799AB246E5F90AF97AA0EF /* Frameworks */,
+ 62DBF7D64596272A11ECB760 /* xcuserdata */,
);
sourceTree = "";
};