Have you ever scrolled through a list of live prices—stocks 📈 (like Apple or Tesla), crypto pairs 💰 (like BTC/USDT or ETH/BUSD)—and wondered how it stays smooth with all that data flying in? I’ve been curious about that too. Showing hundreds or even thousands of assets updating in real time, whether it’s the steady pace of stocks or crypto’s wild swings, feels like a big challenge.
I decided to take a stab at it in Flutter, using Binance’s crypto ticker stream as my sandbox. I’m not saying I’ve cracked the perfect solution, but here’s how I tried to answer that question—hopefully it’s helpful to someone out there. 🙏
Step 1: Selective Updates That Don’t Break the App
Why It’s Tricky: Don’t Redraw Everything
Imagine a list of 2000 crypto pairs, and every price tick—say, 10 per second—redraws the whole thing. In Flutter, a naive setState
would tank performance faster than you can say “lag.” I needed updates to hit only what changed, not the entire screen.
How I Approached It: Targeting Specific Changes
First, I modeled each crypto pair and its live data. Here’s the MarketSymbol
for structure:
class MarketSymbol {
final String id; // e.g., "BTCUSDT"
final String baseAsset; // "BTC"
final String quoteAsset; // "USDT"
final SymbolSnapshot? snapshot; // Live price stats
MarketSymbol({required this.id, required this.baseAsset, required this.quoteAsset, this.snapshot});
}
And SymbolSnapshot
for the latest stats:
class SymbolSnapshot {
final String symbol;
final double lastPrice; // Latest price
final double priceChangePercent; // 24hr change
// ... openPrice, highPrice, etc.
}
Binance sends updates via a ticker stream, captured in SymbolTickerEvent
:
class SymbolTickerEvent {
final String eventType; // "24hrTicker"
final int eventTime; // Timestamp
final String symbol; // "BTCUSDT"
final double closePrice; // Latest price
final double openPrice;
final double highPrice;
final double lowPrice;
final double totalTradedBaseAssetVolume; // Base volume
final double totalTradedQuoteAssetVolume; // Quote volume
SymbolSnapshot get snapshot => SymbolSnapshot(
symbol: symbol,
lastPrice: closePrice,
priceChangePercent: (closePrice - openPrice) / openPrice * 100,
// ... other fields
);
}
Using Riverpod in Flutter, I stored symbols in a Map
and updated only what changed:
void _onSymbolTickerEvent(SymbolTickerEvent event) {
final symbol = state.symbolMap[event.symbol];
if (symbol != null && symbol.snapshot?.lastPrice != event.snapshot.lastPrice) {
state = state.copyWith(
symbolMap: {
...state.symbolMap,
symbol.id: symbol.copyWith(snapshot: event.snapshot),
},
);
}
}
In the UI, I hook into Riverpod’s select
to watch just that symbol:
class MarketSymbolRow extends ConsumerWidget {
const MarketSymbolRow({required this.symbolId});
static const double height = 60;
@override
Widget build(BuildContext context, WidgetRef ref) {
final symbol = ref.watch(marketVMProvider.select((state) => state.symbolMap[symbolId]!));
// Returns fancy UI with a height of 60 px
}
}
It’s not fancy—just a way to keep updates small. When “BTCUSDT” or "ETHUSDT” changes, only its row refreshes, which felt like a decent start.
Step 2: Just Listen to What You See
Why It’s Tricky: Too Much Data
Binance streams data for every pair, but why grab “XRPUSDT” if it’s 300 rows off-screen? Subscribing to all 2000+ pairs would choke the network. I needed to focus on what’s visible.
How I Approached It: Watching the Viewport
In Flutter, I used a ScrollController
and a GlobalKey
to figure out what’s visible:
void _updateVisibleSymbols() {
final symbolIds = ref.read(marketVMProvider.select((state) => state.symbolIds));
final viewportSymbolRows = (_viewportKey.height / MarketSymbolRow.height).ceil(); // Visible rows
final firstIndex = max(_scrollController.offset ~/ MarketSymbolRow.height, 0); // Top row
final lastIndex = min(firstIndex + viewportSymbolRows, symbolIds.length - 1); // Bottom row
final newVisibleSymbolIds = symbolIds.toList().sublist(firstIndex, lastIndex + 1).toSet();
if (newVisibleSymbolIds.difference(_visibleSymbolIds).isNotEmpty) {
_visibleSymbolIds = newVisibleSymbolIds;
ref.read(marketVMProvider.notifier).syncPriceTracking(_visibleSymbolIds);
}
}
The view model handles subscriptions, unsubscribes from symbols that are no longer needed and subscribes only to new ones. Overlapping symbols (already subscribed) are ignored to avoid redundant operations.:
void syncPriceTracking(Set<String> symbolIds) async {
final symbolIdsToUnsubscribe = _currentSubscribedSymbolIds.where((s) => !symbolIds.contains(s)).toList();
final symbolIdsToSubscribe = symbolIds.where((s) => !_currentSubscribedSymbolIds.contains(s)).toList();
if (symbolIdsToUnsubscribe.isNotEmpty) _marketRepository.unsubscribeSymbolMiniTickerStream(symbols: symbolIdsToUnsubscribe);
if (symbolIdsToSubscribe.isNotEmpty) {
await _marketRepository.subscribeSymbolMiniTickerStream(symbols: symbolIdsToSubscribe);
_currentSubscribedSymbolIds = symbolIds;
}
}
This kept it simple—streaming maybe 15-20 assets instead of 500. It’s not perfect, but it worked for my little experiment. (WebSocket details? See my other post)
Step 3: Scroll Without the Stutter
Why It’s Tricky: Laggy Scrolling
Even with those tweaks, scrolling felt rough because I was checking the viewport too often. I wanted it to feel smooth, even with all that live data.
How I Approached It: Giving It a Breather and Fixed Sizes
I added a debounce timer to calm things down:
void _onScroll() {
_debounceTimer?.cancel();
if ((_scrollController.offset - _lastOffset).abs() < 30) return; // Skip small nudges
_debounceTimer = Timer(Duration(milliseconds: 300), () {
_lastOffset = _scrollController.offset;
_updateVisibleSymbols(); // Update after a breather
});
}
And built the list with a fixed itemExtent
in Flutter’s ListView.builder
:
ListView.builder(
key: _viewportKey,
controller: _scrollController,
itemExtent: MarketSymbolRow.height, // Locked at 60px
itemCount: symbolIds.length,
itemBuilder: (context, index) {
final symbolId = symbolIds[index];
return MarketSymbolRow(symbolId: symbolId);
},
)
The debounce waits 300ms after scrolling stops, and itemExtent
keeps layout quick. It’s a small trick, but it made scrolling nicer for me.
What I Took Away
This was just my attempt to wrestle with real-time data in Flutter—Riverpod for small updates, viewport checks to cut data, and a few tweaks for smoother scrolling. Binance’s stream was a fun testbed, and while it’s not the ultimate answer, it got the job done for me. If you’re tackling live lists in Flutter, maybe these bits can help—or spark a better idea. Let me know what you think!
Top comments (0)