+51
-44
lib/screens/home/post_detail_screen.dart
+51
-44
lib/screens/home/post_detail_screen.dart
···
37
37
/// - Loading, empty, and error states
38
38
/// - Automatic comment loading on screen init
39
39
class PostDetailScreen extends StatefulWidget {
40
-
const PostDetailScreen({required this.post, this.isOptimistic = false, super.key});
40
+
const PostDetailScreen({
41
+
required this.post,
42
+
this.isOptimistic = false,
43
+
super.key,
44
+
});
41
45
42
46
/// Post to display (passed via route extras)
43
47
final FeedViewPost post;
···
51
55
}
52
56
53
57
class _PostDetailScreenState extends State<PostDetailScreen> {
54
-
final ScrollController _scrollController = ScrollController();
58
+
// ScrollController created lazily with cached scroll position for instant restoration
59
+
late ScrollController _scrollController;
55
60
final GlobalKey _commentsHeaderKey = GlobalKey();
56
61
57
62
// Cached provider from CommentsProviderCache
···
67
72
@override
68
73
void initState() {
69
74
super.initState();
70
-
_scrollController.addListener(_onScroll);
75
+
// ScrollController and provider initialization moved to didChangeDependencies
76
+
// where we have access to context for synchronous provider acquisition
77
+
}
71
78
72
-
// Initialize provider after frame is built
73
-
WidgetsBinding.instance.addPostFrameCallback((_) {
74
-
if (mounted) {
75
-
_initializeProvider();
76
-
_setupAuthListener();
77
-
}
78
-
});
79
+
@override
80
+
void didChangeDependencies() {
81
+
super.didChangeDependencies();
82
+
// Initialize provider synchronously on first call (has context access)
83
+
// This ensures cached data is available for the first build, avoiding
84
+
// the flash from loading state → content → scroll position jump
85
+
if (!_isInitialized) {
86
+
_initializeProviderSync();
87
+
}
79
88
}
80
89
81
90
/// Listen for auth state changes to handle sign-out
···
92
101
93
102
// If user signed out while viewing this screen, navigate back
94
103
// The CommentsProviderCache has already disposed our provider
95
-
if (!authProvider.isAuthenticated && _isInitialized && !_providerInvalidated) {
104
+
if (!authProvider.isAuthenticated &&
105
+
_isInitialized &&
106
+
!_providerInvalidated) {
96
107
_providerInvalidated = true;
97
108
98
109
if (kDebugMode) {
···
113
124
}
114
125
}
115
126
116
-
/// Initialize provider from cache and restore state
117
-
void _initializeProvider() {
127
+
/// Initialize provider synchronously from cache
128
+
///
129
+
/// Called from didChangeDependencies to ensure cached data is available
130
+
/// for the first build. Creates ScrollController with initialScrollOffset
131
+
/// set to cached position for instant scroll restoration without flicker.
132
+
void _initializeProviderSync() {
118
133
// Get or create provider from cache
119
134
final cache = context.read<CommentsProviderCache>();
120
135
_commentsCache = cache;
···
123
138
postCid: widget.post.post.cid,
124
139
);
125
140
141
+
// Create scroll controller with cached position for instant restoration
142
+
// This avoids the flash: loading → content at top → jump to cached position
143
+
final cachedScrollPosition = _commentsProvider.scrollPosition;
144
+
_scrollController = ScrollController(
145
+
initialScrollOffset: cachedScrollPosition,
146
+
);
147
+
_scrollController.addListener(_onScroll);
148
+
149
+
if (kDebugMode && cachedScrollPosition > 0) {
150
+
debugPrint(
151
+
'📍 Created ScrollController with initial offset: $cachedScrollPosition',
152
+
);
153
+
}
154
+
126
155
// Listen for changes to trigger rebuilds
127
156
_commentsProvider.addListener(_onProviderChanged);
128
157
158
+
// Setup auth listener
159
+
_setupAuthListener();
160
+
161
+
// Mark as initialized before triggering any loads
162
+
// This ensures the first build shows content (not loading) when cached
163
+
_isInitialized = true;
164
+
129
165
// Skip loading for optimistic posts (just created, not yet indexed)
130
166
if (widget.isOptimistic) {
131
167
if (kDebugMode) {
···
133
169
}
134
170
// Don't load comments - there won't be any yet
135
171
} else if (_commentsProvider.comments.isNotEmpty) {
136
-
// Already have data - restore scroll position immediately
172
+
// Already have cached data - it will render immediately
137
173
if (kDebugMode) {
138
174
debugPrint(
139
175
'📦 Using cached comments (${_commentsProvider.comments.length})',
140
176
);
141
177
}
142
-
_restoreScrollPosition();
143
178
144
-
// Background refresh if data is stale
179
+
// Background refresh if data is stale (won't cause flicker)
145
180
if (_commentsProvider.isStale) {
146
181
if (kDebugMode) {
147
182
debugPrint('🔄 Data stale, refreshing in background');
···
152
187
// No cached data - load fresh
153
188
_commentsProvider.loadComments(refresh: true);
154
189
}
155
-
156
-
setState(() {
157
-
_isInitialized = true;
158
-
});
159
190
}
160
191
161
192
@override
···
194
225
if (mounted) {
195
226
setState(() {});
196
227
}
197
-
}
198
-
199
-
/// Restore scroll position from provider
200
-
void _restoreScrollPosition() {
201
-
final savedPosition = _commentsProvider.scrollPosition;
202
-
if (savedPosition <= 0) {
203
-
return;
204
-
}
205
-
206
-
WidgetsBinding.instance.addPostFrameCallback((_) {
207
-
if (!mounted || !_scrollController.hasClients) {
208
-
return;
209
-
}
210
-
211
-
final maxExtent = _scrollController.position.maxScrollExtent;
212
-
final targetPosition = savedPosition.clamp(0.0, maxExtent);
213
-
214
-
if (targetPosition > 0) {
215
-
_scrollController.jumpTo(targetPosition);
216
-
if (kDebugMode) {
217
-
debugPrint('📍 Restored scroll to $targetPosition (max: $maxExtent)');
218
-
}
219
-
}
220
-
});
221
228
}
222
229
223
230
/// Handle sort changes from dropdown