{"id":19301870,"url":"https://github.com/darkness4/train-station","last_synced_at":"2025-08-11T18:31:08.252Z","repository":{"id":36975271,"uuid":"310007491","full_name":"Darkness4/train-station","owner":"Darkness4","description":"A Full Stack demo app with gRPC and Modern Android Development/Svelte to keep up with today's standards.","archived":false,"fork":false,"pushed_at":"2024-10-30T03:23:28.000Z","size":19162,"stargazers_count":10,"open_issues_count":4,"forks_count":3,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-10-30T04:56:54.723Z","etag":null,"topics":["android","grpc","grpc-android","grpc-go","jetpack-compose","kotlin","mvvm-android","mvvm-architecture","nextauthjs","room","svelte","sveltekit"],"latest_commit_sha":null,"homepage":"https://train.mnguyen.fr","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Darkness4.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2020-11-04T13:15:18.000Z","updated_at":"2024-10-30T03:23:31.000Z","dependencies_parsed_at":"2024-01-03T02:59:22.211Z","dependency_job_id":"2c77d3df-56f4-4004-be62-9af0bbe8d3a0","html_url":"https://github.com/Darkness4/train-station","commit_stats":null,"previous_names":[],"tags_count":43,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkness4%2Ftrain-station","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkness4%2Ftrain-station/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkness4%2Ftrain-station/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Darkness4%2Ftrain-station/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Darkness4","download_url":"https://codeload.github.com/Darkness4/train-station/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":229575163,"owners_count":18094725,"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":["android","grpc","grpc-android","grpc-go","jetpack-compose","kotlin","mvvm-android","mvvm-architecture","nextauthjs","room","svelte","sveltekit"],"created_at":"2024-11-09T23:19:09.881Z","updated_at":"2024-12-13T17:29:01.765Z","avatar_url":"https://github.com/Darkness4.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Train Station\n\nBy Marc Nguyen and Jean-Baptiste Rubio.\n\n## Specifications\n\n### API\n\nSpecifications are given here: [Protos](./protos) and [`docs`](./docs)\n\n### Android\n\n- Fetch data from the api and display in a list and a screen with the details\n\n- Possibility to bookmark certain items per user\n\n- OAuth Authentication\n\n- Mockup:\n\n  ![maquette](assets/image-20201128010714763.png)\n\n- Implementation of a search/filter system on the displayed list\n\n- Setting up a local database to display the item list in offline mode\n\n- Usage of StateFlow\n\n## Screenshots\n\n![Screenshot_20201129-053140](assets/Screenshot_20201129-053140.png) ![Screenshot_20201129-053208](assets/Screenshot_20201129-053208.png) ![Screenshot_20201129-053214](assets/Screenshot_20201129-053214.png) ![Screenshot_20201129-053223](assets/Screenshot_20201129-053223.png)\n\n# Modern Android Development (MAD)\n\n[MAD scorecard](https://madscorecard.withgoogle.com/scorecard/share/4258311558/)\n\n![summary](assets/summary.png)\n\n# Documentation\n\n## API\n\n### Setup\n\n#### Production build and deployment\n\nUse docker/kubernetes/openshift to deploy the container.\n\n```sh\ndocker pull ghcr.io/darkness4/train-station-api:latest\n```\n\nAvailable arch are: `arm64` and `amd64`.\n\nAn example of docker-compose.yml:\n\n```sh\nversion: '3.9'\nservices:\n  train-station-api:\n    build: ghcr.io/darkness4/train-station-api:amd64\n    ports:\n      - 3000:3000\n    volumes:\n      - ./db:/db\n    environment:\n      JWT_SECRET: \u003cbase64 secret\u003e\n      LISTEN_ADDRESS: 0.0.0.0:3000\n      DB_PATH: /db/db.sqlite3\n      DEBUG: true\n      TLS_ENABLE: false\n```\n\n#### Setup a development environment\n\n1. Install [golang](https://golang.org) et install the dependencies\n\n   ```sh\n   go mod download\n   ```\n\n2. ```sh\n   # Inside: ./train-station-api\n   make\n   ./bin/train-station-api\n   make unit # Run unit tests\n   ```\n\n### Architecture\n\n```mermaid\nflowchart TD\n\tJWT\n\t*sql.SQL\n\tDB\n\tsubgraph server[gRPC server]\n        healthAPI\n        stationAPI\n        authAPI\n    end\n    *sql.SQL --\u003e DB\n    DB --\u003e stationAPI\n    JWT --\u003e authAPI\n    JWT --\u003e stationAPI\n```\n\nIf you have seen the old versions before version 2, we were using the SOLID architecture in Go. After years of experience, we realized that the SOLID architecture tells us how to organize our code and how to inject dependencies.\n\nHowever, the explicit layering adds standard code and incomprehensible \"data mappings\", which hinders maintainability and understanding of the project. While the SOLID architecture seems ideal for object-oriented languages such as Kotlin, for Go it adds too much boilerplate code with no benefit other than having to \"pseudo-satisfy\" the SOLID principles.\n\nIn reality, the SOLID priciples goes against the [Effective Go](https://go.dev/doc/effective_go) recommendations which is way more important since it is the base for every Go developers, while SOLID are principles for object-oriented programming.\n\nSince SOLID offers no real benefits outside of pain, we decided to remove the explicit layering while still sticking to domain-oriented development.\n\nThe contract is as follows:\n\n- Retrieve the data at the start of the program\n- Retrieve stations (several or one)\n- The user can add a station to his favorites\n\nThis translates into :\n\n- Download the data in the `main.go` as the `main` function indicates the start. The data is stored in a database or cache.\n- The gRPC models are the domain entities and we serve them. This means that we translate the database models into gRPC models.\n- Define a `favoriteSetter` interface and implement it. And the database can implement the interface perfectly.\n\n### Entity relationship\n\n```mermaid\nerDiagram\n    Station }|..|{ User : favorite\n```\n\n### Technologies used\n\n- sqlc for database-first approach and type-safe SQL\n- go-migrate for database migrations\n- gRPC as HTTP server and main entrypoint\n- urfave/cli for the CLI tooling\n- JWT for session handling\n- OAuth2 for Authentication\n\n## Web Front-End\n\n### Setup\n\nInstall [bun](https://bun.sh) and install the dependencies:\n\n```shell\nbun install --frozen-lockfile\n```\n\nCreate a [Github OAuth App](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app), generate a secret with `openssl rand -base64 32`, and fill a `.env` file with the following content:\n\n```shell\nGITHUB_ID=\u003cGithub OAUTH App ID\u003e\nGITHUB_SECRET=\u003cGithub OAUTH App Secret\u003e\nAUTH_SECRET=\u003cRandom Secret\u003e\n```\n\nServe in development mode:\n\n```shell\nbun run dev\n```\n\nOr deploy in production:\n\n```shell\nbun run build\n# bun run preview # for demonstration\n```\n\n### Technologies used\n\n- SvelteKit with SSR as main web framework\n- Pure JWT as authentication helpers\n- protobuf-ts + gRPC as transport\n- ViteJS for bundling and optimizing\n\n## Android App\n\n### Architecture\n\n```mermaid\nflowchart TD\n\toauth[OAuth provider]\n\tsubgraph data[Data Layer]\n\t\tsubgraph cache[Cache]\n          \toauthDataStore\n         \tjwtDataStore\n         \tRoom\n        end\n        oauth\n        StationRepositoryImpl\n        authAPI\n        stationAPI\n    end\n\n    subgraph domain[Domain Layer]\n\t\tStationRepository\n    end\n\n    subgraph presentation[Presentation Layer]\n\t\tLoginViewModel\n\t\tDetailViewModel\n\t\tStationListViewModel\n\t\tMainActivity\n    end\n\n    Room--\u003eStationRepositoryImpl\n    jwtDataStore--\u003eStationRepositoryImpl\n    StationRepositoryImpl--\u003e|implements|StationRepository\n    stationAPI--\u003eStationRepositoryImpl\n    jwtDataStore--\u003eLoginViewModel\n    authAPI--\u003eLoginViewModel\n    oauthDataStore--\u003eLoginViewModel\n    StationRepository--\u003eDetailViewModel\n    StationRepository--\u003eStationListViewModel\n    oauth--\u003eMainActivity\n    oauthDataStore--\u003eMainActivity\n```\n\nThe **Data** layer:\n\n- The Data layer runs under Kotlin Coroutines and Kotlin Flow.\n- _Room_ and the _DataStores_ is the application's cache\n  - The cache temporarily stores the `Stations`\n  - The cache is observable using Kotlin Flow\n  - _Room_ is able to provide a [`PagingSource`](https://developer.android.com/reference/kotlin/androidx/paging/PagingSource). The `PagingSource` is able to load pages of data stored in a [`PagingData`](https://developer.android.com/reference/kotlin/androidx/paging/PagingData).\n  - _Room_ executes requests in a Kotlin coroutine in the [IO thread](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html).\n- _stationAPI_ is a gRPC data source which permits to retrieves `Stations`. It needs a JWT token to fetch datas.\n- _OAuth provider_ gives the OAuth Access Token which is use to authenticate and identify users. The accessToken is cached inside the _oauthDataStore_. Upon receiving the OAuth Access Token, the _authAPI_ tries to fetch a JWT token.\n- The `StationRepositoryImpl` implements `StationRepository` and executes CRUD methods.\n  - For asynchronous actions, the `Station` of the response is cached and returned.\n  - For a watch action (`watch`/`watchOne`), we observe the cache and may fetch the initial values from a data source.\n  - For paged data, we create and run the [`Pager`](https://developer.android.com/reference/kotlin/androidx/paging/Pager) to **retrieve the `PagingData` from the cache.** The pager uses the `StationRemoteMediator` which is responsible to fetch and cache pages of `Station` from a data source.\n\nIn the **Domain** layer:\n\n- Entities and contracts are defined here.\n- Currently, our `stationRepository` satisfies most use cases (displaying a list of `Stations`, displaying details of a `Station`, updating a `Station`...).\n\nIn the **Presentation** layer :\n\n- Data is observable in the `ViewModels`. The `ViewModels` act as the middle man between the presentation layer and domain layer. This is to follow the **[Modern Android App Architecture](https://developer.android.com/topic/architecture)**.\n- The `MainActivity` renders a `Scaffold` with its `TopAppBar`. Inside that scaffold is a `NavigationHost` composable.\n- The `NavigationHost` renders a page based on a route:\n  - The default route is `/login`, and shows a login button. The button triggers a redirection to the OAuth provider, which then send the resulting OAuth Access Token to the `MainActivity` and triggers the `authAPI` to fetch a JWT. Upon receiving a JWT, the user is authenticated and is redirected to the `/stations` route.\n  - The `/stations` route shows a `LazyColumn` which listen to a `Flow\u003cPagingData\u003cStation\u003e\u003e`. This allows lazy loading of the data, and therefore, the lazy loading of \"station cards\". The page also shows a \"About\" page. When the user push on a \"station card\", the user is redirected to the `/details` route.\n  - The `/details` route shows the position of the train station on Google Maps and details about that station on a Bottom Sheet.\n\n### Technologies used\n\n#### Android dependencies and AndroidX\n\n- Room and Protobuf DataStore, as a cache.\n- Retrofit + OkHttp 4 + gRPC, as data sources.\n- Jetpack Compose, for bidirectional data binding and UI development.\n- ViewModel and StateFlow, to follow the Modern Android App Architecture and avoid fragment/activities lifecycle issues\n- Paging 3, as a solution for paged data\n- Hilt, for dependency injection\n- Google Maps SDK for Android\n\n#### Kotlin in general\n\n- Kotlin Coroutines + Kotlin Flow, for async\n- Kotlinx.serialization, for JSON serialization\n\n# LICENSE\n\n```\nMIT License\n\nCopyright (c) 2021 Marc NGUYEN, Jean-Baptiste RUBIO\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkness4%2Ftrain-station","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarkness4%2Ftrain-station","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkness4%2Ftrain-station/lists"}