[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf: reduce redundant network calls

+158 -64
+9 -2
lib/src/core/network/atproto/data/models/pref_models.dart
··· 92 92 }) = _Preferences; 93 93 const Preferences._(); 94 94 95 - factory Preferences.fromJson(Map<String, dynamic> json) => 96 - _$PreferencesFromJson(json); 95 + factory Preferences.fromJson(Map<String, dynamic> json) { 96 + // Parse the preferences list and use the custom factory constructor 97 + // which extracts labelers, savedFeeds, etc. 98 + final preferencesJson = json['preferences'] as List<dynamic>? ?? []; 99 + final preferences = preferencesJson 100 + .map((e) => Preference.fromJson(e as Map<String, dynamic>)) 101 + .toList(); 102 + return Preferences(preferences: preferences); 103 + } 97 104 } 98 105 99 106 @Freezed(unionKey: r'$type')
+68 -38
lib/src/features/feed/ui/pages/feeds_page.dart
··· 39 39 if (_isPageControllerUpdating) return; 40 40 41 41 final activeIndex = feeds.indexOf(activeFeed); 42 - if (activeIndex < 0) return; 42 + 43 + // Always try to create controller if we don't have one and have feeds 44 + if (_pageController == null && feeds.isNotEmpty) { 45 + _pageController = PageController( 46 + initialPage: activeIndex >= 0 ? activeIndex : 0, 47 + ); 48 + return; 49 + } 43 50 44 - if (_pageController == null) { 45 - _pageController = PageController(initialPage: activeIndex); 51 + if (activeIndex < 0 && feeds.isNotEmpty) { 52 + // If active feed not in list but we have feeds, ensure controller exists 53 + if (_pageController == null) { 54 + _pageController = PageController(initialPage: 0); 55 + } 46 56 return; 47 57 } 48 58 59 + if (_pageController == null) return; 49 60 if (!_pageController!.hasClients) return; 50 61 51 62 final currentPage = _pageController!.page?.round() ?? 0; 52 - if (currentPage != activeIndex) { 63 + if (currentPage != activeIndex && activeIndex >= 0) { 53 64 _isPageControllerUpdating = true; 54 65 WidgetsBinding.instance.addPostFrameCallback((_) { 55 66 if (mounted && _pageController!.hasClients) { ··· 105 116 _lastFeedsList = List.from(feeds); // Create a copy 106 117 } 107 118 119 + // Ensure controller is created if we have feeds but controller is null 120 + // This prevents the FeedsBar from disappearing during initialization 121 + // Also create it early if feeds list is still empty (handles transition from initial empty state) 108 122 if (_pageController == null) { 109 - return const Center(child: CircularProgressIndicator()); 123 + if (feeds.isNotEmpty) { 124 + final activeIndex = feeds.indexOf(activeFeed); 125 + _pageController = PageController( 126 + initialPage: activeIndex >= 0 ? activeIndex : 0, 127 + ); 128 + } else { 129 + // Create controller even when feeds list is empty to keep FeedsBar visible 130 + // This handles the case when settings are still loading (feeds will populate soon) 131 + _pageController = PageController(initialPage: 0); 132 + } 110 133 } 111 134 112 135 return Scaffold( 113 136 backgroundColor: AppColors.black, 114 137 body: Stack( 115 138 children: [ 116 - PageView.builder( 117 - controller: _pageController, 118 - itemCount: feeds.length, 119 - onPageChanged: (index) { 120 - // Prevent recursive updates 121 - if (_isPageControllerUpdating) return; 139 + if (_pageController != null && feeds.isNotEmpty) 140 + PageView.builder( 141 + controller: _pageController, 142 + itemCount: feeds.length, 143 + onPageChanged: (index) { 144 + // Prevent recursive updates 145 + if (_isPageControllerUpdating) return; 122 146 123 - // Update the active feed when page changes via swipe 124 - if (index >= 0 && index < feeds.length) { 125 - final selectedFeed = feeds[index]; 126 - if (selectedFeed != activeFeed) { 127 - ref 128 - .read(settingsProvider.notifier) 129 - .setActiveFeed(selectedFeed); 147 + // Update the active feed when page changes via swipe 148 + if (index >= 0 && index < feeds.length) { 149 + final selectedFeed = feeds[index]; 150 + if (selectedFeed != activeFeed) { 151 + ref 152 + .read(settingsProvider.notifier) 153 + .setActiveFeed(selectedFeed); 154 + } 130 155 } 131 - } 132 - }, 133 - itemBuilder: (context, index) { 134 - if (index >= 0 && index < feeds.length) { 135 - // Use feed ID as key to preserve state across reordering 136 - return KeyedSubtree( 137 - key: ValueKey(feeds[index].config.id), 138 - child: FeedPage(feed: feeds[index]), 156 + }, 157 + itemBuilder: (context, index) { 158 + if (index >= 0 && index < feeds.length) { 159 + // Use feed ID as key to preserve state across reordering 160 + return KeyedSubtree( 161 + key: ValueKey(feeds[index].config.id), 162 + child: FeedPage(feed: feeds[index]), 163 + ); 164 + } 165 + return const DecoratedBox( 166 + decoration: BoxDecoration(color: AppColors.black), 139 167 ); 140 - } 141 - return const DecoratedBox( 142 - decoration: BoxDecoration(color: AppColors.black), 143 - ); 144 - }, 145 - ), 146 - Positioned( 147 - top: 0, 148 - left: 0, 149 - right: 0, 150 - child: FeedsBar(pageController: _pageController!), 151 - ), 168 + }, 169 + ) 170 + else 171 + const Center(child: CircularProgressIndicator()), 172 + // Always show FeedsBar once we have a controller 173 + // The controller is created as soon as we have activeFeed, 174 + // keeping it visible through the initialization transition 175 + if (_pageController != null) 176 + Positioned( 177 + top: 0, 178 + left: 0, 179 + right: 0, 180 + child: FeedsBar(pageController: _pageController!), 181 + ), 152 182 ], 153 183 ), 154 184 );
+79 -20
lib/src/features/settings/providers/settings_provider.dart
··· 33 33 /// This prevents repeated network calls to getServices for the same labelers. 34 34 final Set<String> _labelerPoliciesChecked = {}; 35 35 36 + /// Tracks if the default labeler has been ensured this session 37 + bool _defaultLabelerEnsured = false; 38 + 39 + /// Tracks if settings have been loaded to prevent resetting state on rebuild 40 + bool _hasLoadedSettings = false; 41 + 42 + /// Tracks if loadSettings is currently in progress to prevent concurrent calls 43 + bool _isLoadingSettings = false; 44 + 45 + SettingsState? _preservedState; 46 + 36 47 FeedRepository get feedRepository => 37 48 _feedRepository ??= _sprkRepository!.feed; 38 49 SprkRepository get sprkRepository => ··· 76 87 // Watch the preferences provider - when it updates, we'll rebuild 77 88 ref.watch(userPreferencesProvider); 78 89 90 + // Preserve state across rebuilds to prevent the feeds tabs from disappearing 91 + listenSelf((previous, next) { 92 + _preservedState = next; 93 + }); 94 + 95 + // If we've already loaded settings once, preserve the state instead of 96 + // resetting to default. This prevents the UI from flickering when 97 + // userPreferencesProvider triggers a rebuild. 98 + if (_hasLoadedSettings && _preservedState != null) { 99 + return _preservedState!; 100 + } 101 + 79 102 // Load settings asynchronously but return a temporary state immediately 80 103 // This prevents blocking the UI while loading 81 - Future.microtask(loadSettings); 104 + if (!_hasLoadedSettings && !_isLoadingSettings) { 105 + Future.microtask(loadSettings); 106 + } 82 107 83 108 // Return temporary default state that will be replaced by loadSettings() 84 109 return SettingsState( ··· 88 113 89 114 /// Loads all settings from the preferences provider 90 115 Future<void> loadSettings() async { 116 + // Guard against concurrent calls 117 + if (_isLoadingSettings) { 118 + logger.d('loadSettings already in progress, skipping duplicate call'); 119 + return; 120 + } 121 + 122 + // If already loaded, skip (but allow explicit refresh via syncPreferencesFromServer) 123 + if (_hasLoadedSettings) { 124 + logger.d('Settings already loaded, skipping'); 125 + return; 126 + } 127 + 128 + _isLoadingSettings = true; 129 + 91 130 try { 92 131 logger.d('Loading settings from preferences...'); 93 132 ··· 132 171 feeds: updatedFeeds, 133 172 likedFeeds: likedFeeds, 134 173 ); 174 + _hasLoadedSettings = true; 135 175 return; 136 176 } catch (e) { 137 177 logger.e('Error setting default preferences: $e'); ··· 158 198 feeds: feeds, 159 199 likedFeeds: likedFeeds, 160 200 ); 201 + _hasLoadedSettings = true; 161 202 162 203 logger.d('Settings state updated successfully'); 163 204 } catch (e) { 164 205 logger.e('Error loading settings: $e'); 206 + } finally { 207 + _isLoadingSettings = false; 165 208 } 166 209 } 167 210 ··· 245 288 /// - Manually from the settings UI if user wants to refresh preferences 246 289 Future<void> syncPreferencesFromServer() async { 247 290 try { 291 + // Reset load state to force a fresh load from server 292 + _hasLoadedSettings = false; 248 293 await loadSettings(); 249 294 logger.d('Preferences synced successfully'); 250 295 } catch (e) { ··· 322 367 /// Debug method to reload settings and verify persistence 323 368 Future<void> reloadSettingsForTesting() async { 324 369 logger.d('Manually reloading settings for testing...'); 370 + _hasLoadedSettings = false; 325 371 await loadSettings(); 326 372 } 327 373 ··· 372 418 preferences.labelers?.map((labeler) => labeler.did).toList() ?? []; 373 419 374 420 // Ensure default mod service labeler is always present 421 + // Only check once per session to avoid unnecessary putPreferences calls 375 422 final modServiceDid = _defaultModServiceDid; 376 - final wasAdded = !labelers.contains(modServiceDid); 377 - if (wasAdded) { 378 - logger.d('Default mod service labeler not found, adding it'); 379 - labelers = [modServiceDid, ...labelers]; 423 + if (!_defaultLabelerEnsured) { 424 + _defaultLabelerEnsured = true; 425 + final isMissing = !labelers.contains(modServiceDid); 426 + if (isMissing) { 427 + logger.d('Default mod service labeler not found, adding it'); 428 + labelers = [modServiceDid, ...labelers]; 380 429 381 - // Update preferences to include default labeler 382 - final updatedLabelers = <LabelerPrefItem>[ 383 - LabelerPrefItem(did: modServiceDid), 384 - ...(preferences.labelers ?? []), 385 - ]; 386 - final updatedPreferencesList = 387 - preferences.preferences 388 - .where((pref) => !pref.isLabelersPref(pref)) 389 - .toList() 390 - ..add( 391 - Preference.labelersPref(labelers: updatedLabelers), 392 - ); 430 + // Update preferences to include default labeler 431 + final updatedLabelers = <LabelerPrefItem>[ 432 + LabelerPrefItem(did: modServiceDid), 433 + ...(preferences.labelers ?? []), 434 + ]; 435 + final updatedPreferencesList = 436 + preferences.preferences 437 + .where((pref) => !pref.isLabelersPref(pref)) 438 + .toList() 439 + ..add( 440 + Preference.labelersPref(labelers: updatedLabelers), 441 + ); 393 442 394 - await _updatePreferences(Preferences(preferences: updatedPreferencesList)); 443 + await _updatePreferences( 444 + Preferences(preferences: updatedPreferencesList), 445 + ); 446 + } 447 + } else if (!labelers.contains(modServiceDid)) { 448 + // Already checked this session but still need to include it in return 449 + labelers = [modServiceDid, ...labelers]; 395 450 } 396 451 397 452 // Ensure all labelers' label values are set as preferences ··· 499 554 Future<void> syncLabelers() async { 500 555 try { 501 556 logger.d('Syncing labelers from server...'); 502 - // Clear the policies cache so we re-fetch them 557 + // Clear the caches so we re-check everything 503 558 _labelerPoliciesChecked.clear(); 559 + _defaultLabelerEnsured = false; 504 560 // Refresh preferences from server first 505 561 await ref.read(userPreferencesProvider.notifier).refresh(); 506 562 ··· 510 566 511 567 // Ensure default mod service labeler is present 512 568 final modServiceDid = _defaultModServiceDid; 569 + _defaultLabelerEnsured = true; 513 570 if (!labelers.contains(modServiceDid)) { 514 571 labelers.insert(0, modServiceDid); 515 572 final updatedLabelers = <LabelerPrefItem>[ ··· 523 580 ..add( 524 581 Preference.labelersPref(labelers: updatedLabelers), 525 582 ); 526 - await _updatePreferences(Preferences(preferences: updatedPreferencesList)); 583 + await _updatePreferences( 584 + Preferences(preferences: updatedPreferencesList), 585 + ); 527 586 } 528 587 529 588 logger.d(
+2 -4
lib/src/sprk_app.dart
··· 33 33 try { 34 34 _logger.d('Syncing user preferences from server...'); 35 35 final settingsNotifier = ref.read(settingsProvider.notifier); 36 + // syncPreferencesFromServer already calls loadSettings internally 36 37 await settingsNotifier.syncPreferencesFromServer(); 37 - _logger 38 - ..d('User preferences synced successfully') 39 - ..d('Loading settings...'); 40 - await settingsNotifier.loadSettings(); 38 + _logger.d('User preferences synced successfully'); 41 39 42 40 if (!mounted) return; 43 41