Documents the app-lock layer and multi-account storage/session model in architecture.md/server.md, and adds a standards.md note that `flutter install` wipes app data (it uninstalls before installing) — use `adb install -r` for local test deploys instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.7 KiB
Code standards
Linting
analysis_options.yaml includes package:flutter_lints/flutter.yaml with
no rules added/relaxed. Run flutter analyze before considering a change
done; it should stay clean.
Comments
Comments are used sparingly and only for non-obvious why — a hidden
constraint, a workaround, or the rationale for overriding a framework
default. See AppTheme._pageTransitionsTheme/_sliderTheme,
NextcloudService._parseDavDate/_davPath, and LoginFlowService's class
doc comment for the house style: one short doc comment on the
class/function explaining why it exists, not what each line does. Don't
add comments that restate the code or describe what a well-named
class/method already makes obvious.
Widget structure
- Screens (
views/) are typicallyStatefulWidgetwhen they own controllers/local UI state (e.g.LoginView's form key + text controller); presentation is frequently split into small privateStatelessWidgets in the same file (_ServerForm,_WaitingForBrowserinlogin_view.dart) rather than inlined in one largebuild. Follow this split for any view complex enough to have more than one visual "mode". - Private helpers/widgets are prefixed with
_and live in the same file as their one caller; promote towidgets/only once something is reused across files. - Read provider state with
context.watch<ServerProvider>()inbuild, andcontext.read<ServerProvider>()for one-off calls from callbacks (matchesLoginView._handleContinue).
ServerProvider conventions
- Any method that fetches data and writes it into a shared field
(
refreshData,fetchAllMedia,fetchTrash,fetchShares,fetchRecent,_applyCredentialsForAccount) must guard against a stale write from an account the user has since switched away from: capturefinal gen = _sessionGeneration;at entry, and checkif (gen != _sessionGeneration) return;immediately after eachawaitbefore touching any field or callingnotifyListeners(). Follow this pattern for any new fetch method added to the provider. - New persisted state on
ServerProvidermust be classified global vs. per-account (seearchitecture.md/server.md) up front — global state uses a plain_prefsFuture.then((p) => p.setX(key, value)); per-account state goes through_persistAccountPref(key, (p, namespacedKey) => p.setX(namespacedKey, value))and must also be handled in_applyAccountPrefs(both the "reset to default when no account" and the "load for this account" branches) so it's correct immediately after a switch, not just at startup.
Testing
- Widget tests must mock platform channels that the app touches on startup
—
SharedPreferences.setMockInitialValues({})and a mockMethodChannel('plugins.it_nomads.com/flutter_secure_storage')handler — and disable Google Fonts network fetching (GoogleFonts.config.allowRuntimeFetching = false) insetUpAll. Without these,ServerProvider's session restore never resolves in the test sandbox (no plugin implementation is registered, so the read future just never completes) and the app stays on_SplashView's indeterminate spinner, which makespumpAndSettle()hang until its own timeout instead of failing fast. Seetest/widget_test.dartfor the reference setup. - Run with
flutter test.
Local install/deploy
Never use flutter install to push a build to a test device — it always
does a full uninstall-then-install (prints "Uninstalling old
version..."), and Android deletes all app data (SharedPreferences, secure
storage — every saved account/preference) on uninstall. This wipes the app
clean on every single deploy, which looks like an account/settings-loss bug
but is actually just the install method.
Instead, build then install with adb's replace flag, which updates the
APK in place and preserves app data:
flutter build apk --release
adb install -r build/app/outputs/flutter-apk/app-release.apk
This only preserves data if the new APK's signature matches what's already
on the device — a local build's release variant currently reuses the debug
signing config (android/app/build.gradle.kts), so consecutive local
builds share a key and adb install -r works cleanly. Installing a build
signed with a different key (e.g. a CI-signed release APK from the Gitea
release pipeline) over a differently-signed local build forces Android to
require a full uninstall regardless of the install method used — there's no
way around that from the tooling side.
Dependencies
Networking is deliberately split: package:http for simple JSON/XML
request-response calls, package:dio only where streaming/progress is
needed (downloads). Don't introduce a third HTTP client — extend the
existing split instead.