Skip to content

Conversation

@pastordee
Copy link
Contributor

@pastordee pastordee commented Dec 6, 2025

Summary

This PR adds comprehensive offline mode support and introduces three new live query widgets for displaying Parse objects in Flutter applications.

New Features

New Widgets

  • ParseLiveSliverListWidget - Sliver-based list for use within CustomScrollView
  • ParseLiveSliverGridWidget - Sliver-based grid for use within CustomScrollView
  • ParseLiveListPageView - PageView widget for swiping through Parse objects

Offline Mode Support

  • Local caching via ParseObjectOffline extension methods
  • Automatic connectivity detection and fallback to cached data
  • Batch save operations for efficient caching
  • LRU memory management via CachedParseLiveList

New API

  • offlineMode parameter on all live query widgets
  • refreshData() and loadMoreData() public methods on sliver widget states
  • ConnectivityHandlerMixin for consistent offline handling across widgets

Changes

New Files (Flutter Package)

  • lib/src/utils/parse_live_sliver_list.dart
  • lib/src/utils/parse_live_sliver_grid.dart
  • lib/src/utils/parse_live_page_view.dart
  • lib/src/utils/parse_cached_live_list.dart
  • lib/src/mixins/connectivity_handler_mixin.dart

New Files (Dart Package)

  • lib/src/objects/parse_offline_object.dart

Documentation

  • Updated README with comprehensive examples for all new widgets
  • Added offline mode configuration documentation

Testing

  • All existing tests pass
  • Manual testing of offline mode functionality completed

Breaking Changes

None - fully backward compatible

Summary by CodeRabbit

  • New Features

    • Offline caching and sync for Parse objects; connectivity-aware cache-first loading.
    • New live UI components: sliver list, sliver grid, grid, list, and page-view with pagination and lazy loading.
    • Analytics integration: event tracking, user/installation metrics, time-series and retention APIs.
  • Documentation

    • Comprehensive guides and examples for offline mode, live widgets, and analytics/server integration.
  • Tests

    • Extensive unit and widget tests for offline caching, connectivity handling, live widgets, and analytics.
  • Bug Fixes

    • Improved connectivity handling and more robust storage type handling.

✏️ Tip: You can customize this high-level summary in your review settings.

pastordee added 30 commits May 1, 2025 01:09
Adding Optional Index Parameter to ParseLiveListElementWidget
A RenderAnimatedSize was mutated in its own performLayout implementation.
Complete implementation of ParseLiveListPageView that matches your existing components like Live Grid and Live List
Forgot to add onPageChanged
I implemented pagination for both widgets to efficiently handle large datasets. This will enable loading data in chunks as users scroll through the content.
 An option to use an AnimatedList or ListBuilder
Added CachedParseLiveList: This wrapper class manages caching for better performance.

Simplified Data Flow: The implementation now uses a more direct approach where Stream.value(item) is used for smoother updates.

Better State Management: All state updates are contained in setState calls with error handling.

More Efficient Refresh: The refresh mechanism has been simplified to avoid animation issues.

Performance Optimization: Using ListView.builder as the default implementation provides better performance.
Cached Data Access: Using CachedParseLiveList to store frequently accessed items.

Pull-to-Refresh: Added RefreshIndicator for easy data refreshing.

Direct Item Access: Simplified item access by using Stream.value(item) for better performance.

Memory Management: Using the cache to manage memory efficiently based on the cacheSize parameter.

Cache Updates: Added calls to updateCache and removeFromCache to maintain cache integrity.

Error Handling: Better error reporting in debug mode.
fix lazyLoading in CachedParseLiveList
Fixing Lazy Loading in ParseLiveListPageView and ParseLiveGridWidget
Attempting to implement parse server off-line object
Updated ParseObjectOffline with some new methods
Safely cast and convert the list to List<String> before using it.
Introduces test_offline_mode.dart to verify offline caching features for Parse objects. Adds PR_CHECKLIST.md for pre-PR validation and updates README.md with new sections and examples for live query widgets and offline support.
ParseLiveSliverListWidget and ParseLiveSliverGridWidget now expose public State classes with refreshData and loadMoreData methods, accessible via GlobalKey for parent control. Documentation and README updated to reflect new usage patterns. Minor code cleanup and formatting improvements applied throughout.
Copilot AI review requested due to automatic review settings December 6, 2025 09:04
@parse-github-assistant
Copy link

parse-github-assistant bot commented Dec 6, 2025

🚀 Thanks for opening this pull request!

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds offline caching for ParseObjects, a ConnectivityHandlerMixin for online/offline widget flows, new cached/live Parse widgets (including sliver and page-view variants), and a Parse Dashboard analytics subsystem with endpoints, docs, and tests. Removes ethernet case from connectivity mapping.

Changes

Cohort / File(s) Change Summary
Offline Caching
packages/dart/lib/src/objects/parse_offline_object.dart, packages/dart/test/src/objects/parse_offline_object_test.dart, packages/dart/lib/parse_server_sdk.dart, packages/dart/test_extension.dart
Adds ParseObjectOffline extension with methods to save/load/update/remove objects and batches in a local CoreStore cache (key: offline_cache_<ClassName>), plus sync-to-server. Exposes part from library and adds unit tests and a small test harness.
Connectivity Handler
packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart, packages/flutter/test/src/mixins/connectivity_handler_mixin_test.dart
Introduces ConnectivityHandlerMixin (abstract contract: loadDataFromServer/loadDataFromCache/disposeLiveList) to manage connectivity subscriptions, initial checks, and online↔offline transitions; adds tests with a mock ConnectivityPlatform.
Live Query Widgets — Enhancements
packages/flutter/lib/src/utils/parse_live_list.dart, packages/flutter/lib/src/utils/parse_live_grid.dart
Extends ParseLiveList/Grid widgets with pagination, footer/load-more support, cacheSize/offlineMode/lazy-loading, index-aware child builders, and integrates connectivity-aware flows and caching. Public APIs and constructors updated.
Live Query Widgets — New
packages/flutter/lib/src/utils/parse_live_sliver_list.dart, packages/flutter/lib/src/utils/parse_live_sliver_grid.dart, packages/flutter/lib/src/utils/parse_live_page_view.dart, packages/flutter/lib/src/utils/parse_live_sliver_list.dart
Adds sliver and PageView live widgets (ParseLiveSliverList/ParseLiveSliverGrid/ParseLiveListPageView) with pagination, offline cache support, lazy prefetching, and connectivity-aware lifecycle.
Cached Live List Utility
packages/flutter/lib/src/utils/parse_cached_live_list.dart, packages/flutter/test/src/utils/parse_cached_live_list_test.dart
Adds CachedParseLiveList<T> backed by an internal LRU cache keyed by objectId, exposes stream/size/getAt/getIdentifier and lazy-cache helpers; includes unit tests.
Cache Storage Robustness
packages/dart/lib/src/storage/core_store_memory.dart, packages/dart/lib/src/storage/core_store_sem_impl.dart, packages/dart/test/src/storage/core_store_test.dart
Makes getStringList more robust by coercing Iterables and non-List shapes to List<String> when possible; adds tests for string-list behavior and basic core store ops.
Analytics Integration
packages/flutter/lib/src/analytics/parse_analytics.dart, packages/flutter/lib/src/analytics/parse_analytics_endpoints.dart, packages/flutter/lib/src/analytics/README.md, packages/flutter/lib/src/analytics/USAGE.md, packages/flutter/lib/src/analytics/example_server.js, packages/flutter/lib/src/analytics/parse_analytics_test.dart, packages/flutter/lib/src/analytics/parse_analytics_endpoints_test.dart
Adds ParseAnalytics (event tracking, offline event storage, time-series, retention, eventsStream) and ParseAnalyticsEndpoints (handlers + Express/Shelf boilerplate), documentation, example analytics server, and tests for analytics and endpoint handlers.
Library Surface & Exports
packages/flutter/lib/parse_server_sdk_flutter.dart, packages/dart/lib/parse_server_sdk.dart
Exposes analytics files from the Flutter library, adds part directive for parse_offline_object.dart in Dart library, includes new part files for live widgets, and removes ethernet case from connectivity mapping.
Pubspec / Packaging
packages/dart/pubspec.yaml, packages/flutter/pubspec.yaml
Sets publish_to: none in Dart package and adds a dependency_overrides local path for parse_server_sdk in Flutter package (commented git alternative present).
Documentation & Examples
packages/flutter/ANALYTICS_INTEGRATION_GUIDE.md, packages/flutter/lib/src/analytics/README.md, packages/flutter/README.md, packages/flutter/example/lib/live_list/main.dart
Adds analytics integration guide and analytics README/USAGE; expands Flutter README with Live Queries & Offline Support info; updates example to accept fromJson and index-aware builders.
Tests & Test Cleanup
packages/flutter/test/parse_connectivity_implementation_test.dart, packages/flutter/test/src/utils/parse_live_widgets_test.dart, various new tests
Adds comprehensive widget/unit tests for live widgets, mixin, analytics, cached live list, core store; removes ethernet-related connectivity tests to match mapping change.
Example app & mocks
packages/flutter/example/pubspec.yaml, packages/flutter/example/test/data/repository/repository_mock_utils.dart
Upgrades cupertino_icons and comments out mock provider imports/classes in example tests.

Sequence Diagram(s)

sequenceDiagram
    participant Widget as Live Widget
    participant Handler as ConnectivityHandlerMixin
    participant Conn as ConnectivityPlatform
    participant Server as Parse Server
    participant Cache as Local Cache (CoreStore)

    Widget->>Handler: initConnectivityHandler()
    Handler->>Conn: subscribe & checkConnectivity()
    Conn-->>Handler: connectivity status (wifi/mobile/none)

    alt Online
        Handler->>Server: loadDataFromServer()
        Server-->>Handler: objects
        Handler->>Cache: _saveBatchToCache(objects)
        Handler->>Widget: render server data
    else Offline (offlineMode)
        Handler->>Cache: loadDataFromCache()
        Cache-->>Handler: cached objects
        Handler->>Widget: render cached data
    end

    Conn->>Handler: connectivity change (lost)
    Handler->>Widget: disposeLiveList()
    Handler->>Cache: loadDataFromCache()
    Handler->>Widget: render offline

    Conn->>Handler: connectivity restored
    Handler->>Server: loadDataFromServer()
    Server-->>Handler: fresh objects
    Handler->>Cache: syncLocalCacheWithServer / update cache
    Handler->>Widget: update UI

    Server->>Handler: live update (add/update/delete)
    Handler->>Cache: persist change (if offline or to keep cache fresh)
    Handler->>Widget: incremental update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Areas needing extra attention:
    • ConnectivityHandlerMixin: race conditions, initial-check vs. stream updates, subscription disposal.
    • Live widgets: pagination, lazy prefetch, LRU cache eviction, stream integration, and index propagation.
    • ParseObjectOffline: serialization assumptions, key naming, and sync-to-server behavior.
    • Analytics endpoints: endpoint contracts, middleware/shelf boilerplate security (master-key handling) and data shapes.

Possibly related PRs

Suggested reviewers

  • mtrezza

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main features being added: offline mode support and new live query widgets.
Description check ✅ Passed The description covers most required sections from the template but omits the 'Issue' and 'Tasks' sections, though the summary and changes are comprehensive.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive offline mode support and introduces three new live query widgets for the Parse Flutter SDK. The implementation includes local caching via ParseObjectOffline extension methods, automatic connectivity detection, and batch save operations for efficient caching.

