{"id":17248898,"url":"https://github.com/serras/weatherapp","last_synced_at":"2025-04-14T05:31:35.510Z","repository":{"id":195854651,"uuid":"693817054","full_name":"serras/WeatherApp","owner":"serras","description":"Weather App with Arrow + Compose Desktop","archived":false,"fork":false,"pushed_at":"2024-10-29T22:13:57.000Z","size":34382,"stargazers_count":31,"open_issues_count":9,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-30T00:42:19.307Z","etag":null,"topics":["arrow-kt","compose-desktop","context-receiver","kotlin"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/serras.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-19T19:16:04.000Z","updated_at":"2024-10-03T09:04:35.000Z","dependencies_parsed_at":"2023-09-22T15:46:10.744Z","dependency_job_id":"365c52ba-9a5f-4af2-bdaf-33d9eede7793","html_url":"https://github.com/serras/WeatherApp","commit_stats":null,"previous_names":["serras/weatherapp"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2FWeatherApp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2FWeatherApp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2FWeatherApp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/serras%2FWeatherApp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/serras","download_url":"https://codeload.github.com/serras/WeatherApp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248826588,"owners_count":21167717,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arrow-kt","compose-desktop","context-receiver","kotlin"],"created_at":"2024-10-15T06:42:23.927Z","updated_at":"2025-04-14T05:31:30.500Z","avatar_url":"https://github.com/serras.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Weather App with Arrow + Compose Desktop\n\n\u003e Based on the [How to Build an MVI Clean Code Weather App](https://www.youtube.com/watch?v=eAbKK7JNxCE) tutorial\n\u003e by [Philipp Lackner](https://www.youtube.com/@PhilippLackner). The Weather domain model is heavily based on\n\u003e [his original implementation](https://github.com/philipplackner/WeatherApp).\n\nThis repository contains an implementation of a small weather forecast application using functional style, as\ndescribed in [Arrow's design section](https://arrow-kt.io/learn/design/) and the book [_Functional Ideas for\nthe Curious Kotliner_](https://leanpub.com/fp-ideas-kotlin).\n\nThe application uses [Open-Meteo](https://open-meteo.com/) to gather forecast data, following the [original \ntutorial](https://www.youtube.com/watch?v=eAbKK7JNxCE). [GeoIP2](https://www.maxmind.com/en/geoip2-city) is\nused to map IPs to locations, since we don't use location services.\n\n### Compose Desktop\n\nThe application is implemented in [Compose Multiplatform Desktop](https://www.jetbrains.com/lp/compose-multiplatform/)\ninstead of Android. The main reason is being able to use experimental Kotlin features, which are only available\nin the JVM back-end. Furthermore, it makes it possible for everybody to check the application, even if they don't\nown an Android phone nor want to download a simulator.\n\n## State as sealed interface\n\nThe [original tutorial](https://www.youtube.com/watch?v=eAbKK7JNxCE) uses a class with nullable fields to represent\nthe different states of the application (loading, error, success).\n\n```kotlin\ndata class WeatherState(\n    val isLoading: Boolean, \n    val weatherInfo: WeatherInfo?, \n    val error: String?\n)\n```\n\n[Our implementation](https://github.com/serras/WeatherApp/blob/main/src/main/kotlin/presentation/model/WeatherState.kt)\nuses [sealed interfaces](https://kotlinlang.org/docs/sealed-classes.html) instead.\nEach state gets its own type, making it [impossible to represent invalid states](https://arrow-kt.io/learn/design/domain-modeling/),\n\n```kotlin\nsealed interface WeatherState {\n    data object Loading : WeatherState\n    data class Error(val error: String) : WeatherState\n    data class Ok(val place: String?, val weatherInfo: WeatherInfo) : WeatherState\n}\n```\n\n## Context receivers\n\nOur implementation doesn't use dependency injection framework, as opposed to most Android applications, which use\n[Hilt](https://developer.android.com/training/dependency-injection/hilt-android). Instead, the dependencies are\nrepresented as [context receivers](https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md),\n\n```kotlin\ncontext(WeatherRepository, LocationTracker)\nclass WeatherViewModel { /* implementation */ }\n```\n\nThe actual injection of dependencies is performed manually in the [entry point](https://github.com/serras/WeatherApp/blob/main/src/main/kotlin/Main.kt),\n\n```kotlin\nsuspend fun \u003cA\u003e injectDependencies(\n    block: context(WeatherRepository, LocationTracker) () -\u003e A\n): A = resourceScope {\n    val weather: WeatherRepository = WeatherRepositoryImpl(autoCloseable { WeatherApi() })\n    val location: LocationTracker = autoCloseable { LocationTrackerImpl() }\n    block(weather, location)\n}\n```\n\nAnother advantage of this approach, apart from the speed gains at both compile and run time, is that resources\nare managed correctly using [Arrow's `resourceScope`](https://arrow-kt.io/learn/coroutines/resource-safety/).\nThis is often a convoluted task when using dependency injection frameworks -- when are instances actually created\nand disposed -- whereas here everything is explicit.\n\n### Lifecycle as `CoroutineScope` context\n\nJetpack Compose encourages to keep the activity state in a\n[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel). One of the main benefits of\nthis approach is that ViewModels are lifecycle-aware. For example, if you launch a concurrent coroutine and the\nactivity is then closed, the coroutine is automatically cancelled.\n\nThis ability comes in a great deal from the\n[structured concurrency](https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency)\nguarantees from Kotlin's coroutines. If you capture a `CoroutineScope`, you can launch new coroutines tied to\nthe lifecycle of that scope. This is exactly what we do in\n[our ViewModel](https://github.com/serras/WeatherApp/blob/main/src/main/kotlin/presentation/model/WeatherViewModel.kt),\n\n```kotlin\ncontext(/* other contexts */, CoroutineScope)\nclass WeatherViewModel {\n    /* ... */\n    \n    fun loadWeatherInfo() {\n        // 'launch' comes from the CoroutineScope\n        launch(Dispatchers.IO) {\n            /* ... */\n        }\n    }\n}\n```\n\nIn our case we want to tie the lifecycle of the ViewModel to that of the entire application. The `CoroutineScope`\ncomes from the outermost call to `SuspendApp`.\n\n## [Arrow](https://arrow-kt.io/) DSLs\n\nWe've already mentioned that [`resourceScope`](https://arrow-kt.io/learn/coroutines/resource-safety/) is used\nto correctly manage resource acquisition and disposal. This is one of Arrow's DSLs, each of them providing\nadditional features within a certain scope. The other one used heavily within this application are\n[typed errors](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/).\n\nThe [implementation of `LocationTracker`](https://github.com/serras/WeatherApp/blob/main/src/main/kotlin/data/location/LocationTrackerImpl.kt)\nshowcases how the DSLs can be used and combined.\n\n## Tests with [Turbine](https://cashapp.github.io/turbine/docs/1.x/)\n\nOne of the advantages of having a `Flow` as source of truth for our application is the availability of specialized\ntesting libraries. In particular, [Turbine](https://cashapp.github.io/turbine/docs/1.x/) allows us to specify how\nthe flow should evolve over time.\n\nFor example, one of our tests simulates that our location tracking is failing by providing a `LocationTracker`\ninstance that always returns `null`. In that case, we know that the expected turn of events is _loading_,\nand then _error_.\n\n```kotlin\n\"errors when location is down\" {\n    // set up WeatherViewModel with a LocationTracker that always fails\n    model.state.test {\n        awaitItem().shouldBeInstanceOf\u003cWeatherState.Loading\u003e()\n        model.loadWeatherInfo()\n        awaitItem().shouldBeInstanceOf\u003cWeatherState.Error\u003e()\n    }\n}\n```\n\nAnother tool in our tests is _property-based testing_, brought by [Kotest](https://kotest.io/). Shortly, property-\nbased testing executes the same tests several times with arbitrary data, ensuring that more complex conditions and\ncorner cases are covered. By using their [reflective generators](https://kotest.io/docs/proptest/reflective-arbs.html),\nstarting with a random location and weather data is quite simple.\n\n```kotlin\ncheckAll(\n    Arb.bind\u003cLocation\u003e(),\n    Arb.list(Arb.bind\u003cWeatherData\u003e(), 24..48)\n) { location, weatherData -\u003e /* test */ }\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fserras%2Fweatherapp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fserras%2Fweatherapp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fserras%2Fweatherapp/lists"}