By Marc Nguyen and Jean-Baptiste Rubio.
Specifications are given here: Protos and docs
-
Fetch data from the api and display in a list and a screen with the details
-
Possibility to bookmark certain items per user
-
OAuth Authentication
-
Mockup:
-
Implementation of a search/filter system on the displayed list
-
Setting up a local database to display the item list in offline mode
-
Usage of StateFlow
Use docker/kubernetes/openshift to deploy the container.
docker pull ghcr.io/darkness4/train-station-api:latest
Available arch are: arm64
and amd64
.
An example of docker-compose.yml:
version: '3.9'
services:
train-station-api:
build: ghcr.io/darkness4/train-station-api:amd64
ports:
- 3000:3000
volumes:
- ./db:/db
environment:
JWT_SECRET: <base64 secret>
LISTEN_ADDRESS: 0.0.0.0:3000
DB_PATH: /db/db.sqlite3
DEBUG: true
TLS_ENABLE: false
-
Install golang et install the dependencies
go mod download
-
# Inside: ./train-station-api make ./bin/train-station-api make unit # Run unit tests
flowchart TD
JWT
*sql.SQL
DB
subgraph server[gRPC server]
healthAPI
stationAPI
authAPI
end
*sql.SQL --> DB
DB --> stationAPI
JWT --> authAPI
JWT --> stationAPI
If 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.
However, 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.
In reality, the SOLID priciples goes against the 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.
Since SOLID offers no real benefits outside of pain, we decided to remove the explicit layering while still sticking to domain-oriented development.
The contract is as follows:
- Retrieve the data at the start of the program
- Retrieve stations (several or one)
- The user can add a station to his favorites
This translates into :
- Download the data in the
main.go
as themain
function indicates the start. The data is stored in a database or cache. - The gRPC models are the domain entities and we serve them. This means that we translate the database models into gRPC models.
- Define a
favoriteSetter
interface and implement it. And the database can implement the interface perfectly.
erDiagram
Station }|..|{ User : favorite
- sqlc for database-first approach and type-safe SQL
- go-migrate for database migrations
- gRPC as HTTP server and main entrypoint
- urfave/cli for the CLI tooling
- JWT for session handling
- OAuth2 for Authentication
Install bun and install the dependencies:
bun install --frozen-lockfile
Create a Github OAuth App, generate a secret with openssl rand -base64 32
, and fill a .env
file with the following content:
GITHUB_ID=<Github OAUTH App ID>
GITHUB_SECRET=<Github OAUTH App Secret>
AUTH_SECRET=<Random Secret>
Serve in development mode:
bun run dev
Or deploy in production:
bun run build
# bun run preview # for demonstration
- SvelteKit with SSR as main web framework
- Pure JWT as authentication helpers
- protobuf-ts + gRPC as transport
- ViteJS for bundling and optimizing
flowchart TD
oauth[OAuth provider]
subgraph data[Data Layer]
subgraph cache[Cache]
oauthDataStore
jwtDataStore
Room
end
oauth
StationRepositoryImpl
authAPI
stationAPI
end
subgraph domain[Domain Layer]
StationRepository
end
subgraph presentation[Presentation Layer]
LoginViewModel
DetailViewModel
StationListViewModel
MainActivity
end
Room-->StationRepositoryImpl
jwtDataStore-->StationRepositoryImpl
StationRepositoryImpl-->|implements|StationRepository
stationAPI-->StationRepositoryImpl
jwtDataStore-->LoginViewModel
authAPI-->LoginViewModel
oauthDataStore-->LoginViewModel
StationRepository-->DetailViewModel
StationRepository-->StationListViewModel
oauth-->MainActivity
oauthDataStore-->MainActivity
The Data layer:
- The Data layer runs under Kotlin Coroutines and Kotlin Flow.
- Room and the DataStores is the application's cache
- The cache temporarily stores the
Stations
- The cache is observable using Kotlin Flow
- Room is able to provide a
PagingSource
. ThePagingSource
is able to load pages of data stored in aPagingData
. - Room executes requests in a Kotlin coroutine in the IO thread.
- The cache temporarily stores the
- stationAPI is a gRPC data source which permits to retrieves
Stations
. It needs a JWT token to fetch datas. - 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.
- The
StationRepositoryImpl
implementsStationRepository
and executes CRUD methods.- For asynchronous actions, the
Station
of the response is cached and returned. - For a watch action (
watch
/watchOne
), we observe the cache and may fetch the initial values from a data source. - For paged data, we create and run the
Pager
to retrieve thePagingData
from the cache. The pager uses theStationRemoteMediator
which is responsible to fetch and cache pages ofStation
from a data source.
- For asynchronous actions, the
In the Domain layer:
- Entities and contracts are defined here.
- Currently, our
stationRepository
satisfies most use cases (displaying a list ofStations
, displaying details of aStation
, updating aStation
...).
In the Presentation layer :
- Data is observable in the
ViewModels
. TheViewModels
act as the middle man between the presentation layer and domain layer. This is to follow the Modern Android App Architecture. - The
MainActivity
renders aScaffold
with itsTopAppBar
. Inside that scaffold is aNavigationHost
composable. - The
NavigationHost
renders a page based on a route:- 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 theMainActivity
and triggers theauthAPI
to fetch a JWT. Upon receiving a JWT, the user is authenticated and is redirected to the/stations
route. - The
/stations
route shows aLazyColumn
which listen to aFlow<PagingData<Station>>
. 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. - The
/details
route shows the position of the train station on Google Maps and details about that station on a Bottom Sheet.
- The default route is
- Room and Protobuf DataStore, as a cache.
- Retrofit + OkHttp 4 + gRPC, as data sources.
- Jetpack Compose, for bidirectional data binding and UI development.
- ViewModel and StateFlow, to follow the Modern Android App Architecture and avoid fragment/activities lifecycle issues
- Paging 3, as a solution for paged data
- Hilt, for dependency injection
- Google Maps SDK for Android
- Kotlin Coroutines + Kotlin Flow, for async
- Kotlinx.serialization, for JSON serialization
MIT License
Copyright (c) 2021 Marc NGUYEN, Jean-Baptiste RUBIO
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.