Titan is a cross platform frontend written in Flutter for an open-source project launched by ÉCLAIR and maintained by ProximApp. This project aims to provide students of business and engineering schools a digital tool to simplify campus life and student association activities.
Titan supports flavors, which allows the developer to easily switch between several versions of Titan for several use cases.
Titan includes 3 flavors: dev, alpha, prod. On VSCode, you can choose which flavor to use when launching the debugger
Each flavor is associated with a specific app package name (*.titan.dev, *.titan.alpha, *.titan) allowing the three app to be installed simultaneously on the same device.
You need to create config json files with required variables:
- config/config-dev.json
- config/config-alpha.json
- config/config-prod.json
Install Flutter: https://docs.flutter.dev/get-started/install
Setup VS Code for Flutter development: https://docs.flutter.dev/get-started/editor?tab=vscode
Titan is designed to be launched on Web, Android and iOS platforms.
flutter run --flavor dev --dart-define-from-file=config/config-dev.json --web-port 3000
# flutter run --flavor alpha --dart-define-from-file=config/config-alpha.json --web-port 3000
# flutter run --flavor prod --dart-define-from-file=config/config-prod.json --web-port 3000On the web, appending --wasm --no-cross-origin-isolation runs a version compiled to WebAssembly instead of JavaScript:
flutter run --flavor dev --dart-define-from-file=config/config-dev.json --web-port 3000 --wasm --no-cross-origin-isolation--no-cross-origin-isolation is required: cross-origin isolation would otherwise be turned on along with WebAssembly, and it breaks popups — which is how both login and HelloAsso funding work. The same reasoning drives the Cross-Origin-Embedder-Policy: credentialless / Cross-Origin-Opener-Policy: unsafe-none headers in web_dev_config.yaml and nginx.conf.
Web release builds pass --wasm too, which emits both the WebAssembly and the JavaScript bundles; the loader in web/index.html picks whichever the browser supports. Note that flutter build web rejects --flavor — the flavor comes from the flavor key of the config file instead (see getAppFlavor() in lib/tools/functions.dart):
flutter build web --release --dart-define-from-file=config/config-alpha.json --wasmTitan can be launched from VS Code Run and Debug menu.
A few things in this repository exist only to keep the web build fast. They are easy to undo by accident, so they are worth knowing about.
web/index.htmlowns the Core Web Vitals. The splash screen isposition: fixedand contains real text. Both matter: a splash that takes part in layout costs ~0.2 of Cumulative Layout Shift when Flutter attaches its view, and a splash made only of coloured<div>s is not "contentful", so First Contentful Paint does not fire until Flutter's first frame — several seconds later. The splash removes itself on the engine'sflutter-first-frameevent.- Brotli. The
Dockerfilebuildsngx_brotliagainst the nginx it ships and precompresses every asset, andnginx.confenablesbrotli_static. It is worth ~23% over gzip on the Dart bundle. - Fonts are bundled, subset to Latin and stripped of hinting, in
assets/google_fonts/.google_fontsfinds them through the asset manifest and loads them lazily, so unused weights cost nothing. Adding a newGoogleFonts.x()call without adding the matchingX-Weight.ttfsilently brings back a runtime download fromfonts.gstatic.com. Cut new subsets withtool/subset_font.py, which is the recipe the existing ones were made with. Robotois declared inpubspec.yamlunderfonts:, notassets:. The web engine lays text out in Roboto before anything else is registered, and fetches it fromfonts.gstatic.com— 62 KB, cross-origin, awaited before the first frame — unless the font manifest already declares that family. Onlyfonts:entries reachFontManifest.json, so moving it would bring the download back.- The renderer is self-hosted. The web build passes
--no-web-resources-cdn, which serves CanvasKit/skwasm from our own origin rather thanwww.gstatic.com. It is 1.2 MB on the critical path, so dropping the third-party DNS lookup and TLS handshake — and serving it with our own brotli — is worth ~100 ms of first frame on a throttled mobile profile. - Routes are deferred. Every module router imports its pages
deferred as …and guards them withDeferredLoadingMiddleware. A plain import moves that page, and everything it references, into the bundle that every visitor downloads before the first frame. lib/tools/functions.dartis imported by almost everything, so anything heavy it imports lands in that same eager bundle. That is whygetDateInRecurrencelives inlib/tools/recurrence.dartinstead: it was dragging the whole Syncfusion calendar in with it.- Images are WebP. School-supplied artwork is normalised by the
Normalize injected assetsstep of.github/workflows/release-web.yml. - Icons are compiled in, not bundled as assets.
lib/tools/ui/heroicons.dartreplaces theheroiconspackage, whose 1288 SVGs turned into 1288 entries inAssetManifest.bin.json— 193 KB that every visitor downloaded and parsed before the first frame — and one HTTP request per icon drawn. After adding aHeroIcons.foothat the app has not used before, rundart run tool/gen_heroicons.dart; styles other than outline additionally have to be declared intool/heroicons.yaml.
To format code use dart format .
dart format .
Titan support linting according to the official Flutter static analysis options.
The linter can be launched using:
dart analyze
Dart allows you to fix issues in your code with the dart command dart fix.
To preview proposed changes, use the --dry-run flag:
dart fix --dry-run
To apply the proposed changes, use the --apply flag:
dart fix --apply
Titan's tests follow the official Flutter documentation.
Tests can be run using:
flutter test --flavor devTo run a specific test file :
flutter test --flavor dev path/to/file.dartNotifications are handled using the Firebase Cloud Messaging API. On mobile platforms, a valid notification configuration is required to debug Titan. Notifications are disabled on web builds.
Please refer to the documentation of the corresponding Flutter's package to correctly setup notifications.
Please follow Android or iOS Firebase documentation to setup notifications.
For Android, add your google-services.json in android/app/src/<flavor>/.
It has to be the file for that flavor. Each flavor builds a different
application id — <APP_ID_PREFIX>.titan, .titan.alpha, .titan.dev, from the
APP_ID_PREFIX in the matching config/config-<flavor>.json — and the Google
Services Gradle plugin fails the build outright when no client in the file
matches:
Execution failed for task ':app:processDevDebugGoogleServices'.
> No matching client found for package name 'com.myemapp.titan.dev'
Copying one flavor's file into another flavor's directory is the usual cause. These files are gitignored, so the mistake only shows up on the machine that made it.
For iOS, add your GoogleService-Info.plist in ios/config/<flavor>/.
iOS is the more dangerous of the two: nothing checks that the plist's
BUNDLE_ID matches the flavor being built, so the wrong file builds cleanly and
silently registers the device against another Firebase project at runtime.
plutil -extract BUNDLE_ID raw ios/config/<Flavor>/GoogleService-Info.plist is
worth a look if notifications land in the wrong app.
On mobile, using plaintext HTTP connexions may raise issues.
Update AndroidManifest.xml:
<application
...
android:usesCleartextTraffic="true"
... >
Update Info.plist:
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>yourdomain.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
</dict>
</dict>
First update the icon's file and update pubspec.yaml.
Then run flutter_launcher_icons to generate all variants of the icon:
flutter pub get
flutter pub run flutter_launcher_iconsGuided upgrade using Android Studio Java and Gradle compatibility
For automated signature and upload, you need to provide the following keys:
- Google service account
android/fastlane-service-account.json
- Apple App Store Connect API key
ios/app-store-connect-api.p8
cd ios # or android
bundle exec fastlane beta flavor:alpha # or prod or devbundle update fastlane
cd ios
bundle update fastlane
cd android
bundle update fastlane