mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-22 02:52:37 -05:00
Refactor WeatherHome to process data outside build method
Moved expensive data processing (date parsing, loops for filtering hours, rain logic) out of the `build` method in `lib/screens/home.dart`. Created `WeatherDisplayData` model to hold processed data. Updated `FutureBuilder` to use pre-calculated data. This reduces CPU usage on every rebuild of the home screen.
This commit is contained in:
1 parent
82726b6060
commit
299728b5ec
3 files changed
+262
-138
No files matched your search
@@ -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.
|
||||
@@ -0,0 +1,232 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class WeatherDisplayData {
|
||||
final Map<String, dynamic> raw;
|
||||
|
||||
// Hourly Data (Filtered)
|
||||
final List<dynamic> hourlyTime;
|
||||
final List<dynamic> hourlyTemps;
|
||||
final List<dynamic> hourlyWeatherCodes;
|
||||
final List<dynamic> hourlyPrecpProb;
|
||||
final List<dynamic> hourlyDewPoint;
|
||||
final List<dynamic> hourlyVisibility;
|
||||
final List<dynamic> hourlyUvIndex;
|
||||
|
||||
// Daily Data
|
||||
final List<dynamic> dailyDates;
|
||||
final List<dynamic> dailyTempsMin;
|
||||
final List<dynamic> dailyTempsMax;
|
||||
final List<dynamic> dailyPrecProb;
|
||||
final List<dynamic> dailyPrecipSum;
|
||||
final List<dynamic> dailyDaylightDuration;
|
||||
final List<dynamic> dailyWeatherCodes;
|
||||
final List<dynamic> sunriseTimes;
|
||||
final List<dynamic> sunsetTimes;
|
||||
|
||||
// Processed Data
|
||||
final Map<String, (DateTime, DateTime)> daylightMap;
|
||||
final bool shouldShowRainBlock;
|
||||
final int? rainStart;
|
||||
final int? bestStart;
|
||||
final int? bestEnd;
|
||||
final List<String> timeNext12h;
|
||||
final List<double> precpNext12h;
|
||||
final List<int> 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<String, dynamic> raw) {
|
||||
final weatherData = raw['data'] ?? raw;
|
||||
|
||||
final hourly = weatherData['hourly'] ?? {};
|
||||
final daily = weatherData['daily'] ?? {};
|
||||
|
||||
final List<dynamic> hourlyTimeNoFilter = hourly['time'] ?? [];
|
||||
final List<dynamic> hourlyTempsNoFilter = hourly['temperature_2m'] ?? [];
|
||||
final List<dynamic> hourlyWeatherCodesNoFilter = hourly['weather_code'] ?? [];
|
||||
final List<dynamic> hourlyPrecpProbNoFilter = hourly['precipitation_probability'] ?? [];
|
||||
final List<dynamic> hourlyDewPointNoFilter = hourly['dew_point_2m'] ?? [];
|
||||
final List<dynamic> hourlyVisibilityNoFilter = hourly['visibility'] ?? [];
|
||||
final List<dynamic> hourlyUvIndexNoFilter = hourly['uv_index'] ?? [];
|
||||
|
||||
// Filter hourly data
|
||||
final now = DateTime.now();
|
||||
final todayMidnight = DateTime(now.year, now.month, now.day);
|
||||
|
||||
final filteredIndices = <int>[];
|
||||
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<dynamic> dailyDates = daily['time'] ?? [];
|
||||
final List<dynamic> sunriseTimes = daily['sunrise'] ?? [];
|
||||
final List<dynamic> sunsetTimes = daily['sunset'] ?? [];
|
||||
final List<dynamic> dailyTempsMin = daily['temperature_2m_min'] ?? [];
|
||||
final List<dynamic> dailyTempsMax = daily['temperature_2m_max'] ?? [];
|
||||
final List<dynamic> dailyPrecProb = daily['precipitation_probability_max'] ?? [];
|
||||
final List<dynamic> dailyPrecipSum = daily['precipitation_sum'] ?? [];
|
||||
final List<dynamic> dailyDaylightDuration = daily['daylight_duration'] ?? [];
|
||||
final List<dynamic> dailyWeatherCodes = daily['weather_code'] ?? [];
|
||||
|
||||
// Daylight Map
|
||||
final Map<String, (DateTime, DateTime)> 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<String> allTimeStrings = (hourly['time'] as List?)?.cast<String>() ?? [];
|
||||
final List<double> allPrecip = (hourly['precipitation'] as List?)
|
||||
?.map((e) => (e as num?)?.toDouble() ?? 0.0)
|
||||
.toList() ??
|
||||
[];
|
||||
final List<int> allPrecipProb = (hourly['precipitation_probability'] as List?)
|
||||
?.map((e) => (e as num?)?.toInt() ?? 0)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final List<String> timeNext12h = [];
|
||||
final List<double> precpNext12h = [];
|
||||
final List<int> 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,
|
||||
);
|
||||
}
|
||||
+27
-138
@@ -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<WeatherHome> {
|
||||
List<LayoutBlockConfig> layoutConfig = [];
|
||||
|
||||
// late Future<Map<String, dynamic>?>? weatherFuture;
|
||||
Future<Map<String, dynamic>?>? weatherFuture;
|
||||
Future<WeatherDisplayData?>? weatherFuture;
|
||||
|
||||
Map<String, dynamic>? weatherData;
|
||||
|
||||
@@ -227,7 +228,7 @@ class _WeatherHomeState extends State<WeatherHome> {
|
||||
await prefs.setString('currentLocation', jsonEncode(locationData));
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getWeatherFromCache() async {
|
||||
Future<WeatherDisplayData?> 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<WeatherHome> {
|
||||
isFirstAppBuild = false;
|
||||
}
|
||||
}
|
||||
return json.decode(cached);
|
||||
final decoded = json.decode(cached);
|
||||
return processWeatherData(decoded);
|
||||
}
|
||||
|
||||
Future<void> _loadWeatherIconFroggy(
|
||||
@@ -868,7 +870,7 @@ class _WeatherHomeState extends State<WeatherHome> {
|
||||
.get(1),
|
||||
];
|
||||
|
||||
return FutureBuilder<Map<String, dynamic>?>(
|
||||
return FutureBuilder<WeatherDisplayData?>(
|
||||
future: weatherFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
@@ -888,56 +890,33 @@ class _WeatherHomeState extends State<WeatherHome> {
|
||||
);
|
||||
}
|
||||
|
||||
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 hourly = weather['hourly'] ?? {}; // Still needed for some widgets that take maps?
|
||||
// Actually, Widgets take lists. I should use displayData properties.
|
||||
|
||||
final List<dynamic> hourlyTimeNoFilter = hourly['time'];
|
||||
final List<dynamic> hourlyTempsNoFilter = hourly['temperature_2m'];
|
||||
final List<dynamic> hourlyWeatherCodesNoFilter =
|
||||
hourly['weather_code'];
|
||||
final List<dynamic> hourlyPrecpProbNoFilter =
|
||||
hourly['precipitation_probability'];
|
||||
// Using processed data
|
||||
final hourlyTime = displayData.hourlyTime;
|
||||
final hourlyTemps = displayData.hourlyTemps;
|
||||
final hourlyWeatherCodes = displayData.hourlyWeatherCodes;
|
||||
final hourlyPrecpProb = displayData.hourlyPrecpProb;
|
||||
|
||||
// Convert times to DateTime and filter out past-day entries
|
||||
final now = DateTime.now();
|
||||
final todayMidnight = DateTime(now.year, now.month, now.day);
|
||||
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 filteredIndices = <int>[];
|
||||
for (int i = 0; i < hourlyTimeNoFilter.length; i++) {
|
||||
final time = DateTime.parse(hourlyTimeNoFilter[i]);
|
||||
if (time.isAfter(todayMidnight) ||
|
||||
time.isAtSameMomentAs(todayMidnight)) {
|
||||
filteredIndices.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 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<dynamic> dailyDates = daily['time'];
|
||||
final List<dynamic> sunriseTimes = daily['sunrise'];
|
||||
final List<dynamic> sunsetTimes = daily['sunset'];
|
||||
final List<dynamic> dailyTempsMin = daily['temperature_2m_min'];
|
||||
final List<dynamic> dailyTempsMax = daily['temperature_2m_max'];
|
||||
final List<dynamic> dailyPrecProb =
|
||||
daily['precipitation_probability_max'];
|
||||
final List<dynamic> dailyWeatherCodes = daily['weather_code'];
|
||||
final dailyDates = displayData.dailyDates;
|
||||
final dailyTempsMin = displayData.dailyTempsMin;
|
||||
final dailyTempsMax = displayData.dailyTempsMax;
|
||||
final dailyPrecProb = displayData.dailyPrecProb;
|
||||
final dailyWeatherCodes = displayData.dailyWeatherCodes;
|
||||
|
||||
void maybeUpdateWeatherAnimation(Map<String, dynamic> current,
|
||||
{isForce = false}) {
|
||||
@@ -982,13 +961,7 @@ class _WeatherHomeState extends State<WeatherHome> {
|
||||
onLoadForceCall = true;
|
||||
}
|
||||
|
||||
final Map<String, (DateTime, DateTime)> 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<WeatherHome> {
|
||||
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));
|
||||
final bool shouldShowRainBlock = displayData.shouldShowRainBlock;
|
||||
|
||||
nowPrecip = DateTime(
|
||||
nowPrecip.year,
|
||||
nowPrecip.month,
|
||||
nowPrecip.day,
|
||||
nowPrecip.hour,
|
||||
nowPrecip.minute,
|
||||
nowPrecip.second,
|
||||
nowPrecip.millisecond,
|
||||
nowPrecip.microsecond,
|
||||
);
|
||||
|
||||
final List<String> allTimeStrings =
|
||||
(hourly['time'] as List?)?.cast<String>() ?? [];
|
||||
final List<double> allPrecip = (hourly['precipitation'] as List?)
|
||||
?.map((e) => (e as num?)?.toDouble() ?? 0.0)
|
||||
.toList() ??
|
||||
[];
|
||||
final List<int> allPrecipProb =
|
||||
(hourly['precipitation_probability'] as List?)
|
||||
?.map((e) => (e as num?)?.toInt() ?? 0)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final List<String> timeNext12h = [];
|
||||
final List<double> precpNext12h = [];
|
||||
final List<int> 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<double> 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 = bestStart != null && bestEnd != null;
|
||||
final colorTheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (!widgetsUpdated) {
|
||||
@@ -1511,7 +1400,7 @@ class _WeatherHomeState extends State<WeatherHome> {
|
||||
_istriggeredFromLocations = true;
|
||||
themeCalled = false;
|
||||
_isLoadingFroggy = true;
|
||||
weatherFuture = Future.value(result);
|
||||
weatherFuture = Future.value(processWeatherData(result));
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user