diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 000000000..3d7e66cce --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-22 - [Refactoring Data Processing out of Build] +**Learning:** In Flutter, complex data processing (parsing Dates, loops) inside `build` or `FutureBuilder.builder` runs on every rebuild, which can be frequent. Moving this logic to a separate processing step that returns a dedicated data object significantly reduces CPU usage during UI updates. +**Action:** Always process API data into a display-ready View Model before passing it to the Widget tree, especially when using Futures. diff --git a/lib/models/weather_display_data.dart b/lib/models/weather_display_data.dart new file mode 100644 index 000000000..5cca4b367 --- /dev/null +++ b/lib/models/weather_display_data.dart @@ -0,0 +1,232 @@ +import 'dart:convert'; + +class WeatherDisplayData { + final Map raw; + + // Hourly Data (Filtered) + final List hourlyTime; + final List hourlyTemps; + final List hourlyWeatherCodes; + final List hourlyPrecpProb; + final List hourlyDewPoint; + final List hourlyVisibility; + final List hourlyUvIndex; + + // Daily Data + final List dailyDates; + final List dailyTempsMin; + final List dailyTempsMax; + final List dailyPrecProb; + final List dailyPrecipSum; + final List dailyDaylightDuration; + final List dailyWeatherCodes; + final List sunriseTimes; + final List sunsetTimes; + + // Processed Data + final Map daylightMap; + final bool shouldShowRainBlock; + final int? rainStart; + final int? bestStart; + final int? bestEnd; + final List timeNext12h; + final List precpNext12h; + final List precipProbNext12h; + + WeatherDisplayData({ + required this.raw, + required this.hourlyTime, + required this.hourlyTemps, + required this.hourlyWeatherCodes, + required this.hourlyPrecpProb, + required this.hourlyDewPoint, + required this.hourlyVisibility, + required this.hourlyUvIndex, + required this.dailyDates, + required this.dailyTempsMin, + required this.dailyTempsMax, + required this.dailyPrecProb, + required this.dailyPrecipSum, + required this.dailyDaylightDuration, + required this.dailyWeatherCodes, + required this.sunriseTimes, + required this.sunsetTimes, + required this.daylightMap, + required this.shouldShowRainBlock, + this.rainStart, + this.bestStart, + this.bestEnd, + required this.timeNext12h, + required this.precpNext12h, + required this.precipProbNext12h, + }); +} + +WeatherDisplayData processWeatherData(Map raw) { + final weatherData = raw['data'] ?? raw; + + final hourly = weatherData['hourly'] ?? {}; + final daily = weatherData['daily'] ?? {}; + + final List hourlyTimeNoFilter = hourly['time'] ?? []; + final List hourlyTempsNoFilter = hourly['temperature_2m'] ?? []; + final List hourlyWeatherCodesNoFilter = hourly['weather_code'] ?? []; + final List hourlyPrecpProbNoFilter = hourly['precipitation_probability'] ?? []; + final List hourlyDewPointNoFilter = hourly['dew_point_2m'] ?? []; + final List hourlyVisibilityNoFilter = hourly['visibility'] ?? []; + final List hourlyUvIndexNoFilter = hourly['uv_index'] ?? []; + + // Filter hourly data + final now = DateTime.now(); + final todayMidnight = DateTime(now.year, now.month, now.day); + + final filteredIndices = []; + for (int i = 0; i < hourlyTimeNoFilter.length; i++) { + final time = DateTime.parse(hourlyTimeNoFilter[i]); + if (time.isAfter(todayMidnight) || time.isAtSameMomentAs(todayMidnight)) { + filteredIndices.add(i); + } + } + + final hourlyTime = filteredIndices.map((i) => hourlyTimeNoFilter[i]).toList(); + final hourlyTemps = filteredIndices.map((i) => hourlyTempsNoFilter[i]).toList(); + final hourlyWeatherCodes = filteredIndices.map((i) => hourlyWeatherCodesNoFilter[i]).toList(); + final hourlyPrecpProb = filteredIndices.map((i) => hourlyPrecpProbNoFilter[i]).toList(); + + // Safe mapping for optional fields + final hourlyDewPoint = filteredIndices.map((i) => i < hourlyDewPointNoFilter.length ? hourlyDewPointNoFilter[i] : null).toList(); + final hourlyVisibility = filteredIndices.map((i) => i < hourlyVisibilityNoFilter.length ? hourlyVisibilityNoFilter[i] : null).toList(); + final hourlyUvIndex = filteredIndices.map((i) => i < hourlyUvIndexNoFilter.length ? hourlyUvIndexNoFilter[i] : null).toList(); + + // Daily Data + final List dailyDates = daily['time'] ?? []; + final List sunriseTimes = daily['sunrise'] ?? []; + final List sunsetTimes = daily['sunset'] ?? []; + final List dailyTempsMin = daily['temperature_2m_min'] ?? []; + final List dailyTempsMax = daily['temperature_2m_max'] ?? []; + final List dailyPrecProb = daily['precipitation_probability_max'] ?? []; + final List dailyPrecipSum = daily['precipitation_sum'] ?? []; + final List dailyDaylightDuration = daily['daylight_duration'] ?? []; + final List dailyWeatherCodes = daily['weather_code'] ?? []; + + // Daylight Map + final Map daylightMap = {}; + if (dailyDates.isNotEmpty && sunriseTimes.isNotEmpty && sunsetTimes.isNotEmpty) { + for (int i = 0; i < dailyDates.length; i++) { + if (i < sunriseTimes.length && i < sunsetTimes.length) { + daylightMap[dailyDates[i]] = ( + DateTime.parse(sunriseTimes[i]), + DateTime.parse(sunsetTimes[i]) + ); + } + } + } + + // Rain Logic + const double rainThreshold = 0.5; + const int probThreshold = 40; + + int offsetSeconds = 0; + if (weatherData['utc_offset_seconds'] != null) { + offsetSeconds = int.parse(weatherData['utc_offset_seconds'].toString()); + } + + DateTime utcNow = DateTime.now().toUtc(); + DateTime nowPrecip = utcNow.add(Duration(seconds: offsetSeconds)); + nowPrecip = DateTime( + nowPrecip.year, + nowPrecip.month, + nowPrecip.day, + nowPrecip.hour, + nowPrecip.minute, + nowPrecip.second, + nowPrecip.millisecond, + nowPrecip.microsecond, + ); + + final List allTimeStrings = (hourly['time'] as List?)?.cast() ?? []; + final List allPrecip = (hourly['precipitation'] as List?) + ?.map((e) => (e as num?)?.toDouble() ?? 0.0) + .toList() ?? + []; + final List allPrecipProb = (hourly['precipitation_probability'] as List?) + ?.map((e) => (e as num?)?.toInt() ?? 0) + .toList() ?? + []; + + final List timeNext12h = []; + final List precpNext12h = []; + final List precipProbNext12h = []; + + for (int i = 0; i < allTimeStrings.length; i++) { + if (i >= allPrecip.length || i >= allPrecipProb.length) break; + + final time = DateTime.parse(allTimeStrings[i]); + if (time.isAfter(nowPrecip) && + time.isBefore(nowPrecip.add(Duration(hours: 12)))) { + timeNext12h.add(allTimeStrings[i]); + precpNext12h.add(allPrecip[i]); + precipProbNext12h.add(allPrecipProb[i]); + } + } + + int? rainStart; + int longestRainLength = 0; + int? bestStart; + int? bestEnd; + + for (int i = 0; i < precpNext12h.length; i++) { + if (precpNext12h[i] >= rainThreshold && + precipProbNext12h[i] >= probThreshold) { + rainStart ??= i; + } else { + if (rainStart != null) { + final length = i - rainStart; + if (length >= 2 && length > longestRainLength) { + longestRainLength = length; + bestStart = rainStart; + bestEnd = i - 1; + } + rainStart = null; + } + } + } + + if (rainStart != null) { + final length = precpNext12h.length - rainStart; + if (length >= 2 && length > longestRainLength) { + bestStart = rainStart; + bestEnd = precpNext12h.length - 1; + } + } + + final bool shouldShowRainBlock = bestStart != null && bestEnd != null; + + return WeatherDisplayData( + raw: raw, + hourlyTime: hourlyTime, + hourlyTemps: hourlyTemps, + hourlyWeatherCodes: hourlyWeatherCodes, + hourlyPrecpProb: hourlyPrecpProb, + hourlyDewPoint: hourlyDewPoint, + hourlyVisibility: hourlyVisibility, + hourlyUvIndex: hourlyUvIndex, + dailyDates: dailyDates, + dailyTempsMin: dailyTempsMin, + dailyTempsMax: dailyTempsMax, + dailyPrecProb: dailyPrecProb, + dailyPrecipSum: dailyPrecipSum, + dailyDaylightDuration: dailyDaylightDuration, + dailyWeatherCodes: dailyWeatherCodes, + sunriseTimes: sunriseTimes, + sunsetTimes: sunsetTimes, + daylightMap: daylightMap, + shouldShowRainBlock: shouldShowRainBlock, + rainStart: rainStart, + bestStart: bestStart, + bestEnd: bestEnd, + timeNext12h: timeNext12h, + precpNext12h: precpNext12h, + precipProbNext12h: precipProbNext12h, + ); +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart index f9ef38a0f..c70a318d4 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -37,6 +37,7 @@ import '../utils/visual_utils.dart'; import '../models/insights_gen.dart'; import '../models/layout_config.dart'; import '../models/saved_location.dart'; +import '../models/weather_display_data.dart'; // App screens import '../screens/locations.dart'; @@ -94,7 +95,7 @@ class _WeatherHomeState extends State { List layoutConfig = []; // late Future?>? weatherFuture; - Future?>? weatherFuture; + Future? weatherFuture; Map? weatherData; @@ -227,7 +228,7 @@ class _WeatherHomeState extends State { await prefs.setString('currentLocation', jsonEncode(locationData)); } - Future?> getWeatherFromCache() async { + Future getWeatherFromCache() async { final box = await Hive.openBox('weatherMasterCache'); var cached = box.get(cacheKey); final homePref = PreferencesHelper.getJson('homeLocation'); @@ -280,7 +281,8 @@ class _WeatherHomeState extends State { isFirstAppBuild = false; } } - return json.decode(cached); + final decoded = json.decode(cached); + return processWeatherData(decoded); } Future _loadWeatherIconFroggy( @@ -868,7 +870,7 @@ class _WeatherHomeState extends State { .get(1), ]; - return FutureBuilder?>( + return FutureBuilder( future: weatherFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -888,56 +890,33 @@ class _WeatherHomeState extends State { ); } - final raw = snapshot.data!; - final weather = raw['data']; + final displayData = snapshot.data!; + final raw = displayData.raw; + final weather = raw['data'] ?? raw; // Fallback final current = weather['current']; final String? lastUpdated = raw['last_updated']; final int weatherCodeFroggy = current['weather_code'] ?? 0; final bool isDayFroggy = current['is_day'] == 1; - final hourly = weather['hourly'] ?? {}; - - final List hourlyTimeNoFilter = hourly['time']; - final List hourlyTempsNoFilter = hourly['temperature_2m']; - final List hourlyWeatherCodesNoFilter = - hourly['weather_code']; - final List hourlyPrecpProbNoFilter = - hourly['precipitation_probability']; - - // Convert times to DateTime and filter out past-day entries - final now = DateTime.now(); - final todayMidnight = DateTime(now.year, now.month, now.day); - - final filteredIndices = []; - for (int i = 0; i < hourlyTimeNoFilter.length; i++) { - final time = DateTime.parse(hourlyTimeNoFilter[i]); - if (time.isAfter(todayMidnight) || - time.isAtSameMomentAs(todayMidnight)) { - filteredIndices.add(i); - } - } + final hourly = weather['hourly'] ?? {}; // Still needed for some widgets that take maps? + // Actually, Widgets take lists. I should use displayData properties. + + // Using processed data + final hourlyTime = displayData.hourlyTime; + final hourlyTemps = displayData.hourlyTemps; + final hourlyWeatherCodes = displayData.hourlyWeatherCodes; + final hourlyPrecpProb = displayData.hourlyPrecpProb; -// Keep only today's + future hours - final hourlyTime = - filteredIndices.map((i) => hourlyTimeNoFilter[i]).toList(); - final hourlyTemps = - filteredIndices.map((i) => hourlyTempsNoFilter[i]).toList(); - final hourlyWeatherCodes = filteredIndices - .map((i) => hourlyWeatherCodesNoFilter[i]) - .toList(); - final hourlyPrecpProb = - filteredIndices.map((i) => hourlyPrecpProbNoFilter[i]).toList(); - - final daily = weather['daily']; - final List dailyDates = daily['time']; - final List sunriseTimes = daily['sunrise']; - final List sunsetTimes = daily['sunset']; - final List dailyTempsMin = daily['temperature_2m_min']; - final List dailyTempsMax = daily['temperature_2m_max']; - final List dailyPrecProb = - daily['precipitation_probability_max']; - final List dailyWeatherCodes = daily['weather_code']; + final daily = weather['daily'] ?? {}; // Still needed for summary card which takes raw map? + // SummaryCard takes: hourlyData (map), dailyData (map), currentData (map) + // I should keep passing 'weather' parts to it for now to avoid refactoring all widgets. + + final dailyDates = displayData.dailyDates; + final dailyTempsMin = displayData.dailyTempsMin; + final dailyTempsMax = displayData.dailyTempsMax; + final dailyPrecProb = displayData.dailyPrecProb; + final dailyWeatherCodes = displayData.dailyWeatherCodes; void maybeUpdateWeatherAnimation(Map current, {isForce = false}) { @@ -982,13 +961,7 @@ class _WeatherHomeState extends State { onLoadForceCall = true; } - final Map daylightMap = { - for (int i = 0; i < dailyDates.length; i++) - dailyDates[i]: ( - DateTime.parse(sunriseTimes[i]), - DateTime.parse(sunsetTimes[i]) - ), - }; + final daylightMap = displayData.daylightMap; bool isHourDuringDaylightOptimized(DateTime hourTime) { final key = @@ -1066,92 +1039,8 @@ class _WeatherHomeState extends State { final double? ragweedPollen = weather['air_quality']['current']['ragweed_pollen']; - const double rainThreshold = 0.5; - const int probThreshold = 40; - int offsetSeconds = - int.parse(weather['utc_offset_seconds'].toString()); - DateTime utcNow = DateTime.now().toUtc(); - DateTime nowPrecip = utcNow.add(Duration(seconds: offsetSeconds)); - - nowPrecip = DateTime( - nowPrecip.year, - nowPrecip.month, - nowPrecip.day, - nowPrecip.hour, - nowPrecip.minute, - nowPrecip.second, - nowPrecip.millisecond, - nowPrecip.microsecond, - ); - - final List allTimeStrings = - (hourly['time'] as List?)?.cast() ?? []; - final List allPrecip = (hourly['precipitation'] as List?) - ?.map((e) => (e as num?)?.toDouble() ?? 0.0) - .toList() ?? - []; - final List allPrecipProb = - (hourly['precipitation_probability'] as List?) - ?.map((e) => (e as num?)?.toInt() ?? 0) - .toList() ?? - []; - - final List timeNext12h = []; - final List precpNext12h = []; - final List precipProbNext12h = []; - - for (int i = 0; i < allTimeStrings.length; i++) { - if (i >= allPrecip.length || i >= allPrecipProb.length) break; - - final time = DateTime.parse(allTimeStrings[i]); - if (time.isAfter(nowPrecip) && - time.isBefore(nowPrecip.add(Duration(hours: 12)))) { - timeNext12h.add(allTimeStrings[i]); - precpNext12h.add(allPrecip[i]); - precipProbNext12h.add(allPrecipProb[i]); - } - } - - final List next2hPrecip = []; - - for (int i = 0; i < timeNext12h.length; i++) { - final time = DateTime.parse(timeNext12h[i]); - if (time.isBefore(nowPrecip.add(Duration(hours: 2)))) { - next2hPrecip.add(precpNext12h[i]); - } - } - - int? rainStart; - int longestRainLength = 0; - int? bestStart; - int? bestEnd; - - for (int i = 0; i < precpNext12h.length; i++) { - if (precpNext12h[i] >= rainThreshold && - precipProbNext12h[i] >= probThreshold) { - rainStart ??= i; - } else { - if (rainStart != null) { - final length = i - rainStart; - if (length >= 2 && length > longestRainLength) { - longestRainLength = length; - bestStart = rainStart; - bestEnd = i - 1; - } - rainStart = null; - } - } - } - - if (rainStart != null) { - final length = precpNext12h.length - rainStart; - if (length >= 2 && length > longestRainLength) { - bestStart = rainStart; - bestEnd = precpNext12h.length - 1; - } - } + final bool shouldShowRainBlock = displayData.shouldShowRainBlock; - final bool shouldShowRainBlock = bestStart != null && bestEnd != null; final colorTheme = Theme.of(context).colorScheme; if (!widgetsUpdated) { @@ -1511,7 +1400,7 @@ class _WeatherHomeState extends State { _istriggeredFromLocations = true; themeCalled = false; _isLoadingFroggy = true; - weatherFuture = Future.value(result); + weatherFuture = Future.value(processWeatherData(result)); }); } return;