SwingTribe app icon SwingTribe
What's New in SwingTribe

Consistent loading skeletons across the app, with no layout shift

View the App →

What changed

What you'll notice

Loading now looks the same everywhere in the app, and the screen stops jumping around while it loads.

Before, almost every part of the app drew its own "loading" look. Some places pulsed, some sat as flat grey blocks that never moved, one showed a spinner, and several showed nothing at all until their data arrived. Worse, those placeholders rarely matched what replaced them, so the home feed shuffled and resettled as each section finished loading.

Now every loading state is the same shimmer, and the placeholder is the real layout — same cards, same heights — so when your data lands nothing moves. If you were mid-round when you last closed the app, the "Resume" banner is already there when you open it, instead of dropping in a moment later and pushing everything down.

Worth checking on device: cold-launch the dashboard on a slow connection. Every rail should shimmer in the same way, nothing should shift vertically as sections fill in, and pull-to-refresh should not grey out content that's already on screen.

What changed, technically

Adopts skeletonizer, which paints bones over the *real* widget tree rendered with placeholder data. The skeleton is then the real layout by construction, which removes the whole class of layout-shift bugs rather than patching each one.

Foundation

Layout shifts fixed

Banner caching

The three conditional banners (active game, recent round, setup welcome) can't be skeletonised — most users have none of them, so a placeholder would flash for everyone. HomeBannerCacheNotifier reads shared_preferences synchronously in build(), so the last-known banner is on screen for the very first frame and nothing moves when the RPC agrees.

Coverage

All 12 dashboard home rails, the four dashboard tabs, the four tribe rails, member and ranking lists, the leaderboard boards, Fairdrop, the course picker, the contacts sheet and the conversations list.

On the tournament side that means every board: the six that share LeaderboardSkeleton (birdies, skins, battles and the live view's two), plus the individual and team boards on leaderboard_screen, which showed a mascot LoadingState and now skeletonise the real LeaderboardTable / TeamLeaderboardTable. LeaderboardSkeleton would have been the wrong tool there — it mirrors the bare zebra rows of the birdies/skins boards, while these are bordered cards with a header row. The unpaid-tournaments sheet and the battle player picker went the same way; in the picker the section label and search field stay live and usable above the skeleton rows, since neither needs data to work. The shared primitives in skeleton_loader.dart are rebuilt on skeletonizer, so their remaining callers inherit the same shimmer untouched; SkeletonPulse is renamed SkeletonGroup since it no longer owns an animation.

Also deletes four dashboard files nothing imports (hero_section, closing_soon_rail, recommended_courses_rail, performance_stats_section) — about 1,000 lines carrying four more skeletons.

Tests

Assert the guarantee directly rather than the markup: loading and loaded measure the same height, for SkeletonSection and for real rails. Plus banner-cache coverage — warm cache draws on frame one at an unchanged height, a stale cache is ignored, a finished round collapses away, and corrupt JSON is a cache miss rather than a launch crash.

leaderboard_table_skeleton_test measures the same table skeletonised and loaded. That isn't tautological: a text Bone is sized from the painted text, so a config change (ignoreContainers off, a different text bone radius) can resize rows — which is what it catches.

Known remainder

19 files still show a spinner where content will appear — mostly sheets and screens (tribe settings, schedule create, inter-tribe pairing, message thread, GPS panel). Each needs its own placeholder model and none is a list with an obvious shape, so they're better done alongside work on those screens than in a sweep. (An earlier commit message in this branch estimated "~12"; the count was 21 before the tournament sheets above, 19 now.)

LoadingState and in-button spinners are unchanged by design — the convention now recorded in CLAUDE.md is skeletonize content, spin for actions.