pikd_flutter_ui 0.8.0-beta.5
pikd_flutter_ui: ^0.8.0-beta.5 copied to clipboard
Themeable Flutter widgets for PIKD leaderboards, challenges, profiles, feeds, collections, and geospatial discovery experiences.
pikd_flutter_ui — PIKD prebuilt UI modules #
Themeable, drop-in Flutter widgets for the PIKD experience: Leaderboard, Explore
map, Profile/Inventory, Feed/Updates, Challenges/Quests. Take the modules you
want, theme them to your brand, and back them with either the live /sdk/v1
client or built-in sample data, so you can see a module before you have a key.
Each module is the same shape:
view (PikdXView) ← repository (XRepository) ← data
├── SampleXRepository (built-in sample data)
└── PikdSdkXRepository (live /sdk/v1)
The view never talks to the network itself — it always reads through a repository. That one seam is what lets you start with sample data, switch to the live client, or plug in your own backend, without changing the view.
Release candidate:
pikd_flutter_uiversion0.8.0-beta.5.
Install #
dependencies:
pikd_flutter_ui: ^0.8.0-beta.5
pikd_flutter_api: ^0.8.0-beta.5 # Import directly when configuring live repositories.
Then run flutter pub get. No PIKD repository access is required.
Runtime deps pulled in transitively: google_maps_flutter, lottie,
cached_network_image, palette_generator, flutter_widget_from_html_core.
1. Wrap your app in the theme #
Provide a PikdTheme above your Navigator (e.g. via MaterialApp.builder)
so pushed detail routes inherit it too. Omit theme to get the PIKD default dark.
MaterialApp(
builder: (context, child) => PikdThemeProvider(
theme: PikdTheme.pikdDefault(), // or your rebrand — see Theming
child: child!,
),
home: const MyHome(),
);
2. Sample data (no backend — great for a first run) #
Every module takes a repository, so you can render one before you have an SDK key.
The Sample* repositories return fixed in-memory data: no network calls, no
artificial delays.
const PikdLeaderboardView(repository: SampleLeaderboardRepository());
const PikdProfileView(repository: SampleProfileRepository());
const PikdFeedView(repository: SampleFeedRepository());
const PikdChallengesView(repository: SampleChallengeRepository());
Don't ship these in an app you hand to users, and don't use them as a fallback when credentials are missing — fabricated rows that look real are worse than an honest error. The repository demo deliberately has no such fallback for that reason.
3. Live mode (/sdk/v1) #
Build one ApiClient (base URL + SDK key), hand each module its *Api +
your userRef (the host-side user reference; the server maps it to the PIKD
user). User-scoped modules need it; anonymous (Tier 0) can omit it where allowed.
import 'package:pikd_flutter_api/api.dart';
ApiClient client() {
final c = ApiClient(basePath: 'https://api.pikd.app/sdk/v1');
c.addDefaultHeader('x-pikd-sdk-key', 'pk_live_xxx');
return c;
}
const userRef = 'your-host-user-id';
// Leaderboard — own-rank card needs userRef, list works without it.
PikdLeaderboardView(
repository: PikdSdkLeaderboardRepository(LeaderboardApi(client()), userRef: userRef),
);
// Profile / Inventory — user-scoped, userRef required.
PikdProfileView(
repository: PikdSdkProfileRepository(ProfileApi(client()), userRef: userRef),
);
// Feed / Updates.
PikdFeedView(
repository: PikdSdkFeedRepository(FeedApi(client()), userRef: userRef),
onOpenUpdate: (item) { /* push your detail route */ },
onOpenComments: (item) { /* push your comments route */ },
);
// Challenges / Quests.
PikdChallengesView(
repository: PikdSdkChallengeRepository(ChallengesApi(client()), userRef: userRef),
onOpenChallenge: (challenge) { /* push detail */ },
);
4. Explore map (extra setup) #
The Explore module renders a Google map, so it needs a Google Maps API key
in your host app (iOS Info.plist GMSApiKey + AppDelegate, Android manifest
com.google.android.geo.API_KEY) and iOS deployment target ≥ 14.0. It also
takes a location provider and callbacks for opening AR:
PikdExploreMapView(
repository: PikdSdkExploreRepository(
CollectiblesApi(client()),
languageRef: tenantLanguageRef,
xPikdUser: userRef,
),
locationProvider: myLocationProvider, // implement ExploreLocationProvider
onCollect: (collectible) { /* launch AR collect — see the AR binding */ },
onNavigateAr: (collectible) { /* launch AR wayfinding */ },
onOpenExternalMap: (lat, lng) { /* open Apple/Google Maps */ },
);
The two nearby operations have different jobs and must not be interchanged:
PikdExploreMapViewuses/collectibles/nearby-withinfor the map's paginated Nearby 100, challenge, and expiry-filtered discovery.- An AR camera uses
PikdSdkExploreRepository.fetchArNearby()and therefore/collectibles/nearby, passing the collection radius and current user.
/collectibles/nearby caps the radius at 5 km, and /sdk/v1 does not yet offer
the bounding-box or distinct-nearby endpoints the React Native app uses — so both
the viewport search and the nearby sheet filter that radius result for now.
3D / AR is a separate package.
pikd_flutter_uiExplore is the map and discovery only; it does not render 3D. EachExploreCollectiblecarriesassetModelUrlIos(USDZ) /assetModelUrlAndroid(GLB) from the nearby payload — take those intopikd_flutter_ar(PikdArView) to place and render the asset in AR. See that package's README for native integration details.
Repeatable (multi-collect) assets #
Some collectibles are repeatable: they stay pinned on the map permanently and
a user can collect a duplicate until the asset's instances run out. They arrive
flagged as ExploreCollectible.isMultipleInstance (from the nearby payload) and
the map gives them a distinct, themed marker so they read differently from normal
one-off drops.
Three things to handle if you build your own collect surface:
- Don't remove them from the map after a collect — the backend keeps returning them, and they're meant to stay visible.
collectreturnsstatus: instance_maxed_outonce the asset has no collections left. It comes back HTTP 201, not an error — treat it as its own "nothing left to collect" state. If you branch onstatus == 'collected'and lump everything else into a failure message, users will see "collect failed" when they've simply hit the cap. There is no remaining-count or reset-at field, so the limit is only discoverable this way. (The API docs example showsthreshold_exceededfor the same case; the deployed API sendsinstance_maxed_out, so accept both.)- The cap is global per asset, and permanent. Backend-confirmed 2026-07-31: it is not per user and it never resets — once an asset's instances are exhausted it is finished for everybody. We previously documented it as a per-user daily limit and told users to "check back tomorrow"; both halves were wrong. Word your copy as a permanent state, and note there's no remaining-count field, so you can't warn a user that they're taking the last one.
- Inventory returns one row per collect — repeated collects of the same asset
appear as separate instances (distinct
id+collectedAt), not a single row.
The PIKD demo application wires onCollect end-to-end into an AR collect screen
with instant placement, tap-to-collect, a loading state, collection success, and
the repeatable-asset limit state.
Don't call
startArSession()at all when you mountPikdArView— the view owns the session. It auto-starts on attach on both platforms (and handles the not-yet-initialised case itself). Starting it yourself too is a real double-start: both land in one pending-completion slot, so the view's (completion-less) start replaces yours and yourawaitnever resumes — the session runs fine, your screen waits forever — and the duplicate start can leave native session state inconsistent across a close/reopen. Instead subscribe toPikdAr.eventsfirst and wait forArSessionStarted, with a timeout that proceeds anyway (an already-running session emits no new event). The reference screen does exactly this — copy that shape.
Theming (rebranding) #
Every brand/surface/accent/glass colour and the fonts route through PikdTheme.
Pass a custom theme to fully rebrand — no view code changes:
final myBrand = PikdTheme.pikdDefault().copyWith(
colors: PikdColors.dark.copyWith(
primary: const Color(0xFF7C4DFF),
activeAccent: const Color(0xFFFF6D00),
// surfaceBrand, glass, primaryGradient, rank1/2/3, ...
),
);
A handful of values are intentionally fixed (shadows, scrims, medal gold/silver/bronze, neutral control greys) — see the theming notes. Provide the theme above the Navigator so pushed routes inherit it.
Notifications: use PikdToast, not SnackBar #
Transient messages go through PikdToast — a top-anchored card with a title, an
optional description, and a severity accent taken from the success / error /
warning / info roles. It auto-hides after 3 s and dismisses on tap or swipe-up.
PikdToast.show(
context,
title: 'Missing iOS 3D file',
description: 'iOS requires USDZ format.',
type: PikdToastType.warning,
);
Reach for this instead of Flutter's SnackBar: a SnackBar paints Material's own
colours, so it is the one surface in a screen that would not follow your
rebrand. PikdToast needs an Overlay in scope (any MaterialApp has one) and
no-ops rather than throwing if there isn't.
Bring your own data #
Any module works against your own backend — implement the repository interface
(LeaderboardRepository, ProfileRepository, FeedRepository,
ChallengeRepository, ExploreRepository) and pass it to the view. The
PikdSdk* and Sample* classes are just two implementations.
Try it #
Run the package example to preview the leaderboard module without credentials:
flutter run example/pikd_flutter_ui_example.dart
The full PIKD demo application runs all modules against /sdk/v1 and therefore
requires issued credentials:
flutter run \
--dart-define=PIKD_BASE=https://api.pikd.app/sdk/v1 \
--dart-define=PIKD_SDK_KEY=pk_live_xxx \
--dart-define=PIKD_USER=your-host-user-id \
--dart-define=PIKD_LANGUAGE_REF=tenant-language
The full demo additionally requires a Google Maps key and platform-specific AR configuration.
License #
This package is proprietary software. Use requires a written SDK agreement with ELEOS WORLD LTD. See LICENSE and the PIKD SDK license page.