Key Changes

  • Three new live query widgets: ParseLiveSliverListWidget, ParseLiveSliverGridWidget, and ParseLiveListPageView
  • Offline mode support with local caching and automatic connectivity fallback
  • Connectivity handler mixin for consistent offline handling across widgets
  • LRU memory management via CachedParseLiveList

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
packages/dart/lib/src/objects/parse_offline_object.dart New extension providing offline caching methods for ParseObject
packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart Mixin for handling connectivity state and data loading transitions
packages/flutter/lib/src/utils/parse_cached_live_list.dart LRU cache wrapper for ParseLiveList
packages/flutter/lib/src/utils/parse_live_sliver_list.dart New sliver list widget with offline support
packages/flutter/lib/src/utils/parse_live_sliver_grid.dart New sliver grid widget with offline support
packages/flutter/lib/src/utils/parse_live_page_view.dart New PageView widget with offline support
packages/flutter/lib/src/utils/parse_live_list.dart Updated with offline mode and pagination support
packages/flutter/lib/src/utils/parse_live_grid.dart Updated with offline mode and pagination support
packages/flutter/lib/src/analytics/* Unrelated analytics integration (should be separate PR)
packages/flutter/pubspec.yaml Configuration changes including git dependency and publish_to: none
packages/flutter/README.md Comprehensive documentation for new features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

repository: https://github.com/parse-community/Parse-SDK-Flutter
issue_tracker: https://github.com/parse-community/Parse-SDK-Flutter/issues
documentation: https://docs.parseplatform.org/dart/guide
publish_to: none
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publish_to: none should be removed before merging. This prevents the dart package from being published to pub.dev.

Suggested change
publish_to: none

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 388
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:parse_server_sdk/parse_server_sdk.dart';

/// Analytics collection utility for Parse Dashboard integration
///
/// This class provides methods to collect user, installation, and event data
/// that can be fed to Parse Dashboard analytics endpoints.
class ParseAnalytics {
static StreamController<Map<String, dynamic>>? _eventController;
static const String _eventsKey = 'parse_analytics_events';

/// Initialize the analytics system
static Future<void> initialize() async {
_eventController ??= StreamController<Map<String, dynamic>>.broadcast();
}

/// Get comprehensive user analytics for Parse Dashboard
static Future<Map<String, dynamic>> getUserAnalytics() async {
try {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
final weekAgo = now.subtract(const Duration(days: 7));
final monthAgo = now.subtract(const Duration(days: 30));

// Get user count queries using QueryBuilder
final totalUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery());
final totalUsersResult = await totalUsersQuery.count();
final totalUsers = totalUsersResult.count;

final activeUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final activeUsersResult = await activeUsersQuery.count();
final activeUsers = activeUsersResult.count;

final dailyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', yesterday);
final dailyUsersResult = await dailyUsersQuery.count();
final dailyUsers = dailyUsersResult.count;

final weeklyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final weeklyUsersResult = await weeklyUsersQuery.count();
final weeklyUsers = weeklyUsersResult.count;

final monthlyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', monthAgo);
final monthlyUsersResult = await monthlyUsersQuery.count();
final monthlyUsers = monthlyUsersResult.count;

return {
'timestamp': now.millisecondsSinceEpoch,
'total_users': totalUsers,
'active_users': activeUsers,
'daily_users': dailyUsers,
'weekly_users': weeklyUsers,
'monthly_users': monthlyUsers,
};
} catch (e) {
if (kDebugMode) {
print('Error getting user analytics: $e');
}
return {
'timestamp': DateTime.now().millisecondsSinceEpoch,
'total_users': 0,
'active_users': 0,
'daily_users': 0,
'weekly_users': 0,
'monthly_users': 0,
};
}
}

/// Get installation analytics for Parse Dashboard
static Future<Map<String, dynamic>> getInstallationAnalytics() async {
try {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
final weekAgo = now.subtract(const Duration(days: 7));
final monthAgo = now.subtract(const Duration(days: 30));

// Get installation count queries
final totalInstallationsQuery = QueryBuilder<ParseInstallation>(ParseInstallation.forQuery());
final totalInstallationsResult = await totalInstallationsQuery.count();
final totalInstallations = totalInstallationsResult.count;

final activeInstallationsQuery = QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final activeInstallationsResult = await activeInstallationsQuery.count();
final activeInstallations = activeInstallationsResult.count;

final dailyInstallationsQuery = QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', yesterday);
final dailyInstallationsResult = await dailyInstallationsQuery.count();
final dailyInstallations = dailyInstallationsResult.count;

final weeklyInstallationsQuery = QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final weeklyInstallationsResult = await weeklyInstallationsQuery.count();
final weeklyInstallations = weeklyInstallationsResult.count;

final monthlyInstallationsQuery = QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', monthAgo);
final monthlyInstallationsResult = await monthlyInstallationsQuery.count();
final monthlyInstallations = monthlyInstallationsResult.count;

return {
'timestamp': now.millisecondsSinceEpoch,
'total_installations': totalInstallations,
'active_installations': activeInstallations,
'daily_installations': dailyInstallations,
'weekly_installations': weeklyInstallations,
'monthly_installations': monthlyInstallations,
};
} catch (e) {
if (kDebugMode) {
print('Error getting installation analytics: $e');
}
return {
'timestamp': DateTime.now().millisecondsSinceEpoch,
'total_installations': 0,
'active_installations': 0,
'daily_installations': 0,
'weekly_installations': 0,
'monthly_installations': 0,
};
}
}

/// Track custom events for analytics
static Future<void> trackEvent(String eventName, [Map<String, dynamic>? parameters]) async {
try {
await initialize();

final currentUser = await ParseUser.currentUser();
final currentInstallation = await ParseInstallation.currentInstallation();

final event = {
'event_name': eventName,
'parameters': parameters ?? {},
'timestamp': DateTime.now().millisecondsSinceEpoch,
'user_id': currentUser?.objectId,
'installation_id': currentInstallation.objectId,
};

// Add to stream for real-time tracking
_eventController?.add(event);

// Store locally for later upload
await _storeEventLocally(event);

if (kDebugMode) {
print('Analytics event tracked: $eventName');
}
} catch (e) {
if (kDebugMode) {
print('Error tracking event: $e');
}
}
}

/// Get time series data for Parse Dashboard charts
static Future<List<List<num>>> getTimeSeriesData({
required String metric,
required DateTime startDate,
required DateTime endDate,
String interval = 'day',
}) async {
try {
final data = <List<num>>[];
final intervalDuration = interval == 'hour'
? const Duration(hours: 1)
: const Duration(days: 1);

DateTime current = startDate;
while (current.isBefore(endDate)) {
final next = current.add(intervalDuration);
int value = 0;

switch (metric) {
case 'users':
final query = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;

case 'installations':
final query = QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;
}

data.add([current.millisecondsSinceEpoch, value]);
current = next;
}

return data;
} catch (e) {
if (kDebugMode) {
print('Error getting time series data: $e');
}
return [];
}
}

/// Calculate user retention metrics
static Future<Map<String, double>> getUserRetention({DateTime? cohortDate}) async {
try {
final cohort = cohortDate ?? DateTime.now().subtract(const Duration(days: 30));
final cohortEnd = cohort.add(const Duration(days: 1));

// Get users who signed up in the cohort period
final cohortQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('createdAt', cohort)
..whereLessThan('createdAt', cohortEnd);

final cohortUsers = await cohortQuery.find();
if (cohortUsers.isEmpty) {
return {'day1': 0.0, 'day7': 0.0, 'day30': 0.0};
}

final cohortUserIds = cohortUsers.map((user) => user.objectId!).toList();

// Calculate retention
final day1Retention = await _calculateRetention(cohortUserIds, cohort, 1);
final day7Retention = await _calculateRetention(cohortUserIds, cohort, 7);
final day30Retention = await _calculateRetention(cohortUserIds, cohort, 30);

return {
'day1': day1Retention,
'day7': day7Retention,
'day30': day30Retention,
};
} catch (e) {
if (kDebugMode) {
print('Error calculating user retention: $e');
}
return {'day1': 0.0, 'day7': 0.0, 'day30': 0.0};
}
}

/// Get stream of real-time analytics events
static Stream<Map<String, dynamic>>? get eventsStream => _eventController?.stream;

/// Store event locally for offline support
static Future<void> _storeEventLocally(Map<String, dynamic> event) async {
try {
final coreStore = ParseCoreData().getStore();
final existingEvents = await coreStore.getStringList(_eventsKey) ?? [];

existingEvents.add(jsonEncode(event));

// Keep only last 1000 events
if (existingEvents.length > 1000) {
existingEvents.removeRange(0, existingEvents.length - 1000);
}

await coreStore.setStringList(_eventsKey, existingEvents);
} catch (e) {
if (kDebugMode) {
print('Error storing event locally: $e');
}
}
}

/// Get locally stored events
static Future<List<Map<String, dynamic>>> getStoredEvents() async {
try {
final coreStore = ParseCoreData().getStore();
final eventStrings = await coreStore.getStringList(_eventsKey) ?? [];

return eventStrings.map((eventString) {
try {
return jsonDecode(eventString) as Map<String, dynamic>;
} catch (e) {
if (kDebugMode) {
print('Error parsing stored event: $e');
}
return <String, dynamic>{};
}
}).where((event) => event.isNotEmpty).toList();
} catch (e) {
if (kDebugMode) {
print('Error getting stored events: $e');
}
return [];
}
}

/// Clear locally stored events
static Future<void> clearStoredEvents() async {
try {
final coreStore = ParseCoreData().getStore();
await coreStore.remove(_eventsKey);
} catch (e) {
if (kDebugMode) {
print('Error clearing stored events: $e');
}
}
}

/// Calculate retention for a specific period
static Future<double> _calculateRetention(List<String> cohortUserIds, DateTime cohortStart, int days) async {
try {
if (cohortUserIds.isEmpty) return 0.0;

final retentionDate = cohortStart.add(Duration(days: days));
final retentionEnd = retentionDate.add(const Duration(days: 1));

final retentionQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereContainedIn('objectId', cohortUserIds)
..whereGreaterThan('updatedAt', retentionDate)
..whereLessThan('updatedAt', retentionEnd);

final activeUsers = await retentionQuery.find();

return activeUsers.length / cohortUserIds.length;
} catch (e) {
if (kDebugMode) {
print('Error calculating retention for day $days: $e');
}
return 0.0;
}
}

/// Dispose resources
static void dispose() {
_eventController?.close();
_eventController = null;
}
}

/// Event model for analytics
class AnalyticsEventData {
final String eventName;
final Map<String, dynamic> parameters;
final DateTime timestamp;
final String? userId;
final String? installationId;

AnalyticsEventData({
required this.eventName,
this.parameters = const {},
DateTime? timestamp,
this.userId,
this.installationId,
}) : timestamp = timestamp ?? DateTime.now();

Map<String, dynamic> toJson() => {
'event_name': eventName,
'parameters': parameters,
'timestamp': timestamp.millisecondsSinceEpoch,
'user_id': userId,
'installation_id': installationId,
};

factory AnalyticsEventData.fromJson(Map<String, dynamic> json) => AnalyticsEventData(
eventName: json['event_name'] as String,
parameters: Map<String, dynamic>.from(json['parameters'] ?? {}),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
userId: json['user_id'] as String?,
installationId: json['installation_id'] as String?,
);
}



/* // Initialize analytics
await ParseAnalytics.initialize();
// Track events
await ParseAnalytics.trackEvent('app_opened');
await ParseAnalytics.trackEvent('purchase', {'amount': 9.99});
// Get analytics data
final userStats = await ParseAnalytics.getUserAnalytics();
final retention = await ParseAnalytics.getUserRetention();
// Real-time event streaming
ParseAnalytics.eventsStream?.listen((event) {
print('New event: ${event['event_name']}');
});*/ No newline at end of file
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire analytics implementation appears unrelated to the PR's stated purpose of "Add offline mode support for ParseLive and new live query widgets". The PR description doesn't mention analytics features. Consider:

  1. Moving analytics to a separate PR for focused review
  2. Updating the PR description if analytics is intentionally included
  3. Explaining the relationship between analytics and offline mode if there is one

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 104
import 'package:parse_server_sdk/parse_server_sdk.dart';

void main() async {
// Initialize Parse
await Parse().initialize(
'test_app_id',
'https://test.com',
clientKey: 'test_client_key',
debug: false,
);

print('=== Testing Offline Mode Functionality ===\n');

// Test 1: Save single object to cache
print('Test 1: Save single object to cache');
final testObject = ParseObject('TestClass');
testObject.set('name', 'Test Object');
testObject.objectId = 'test-id-1';
await testObject.saveToLocalCache();
print('✅ Single object saved to cache\n');

// Test 2: Load single object from cache
print('Test 2: Load single object from cache');
final loadedObject = await ParseObjectOffline.loadFromLocalCache(
'TestClass',
'test-id-1',
);
if (loadedObject != null &&
loadedObject.get<String>('name') == 'Test Object') {
print('✅ Single object loaded from cache successfully\n');
} else {
print('❌ Failed to load object from cache\n');
}

// Test 3: Save multiple objects efficiently
print('Test 3: Save multiple objects to cache');
final objectsToSave = <ParseObject>[];
for (int i = 1; i <= 5; i++) {
final obj = ParseObject('TestClass');
obj.set('name', 'Object $i');
obj.objectId = 'test-id-$i';
objectsToSave.add(obj);
}
await ParseObjectOffline.saveAllToLocalCache('TestClass', objectsToSave);
print('✅ Multiple objects saved to cache\n');

// Test 4: Load all objects from cache
print('Test 4: Load all objects from cache');
final allCached = await ParseObjectOffline.loadAllFromLocalCache('TestClass');
print('✅ Loaded ${allCached.length} objects from cache\n');

// Test 5: Check if object exists in cache
print('Test 5: Check if object exists in cache');
final exists = await ParseObjectOffline.existsInLocalCache(
'TestClass',
'test-id-1',
);
if (exists) {
print('✅ Object existence check passed\n');
} else {
print('❌ Object existence check failed\n');
}

// Test 6: Update object in cache
print('Test 6: Update object in cache');
await testObject.updateInLocalCache({'name': 'Updated Object'});
final updatedObject = await ParseObjectOffline.loadFromLocalCache(
'TestClass',
'test-id-1',
);
if (updatedObject?.get<String>('name') == 'Updated Object') {
print('✅ Object updated in cache successfully\n');
}

// Test 7: Get all object IDs
print('Test 7: Get all object IDs from cache');
final objectIds = await ParseObjectOffline.getAllObjectIdsInLocalCache(
'TestClass',
);
print('✅ Retrieved ${objectIds.length} object IDs from cache\n');

// Test 8: Remove object from cache
print('Test 8: Remove object from cache');
await testObject.removeFromLocalCache();
final removedCheck = await ParseObjectOffline.existsInLocalCache(
'TestClass',
'test-id-1',
);
if (!removedCheck) {
print('✅ Object removed from cache successfully\n');
}

// Test 9: Clear all objects for a class
print('Test 9: Clear all objects for a class');
await ParseObjectOffline.clearLocalCacheForClass('TestClass');
final clearedObjects = await ParseObjectOffline.loadAllFromLocalCache(
'TestClass',
);
if (clearedObjects.isEmpty) {
print('✅ Cache cleared successfully\n');
}

print('=== All Offline Mode Tests Completed Successfully! ===');
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test files should be in the test/ directory, not in the package root. These manual test scripts (test_offline_mode.dart, test_offline.dart) should either be:

  1. Moved to proper test directories with proper test framework usage
  2. Moved to an example/ directory if they're meant as examples
  3. Removed before merging if they're only for local development

Copilot uses AI. Check for mistakes.
Comment on lines 332 to 333
if (!item.containsKey(sdk.keyVarUpdatedAt)) {
fetchFutures.add(item.fetch().then((_) {
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lazy loading check uses !item.containsKey(sdk.keyVarUpdatedAt) which may not accurately determine if an item needs fetching. An item could have an updatedAt field but still be incomplete if lazy loading is enabled. Consider checking a more reliable indicator or using a dedicated flag to track whether the full object has been loaded.

Suggested change
if (!item.containsKey(sdk.keyVarUpdatedAt)) {
fetchFutures.add(item.fetch().then((_) {
if (item['isFullyLoaded'] != true) {
fetchFutures.add(item.fetch().then((_) {
item['isFullyLoaded'] = true;

Copilot uses AI. Check for mistakes.
// Analytics Overview - Audience Metrics
app.get('/apps/:appSlug/analytics_content_audience', async (req, res) => {
try {
const { audienceType, at } = req.query;
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable at.

Suggested change
const { audienceType, at } = req.query;
const { audienceType } = req.query;

Copilot uses AI. Check for mistakes.
// Slow Queries Endpoint
app.get('/apps/:appSlug/slow_queries', async (req, res) => {
try {
const { className, os, version, from, to } = req.query;
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable from.

Suggested change
const { className, os, version, from, to } = req.query;
const { className, os, version } = req.query;

Copilot uses AI. Check for mistakes.
// Slow Queries Endpoint
app.get('/apps/:appSlug/slow_queries', async (req, res) => {
try {
const { className, os, version, from, to } = req.query;
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable to.

Suggested change
const { className, os, version, from, to } = req.query;
const { className, os, version } = req.query;

Copilot uses AI. Check for mistakes.

// Helper function to generate time series data
async function generateTimeSeriesData(options) {
const { endpoint, audienceType, stride, from, to } = options;
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable audienceType.

Suggested change
const { endpoint, audienceType, stride, from, to } = options;
const { endpoint, stride, from, to } = options;

Copilot uses AI. Check for mistakes.
const { audienceType, at } = req.query;
console.log(`Analytics audience request: ${audienceType}`);

let result = { total: 0, content: 0 };
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial value of result is unused, since it is always overwritten.

Suggested change
let result = { total: 0, content: 0 };
let result;

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (24)
packages/flutter/lib/src/analytics/parse_analytics.dart-136-145 (1)

136-145: Potential PII leakage: user_id and installation_id stored in events.

The event payload includes user_id and installation_id. Depending on jurisdiction (GDPR/CCPA), storing user identifiers in analytics events may require user consent. Consider documenting this or providing an opt-out mechanism.

At minimum, add documentation warning about PII:

/// Track custom events for analytics
/// 
/// Note: Events include user_id and installation_id for attribution.
/// Ensure appropriate user consent is obtained per applicable privacy regulations.
static Future<void> trackEvent(String eventName, [Map<String, dynamic>? parameters]) async {
packages/flutter/lib/src/analytics/parse_analytics.dart-163-210 (1)

163-210: Unbounded loop in getTimeSeriesData could cause performance issues.

If startDate and endDate span a long period with hourly intervals, this loop could execute thousands of iterations, each making a network request. Consider adding a limit or warning.

 static Future<List<List<num>>> getTimeSeriesData({
   required String metric,
   required DateTime startDate,
   required DateTime endDate,
   String interval = 'day',
 }) async {
   try {
+    // Guard against excessive iterations
+    final maxIterations = interval == 'hour' ? 168 : 365; // 1 week hourly or 1 year daily
+    final estimatedIterations = endDate.difference(startDate).inHours ~/ 
+        (interval == 'hour' ? 1 : 24);
+    if (estimatedIterations > maxIterations) {
+      if (kDebugMode) {
+        print('Warning: Time range too large ($estimatedIterations intervals). Consider a larger interval.');
+      }
+    }
+    
     final data = <List<num>>[];
packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart-40-51 (1)

40-51: Subscription callback does not await async handler, potentially causing race conditions.

The listen callback invokes _internalUpdateConnectionStatus (which is async) but doesn't await it. Multiple rapid connectivity changes could trigger overlapping async state updates, leading to inconsistent state.

Consider using a queue or mutex pattern, or at minimum track pending operations:

 _connectivitySubscription =
     _connectivity.onConnectivityChanged.listen((List<ConnectivityResult> results) {
   final newResult = results.contains(ConnectivityResult.mobile)
       ? ConnectivityResult.mobile
       : results.contains(ConnectivityResult.wifi)
           ? ConnectivityResult.wifi
           : results.contains(ConnectivityResult.none)
               ? ConnectivityResult.none
               : ConnectivityResult.other;

-  _internalUpdateConnectionStatus(newResult);
+  // Fire and forget, but consider debouncing for rapid changes
+  _internalUpdateConnectionStatus(newResult);
 });

Alternatively, implement debouncing or a serial queue to handle rapid connectivity changes gracefully.

Committable suggestion skipped: line range outside the PR's diff.

packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart-42-48 (1)

42-48: Consider handling ConnectivityResult.ethernet and ConnectivityResult.vpn.

The current logic maps ethernet, VPN, and bluetooth connections to ConnectivityResult.other, which may not be treated as "online" by the downstream logic (line 93). Users on ethernet or VPN would be incorrectly considered offline.

 final newResult = results.contains(ConnectivityResult.mobile)
     ? ConnectivityResult.mobile
     : results.contains(ConnectivityResult.wifi)
         ? ConnectivityResult.wifi
+        : results.contains(ConnectivityResult.ethernet)
+            ? ConnectivityResult.ethernet
+            : results.contains(ConnectivityResult.vpn)
+                ? ConnectivityResult.vpn
-        : results.contains(ConnectivityResult.none)
-            ? ConnectivityResult.none
-            : ConnectivityResult.other;
+                : results.contains(ConnectivityResult.none)
+                    ? ConnectivityResult.none
+                    : ConnectivityResult.other;

And update the isOnline check at line 93:

-bool isOnline = result == ConnectivityResult.mobile || result == ConnectivityResult.wifi;
+bool isOnline = result == ConnectivityResult.mobile || 
+    result == ConnectivityResult.wifi ||
+    result == ConnectivityResult.ethernet ||
+    result == ConnectivityResult.vpn;
packages/dart/lib/src/objects/parse_offline_object.dart-1-165 (1)

1-165: Replace print statements with proper logging.

The extension uses print() statements throughout, which is not appropriate for library code. Consider using the Parse SDK's logger (ParseLogger) for consistent logging that respects the user's debug settings.

Example:

ParseCoreData().logger.log(
  ParseLogLevel.info,
  'Loaded object $objectId from local cache for $className'
);

This would:

  • Respect the debug flag set during Parse initialization
  • Provide consistent logging across the SDK
  • Allow filtering by log level
  • Avoid polluting production console output
packages/dart/lib/src/storage/core_store_sem_impl.dart-106-110 (1)

106-110: Remove commented-out code.

The old implementation should be removed to keep the codebase clean.

-// @override
-//   Future<List<String>?> getStringList(String key) async {
-//     final List<String>? storedItem = await get(key);
-//     return storedItem;
-//   }
packages/dart/lib/src/storage/core_store_memory.dart-50-53 (1)

50-53: Remove commented-out code.

The old implementation should be removed for code cleanliness.

-  // @override
-  // Future<List<String>?> getStringList(String key) async {
-  //   return _data[key];
-  // }
packages/dart/lib/src/objects/parse_offline_object.dart-95-104 (1)

95-104: Add error handling for loading all cached objects.

The loadAllFromLocalCache method should handle JSON decode failures gracefully to avoid losing all cached data due to a single corrupted entry.

 static Future<List<ParseObject>> loadAllFromLocalCache(String className) async {
   final CoreStore coreStore = ParseCoreData().getStore();
   final String cacheKey = 'offline_cache_$className';
   final List<String> cached = await _getStringListAsStrings(coreStore, cacheKey);
   print('Loaded ${cached.length} objects from local cache for $className');
-  return cached.map<ParseObject>((s) {
-    final jsonObj = json.decode(s);
-    return ParseObject(className).fromJson(jsonObj);
-  }).toList();
+  final List<ParseObject> objects = [];
+  for (final s in cached) {
+    try {
+      final jsonObj = json.decode(s);
+      objects.add(ParseObject(className).fromJson(jsonObj));
+    } catch (e) {
+      print('Error decoding cached object for $className: $e');
+      // Skip corrupted entries but continue processing
+    }
+  }
+  return objects;
 }
packages/dart/lib/src/objects/parse_offline_object.dart-106-120 (1)

106-120: Add error handling for cache update operation.

The updateInLocalCache method should handle JSON operations safely.

 Future<void> updateInLocalCache(Map<String, dynamic> updates) async {
+  try {
     final CoreStore coreStore = ParseCoreData().getStore();
     final String cacheKey = 'offline_cache_$parseClassName';
     final List<String> cached = await _getStringListAsStrings(coreStore, cacheKey);
     for (int i = 0; i < cached.length; i++) {
-      final jsonObj = json.decode(cached[i]);
-      if (jsonObj['objectId'] == objectId) {
-        jsonObj.addAll(updates);
-        cached[i] = json.encode(jsonObj);
-        break;
+      try {
+        final jsonObj = json.decode(cached[i]);
+        if (jsonObj['objectId'] == objectId) {
+          jsonObj.addAll(updates);
+          cached[i] = json.encode(jsonObj);
+          break;
+        }
+      } catch (e) {
+        print('Error decoding cached object during update: $e');
+        continue;
       }
     }
     await coreStore.setStringList(cacheKey, cached);
     print('Updated object ${objectId ?? "(no objectId)"} in local cache for $parseClassName');
+  } catch (e) {
+    print('Error updating object in local cache for $parseClassName: $e');
+    rethrow;
+  }
 }
packages/dart/lib/src/objects/parse_offline_object.dart-5-17 (1)

5-17: Add error handling and consider performance optimization.

This method has two concerns:

  1. Missing error handling: JSON decoding can fail if the cached data is corrupted, which would crash the method.
  2. Performance: Loading the entire cache to find a single object is inefficient for large datasets.

Apply this diff to add error handling:

 static Future<ParseObject?> loadFromLocalCache(String className, String objectId) async {
   final CoreStore coreStore = ParseCoreData().getStore();
   final String cacheKey = 'offline_cache_$className';
   final List<String> cached = await _getStringListAsStrings(coreStore, cacheKey);
   for (final s in cached) {
-    final jsonObj = json.decode(s);
-    if (jsonObj['objectId'] == objectId) {
-      print('Loaded object $objectId from local cache for $className');
-      return ParseObject(className).fromJson(jsonObj);
+    try {
+      final jsonObj = json.decode(s);
+      if (jsonObj['objectId'] == objectId) {
+        print('Loaded object $objectId from local cache for $className');
+        return ParseObject(className).fromJson(jsonObj);
+      }
+    } catch (e) {
+      print('Error decoding cached object for $className: $e');
+      continue;
     }
   }
   return null;
 }

Consider using a more efficient storage structure (e.g., separate key per object: offline_cache_$className\_$objectId) to avoid loading all objects.

packages/dart/lib/src/objects/parse_offline_object.dart-82-92 (1)

82-92: Add error handling for cache removal operation.

The removeFromLocalCache method needs error handling to prevent crashes and ensure data integrity.

 Future<void> removeFromLocalCache() async {
+  try {
     final CoreStore coreStore = ParseCoreData().getStore();
     final String cacheKey = 'offline_cache_$parseClassName';
     final List<String> cached = await _getStringListAsStrings(coreStore, cacheKey);
     cached.removeWhere((s) {
-      final jsonObj = json.decode(s);
-      return jsonObj['objectId'] == objectId;
+      try {
+        final jsonObj = json.decode(s);
+        return jsonObj['objectId'] == objectId;
+      } catch (e) {
+        print('Error decoding cached object during removal: $e');
+        return false;
+      }
     });
     await coreStore.setStringList(cacheKey, cached);
     print('Removed object ${objectId ?? "(no objectId)"} from local cache for $parseClassName');
+  } catch (e) {
+    print('Error removing object from local cache for $parseClassName: $e');
+    rethrow;
+  }
 }
packages/dart/lib/src/objects/parse_offline_object.dart-152-158 (1)

152-158: Add error handling and progress tracking for sync operation.

The syncLocalCacheWithServer method should handle failures gracefully and provide progress feedback for large datasets.

 static Future<void> syncLocalCacheWithServer(String className) async {
-  final objects = await loadAllFromLocalCache(className);
-  for (final obj in objects) {
-    await obj.save();
+  try {
+    final objects = await loadAllFromLocalCache(className);
+    int synced = 0;
+    int failed = 0;
+    for (final obj in objects) {
+      try {
+        await obj.save();
+        synced++;
+      } catch (e) {
+        print('Error syncing object ${obj.objectId} for $className: $e');
+        failed++;
+      }
+    }
+    print('Synced local cache with server for $className. Success: $synced, Failed: $failed');
+  } catch (e) {
+    print('Error syncing local cache with server for $className: $e');
+    rethrow;
   }
-  print('Synced local cache with server for $className');
 }
packages/dart/lib/src/objects/parse_offline_object.dart-20-32 (1)

20-32: Add error handling and consider concurrency control.

This method needs protection against:

  1. JSON encoding/decoding failures: Can throw if data contains non-serializable objects
  2. Race conditions: Multiple concurrent saves could corrupt the cache

Add error handling:

 Future<void> saveToLocalCache() async {
+  try {
     final CoreStore coreStore = ParseCoreData().getStore();
     final String cacheKey = 'offline_cache_$parseClassName';
     final List<String> cached = await _getStringListAsStrings(coreStore, cacheKey);
     // Remove any existing object with the same objectId
     cached.removeWhere((s) {
-      final jsonObj = json.decode(s);
-      return jsonObj['objectId'] == objectId;
+      try {
+        final jsonObj = json.decode(s);
+        return jsonObj['objectId'] == objectId;
+      } catch (e) {
+        print('Error decoding cached object during save: $e');
+        return false;
+      }
     });
     cached.add(json.encode(toJson(full: true)));
     await coreStore.setStringList(cacheKey, cached);
     print('Saved object ${objectId ?? "(no objectId)"} to local cache for $parseClassName');
+  } catch (e) {
+    print('Error saving object to local cache for $parseClassName: $e');
+    rethrow;
+  }
 }
packages/flutter/lib/src/utils/parse_live_sliver_list.dart-264-289 (1)

264-289: Type safety issue: Stream events not converted via fromJson.

Same issue as in ParseLiveSliverGridWidget: stream event objects are directly inserted without fromJson conversion, but cache-loaded objects use fromJson. Apply the same fix as suggested for the grid widget.

packages/flutter/lib/src/utils/parse_live_page_view.dart-241-266 (1)

241-266: Type safety issue: Stream events not converted via fromJson.

Same issue as in the sliver widgets: stream event objects are directly inserted without fromJson conversion. Apply the same fix pattern.

packages/flutter/lib/src/utils/parse_live_page_view.dart-600-614 (1)

600-614: Potential listener leak when external PageController is provided.

The listener _checkForMoreData is added when widget.pagination is true (line 104), but only removed when widget.pagination && widget.pageController == null (line 606-608). If a user provides their own pageController with pagination enabled, the listener is never removed.

 @override
 void dispose() {
   disposeConnectivityHandler();
   disposeLiveList();
   _noDataNotifier.dispose();
-  // Remove listener only if we added it
-  if (widget.pagination && widget.pageController == null) {
-     _pageController.removeListener(_checkForMoreData);
-  }
+  // Remove listener if pagination is enabled (we always add it in that case)
+  if (widget.pagination) {
+     _pageController.removeListener(_checkForMoreData);
+  }
   // Dispose controller only if we created it
   if (widget.pageController == null) {
     _pageController.dispose();
   }
   super.dispose();
 }
packages/flutter/lib/src/analytics/parse_analytics_endpoints.dart-230-276 (1)

230-276: Security: Hardcoded placeholder master key in generated code.

The generated Dart Shelf handler contains if (masterKey != 'your_master_key') which is a dangerous placeholder. Developers may copy this without changing it. Consider:

  1. Using an environment variable reference: Platform.environment['PARSE_MASTER_KEY']
  2. Adding a clear // TODO: CHANGE THIS comment
  3. Throwing an error if the key appears to be the placeholder
-    if (masterKey != 'your_master_key') {
+    // SECURITY: Replace with your actual master key or use environment variable
+    final expectedKey = Platform.environment['PARSE_MASTER_KEY'];
+    if (expectedKey == null || masterKey != expectedKey) {
       return Response.forbidden(json.encode({'error': 'Unauthorized'}));
     }

Committable suggestion skipped: line range outside the PR's diff.

packages/flutter/lib/src/utils/parse_live_sliver_grid.dart-354-375 (1)

354-375: Type safety issue: Stream events not converted via fromJson.

When processing live stream events, event.object (type sdk.ParseObject) is directly inserted into _items (type List<T>). However, when loading from cache (line 173), objects are converted via widget.fromJson(). This inconsistency may cause type errors when T is a custom subclass.

 if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
-  final addedItem = event.object;
-  setState(() { _items.insert(event.index, addedItem); });
-  objectToCache = addedItem;
+  final addedItem = widget.fromJson(event.object.toJson(full: true));
+  setState(() { _items.insert(event.index, addedItem); });
+  objectToCache = addedItem;
 } 
 // ... similar change for update events
 } else if (event is sdk.ParseLiveListUpdateEvent<sdk.ParseObject>) {
-  final updatedItem = event.object;
+  final updatedItem = widget.fromJson(event.object.toJson(full: true));
   if (event.index >= 0 && event.index < _items.length) {
     setState(() { _items[event.index] = updatedItem; });
packages/flutter/lib/src/utils/parse_live_list.dart-636-646 (1)

636-646: Scroll listener leak when using external ScrollController.

The scroll listener is added unconditionally when widget.pagination is true (line 156), but only removed when widget.scrollController == null (line 640). This causes a listener leak when an external controller is provided.

  @override
  void dispose() {
    disposeConnectivityHandler();

-   if (widget.pagination && widget.scrollController == null) {
+   if (widget.pagination) {
       _scrollController.removeListener(_onScroll);
    }
    if (widget.scrollController == null) {
      _scrollController.dispose();
    }
packages/flutter/lib/src/utils/parse_live_list.dart-442-444 (1)

442-444: Page counter incremented before successful fetch can cause skipped pages on retry.

Same issue as in parse_live_grid.dart: _currentPage++ happens before the async query. If the query fails, retrying will skip to the next page instead of retrying the failed one.

  try {
-   _currentPage++;
-   final skipCount = _currentPage * widget.pageSize;
+   final nextPage = _currentPage + 1;
+   final skipCount = nextPage * widget.pageSize;
    final nextPageQuery = QueryBuilder<T>.copy(widget.query)
      ..setAmountToSkip(skipCount)
      ..setLimit(widget.pageSize);
    
    // ... after successful setState ...
+   _currentPage = nextPage; // Only increment after success

Committable suggestion skipped: line range outside the PR's diff.

packages/flutter/lib/src/utils/parse_live_grid.dart-704-720 (1)

704-720: Scroll listener leak when using external ScrollController.

The listener is added when widget.pagination || widget.lazyLoading (line 149-151), but only removed when widget.scrollController == null (line 709). If an external controller is provided with pagination/lazy loading enabled, the listener is never removed, causing potential issues after disposal.

  @override
  void dispose() {
    disposeConnectivityHandler();

-   if ((widget.pagination || widget.lazyLoading) && widget.scrollController == null) {
+   if (widget.pagination || widget.lazyLoading) {
       _scrollController.removeListener(_onScroll);
    }
    if (widget.scrollController == null) {
      _scrollController.dispose();
    }
packages/flutter/lib/src/utils/parse_live_list.dart-281-306 (1)

281-306: Implicit type cast from ParseObject to generic T may fail at runtime.

Same issue as in the grid widget: stream events return sdk.ParseObject but are assigned to variables expected to be type T. Add explicit casts for type safety.

  if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
-   final addedItem = event.object;
+   final addedItem = event.object as T;
    setState(() { _items.insert(event.index, addedItem); });
    objectToCache = addedItem;
  } else if (event is sdk.ParseLiveListDeleteEvent<sdk.ParseObject>) {
    // ...
  } else if (event is sdk.ParseLiveListUpdateEvent<sdk.ParseObject>) {
-   final updatedItem = event.object;
+   final updatedItem = event.object as T;
packages/flutter/lib/src/utils/parse_live_grid.dart-421-445 (1)

421-445: Implicit type cast from ParseObject to generic T may fail at runtime.

The stream events are typed as sdk.ParseLiveListAddEvent<sdk.ParseObject>, but event.object is inserted directly into _items which expects type T. If T is a custom subclass of ParseObject, this implicit cast will fail.

  if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
-   final addedItem = event.object;
-   setState(() { _items.insert(event.index, addedItem); });
-   objectToCache = addedItem;
+   final addedItem = event.object as T;
+   setState(() { _items.insert(event.index, addedItem); });
+   objectToCache = addedItem;
  } else if (event is sdk.ParseLiveListDeleteEvent<sdk.ParseObject>) {
    // ...
  } else if (event is sdk.ParseLiveListUpdateEvent<sdk.ParseObject>) {
-   final updatedItem = event.object;
+   final updatedItem = event.object as T;
packages/flutter/lib/src/utils/parse_live_grid.dart-273-275 (1)

273-275: Page counter incremented before successful fetch can cause skipped pages on retry.

_currentPage++ on line 274 happens before the async query. If the query fails, the page counter remains incremented, causing the retry to skip to the next page instead of retrying the failed page.

  try {
-   _currentPage++;
    final skipCount = _currentPage * widget.pageSize;
+   final nextPage = _currentPage + 1;
+   final skipCount = nextPage * widget.pageSize;
    final nextPageQuery = QueryBuilder<T>.copy(widget.query)
      ..setAmountToSkip(skipCount)
      ..setLimit(widget.pageSize);

    // ... after successful response handling ...
+   _currentPage = nextPage; // Only increment after success

Committable suggestion skipped: line range outside the PR's diff.

🟡 Minor comments (7)
packages/flutter/lib/src/utils/parse_cached_live_list.dart-99-104 (1)

99-104: Edge case: Empty cache with capacity 0.

If cacheSize is passed as 0 (or negative), the condition _cache.length >= capacity would always be true, and _accessOrder.first would throw on an empty set. Consider validating capacity > 0 in the constructor.

 class _LRUCache<K, V> {
-  _LRUCache(this.capacity);
+  _LRUCache(this.capacity) : assert(capacity > 0, 'Cache capacity must be positive');

Committable suggestion skipped: line range outside the PR's diff.

packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart-12-13 (1)

12-13: late subscription may crash if disposeConnectivityHandler is called before initConnectivityHandler.

If disposeConnectivityHandler() is called before initConnectivityHandler() completes (or if init is never called), accessing _connectivitySubscription will throw a LateInitializationError. Consider using a nullable type with null-check in dispose.

-late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
+StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;

 void disposeConnectivityHandler() {
-  _connectivitySubscription.cancel();
+  _connectivitySubscription?.cancel();
 }

Committable suggestion skipped: line range outside the PR's diff.

packages/flutter/lib/src/utils/parse_live_page_view.dart-375-398 (1)

375-398: Inefficient batch save: Always fetches when lazy loading.

Unlike the sliver widgets which check if items need fetching, this implementation always calls item.fetch() for every item when lazyLoading is true. This causes unnecessary network requests for items that may already have complete data.

 if (widget.lazyLoading) {
   for (final item in itemsToSave) {
-    fetchFutures.add(item.fetch().then((_) {
-       itemsToSaveFinal.add(item);
-    }).catchError((fetchError) {
+    // Only fetch if item doesn't have full data
+    if (!item.containsKey(sdk.keyVarUpdatedAt)) {
+      fetchFutures.add(item.fetch().then((_) {
+         itemsToSaveFinal.add(item);
+      }).catchError((fetchError) {
+         debugPrint('...');
+      }));
+    } else {
+      itemsToSaveFinal.add(item);
+    }
   }

Committable suggestion skipped: line range outside the PR's diff.

packages/flutter/lib/src/utils/parse_live_list.dart-347-372 (1)

347-372: Concurrent list mutation without synchronization.

Same pattern as grid widget: itemsToSaveFinal is mutated from concurrent Future.then() callbacks. Consider collecting results from Future.wait() instead of mutating a shared list.

packages/flutter/lib/src/utils/parse_live_grid.dart-211-226 (1)

211-226: Potential null pointer exception with force-unwrap operator.

Line 212 uses context.findRenderObject()! which will throw if the render object is null. While the try-catch handles this, it's better to use safe navigation.

  int _calculateVisibleMaxIndex(double offset) {
-   if (!mounted || !context.mounted || !context.findRenderObject()!.paintBounds.isFinite) {
+   final renderObject = context.findRenderObject();
+   if (!mounted || !context.mounted || renderObject == null || !renderObject.paintBounds.isFinite) {
      return 0;
    }
packages/flutter/lib/src/utils/parse_live_grid.dart-495-519 (1)

495-519: Concurrent list mutation without synchronization.

itemsToSaveFinal is mutated from multiple concurrent Future.then() callbacks (lines 504-510). While Dart is single-threaded, the callbacks execute in unpredictable order during Future.wait(), and if any callback throws, items may be lost. Consider collecting results from Future.wait() instead.

+   List<Future<T?>> fetchFutures = [];
-   List<Future<void>> fetchFutures = [];

    if (widget.lazyLoading) {
      for (final item in itemsToSave) {
        if (item.get<DateTime>(sdk.keyVarCreatedAt) == null && item.objectId != null) {
-         fetchFutures.add(item.fetch().then((_) {
-            itemsToSaveFinal.add(item);
-         }).catchError((fetchError) {
+         fetchFutures.add(item.fetch().then((_) => item).catchError((fetchError) {
             debugPrint('...');
+            return null;
          }));
        } else {
          itemsToSaveFinal.add(item);
        }
      }
      if (fetchFutures.isNotEmpty) {
-        await Future.wait(fetchFutures);
+        final results = await Future.wait(fetchFutures);
+        itemsToSaveFinal.addAll(results.whereType<T>());
      }
    }
packages/flutter/lib/src/utils/parse_live_list.dart-707-716 (1)

707-716: Inconsistent behavior with comment regarding preLoadedData update.

The comment on line 713 questions whether to keep or update preLoadedData, then says "Let's update" but the code actually keeps the old value (_snapshot.preLoadedData). This inconsistency should be resolved.

If the intent is to update preLoadedData with new stream data:

  setState(() {
    _snapshot = sdk.ParseLiveListElementSnapshot<T>(
      loadedData: data,
-     preLoadedData: _snapshot.preLoadedData, // Keep original preLoadedData? Or update? Let's update.
-     // preLoadedData: data,
+     preLoadedData: data, // Update preLoadedData with latest data
    );
  });
🧹 Nitpick comments (27)
packages/flutter/example/pubspec.yaml (1)

18-24: Consider removing or relocating commented-out dependency alternatives.

These commented-out lines document alternative sources for parse packages but reduce pubspec.yaml clarity. Consider moving this documentation to a separate README or wiki if needed, or remove it if no longer relevant.

PR_CHECKLIST.md (1)

87-87: Align checklist metadata with actual versions and dependencies

The checklist text doesn’t fully match the current codebase:

  • Line 115: Version: 10.2.0+ but packages/flutter/pubspec.yaml shows version: 10.6.2.
  • Line 116: New Dependencies: None, but at least one new dependency (cross_file in the Dart package) and a changed parse_server_sdk dependency are introduced.

I’d suggest updating these checklist fields so they accurately reflect the current version and dependency changes; this avoids confusion when someone uses this file as a release/check reference.

Also applies to: 94-101, 114-116

packages/flutter/lib/src/analytics/README.md (1)

19-70: Harmonize instance vs static API usage and keep endpoint docs in lockstep with code

This README is well‑structured and gives good end‑to‑end coverage (client tracking, data retrieval, Express and Shelf integrations, and API reference). One thing to watch:

  • Here you use final analytics = ParseAnalytics.instance; with instance methods (analytics.trackEvent(...)), whereas USAGE.md primarily demonstrates static calls (ParseAnalytics.trackEvent(...)). If the implementation only supports one of these idioms, the other example will confuse users.

It’d be good to:

  • Pick a primary usage style (instance vs static) that matches parse_analytics.dart and make both README + USAGE consistent.
  • Re‑verify that the documented endpoint handlers and parameter names in the Express/Shelf examples match ParseAnalyticsEndpoints exactly, so dashboard integrations don’t break on small naming mismatches.

Also applies to: 72-153, 155-211, 232-257

packages/flutter/lib/src/utils/parse_cached_live_list.dart (2)

21-27: Null objectId check is good, but consider cache-first lookup for consistency.

getLoadedAt always calls the underlying _parseLiveList.getLoadedAt(index) even if the item might already be cached. For consistency with getPreLoadedAt, you could check the cache first using idOf(index) to avoid redundant underlying calls.

 T? getLoadedAt(int index) {
+  final objectId = _parseLiveList.idOf(index);
+  if (objectId != 'NotFound' && _cache.contains(objectId)) {
+    return _cache.get(objectId);
+  }
   final result = _parseLiveList.getLoadedAt(index);
   if (result != null && result.objectId != null) {
     _cache.put(result.objectId!, result);
   }
   return result;
 }

30-44: Magic string 'NotFound' should be a constant.

The string 'NotFound' appears to be a sentinel value from the underlying ParseLiveList.idOf() API. Extract this to a named constant to improve maintainability and reduce risk of typos.

+/// Sentinel value returned by ParseLiveList.idOf() when object is not found
+static const String _notFoundId = 'NotFound';

 T? getPreLoadedAt(int index) {
   final objectId = _parseLiveList.idOf(index);
   
   // Try cache first
-  if (objectId != 'NotFound' && _cache.contains(objectId)) {
+  if (objectId != _notFoundId && _cache.contains(objectId)) {
     return _cache.get(objectId);
   }
packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart (1)

96-106: State transitions call async methods without error handling.

If loadDataFromServer() or loadDataFromCache() throws, the exception propagates but _isOffline state has already been updated. Consider wrapping in try-catch to maintain consistent state on failure.

 if (isOnline && !wasOnline) {
   // --- Transitioning TO Online ---
-  _isOffline = false;
   debugPrint('$connectivityLogPrefix Transitioning Online: $result. Loading data from server...');
-  await loadDataFromServer();
+  try {
+    await loadDataFromServer();
+    _isOffline = false;
+  } catch (e) {
+    debugPrint('$connectivityLogPrefix Error loading from server: $e');
+    // Optionally fall back to cache or retry
+  }
 }
packages/flutter/lib/src/analytics/parse_analytics.dart (4)

20-73: Performance: Five sequential network queries could be slow.

getUserAnalytics() makes 5 separate count() queries sequentially. Consider using Future.wait to parallelize these queries for better performance.

 static Future<Map<String, dynamic>> getUserAnalytics() async {
   try {
     final now = DateTime.now();
     final yesterday = now.subtract(const Duration(days: 1));
     final weekAgo = now.subtract(const Duration(days: 7));
     final monthAgo = now.subtract(const Duration(days: 30));
     
-    // Get user count queries using QueryBuilder
-    final totalUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery());
-    final totalUsersResult = await totalUsersQuery.count();
-    final totalUsers = totalUsersResult.count;
-    
-    final activeUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
-      ..whereGreaterThan('updatedAt', weekAgo);
-    final activeUsersResult = await activeUsersQuery.count();
-    final activeUsers = activeUsersResult.count;
-    // ... more sequential queries
+    // Parallelize queries for better performance
+    final results = await Future.wait([
+      QueryBuilder<ParseUser>(ParseUser.forQuery()).count(),
+      (QueryBuilder<ParseUser>(ParseUser.forQuery())..whereGreaterThan('updatedAt', weekAgo)).count(),
+      (QueryBuilder<ParseUser>(ParseUser.forQuery())..whereGreaterThan('updatedAt', yesterday)).count(),
+      (QueryBuilder<ParseUser>(ParseUser.forQuery())..whereGreaterThan('updatedAt', weekAgo)).count(),
+      (QueryBuilder<ParseUser>(ParseUser.forQuery())..whereGreaterThan('updatedAt', monthAgo)).count(),
+    ]);
+    
+    return {
+      'timestamp': now.millisecondsSinceEpoch,
+      'total_users': results[0].count,
+      'active_users': results[1].count,
+      'daily_users': results[2].count,
+      'weekly_users': results[3].count,
+      'monthly_users': results[4].count,
+    };

252-270: Event storage truncation silently discards old events.

When exceeding 1000 events, older events are removed without any callback or notification. This could cause data loss if events haven't been uploaded. Consider adding an upload mechanism or warning.

 // Keep only last 1000 events
 if (existingEvents.length > 1000) {
+  if (kDebugMode) {
+    print('Analytics: Truncating ${existingEvents.length - 1000} old events');
+  }
   existingEvents.removeRange(0, existingEvents.length - 1000);
 }

363-369: fromJson may throw on malformed input.

The factory constructor assumes json['event_name'] is a non-null String and json['timestamp'] is a non-null int. Malformed JSON will throw a TypeError. Consider defensive parsing.

 factory AnalyticsEventData.fromJson(Map<String, dynamic> json) => AnalyticsEventData(
-  eventName: json['event_name'] as String,
+  eventName: json['event_name'] as String? ?? '',
   parameters: Map<String, dynamic>.from(json['parameters'] ?? {}),
-  timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
+  timestamp: json['timestamp'] != null 
+      ? DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int)
+      : DateTime.now(),
   userId: json['user_id'] as String?,
   installationId: json['installation_id'] as String?,
 );

374-388: Remove commented-out example code from production file.

This block should be moved to documentation (README or dartdoc) rather than left as a comment in the source file.

packages/flutter/test_offline.dart (2)

1-34: Test file should be moved to proper test directory and use a test framework.

This file is located at packages/flutter/test_offline.dart instead of packages/flutter/test/. It also doesn't use Flutter's test framework (flutter_test), making it incompatible with flutter test and CI pipelines.

Consider restructuring as a proper test:

// Move to: packages/flutter/test/offline_extension_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:parse_server_sdk/parse_server_sdk.dart';

void main() {
  setUpAll(() async {
    await Parse().initialize(
      'test_app_id',
      'https://test.com',
      clientKey: 'test_client_key',
      debug: false,
    );
  });

  test('ParseObjectOffline extension methods are available', () async {
    final object = ParseObject('TestClass');
    object.set('name', 'Test Object');
    
    await object.saveToLocalCache();
    
    final cached = await ParseObjectOffline.loadAllFromLocalCache('TestClass');
    expect(cached, isNotEmpty);
  });
}

5-10: Hardcoded test credentials should use environment variables or test fixtures.

While this is a test file, using hardcoded credentials (even fake ones) sets a poor example. Consider using a test configuration pattern.

packages/dart/lib/src/storage/core_store_sem_impl.dart (1)

98-104: Consider adding error handling for type coercion.

The type coercion logic improves robustness, but silently returning null for unexpected types could mask data corruption issues. Consider logging a warning when encountering unexpected types to aid debugging.

 Future<List<String>?> getStringList(String key) async {
   final value = await get(key);
   if (value == null) return null;
   if (value is List<String>) return value;
   if (value is Iterable) return value.map((e) => e.toString()).toList();
+  if (value != null) {
+    print('Warning: Unexpected type ${value.runtimeType} for key $key in getStringList');
+  }
   return null;
 }
packages/dart/test_extension.dart (1)

1-28: Consider converting to a proper unit test.

This file appears to be a manual test script rather than an automated test. Consider:

  • Using a testing framework (e.g., test package) to enable CI integration
  • Adding assertions to verify expected behavior instead of just printing results
  • Verifying that cached data matches saved data

Example:

import 'package:test/test.dart';
import 'package:parse_server_sdk/parse_server_sdk.dart';

void main() {
  setUp(() async {
    await Parse().initialize(/*...*/);
  });

  test('ParseObjectOffline extension methods work correctly', () async {
    var dietPlan = ParseObject('DietPlan')
      ..set('Name', 'Test')
      ..set('Fat', 50)
      ..objectId = 'test-id';
    
    await dietPlan.saveToLocalCache();
    var cached = await ParseObjectOffline.loadAllFromLocalCache('DietPlan');
    expect(cached.length, greaterThan(0));
  });
}
packages/flutter/README.md (1)

65-256: Excellent documentation of new features!

The documentation is comprehensive and well-structured, covering:

  • Live query widget variants with clear examples
  • Offline mode configuration and usage patterns
  • Complete API reference for offline caching methods

One minor suggestion: Consider adding a troubleshooting section for common offline mode issues (e.g., cache not persisting, sync conflicts, storage limits).

packages/flutter/test_offline_mode.dart (1)

1-104: Consider converting to automated integration test.

While this script provides good coverage of offline functionality, it should be converted to a proper test using the test or flutter_test package. This would:

  • Enable CI/CD integration
  • Use assertions instead of print statements
  • Automatically fail on errors
  • Integrate with test coverage reporting

Example structure:

import 'package:test/test.dart';
import 'package:parse_server_sdk/parse_server_sdk.dart';

void main() {
  group('Offline Mode Tests', () {
    setUp(() async {
      await Parse().initialize(/*...*/);
    });

    test('Save and load single object', () async {
      final obj = ParseObject('TestClass')..set('name', 'Test');
      obj.objectId = 'test-id-1';
      await obj.saveToLocalCache();
      
      final loaded = await ParseObjectOffline.loadFromLocalCache(
        'TestClass', 'test-id-1');
      expect(loaded, isNotNull);
      expect(loaded!.get<String>('name'), equals('Test'));
    });
    // ... more tests
  });
}
packages/dart/lib/src/objects/parse_offline_object.dart (3)

35-79: Good implementation with efficient batch processing.

This method demonstrates better practices than others:

  • Uses a Map for efficient lookups and updates
  • Includes error handling for individual object decode failures
  • Provides useful logging of operation statistics

Consider wrapping the entire method in a try-catch to handle CoreStore failures:

 static Future<void> saveAllToLocalCache(String className, List<ParseObject> objectsToSave) async {
   if (objectsToSave.isEmpty) return;
+  try {
     final CoreStore coreStore = ParseCoreData().getStore();
     // ... existing code ...
     await coreStore.setStringList(cacheKey, updatedCachedStrings);
     print('Batch saved to local cache for $className. Added: $added, Updated: $updated, Total: ${updatedCachedStrings.length}');
+  } catch (e) {
+    print('Error in batch save to local cache for $className: $e');
+    rethrow;
+  }
 }

129-150: Consider optimizing cache lookup operations.

Both existsInLocalCache and getAllObjectIdsInLocalCache iterate through the entire cache, which could be slow for large datasets. Consider:

  1. Maintaining a separate index of objectIds for faster lookups
  2. Using a different CoreStore key pattern (one key per object) for O(1) lookups
  3. Caching the object ID list in memory

Example alternative pattern:

// Store each object with its own key
final objectKey = 'offline_cache_${className}_$objectId';
await coreStore.setString(objectKey, json.encode(toJson(full: true)));

// Maintain an index
final indexKey = 'offline_cache_${className}_index';
final objectIds = await coreStore.getStringList(indexKey) ?? [];
if (!objectIds.contains(objectId)) {
  objectIds.add(objectId);
  await coreStore.setStringList(indexKey, objectIds);
}

1-165: Consider adding concurrency control for cache operations.

Multiple concurrent cache operations (save, update, remove) could lead to race conditions and data corruption. Consider:

  1. Using a lock/mutex for cache operations
  2. Implementing a queue for cache operations
  3. Documenting thread-safety guarantees

This is particularly important for the offline mode use case where multiple widgets might save/update cached data simultaneously.

packages/flutter/example/lib/live_list/main.dart (1)

2-4: Remove commented-out import.

Line 4 contains a commented-out import that appears to be a debug artifact. This should be removed to keep the example clean.

 import 'package:parse_server_sdk/parse_server_sdk.dart';
 import 'package:parse_server_sdk_flutter/parse_server_sdk_flutter.dart' hide Parse;
-// import 'package:parse_server_sdk_flutter/parse_server_sdk_flutter.dart';
packages/flutter/lib/src/analytics/parse_analytics_endpoints.dart (2)

8-71: Reduce duplication in audience handler.

The switch statement has repetitive structure where each case returns the same map pattern with 'total' and 'content' keys containing identical values. Consider refactoring to reduce duplication.

 static Future<Map<String, dynamic>> handleAudienceRequest(String audienceType) async {
   try {
     final userAnalytics = await ParseAnalytics.getUserAnalytics();
     final installationAnalytics = await ParseAnalytics.getInstallationAnalytics();
-      
-    switch (audienceType) {
-      case 'total_users':
-        return {
-          'total': userAnalytics['total_users'],
-          'content': userAnalytics['total_users']
-        };
-        
-      case 'daily_users':
-        return {
-          'total': userAnalytics['daily_users'],
-          'content': userAnalytics['daily_users']
-        };
-        
-      // ... other cases ...
-        
-      default:
-        return {'total': 0, 'content': 0};
-    }
+    
+    final analyticsMap = {
+      'total_users': userAnalytics['total_users'],
+      'daily_users': userAnalytics['daily_users'],
+      'weekly_users': userAnalytics['weekly_users'],
+      'monthly_users': userAnalytics['monthly_users'],
+      'total_installations': installationAnalytics['total_installations'],
+      'daily_installations': installationAnalytics['daily_installations'],
+      'weekly_installations': installationAnalytics['weekly_installations'],
+      'monthly_installations': installationAnalytics['monthly_installations'],
+    };
+    
+    final value = analyticsMap[audienceType] ?? 0;
+    return {'total': value, 'content': value};
   } catch (e) {
     // ...
   }
 }

121-169: Mock implementations may cause confusion in production.

These billing and slow query handlers return hardcoded mock data. While comments indicate they should be replaced, there's no runtime indication that these are mocks. Consider:

  1. Throwing UnimplementedError to force implementation before use
  2. Adding a @visibleForTesting annotation if these are only for testing
  3. Logging a warning when these mocks are called in non-debug mode
static Map<String, dynamic> handleBillingStorageRequest() {
  assert(() {
    debugPrint('WARNING: Using mock billing storage data');
    return true;
  }());
  // ... existing mock implementation
}
packages/flutter/lib/src/utils/parse_live_sliver_list.dart (1)

329-347: Inconsistent lazy loading check across widgets.

The check for whether an item needs fetching before caching differs between widgets:

  • Sliver List (here): !item.containsKey(sdk.keyVarUpdatedAt)
  • Sliver Grid: item.get<DateTime>(sdk.keyVarCreatedAt) == null && item.objectId != null

These should use the same logic for consistency. Consider extracting this check into a shared helper method in the mixin or a utility class.

packages/flutter/lib/src/utils/parse_live_page_view.dart (1)

509-535: Redundant preload calls.

_preloadAdjacentPages is called both in onPageChanged (line 512) and in itemBuilder (line 534). Since itemBuilder is called for every visible item, this results in redundant preload calls. Consider removing one of these calls.

 itemBuilder: (context, index) {
   // Show loading indicator for the last item...
   if (widget.pagination && index >= _items.length) {
     return widget.loadingIndicator ?? ...;
   }

-  // Preload adjacent pages for smoother experience (only if online)
-  if (!isOffline) {
-     _preloadAdjacentPages(index);
-  }

   final item = _items[index];
packages/flutter/lib/src/utils/parse_live_grid.dart (2)

19-39: Several constructor parameters are declared but unused in state logic.

The comments indicate removedItemBuilder, animationController, lazyBatchSize, and lazyTriggerOffset are not used. This adds API surface without functionality.

Consider either:

  1. Implementing the functionality these parameters are meant to support
  2. Removing them until needed (YAGNI principle)
  3. Marking them as @Deprecated with a message about future implementation

677-702: Consider capturing _liveGrid reference to avoid potential null issues.

The method checks _liveGrid == null at line 678, but uses _liveGrid! at lines 687 and 689. While unlikely in practice, capturing the reference avoids repeated null assertions.

  void _triggerBatchLoading(int targetIndex) {
-   if (isOffline || !widget.lazyLoading || _liveGrid == null) return;
+   final liveGrid = _liveGrid;
+   if (isOffline || !widget.lazyLoading || liveGrid == null) return;

    final batchSize = widget.lazyBatchSize > 0 ? widget.lazyBatchSize : widget.crossAxisCount * 2;
    final startIdx = max(0, targetIndex - batchSize);
    final endIdx = min(_items.length - 1, targetIndex + batchSize);

    for (int i = startIdx; i <= endIdx; i++) {
-     if (i >= 0 && i < _liveGrid!.size && !_loadingIndices.contains(i) && _liveGrid!.getLoadedAt(i) == null) {
+     if (i >= 0 && i < liveGrid.size && !_loadingIndices.contains(i) && liveGrid.getLoadedAt(i) == null) {
        _loadingIndices.add(i);
-       _liveGrid!.getAt(i).first.then((loadedItem) {
+       liveGrid.getAt(i).first.then((loadedItem) {
packages/flutter/lib/src/utils/parse_live_list.dart (1)

71-71: Unused parameters declared in widget constructors.

removedItemBuilder (line 71) and error (line 676) are declared but noted as unused in state logic. Consider implementing or removing these to reduce API surface confusion.

Also applies to: 676-676

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9093dbd and 62e80b7.

📒 Files selected for processing (30)
  • PR_CHECKLIST.md (1 hunks)
  • packages/dart/lib/parse_server_sdk.dart (1 hunks)
  • packages/dart/lib/src/objects/parse_offline_object.dart (1 hunks)
  • packages/dart/lib/src/storage/core_store_memory.dart (1 hunks)
  • packages/dart/lib/src/storage/core_store_sem_impl.dart (1 hunks)
  • packages/dart/pubspec.yaml (2 hunks)
  • packages/dart/test_extension.dart (1 hunks)
  • packages/flutter/ANALYTICS_INTEGRATION_GUIDE.md (1 hunks)
  • packages/flutter/README.md (2 hunks)
  • packages/flutter/example/lib/live_list/main.dart (2 hunks)
  • packages/flutter/example/lib/pages/decision_page.dart (1 hunks)
  • packages/flutter/example/pubspec.yaml (1 hunks)
  • packages/flutter/example/test/data/repository/repository_mock_utils.dart (1 hunks)
  • packages/flutter/lib/parse_server_sdk_flutter.dart (4 hunks)
  • packages/flutter/lib/src/analytics/README.md (1 hunks)
  • packages/flutter/lib/src/analytics/USAGE.md (1 hunks)
  • packages/flutter/lib/src/analytics/example_server.js (1 hunks)
  • packages/flutter/lib/src/analytics/parse_analytics.dart (1 hunks)
  • packages/flutter/lib/src/analytics/parse_analytics_endpoints.dart (1 hunks)
  • packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_cached_live_list.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_live_grid.dart (4 hunks)
  • packages/flutter/lib/src/utils/parse_live_list.dart (5 hunks)
  • packages/flutter/lib/src/utils/parse_live_page_view.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_live_sliver_grid.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_live_sliver_list.dart (1 hunks)
  • packages/flutter/pubspec.yaml (3 hunks)
  • packages/flutter/test/parse_connectivity_implementation_test.dart (3 hunks)
  • packages/flutter/test_offline.dart (1 hunks)
  • packages/flutter/test_offline_mode.dart (1 hunks)
🧰 Additional context used
🪛 Biome (2.1.2)
packages/flutter/lib/src/analytics/example_server.js

[error] 37-37: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 42-44: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 49-51: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 56-58: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 63-64: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 69-71: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 76-78: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 83-85: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Agent
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Test Flutter 3.38, Ubuntu
  • GitHub Check: Test Flutter 3.38, Windows

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 6, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 6, 2025
@codecov
Copy link

codecov bot commented Dec 6, 2025

Codecov Report

❌ Patch coverage is 24.71022% with 1429 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.10%. Comparing base (9093dbd) to head (57342bd).

Files with missing lines Patch % Lines
...ackages/flutter/lib/src/utils/parse_live_grid.dart 6.52% 315 Missing ⚠️
...ackages/flutter/lib/src/utils/parse_live_list.dart 11.04% 282 Missing ⚠️
...es/flutter/lib/src/utils/parse_live_page_view.dart 14.67% 250 Missing ⚠️
.../flutter/lib/src/utils/parse_live_sliver_grid.dart 7.69% 240 Missing ⚠️
.../flutter/lib/src/utils/parse_live_sliver_list.dart 8.13% 237 Missing ⚠️
.../flutter/lib/src/utils/parse_cached_live_list.dart 0.00% 44 Missing ⚠️
...ges/flutter/lib/src/analytics/parse_analytics.dart 79.16% 35 Missing ⚠️
...ter/lib/src/mixins/connectivity_handler_mixin.dart 78.84% 11 Missing ⚠️
...ges/dart/lib/src/objects/parse_offline_object.dart 94.17% 6 Missing ⚠️
...r/lib/src/analytics/parse_analytics_endpoints.dart 90.00% 6 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1101      +/-   ##
==========================================
- Coverage   44.26%   39.10%   -5.17%     
==========================================
  Files          61       69       +8     
  Lines        3637     5409    +1772     
==========================================
+ Hits         1610     2115     +505     
- Misses       2027     3294    +1267     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@pastordee pastordee changed the title Add offline mode support for ParseLive and new live query widgets feat: add offline mode support for ParseLive and new live query widgets Dec 6, 2025
@parse-github-assistant
Copy link

I will reformat the title to use the proper commit message syntax.

@parse-github-assistant parse-github-assistant bot changed the title feat: add offline mode support for ParseLive and new live query widgets feat: Add offline mode support for ParseLive and new live query widgets Dec 6, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

♻️ Duplicate comments (3)
packages/flutter/lib/src/utils/parse_live_sliver_list.dart (1)

374-393: Unreliable lazy loading check using containsKey(keyVarUpdatedAt).

This issue was flagged in a previous review. Checking !item.containsKey(sdk.keyVarUpdatedAt) may not accurately determine if an item needs fetching, as an item could have an updatedAt field but still be incomplete when lazy loading is enabled.

packages/flutter/lib/src/utils/parse_live_sliver_grid.dart (1)

467-486: Inconsistent lazy loading check using createdAt instead of updatedAt.

This was flagged in a previous review. The sliver list widget checks keyVarUpdatedAt while this grid widget checks keyVarCreatedAt. Both approaches are unreliable, but they should at least be consistent across widgets.

packages/flutter/lib/src/analytics/parse_analytics.dart (1)

1-10: Analytics implementation appears unrelated to PR objectives.

This PR is titled "Add offline mode support for ParseLive and new live query widgets" but this analytics implementation doesn't appear related to offline mode or live query widgets. As noted in a previous review, consider moving this to a separate PR for focused review.

🧹 Nitpick comments (9)
packages/dart/lib/src/storage/core_store_memory.dart (1)

50-53: Remove or document the commented legacy code.

The commented-out implementation serves no purpose if it's been replaced. Consider removing it to reduce clutter, or add a comment explaining why it's preserved.

Apply this diff to remove the commented code:

-  // @override
-  // Future<List<String>?> getStringList(String key) async {
-  //   return _data[key];
-  // }
-
packages/dart/lib/src/storage/core_store_sem_impl.dart (1)

106-110: Remove or document the commented legacy code.

Similar to the memory store implementation, this commented-out code should be removed to maintain consistency and reduce clutter.

Apply this diff to remove the commented code:

-  // @override
-  //   Future<List<String>?> getStringList(String key) async {
-  //     final List<String>? storedItem = await get(key);
-  //     return storedItem;
-  //   }
-
packages/flutter/lib/src/utils/parse_live_list.dart (1)

71-72: Consider removing or implementing removedItemBuilder.

The comment notes that removedItemBuilder is not currently used in the state logic. If this is intentional dead code, consider removing it to avoid confusion. Otherwise, implement the removal animation logic.

packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart (1)

43-49: Connectivity priority differs from main library mapping.

The mixin prioritizes mobile > wifi > none > other, while _mapConnectivity in the main library uses wifi > mobile > none. This inconsistency could cause different behavior depending on which code path handles connectivity.

Consider aligning the priority order for consistency:

       final newResult = results.contains(ConnectivityResult.wifi)
           ? ConnectivityResult.wifi
           : results.contains(ConnectivityResult.mobile)
           ? ConnectivityResult.mobile
-          : results.contains(ConnectivityResult.none)
-          ? ConnectivityResult.none
-          : ConnectivityResult.other;
+          : ConnectivityResult.none;

Also applies to: 64-71

packages/flutter/test/src/utils/parse_live_widgets_test.dart (2)

128-159: Dispose controllers in tests to prevent resource leaks.

The ScrollController created at line 133 is not disposed after the test. While Flutter test framework may handle this, it's best practice to dispose controllers explicitly, especially for complex widgets.

     testWidgets('should accept optional parameters', (
       WidgetTester tester,
     ) async {
       // Arrange
       final query = QueryBuilder<TestObject>(TestObject());
       final scrollController = ScrollController();
+      addTearDown(() => scrollController.dispose());

       // Act
       await tester.pumpWidget(

The same applies to PageController at line 355.


81-160: Consider adding tests for offline mode behavior.

The test suite covers basic widget creation but doesn't test the core offline mode functionality introduced by this PR. Consider adding tests for:

  • Behavior when offlineMode: true and connectivity changes to offline
  • Cache loading when offline
  • Data synchronization when connectivity is restored
packages/flutter/lib/src/utils/parse_live_page_view.dart (1)

436-465: Inconsistent lazy loading behavior compared to other widgets.

Unlike ParseLiveSliverListWidget and ParseLiveSliverGridWidget, this implementation unconditionally fetches all items when lazy loading is enabled (line 441-456), without first checking if the item already has complete data. This could result in unnecessary network calls.

Consider adding a check similar to the other widgets to skip fetching items that already have complete data, ensuring consistent behavior across all live widgets.

packages/flutter/lib/src/utils/parse_live_grid.dart (2)

19-19: Remove unused widget parameters or implement their functionality.

Several widget parameters are declared but never used in the state implementation:

  • removedItemBuilder (line 19): Not used in any removal logic
  • animationController (line 25): Not used for item animations (see lines 723-725)
  • lazyTriggerOffset (line 37): Despite the comment, this parameter is never referenced in scroll or lazy loading logic

Note: lazyBatchSize IS used in _triggerBatchLoading (lines 785-787), so its comment is incorrect.

Apply this diff to remove the unused parameters:

-    this.removedItemBuilder, // Note: Not currently used in state logic
     this.listenOnAllSubItems,
     this.listeningIncludes,
     this.lazyLoading = true,
     this.preloadedColumns,
     this.excludedColumns,
-    this.animationController, // Note: Not currently used for item animations
     this.crossAxisCount = 3,

And remove the field declarations:

   final ChildBuilder<T>? childBuilder;
-  final ChildBuilder<T>? removedItemBuilder;
 
   final bool? listenOnAllSubItems;
   final List<String>? listeningIncludes;
 
   final bool lazyLoading;
   final List<String>? preloadedColumns;
   final List<String>? excludedColumns;
 
-  final AnimationController? animationController;

And remove or update the lazyTriggerOffset if it's not intended for future use:

   final int lazyBatchSize;
-  final double lazyTriggerOffset;

Also applies to: 25-25, 37-37


271-273: Update misleading comment.

The comment on line 272 says "Await is fine here as this whole function runs in the background" but the code doesn't actually await _saveBatchToCache. While not awaiting is the correct choice (to allow background processing), the comment is confusing.

Apply this diff to clarify:

         if (results.isNotEmpty) {
-          // Use the existing batch save helper (it handles lazy fetching if needed)
-          // Await is fine here as this whole function runs in the background
+          // Use the existing batch save helper (runs in background)
           await _saveBatchToCache(results);

Or if the intent was NOT to await:

         if (results.isNotEmpty) {
-          // Use the existing batch save helper (it handles lazy fetching if needed)
-          // Await is fine here as this whole function runs in the background
-          await _saveBatchToCache(results);
+          // Trigger batch save in background without blocking
+          unawaited(_saveBatchToCache(results));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da21fc8 and 09bcd4b.

📒 Files selected for processing (21)
  • packages/dart/lib/src/objects/parse_offline_object.dart (1 hunks)
  • packages/dart/lib/src/storage/core_store_memory.dart (1 hunks)
  • packages/dart/lib/src/storage/core_store_sem_impl.dart (1 hunks)
  • packages/dart/test/src/objects/parse_offline_object_test.dart (1 hunks)
  • packages/dart/test_extension.dart (1 hunks)
  • packages/flutter/example/lib/live_list/main.dart (1 hunks)
  • packages/flutter/lib/parse_server_sdk_flutter.dart (3 hunks)
  • packages/flutter/lib/src/analytics/parse_analytics.dart (1 hunks)
  • packages/flutter/lib/src/analytics/parse_analytics_endpoints.dart (1 hunks)
  • packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_cached_live_list.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_live_grid.dart (4 hunks)
  • packages/flutter/lib/src/utils/parse_live_list.dart (5 hunks)
  • packages/flutter/lib/src/utils/parse_live_page_view.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_live_sliver_grid.dart (1 hunks)
  • packages/flutter/lib/src/utils/parse_live_sliver_list.dart (1 hunks)
  • packages/flutter/pubspec.yaml (3 hunks)
  • packages/flutter/test/parse_connectivity_implementation_test.dart (0 hunks)
  • packages/flutter/test/src/analytics/parse_analytics_test.dart (1 hunks)
  • packages/flutter/test/src/mixins/connectivity_handler_mixin_test.dart (1 hunks)
  • packages/flutter/test/src/utils/parse_live_widgets_test.dart (1 hunks)
💤 Files with no reviewable changes (1)
  • packages/flutter/test/parse_connectivity_implementation_test.dart
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/flutter/example/lib/live_list/main.dart
  • packages/dart/test_extension.dart
  • packages/flutter/lib/src/utils/parse_cached_live_list.dart
  • packages/dart/lib/src/objects/parse_offline_object.dart
  • packages/flutter/pubspec.yaml
  • packages/flutter/lib/src/analytics/parse_analytics_endpoints.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Test Dart 3.10, Windows
🔇 Additional comments (35)
packages/dart/lib/src/storage/core_store_memory.dart (1)

42-48: LGTM — robust type handling for getStringList.

The implementation correctly handles multiple value shapes (null, List, Iterable) and provides safe fallback behavior. The toString() conversion for Iterable elements ensures compatibility with varying stored formats.

packages/dart/lib/src/storage/core_store_sem_impl.dart (1)

98-104: LGTM — consistent string list handling across storage implementations.

The implementation mirrors the memory store's robust type handling, ensuring consistent behavior across sembast and in-memory storage backends.

packages/dart/test/src/objects/parse_offline_object_test.dart (9)

1-22: LGTM — proper test setup and isolation.

The test suite correctly initializes Parse in setUpAll and ensures test isolation by clearing the cache before and after each test. Using a constant testClassName maintains consistency across tests.


24-63: LGTM — comprehensive save and update tests.

Tests correctly verify both initial save and update scenarios using appropriate verification methods (existence check and field value validation).


65-98: LGTM — thorough load operation coverage.

Tests validate both successful load with field verification and null return for non-existent objects. Good null-safety handling at Line 82.


100-183: LGTM — excellent batch operation coverage.

Tests comprehensively validate batch save scenarios including multiple new objects, mixed updates and additions, and proper handling of objects without objectId. The assertion at Line 155 correctly expects 3 objects after update operations.


185-213: LGTM — load all operations properly tested.

Tests validate both populated cache and empty cache scenarios. Using a different class name ('EmptyClass') at Line 207 ensures proper isolation.


215-233: LGTM — remove operation correctly verified.

Test validates removal by checking that existsInLocalCache returns false after the operation.


235-255: LGTM — field update operation validated.

Test correctly verifies that updateInLocalCache updates specific fields by loading the object and checking the modified values.


257-285: LGTM — existence checks cover both scenarios.

Tests validate both positive (existing) and negative (non-existing) cases for the existence check operation.


287-343: LGTM — clear and ID retrieval operations properly tested.

The clearLocalCacheForClass test verifies all objects are removed, while getAllObjectIdsInLocalCache tests cover both populated and empty cache scenarios. The use of containsAll at Line 331 is appropriate for set-like validation.

packages/flutter/test/src/mixins/connectivity_handler_mixin_test.dart (3)

10-35: Well-structured mock implementation.

The MockConnectivityPlatform properly implements the platform interface with a broadcast StreamController for simulating connectivity changes. The dispose() method correctly closes the stream controller.


47-90: Test widget correctly implements mixin contract.

The TestConnectivityWidgetState properly implements all abstract methods from ConnectivityHandlerMixin and exposes counters for verification. The lifecycle methods (initState/dispose) correctly call initConnectivityHandler() and disposeConnectivityHandler().


120-289: Comprehensive test coverage for connectivity transitions.

The test suite covers key scenarios: online/offline initialization, bidirectional transitions, mobile connectivity, disabled offline mode, and proper subscription disposal. The assertions use greaterThanOrEqualTo appropriately to account for potential multiple calls during initialization.

packages/flutter/lib/src/utils/parse_live_list.dart (2)

3-9: LGTM - API additions are well-designed.

The ChildBuilder typedef extension with an optional index parameter maintains backward compatibility. The LoadMoreStatus enum and FooterBuilder typedef provide clean abstractions for pagination state management.

Also applies to: 17-22


614-692: Build method handles online/offline states well.

The ValueListenableBuilder pattern efficiently rebuilds only when data availability changes. The loading indicator logic correctly shows loading only when online and _liveList is not yet initialized. The offline fallback to _items provides seamless UX.

packages/flutter/lib/parse_server_sdk_flutter.dart (2)

136-146: Ethernet connections will be treated as offline.

The _mapConnectivity method no longer handles ConnectivityResult.ethernet. Users on wired connections will get ParseConnectivityResult.none, triggering offline mode behavior. This may be intentional for mobile-focused apps but could cause unexpected behavior on desktop platforms.

If ethernet should be treated as online, add it to the mapping:

   sdk.ParseConnectivityResult _mapConnectivity(
     List<ConnectivityResult> results,
   ) {
     if (results.contains(ConnectivityResult.wifi)) {
       return sdk.ParseConnectivityResult.wifi;
+    } else if (results.contains(ConnectivityResult.ethernet)) {
+      return sdk.ParseConnectivityResult.wifi; // Treat ethernet as wifi/online
     } else if (results.contains(ConnectivityResult.mobile)) {
       return sdk.ParseConnectivityResult.mobile;
     } else {
       return sdk.ParseConnectivityResult.none;
     }
   }

25-27: New exports and part files properly declared.

The analytics exports and new part file declarations for sliver widgets and page view are correctly structured.

Also applies to: 37-40

packages/flutter/test/src/analytics/parse_analytics_test.dart (2)

22-110: Good test coverage for ParseAnalytics API.

Tests cover initialization (including idempotency), event tracking with various parameter types, stream availability, stored events retrieval, and clearing. The arrange/act/assert pattern is consistently applied.


123-219: Comprehensive AnalyticsEventData model tests.

The tests verify construction with required and optional fields, JSON serialization/deserialization roundtrip, and null parameter handling. Timestamp handling with millisecondsSinceEpoch is correctly tested.

packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart (2)

109-113: ConnectivityResult.other and ethernet are treated as offline.

The isOnline check only considers mobile and wifi as online states. This means VPN, Bluetooth tethering, or ethernet connections (represented as other or ethernet) will trigger offline mode, which may not be the intended behavior.

If these connection types should be treated as online:

     bool isOnline =
         result == ConnectivityResult.mobile ||
-        result == ConnectivityResult.wifi;
+        result == ConnectivityResult.wifi ||
+        result == ConnectivityResult.ethernet ||
+        result == ConnectivityResult.vpn ||
+        result == ConnectivityResult.other;

89-164: State transition logic is well-structured.

The _internalUpdateConnectionStatus method correctly handles all transition scenarios: online→offline, offline→online, initial state, and no-transition cases. The guard clause for unchanged status prevents unnecessary reloads. Debug logging aids troubleshooting.

packages/flutter/lib/src/utils/parse_live_grid.dart (13)

88-106: LGTM!

The addition of the optional index parameter to defaultChildBuilder enhances debugging and UI clarity. The implementation correctly handles the optional parameter.


123-141: LGTM!

The mixin implementation correctly delegates to private methods and provides appropriate configuration values.


143-158: LGTM!

The initialization correctly handles optional scroll controller creation and conditionally attaches scroll listeners based on feature enablement.


160-200: LGTM!

The cache loading logic properly handles offline mode checks, deserialization errors, and state updates. The error handling around fromJson deserialization is particularly good.


202-226: LGTM!

The scroll handler correctly implements both pagination and lazy loading with appropriate online/offline checks and distance-based triggers.


292-376: LGTM!

The pagination logic correctly handles loading more data, prioritizes UI responsiveness by updating state before triggering background cache operations, and appropriately manages loading states.


565-634: LGTM!

The batch caching logic appropriately handles lazy loading by pre-fetching incomplete items, uses concurrent fetching with Future.wait, and includes proper error handling for individual fetch failures.


636-652: LGTM!

The refresh logic correctly disposes existing resources and reloads data based on connectivity state.


654-689: LGTM!

The build method correctly manages loading, empty, and data states while considering offline mode. The conditional footer rendering is appropriate for paginated views.


692-718: LGTM!

The default footer implementation handles all load states appropriately and provides good UX with tap-to-retry on errors.


721-778: LGTM!

The grid builder correctly handles both online (with stream-based updates) and offline (with direct item access) modes, and generates appropriate unique keys for items.


781-825: LGTM!

The batch loading logic for lazy loading correctly manages loading state tracking, bounds checking, and asynchronous item loading with appropriate error handling.


827-844: LGTM!

The dispose method properly cleans up all resources including mixin-managed connectivity handlers, conditionally-owned scroll controllers, and internal state.

Comment on lines +32 to +45
final activeUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final activeUsersResult = await activeUsersQuery.count();
final activeUsers = activeUsersResult.count;

final dailyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', yesterday);
final dailyUsersResult = await dailyUsersQuery.count();
final dailyUsers = dailyUsersResult.count;

final weeklyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final weeklyUsersResult = await weeklyUsersQuery.count();
final weeklyUsers = weeklyUsersResult.count;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate queries for activeUsers and weeklyUsers.

Both activeUsersQuery (line 32-35) and weeklyUsersQuery (line 42-45) use the same filter (whereGreaterThan('updatedAt', weekAgo)), resulting in identical results and an unnecessary extra network call.

       final activeUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
         ..whereGreaterThan('updatedAt', weekAgo);
       final activeUsersResult = await activeUsersQuery.count();
       final activeUsers = activeUsersResult.count;

       final dailyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
         ..whereGreaterThan('updatedAt', yesterday);
       final dailyUsersResult = await dailyUsersQuery.count();
       final dailyUsers = dailyUsersResult.count;

-      final weeklyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
-        ..whereGreaterThan('updatedAt', weekAgo);
-      final weeklyUsersResult = await weeklyUsersQuery.count();
-      final weeklyUsers = weeklyUsersResult.count;
+      // weeklyUsers is the same as activeUsers (both use weekAgo)
+      final weeklyUsers = activeUsers;

The same issue exists for activeInstallations and weeklyInstallations (lines 90-106).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final activeUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final activeUsersResult = await activeUsersQuery.count();
final activeUsers = activeUsersResult.count;
final dailyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', yesterday);
final dailyUsersResult = await dailyUsersQuery.count();
final dailyUsers = dailyUsersResult.count;
final weeklyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final weeklyUsersResult = await weeklyUsersQuery.count();
final weeklyUsers = weeklyUsersResult.count;
final activeUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', weekAgo);
final activeUsersResult = await activeUsersQuery.count();
final activeUsers = activeUsersResult.count;
final dailyUsersQuery = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', yesterday);
final dailyUsersResult = await dailyUsersQuery.count();
final dailyUsers = dailyUsersResult.count;
// weeklyUsers is the same as activeUsers (both use weekAgo)
final weeklyUsers = activeUsers;
🤖 Prompt for AI Agents
packages/flutter/lib/src/analytics/parse_analytics.dart lines 32-45 and 90-106:
there are duplicate queries (activeUsers vs weeklyUsers both use
whereGreaterThan('updatedAt', weekAgo) and activeInstallations vs
weeklyInstallations likewise) causing redundant network calls; fix by removing
the duplicate query calls and reusing the already-counted result (e.g., compute
weeklyUsers once and assign activeUsers = weeklyUsers or vice versa) or, if the
intent was different time windows, correct the filter for the intended
timeframe; apply the same consolidation/correction for installations so only one
network call is made per unique filter.

Comment on lines +174 to +221
static Future<List<List<num>>> getTimeSeriesData({
required String metric,
required DateTime startDate,
required DateTime endDate,
String interval = 'day',
}) async {
try {
final data = <List<num>>[];
final intervalDuration = interval == 'hour'
? const Duration(hours: 1)
: const Duration(days: 1);

DateTime current = startDate;
while (current.isBefore(endDate)) {
final next = current.add(intervalDuration);
int value = 0;

switch (metric) {
case 'users':
final query = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;

case 'installations':
final query =
QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;
}

data.add([current.millisecondsSinceEpoch, value]);
current = next;
}

return data;
} catch (e) {
if (kDebugMode) {
print('Error getting time series data: $e');
}
return [];
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential performance issue: unbounded query loop in getTimeSeriesData.

The method iterates from startDate to endDate making a network query on each iteration. For hourly intervals over a month, this would make ~720 queries. Consider:

  1. Adding a maximum iteration limit
  2. Using server-side aggregation if available
  3. Batching queries or using caching
   static Future<List<List<num>>> getTimeSeriesData({
     required String metric,
     required DateTime startDate,
     required DateTime endDate,
     String interval = 'day',
   }) async {
     try {
       final data = <List<num>>[];
       final intervalDuration = interval == 'hour'
           ? const Duration(hours: 1)
           : const Duration(days: 1);

+      // Limit iterations to prevent excessive queries
+      const maxIterations = 100;
+      var iterations = 0;
+
       DateTime current = startDate;
-      while (current.isBefore(endDate)) {
+      while (current.isBefore(endDate) && iterations < maxIterations) {
+        iterations++;
         final next = current.add(intervalDuration);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static Future<List<List<num>>> getTimeSeriesData({
required String metric,
required DateTime startDate,
required DateTime endDate,
String interval = 'day',
}) async {
try {
final data = <List<num>>[];
final intervalDuration = interval == 'hour'
? const Duration(hours: 1)
: const Duration(days: 1);
DateTime current = startDate;
while (current.isBefore(endDate)) {
final next = current.add(intervalDuration);
int value = 0;
switch (metric) {
case 'users':
final query = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;
case 'installations':
final query =
QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;
}
data.add([current.millisecondsSinceEpoch, value]);
current = next;
}
return data;
} catch (e) {
if (kDebugMode) {
print('Error getting time series data: $e');
}
return [];
}
}
static Future<List<List<num>>> getTimeSeriesData({
required String metric,
required DateTime startDate,
required DateTime endDate,
String interval = 'day',
}) async {
try {
final data = <List<num>>[];
final intervalDuration = interval == 'hour'
? const Duration(hours: 1)
: const Duration(days: 1);
// Limit iterations to prevent excessive queries
const maxIterations = 100;
var iterations = 0;
DateTime current = startDate;
while (current.isBefore(endDate) && iterations < maxIterations) {
iterations++;
final next = current.add(intervalDuration);
int value = 0;
switch (metric) {
case 'users':
final query = QueryBuilder<ParseUser>(ParseUser.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;
case 'installations':
final query =
QueryBuilder<ParseInstallation>(ParseInstallation.forQuery())
..whereGreaterThan('updatedAt', current)
..whereLessThan('updatedAt', next);
final result = await query.count();
value = result.count;
break;
}
data.add([current.millisecondsSinceEpoch, value]);
current = next;
}
return data;
} catch (e) {
if (kDebugMode) {
print('Error getting time series data: $e');
}
return [];
}
}
🤖 Prompt for AI Agents
In packages/flutter/lib/src/analytics/parse_analytics.dart around lines 174 to
221, the current while-loop issues an unbounded network request per interval
which can produce hundreds of queries; add a safety cap on iterations (e.g.
compute maxBuckets = min( (endDate - startDate) / intervalDuration, a configured
MAX_BUCKETS like 1000) ) and break/return early if exceeded, and prefer a more
efficient approach: replace per-interval queries with either a single
server-side aggregated query (grouped by interval) if Parse supports
aggregation, or batch the time range into larger chunks (e.g. fetch counts for
N-day windows) and post-aggregate on the client; also consider caching recent
results and surface a clear error or partial-result indicator when the cap is
hit so callers can handle incomplete data.

mixin ConnectivityHandlerMixin<T extends StatefulWidget> on State<T> {
// State variables managed by the mixin
bool _isOffline = false;
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential crash if disposeConnectivityHandler() is called before subscription is assigned.

_connectivitySubscription is declared late and assigned in initConnectivityHandler(). If disposeConnectivityHandler() is called before initialization completes (e.g., widget disposed during async init), calling cancel() will throw a LateInitializationError.

-  late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
+  StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;

   void disposeConnectivityHandler() {
-    _connectivitySubscription.cancel();
+    _connectivitySubscription?.cancel();
   }

And update the assignment in initConnectivityHandler():

_connectivitySubscription = _connectivity.onConnectivityChanged.listen(...);

Also applies to: 56-58

🤖 Prompt for AI Agents
packages/flutter/lib/src/mixins/connectivity_handler_mixin.dart lines 12 (and
similar at 56-58): _connectivitySubscription is declared late and may be
cancelled before it's assigned, causing a LateInitializationError; change its
type to a nullable StreamSubscription<List<ConnectivityResult>>? (remove
`late`), assign it in initConnectivityHandler() as shown (use
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(...)),
and guard cancellations in disposeConnectivityHandler() with a null check (call
cancel() only if the subscription is non-null). Apply the same nullable +
null-check fix to the other subscription declarations and cancels at lines
56-58.

Comment on lines +228 to +251
int _calculateVisibleMaxIndex(double offset) {
if (!mounted ||
!context.mounted ||
!context.findRenderObject()!.paintBounds.isFinite) {
return 0;
}
try {
final itemWidth =
(MediaQuery.of(context).size.width -
(widget.crossAxisCount - 1) * widget.crossAxisSpacing -
(widget.padding?.horizontal ?? 0)) /
widget.crossAxisCount;
final itemHeight =
itemWidth / widget.childAspectRatio + widget.mainAxisSpacing;
if (itemHeight <= 0) return 0; // Avoid division by zero
final itemsPerRow = widget.crossAxisCount;
final rowsVisible =
(offset + MediaQuery.of(context).size.height) / itemHeight;
return min((rowsVisible * itemsPerRow).ceil(), _items.length - 1);
} catch (e) {
debugPrint('$connectivityLogPrefix Error calculating visible index: $e');
return _items.isNotEmpty ? _items.length - 1 : 0;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential null pointer exception with force unwrap.

Line 231 uses context.findRenderObject()! with a force unwrap operator. While context.mounted is checked, this doesn't guarantee findRenderObject() returns non-null. If the widget hasn't been laid out yet, this could throw.

Apply this diff to safely handle the null case:

   int _calculateVisibleMaxIndex(double offset) {
     if (!mounted ||
         !context.mounted ||
-        !context.findRenderObject()!.paintBounds.isFinite) {
+        context.findRenderObject()?.paintBounds.isFinite != true) {
       return 0;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int _calculateVisibleMaxIndex(double offset) {
if (!mounted ||
!context.mounted ||
!context.findRenderObject()!.paintBounds.isFinite) {
return 0;
}
try {
final itemWidth =
(MediaQuery.of(context).size.width -
(widget.crossAxisCount - 1) * widget.crossAxisSpacing -
(widget.padding?.horizontal ?? 0)) /
widget.crossAxisCount;
final itemHeight =
itemWidth / widget.childAspectRatio + widget.mainAxisSpacing;
if (itemHeight <= 0) return 0; // Avoid division by zero
final itemsPerRow = widget.crossAxisCount;
final rowsVisible =
(offset + MediaQuery.of(context).size.height) / itemHeight;
return min((rowsVisible * itemsPerRow).ceil(), _items.length - 1);
} catch (e) {
debugPrint('$connectivityLogPrefix Error calculating visible index: $e');
return _items.isNotEmpty ? _items.length - 1 : 0;
}
}
int _calculateVisibleMaxIndex(double offset) {
if (!mounted ||
!context.mounted ||
context.findRenderObject()?.paintBounds.isFinite != true) {
return 0;
}
try {
final itemWidth =
(MediaQuery.of(context).size.width -
(widget.crossAxisCount - 1) * widget.crossAxisSpacing -
(widget.padding?.horizontal ?? 0)) /
widget.crossAxisCount;
final itemHeight =
itemWidth / widget.childAspectRatio + widget.mainAxisSpacing;
if (itemHeight <= 0) return 0; // Avoid division by zero
final itemsPerRow = widget.crossAxisCount;
final rowsVisible =
(offset + MediaQuery.of(context).size.height) / itemHeight;
return min((rowsVisible * itemsPerRow).ceil(), _items.length - 1);
} catch (e) {
debugPrint('$connectivityLogPrefix Error calculating visible index: $e');
return _items.isNotEmpty ? _items.length - 1 : 0;
}
}
🤖 Prompt for AI Agents
In packages/flutter/lib/src/utils/parse_live_grid.dart around lines 228 to 251,
the code force-unwraps context.findRenderObject() which can be null even when
context.mounted is true; change the guard to safely handle a null RenderObject
by replacing the force unwrap with a null-aware check (e.g., obtain final render
= context.findRenderObject(); and if render == null ||
!render.paintBounds.isFinite return 0), so that the function returns early on
null or non-finite paintBounds and avoids throwing a null-pointer exception.

Comment on lines +481 to +486
if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
final addedItem = event.object;
setState(() {
_items.insert(event.index, addedItem);
});
objectToCache = addedItem;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add bounds validation for list insertion.

Line 484 inserts at event.index without validating the index is within valid bounds [0, _items.length]. While the Parse SDK should provide valid indices, defensive programming suggests validating before insertion to prevent RangeError exceptions.

Apply this diff to add bounds validation:

           if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
             final addedItem = event.object;
-            setState(() {
-              _items.insert(event.index, addedItem);
-            });
+            if (event.index >= 0 && event.index <= _items.length) {
+              setState(() {
+                _items.insert(event.index, addedItem);
+              });
+            } else {
+              debugPrint(
+                '$connectivityLogPrefix LiveList Add Event: Invalid index ${event.index}, list size ${_items.length}',
+              );
+            }
             objectToCache = addedItem;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
final addedItem = event.object;
setState(() {
_items.insert(event.index, addedItem);
});
objectToCache = addedItem;
if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
final addedItem = event.object;
if (event.index >= 0 && event.index <= _items.length) {
setState(() {
_items.insert(event.index, addedItem);
});
} else {
debugPrint(
'$connectivityLogPrefix LiveList Add Event: Invalid index ${event.index}, list size ${_items.length}',
);
}
objectToCache = addedItem;
🤖 Prompt for AI Agents
In packages/flutter/lib/src/utils/parse_live_grid.dart around lines 481 to 486,
the code inserts an item at event.index without validating the index; add a
bounds check to ensure event.index is between 0 and _items.length inclusive
before calling _items.insert. If the index is out of range, either clamp it to a
valid value (e.g., 0 or _items.length), or skip the insertion and optionally
log/debug a warning; perform the validation inside setState so you never call
insert with an invalid index.

int _currentPage = 0;
bool _hasMoreData = true;

final Set<int> _loadingIndices = {}; // Used for lazy loading specific items
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused _loadingIndices field.

The _loadingIndices set is declared but never populated or read anywhere in the code. This appears to be dead code.

-  final Set<int> _loadingIndices = {}; // Used for lazy loading specific items
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final Set<int> _loadingIndices = {}; // Used for lazy loading specific items
🤖 Prompt for AI Agents
In packages/flutter/lib/src/utils/parse_live_sliver_grid.dart around line 126,
the private field "final Set<int> _loadingIndices = {};" is unused dead code;
remove this field declaration from the file and run the Dart analyzer/formatter
to ensure no references remain and to update import/unused warnings.

Comment on lines +391 to +447
liveGrid.stream.listen(
(event) {
if (!mounted) return;

T? objectToCache;

try {
if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
final addedItem = event.object;
setState(() {
_items.insert(event.index, addedItem);
});
objectToCache = addedItem;
} else if (event is sdk.ParseLiveListDeleteEvent<sdk.ParseObject>) {
if (event.index >= 0 && event.index < _items.length) {
final removedItem = _items.removeAt(event.index);
setState(() {});
if (widget.offlineMode) {
removedItem.removeFromLocalCache().catchError((e) {
debugPrint(
'$connectivityLogPrefix Error removing item ${removedItem.objectId} from cache: $e',
);
});
}
}
} else if (event is sdk.ParseLiveListUpdateEvent<sdk.ParseObject>) {
final updatedItem = event.object;
if (event.index >= 0 && event.index < _items.length) {
setState(() {
_items[event.index] = updatedItem;
});
objectToCache = updatedItem;
}
}

if (widget.offlineMode && objectToCache != null) {
objectToCache.saveToLocalCache().catchError((e) {
debugPrint(
'$connectivityLogPrefix Error saving stream update for ${objectToCache?.objectId} to cache: $e',
);
});
}

_noDataNotifier.value = _items.isEmpty;
} catch (e) {
debugPrint(
'$connectivityLogPrefix Error processing stream event: $e',
);
}
},
onError: (error) {
debugPrint('$connectivityLogPrefix LiveList Stream Error: $error');
if (mounted) {
setState(() {});
}
},
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Store and cancel the stream subscription to prevent memory leaks.

Same issue as in ParseLiveSliverListWidget - the stream listener subscription is not stored or cancelled, leading to potential memory leaks and duplicate event processing on refresh.

Apply the same fix pattern as suggested for the sliver list widget: store the subscription and cancel it in dispose() and before creating a new one.

🤖 Prompt for AI Agents
In packages/flutter/lib/src/utils/parse_live_sliver_grid.dart around lines 391
to 447, the StreamSubscription returned by liveGrid.stream.listen is not saved
or cancelled which causes memory leaks and duplicate event handling; store the
subscription in a field (e.g. _liveGridSubscription), assign it when calling
listen, cancel the existing subscription before creating a new one, and ensure
you cancel it in the widget's dispose() method (and null it after cancelling) so
the listener is properly cleaned up on refresh/unmount.

Comment on lines +282 to +351
liveList.stream.listen(
(event) {
if (!mounted) return; // Avoid processing if widget is disposed

T? objectToCache; // For single item cache updates from stream

try {
// Wrap event processing in try-catch
if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
final addedItem = event.object;
setState(() {
_items.insert(event.index, addedItem);
});
objectToCache = addedItem;
} else if (event is sdk.ParseLiveListDeleteEvent<sdk.ParseObject>) {
if (event.index >= 0 && event.index < _items.length) {
final removedItem = _items.removeAt(event.index);
setState(() {});
if (widget.offlineMode) {
// Remove deleted item from cache immediately
removedItem.removeFromLocalCache().catchError((e) {
debugPrint(
'$connectivityLogPrefix Error removing item ${removedItem.objectId} from cache: $e',
);
});
}
} else {
debugPrint(
'$connectivityLogPrefix LiveList Delete Event: Invalid index ${event.index}, list size ${_items.length}',
);
}
} else if (event is sdk.ParseLiveListUpdateEvent<sdk.ParseObject>) {
final updatedItem = event.object;
if (event.index >= 0 && event.index < _items.length) {
setState(() {
_items[event.index] = updatedItem;
});
objectToCache = updatedItem;
} else {
debugPrint(
'$connectivityLogPrefix LiveList Update Event: Invalid index ${event.index}, list size ${_items.length}',
);
}
}

// Save single updates from stream immediately if offline mode is on
if (widget.offlineMode && objectToCache != null) {
objectToCache.saveToLocalCache().catchError((e) {
debugPrint(
'$connectivityLogPrefix Error saving stream update for ${objectToCache?.objectId} to cache: $e',
);
});
}

_noDataNotifier.value = _items.isEmpty;
} catch (e) {
debugPrint(
'$connectivityLogPrefix Error processing stream event: $e',
);
}
},
onError: (error) {
debugPrint('$connectivityLogPrefix LiveList Stream Error: $error');
if (mounted) {
setState(() {
/* Potentially update state to show error */
});
}
},
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Store and cancel the stream subscription to prevent memory leaks.

The stream listener is created but the subscription is never stored or cancelled in dispose(). If _loadData() is called multiple times (e.g., on refresh), each call creates a new listener without canceling the previous one, leading to duplicate event processing and memory leaks.

 class ParseLiveSliverListWidgetState<T extends sdk.ParseObject>
     extends State<ParseLiveSliverListWidget<T>>
     with ConnectivityHandlerMixin<ParseLiveSliverListWidget<T>> {
   CachedParseLiveList<T>? _liveList;
   final ValueNotifier<bool> _noDataNotifier = ValueNotifier<bool>(true);
   final List<T> _items = <T>[];
+  StreamSubscription<sdk.ParseLiveListEvent<sdk.ParseObject>>? _streamSubscription;

   LoadMoreStatus _loadMoreStatus = LoadMoreStatus.idle;

Then store and cancel the subscription:

-      liveList.stream.listen(
+      _streamSubscription?.cancel();
+      _streamSubscription = liveList.stream.listen(
         (event) {

And in dispose():

   @override
   void dispose() {
     disposeConnectivityHandler();
+    _streamSubscription?.cancel();
     _liveList?.dispose();
     _noDataNotifier.dispose();
     super.dispose();
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/flutter/lib/src/utils/parse_live_sliver_list.dart around lines 282
to 351, the stream.listen subscription is created but never stored or cancelled,
causing duplicate listeners and memory leaks when _loadData() is called multiple
times; add a class-level StreamSubscription<T>? _liveListSub field, before
creating a new subscription cancel any existing _liveListSub?.cancel(), assign
the returned subscription to _liveListSub when calling
liveList.stream.listen(...), and override dispose() to await/ cancel
_liveListSub and set it to null so the listener is properly cleaned up.

Comment on lines +288 to +325
try {
// Wrap event processing in try-catch
if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
final addedItem = event.object;
setState(() {
_items.insert(event.index, addedItem);
});
objectToCache = addedItem;
} else if (event is sdk.ParseLiveListDeleteEvent<sdk.ParseObject>) {
if (event.index >= 0 && event.index < _items.length) {
final removedItem = _items.removeAt(event.index);
setState(() {});
if (widget.offlineMode) {
// Remove deleted item from cache immediately
removedItem.removeFromLocalCache().catchError((e) {
debugPrint(
'$connectivityLogPrefix Error removing item ${removedItem.objectId} from cache: $e',
);
});
}
} else {
debugPrint(
'$connectivityLogPrefix LiveList Delete Event: Invalid index ${event.index}, list size ${_items.length}',
);
}
} else if (event is sdk.ParseLiveListUpdateEvent<sdk.ParseObject>) {
final updatedItem = event.object;
if (event.index >= 0 && event.index < _items.length) {
setState(() {
_items[event.index] = updatedItem;
});
objectToCache = updatedItem;
} else {
debugPrint(
'$connectivityLogPrefix LiveList Update Event: Invalid index ${event.index}, list size ${_items.length}',
);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe type cast from ParseObject to T in stream event handling.

The stream event's object property is typed as sdk.ParseObject, but it's being inserted directly into _items which is List<T>. While Dart's type system may allow this at compile time due to covariance, this could cause issues if the runtime type doesn't match.

Consider explicit casting with type checking:

           if (event is sdk.ParseLiveListAddEvent<sdk.ParseObject>) {
-              final addedItem = event.object;
+              final addedItem = event.object as T;
               setState(() {
                 _items.insert(event.index, addedItem);
               });

This makes the cast explicit and will fail fast with a clear error if there's a type mismatch.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/flutter/lib/src/utils/parse_live_sliver_list.dart around lines 288
to 325, the code inserts sdk.ParseObject instances directly into _items
(List<T>) which is an unsafe runtime cast; update the event handling to perform
an explicit type check and cast before mutating _items and setting
objectToCache: for each branch that reads event.object, verify event.object is T
(using `is T`), then cast to T and use that value; if the check fails, log a
clear debug/error message and skip inserting/updating/caching to avoid runtime
type errors.

Comment on lines +112 to +120
group('dispose', () {
test('should dispose resources without throwing', () {
// Act & Assert - should not throw (dispose is synchronous void)
expect(() => ParseAnalytics.dispose(), returnsNormally);

// Re-initialize for other tests
ParseAnalytics.initialize();
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing await for ParseAnalytics.initialize() after dispose.

On line 118, ParseAnalytics.initialize() is called without await. Since initialize() is async, subsequent tests in the suite may start before initialization completes, potentially causing flaky test failures.

       test('should dispose resources without throwing', () {
         // Act & Assert - should not throw (dispose is synchronous void)
         expect(() => ParseAnalytics.dispose(), returnsNormally);

         // Re-initialize for other tests
-        ParseAnalytics.initialize();
+        // Note: This runs after tearDown, consider moving to a separate test
+        // or making this test async and awaiting
       });

Alternatively, if re-initialization is needed for cleanup, make the test async:

-      test('should dispose resources without throwing', () {
+      test('should dispose resources without throwing', () async {
         expect(() => ParseAnalytics.dispose(), returnsNormally);
-        ParseAnalytics.initialize();
+        await ParseAnalytics.initialize();
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
group('dispose', () {
test('should dispose resources without throwing', () {
// Act & Assert - should not throw (dispose is synchronous void)
expect(() => ParseAnalytics.dispose(), returnsNormally);
// Re-initialize for other tests
ParseAnalytics.initialize();
});
});
group('dispose', () {
test('should dispose resources without throwing', () async {
// Act & Assert - should not throw (dispose is synchronous void)
expect(() => ParseAnalytics.dispose(), returnsNormally);
// Re-initialize for other tests
await ParseAnalytics.initialize();
});
});
🤖 Prompt for AI Agents
In packages/flutter/test/src/analytics/parse_analytics_test.dart around lines
112 to 120, the test calls ParseAnalytics.initialize() after disposing without
awaiting it; make the test async and await the re-initialization to ensure
initialization completes before other tests run. Change the test signature to
async (e.g., test('...', () async { ... })) and call await
ParseAnalytics.initialize(); after the dispose assertion so re-initialization is
complete before the suite continues.

- Add CoreStore tests for getStringList method and basic operations
- Add ParseAnalyticsEndpoints tests for audience, analytics, retention handlers
- Add ParseLiveListElementSnapshot and LoadMoreStatus tests
- Improves test coverage for PR parse-community#1101 offline mode support
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
packages/dart/test/src/storage/core_store_test.dart (1)

12-109: getStringList tests are thorough; consider adding coverage for Iterable coercion

The CoreStore getStringList group nicely covers missing keys, empty lists, special characters, Unicode, overwrite behavior, long lists, and non-list values returning null. This matches the intended semantics and will catch most regressions.

Given the updated getStringList logic also handles generic Iterable values (per the PR description/summary), you may want to add one or two tests that explicitly exercise that coercion path (e.g., when underlying storage returns an Iterable instead of a List<String>), so future changes to storage backends don’t silently regress this behavior.

packages/flutter/test/src/utils/parse_cached_live_list_test.dart (1)

42-73: Simplify redundant enum tests.

The LoadMoreStatus tests are overly verbose with significant redundancy. Lines 51-65 check array positions individually, which duplicates the validation already done in lines 43-48.

Consider consolidating to a single, more maintainable test:

   group('LoadMoreStatus', () {
-    test('should have all expected enum values', () {
-      expect(LoadMoreStatus.values.length, 4);
-      expect(LoadMoreStatus.idle, isNotNull);
-      expect(LoadMoreStatus.loading, isNotNull);
-      expect(LoadMoreStatus.noMoreData, isNotNull);
-      expect(LoadMoreStatus.error, isNotNull);
-    });
-
-    test('idle should be first value', () {
-      expect(LoadMoreStatus.values[0], LoadMoreStatus.idle);
-    });
-
-    test('loading should be second value', () {
-      expect(LoadMoreStatus.values[1], LoadMoreStatus.loading);
-    });
-
-    test('noMoreData should be third value', () {
-      expect(LoadMoreStatus.values[2], LoadMoreStatus.noMoreData);
-    });
-
-    test('error should be fourth value', () {
-      expect(LoadMoreStatus.values[3], LoadMoreStatus.error);
-    });
-
-    test('enum values should have correct names', () {
+    test('should have all expected enum values with correct names', () {
+      expect(LoadMoreStatus.values.length, 4);
+      expect(LoadMoreStatus.values, [
+        LoadMoreStatus.idle,
+        LoadMoreStatus.loading,
+        LoadMoreStatus.noMoreData,
+        LoadMoreStatus.error,
+      ]);
       expect(LoadMoreStatus.idle.name, 'idle');
       expect(LoadMoreStatus.loading.name, 'loading');
       expect(LoadMoreStatus.noMoreData.name, 'noMoreData');
       expect(LoadMoreStatus.error.name, 'error');
     });
   });
packages/flutter/test/src/analytics/parse_analytics_endpoints_test.dart (4)

22-112: Audience endpoint tests are thorough; consider a small helper to reduce duplication

The individual tests for each audience type plus the unknown-type case give good coverage of the API surface. The repeated pattern for the successful cases (isA<Map<String, dynamic>>(), 'total' and 'content' keys) could be centralized into a private helper to keep this suite easier to extend.

For example, you could introduce a helper (outside the group):

Future<void> _expectAudienceResponse(String type) async {
  final result = await ParseAnalyticsEndpoints.handleAudienceRequest(type);
  expect(result, isA<Map<String, dynamic>>());
  expect(result.containsKey('total'), isTrue);
  expect(result.containsKey('content'), isTrue);
}

And then simplify one of the tests within this range:

-      test('should handle total_users request', () async {
-        final result = await ParseAnalyticsEndpoints.handleAudienceRequest(
-          'total_users',
-        );
-
-        expect(result, isA<Map<String, dynamic>>());
-        expect(result.containsKey('total'), isTrue);
-        expect(result.containsKey('content'), isTrue);
-      });
+      test('should handle total_users request', () async {
+        await _expectAudienceResponse('total_users');
+      });

The same pattern can be applied to the remaining audience-type tests.


114-162: Analytics endpoint tests cover main permutations; optional: add a failure/unknown-path case

The tests here nicely exercise multiple endpoints (audience, installations, custom) and both daily and hourly intervals, and assert the expected response envelope via requested_data. If the implementation distinguishes invalid endpoints or intervals, you might add one more test to lock in that behavior:

+      test('should handle unknown endpoint gracefully', () async {
+        final result = await ParseAnalyticsEndpoints.handleAnalyticsRequest(
+          endpoint: 'unknown',
+          startDate: DateTime.now().subtract(const Duration(days: 7)),
+          endDate: DateTime.now(),
+          interval: 'day',
+        );
+
+        expect(result, isA<Map<String, dynamic>>());
+        // Optionally assert on a specific field if the implementation exposes one.
+      });

This would give you a regression check if behavior for unsupported endpoints ever changes.


186-221: Billing tests are well-structured; you may optionally assert numeric types

The billing tests for storage, database, and data transfer do a good job of validating the response shape and the units values (GB / TB). If you want slightly stronger guarantees, you could also assert that total and limit are numeric (e.g., isA<num>()), to catch accidental stringification:

expect(result['total'], isA<num>());
expect(result['limit'], isA<num>());

That’s purely optional but would make these tests catch more regressions if the implementation changes.


223-258: Slow-queries tests cover parameters; consider asserting more on the filtered case

The slow-queries tests exercise the no-parameter, className-only, and “all parameters” variants, which is good functional coverage. For the final test, you can get a bit more value by asserting on the returned data, not just non-emptiness:

       test('should return slow queries list with all parameters', () {
         final result = ParseAnalyticsEndpoints.handleSlowQueriesRequest(
           className: 'TestClass',
           os: 'iOS',
           version: '1.0.0',
           from: DateTime.now().subtract(const Duration(days: 7)),
           to: DateTime.now(),
         );
 
         expect(result, isA<List<Map<String, dynamic>>>());
         expect(result.isNotEmpty, isTrue);
+        expect(result.first['className'], 'TestClass');
+        // Optionally: assert os/version if they’re reflected in the payload.
       });

That keeps the test aligned with the intention of those extra filters.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09bcd4b and 57342bd.

📒 Files selected for processing (3)
  • packages/dart/test/src/storage/core_store_test.dart (1 hunks)
  • packages/flutter/test/src/analytics/parse_analytics_endpoints_test.dart (1 hunks)
  • packages/flutter/test/src/utils/parse_cached_live_list_test.dart (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test Dart 3.10, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Test Flutter 3.38, Ubuntu
🔇 Additional comments (10)
packages/dart/test/src/storage/core_store_test.dart (3)

1-10: Test setup correctly isolates CoreStoreMemoryImp per test

Using a shared late CoreStoreMemoryImp with setUp that instantiates and clears before each test gives good isolation and keeps tests deterministic. No changes needed.


111-166: Good coverage of basic CoreStore primitives and key lifecycle

The basic operations group validates all the core primitives (string, int, double, bool), existence checks, removal, and clearing. This is a solid smoke test suite for CoreStoreMemoryImp and should help quickly flag regressions in the in-memory implementation.


168-198: Edge-case tests nicely complement the main suites

The null-return checks across all getters and the “special key names” loop are useful edge cases that often get missed. They should help ensure both API contracts and key-encoding remain stable across refactors and backend implementations. No changes needed.

packages/flutter/test/src/utils/parse_cached_live_list_test.dart (3)

6-18: LGTM! Test helper class follows Parse SDK patterns.

The TestCacheObject implementation correctly extends ParseObject and implements ParseCloneable with appropriate factory methods.


20-36: LGTM! Test setup is appropriate.

The test initialization correctly sets up Flutter test bindings, mock SharedPreferences, and Parse configuration for the test environment.


104-165: Add documentation to ParseLiveListElementSnapshot explaining the lazy loading pattern.

The hasData vs hasPreLoadedData distinction is intentional and part of the lazy loading architecture: preLoadedData contains the initial query result (with only preloadedColumns fetched), while loadedData contains the fully loaded object after lazy loading completes. However, this semantic difference is documented in ParseLiveList but not in ParseLiveListElementSnapshot itself, making it unclear for API users.

Consider adding a class-level documentation comment to ParseLiveListElementSnapshot explaining:

  • The purpose of each property in the context of lazy loading
  • When hasData vs hasPreLoadedData should be checked in UI code
  • A reference to ParseLiveList's lazyLoading and preloadedColumns parameters
packages/flutter/test/src/analytics/parse_analytics_endpoints_test.dart (4)

5-20: Parse test setup and initialization look correct

Using TestWidgetsFlutterBinding.ensureInitialized(), mock SharedPreferences, and a single setUpAll Parse().initialize(...) call is a clean pattern for these analytics tests and should avoid state leaks between tests.


164-184: Retention tests validate both default and cohort-specific flows

The retention tests are clear and lightweight, and asserting both the map type and the presence of day1, day7, and day30 for with/without cohortDate gives good signal without over-constraining implementation details.


261-270: Express middleware snippet assertions look appropriate

The getExpressMiddleware test checks that the generated string contains the middleware identifier, master-key header, and the audience content key, which are the important integration points; this is a good balance between robustness and flexibility in how the snippet is formatted.


272-290: Dart Shelf handler snippet tests are comprehensive

These tests assert that the generated handler string includes its own entrypoint name, expected analytics keys, Response.notFound, and the essential dart:convert / shelf imports. That gives strong guarantees the sample code doesn’t regress structurally while still allowing internal formatting changes.

Comment on lines +75 to +88
group('ChildBuilder typedef', () {
test('should accept widget builder function with optional index', () {
// Test that ChildBuilder<T> works with the expected signature
Widget testBuilder(
dynamic context,
ParseLiveListElementSnapshot<TestCacheObject> snapshot, [
int? index,
]) {
return const SizedBox();
}

expect(testBuilder, isA<Function>());
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix type safety: use BuildContext instead of dynamic.

Line 79 uses dynamic for the context parameter, which bypasses type checking. Since this is a widget builder, it should use BuildContext.

Apply this diff:

     test('should accept widget builder function with optional index', () {
       // Test that ChildBuilder<T> works with the expected signature
       Widget testBuilder(
-        dynamic context,
+        BuildContext context,
         ParseLiveListElementSnapshot<TestCacheObject> snapshot, [
         int? index,
       ]) {
         return const SizedBox();
       }

       expect(testBuilder, isA<Function>());
     });

Additionally, consider whether this test adds sufficient value. It only verifies that a function exists, not that it matches the typedef signature. A stronger test would verify the typedef can be assigned:

test('should match ChildBuilder typedef signature', () {
  ChildBuilder<TestCacheObject> builder = (context, snapshot, [index]) {
    return const SizedBox();
  };
  expect(builder, isNotNull);
});

Comment on lines +90 to +102
group('FooterBuilder typedef', () {
test('should accept widget builder function', () {
Widget testFooterBuilder(
dynamic context,
LoadMoreStatus status,
void Function()? onRetry,
) {
return const SizedBox();
}

expect(testFooterBuilder, isA<Function>());
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix type safety: use BuildContext instead of dynamic.

Line 93 uses dynamic for the context parameter, which bypasses type checking. This should be BuildContext for widget builders.

Apply this diff:

     test('should accept widget builder function', () {
       Widget testFooterBuilder(
-        dynamic context,
+        BuildContext context,
         LoadMoreStatus status,
         void Function()? onRetry,
       ) {
         return const SizedBox();
       }

       expect(testFooterBuilder, isA<Function>());
     });

As with the ChildBuilder test, consider strengthening this by directly assigning to the typedef:

test('should match FooterBuilder typedef signature', () {
  FooterBuilder builder = (context, status, onRetry) {
    return const SizedBox();
  };
  expect(builder, isNotNull);
});
🤖 Prompt for AI Agents
In packages/flutter/test/src/utils/parse_cached_live_list_test.dart around lines
90 to 102, the FooterBuilder test uses a parameter typed as dynamic for the
context which bypasses type safety; change the context parameter type from
dynamic to BuildContext, and optionally replace the current test with one that
assigns a closure to FooterBuilder (e.g., FooterBuilder builder = (context,
status, onRetry) => const SizedBox();) then assert the builder is not null to
ensure the signature matches the typedef.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant