Last active
June 30, 2025 13:59
-
-
Save evaisse/da370f456b9d71723cefebab7e474eea to your computer and use it in GitHub Desktop.
Describing usage of context.select/read/watch etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:provider/provider.dart'; | |
/// Represents a city using a record, with its name (including flag emoji), | |
/// population, and seaside status. | |
/// Records provide automatic equality and hash code implementation. | |
typedef City = ({String name, int population, bool isSeaside}); | |
/// Manages the selection of a city and provides available city data. | |
/// | |
/// Notifies listeners when the selected city changes. | |
class CityService extends ValueNotifier<City> { | |
final Set<City> cities; | |
CityService(this.cities): | |
super(cities.first); | |
void update(City city) => value = city; | |
} | |
/// A widget that displays the selected city's name and population, | |
/// and tracks how many times its `build` method has been called. | |
/// | |
/// It uses `context.select` to only rebuild when the `name` or `population` | |
/// of the selected city changes, demonstrating efficient state updates. | |
class CityInfoBox extends StatelessWidget { | |
const CityInfoBox({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
// Use context.select to listen only to changes in name or population. | |
// The build method will only be re-run if these specific properties change. | |
final city = context.watch<CityService?>()?.value; | |
return Card( | |
margin: const EdgeInsets.all(8.0), | |
color: Colors.lightBlue.shade50, | |
elevation: 4.0, | |
child: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
const SizedBox(height: 8.0), | |
Text( | |
'Name: ${city?.name ?? 'N/A'}', | |
style: Theme.of(context).textTheme.bodyLarge, | |
), | |
Text( | |
'Population: ${city?.population.toString() ?? 'N/A'}', | |
style: Theme.of(context).textTheme.bodyLarge, | |
), | |
const SizedBox(height: 8.0), | |
RedrawCounter(), | |
], | |
), | |
), | |
); | |
} | |
} | |
/// A widget that displays a wave emoji if the selected city is on the seaside, | |
/// and tracks how many times its `build` method has been called. | |
/// | |
/// It uses `context.select` to only rebuild when the `isSeaside` property | |
/// of the selected city changes, demonstrating selective rebuilding. | |
class SeasideInfoBox extends StatelessWidget { | |
const SeasideInfoBox({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
// Use context.select to listen only to changes in isSeaside. | |
// The build method will only be re-run if this specific property changes. | |
final isSeaside = context.select<CityService?, bool?>( | |
(data) => data?.value.isSeaside, | |
); | |
return Card( | |
margin: const EdgeInsets.all(8.0), | |
color: Colors.lightGreen.shade50, | |
elevation: 4.0, | |
child: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text( | |
isSeaside == true ? 'On the Seaside! ๐โ' : 'Not on the Seaside ๐', | |
style: Theme.of(context).textTheme.displaySmall, | |
), | |
const SizedBox(height: 8.0), | |
RedrawCounter(), | |
], | |
), | |
), | |
); | |
} | |
} | |
/// A card widget to display a single city and allow selection. | |
class CityCard extends StatelessWidget { | |
final City city; | |
final void Function(City)? onTap; | |
final bool isSelected; | |
const CityCard({ | |
super.key, | |
required this.city, | |
required this.onTap, | |
required this.isSelected, | |
}); | |
@override | |
Widget build(context) { | |
return Card( | |
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), | |
elevation: isSelected ? 8.0 : 2.0, | |
color: isSelected | |
? Theme.of(context).primaryColor.withValues(alpha: 0.05) | |
: null, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(12.0), | |
side: isSelected | |
? BorderSide(color: Theme.of(context).primaryColor, width: 2.0) | |
: BorderSide.none, | |
), | |
child: InkWell( | |
onTap: () => onTap?.call(city), | |
borderRadius: BorderRadius.circular(12.0), | |
child: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Row( | |
children: [ | |
Expanded( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
// City name now includes the flag emoji | |
Text( | |
city.name + (city.isSeaside ? ' ๐' : ''), | |
style: Theme.of(context).textTheme.titleLarge, | |
), | |
const SizedBox(height: 4.0), | |
Text( | |
'Population: ${city.population.toStringAsFixed(0)}', | |
style: Theme.of(context).textTheme.bodyMedium, | |
), | |
], | |
), | |
), | |
if (isSelected) | |
Icon(Icons.check_circle, color: Theme.of(context).primaryColor), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
/// The main screen for city selection, displaying info boxes and a list of cities. | |
class CitySelectionScreen extends StatelessWidget { | |
const CitySelectionScreen({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
// Watch the entire CitySelectionData to get the list of available cities | |
// No need to subscribe, so we use `read` method here. | |
final cityService = context | |
.read<CityService?>(); | |
// we do want to subscribe to this one, because each time we click on a city, | |
// the selector shoud change his state. We use `watch` method | |
final selectedCity = context | |
.watch<CityService?>()?.value; | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('City Selection among '), | |
backgroundColor: Theme.of(context).colorScheme.inversePrimary, | |
), | |
body: Column( | |
children: [ | |
Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), | |
child: Row( | |
children: const [ | |
Expanded(child: CityInfoBox()), | |
Expanded(child: SeasideInfoBox()), | |
], | |
), | |
), | |
const Divider(), | |
Expanded( | |
child: ListView.builder( | |
itemCount: cityService?.cities.length ?? 0, | |
itemBuilder: (context, index) { | |
final city = cityService?.cities.elementAtOrNull(index); | |
if (city == null) return Container(); | |
return CityCard( | |
city: city, | |
isSelected: selectedCity == city, | |
onTap: cityService?.update, | |
); | |
}, | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
/// Simple line of text to display redraw count; | |
class RedrawCounter extends StatefulWidget { | |
@override | |
State<RedrawCounter> createState() => _RedrawCounterState(); | |
} | |
class _RedrawCounterState extends State<RedrawCounter> { | |
int _redrawCount = 0; | |
Widget build(context) { | |
_redrawCount += 1; | |
return Text( | |
'This widget had been redrawed $_redrawCount times', | |
style: Theme.of(context).textTheme.bodySmall, | |
); | |
} | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(context) { | |
// Initial list of cities for the application. | |
// City names now include the flag emoji directly. | |
final initialCities = const { | |
(name: '๐ซ๐ท Paris', population: 2141000, isSeaside: false), | |
(name: '๐ฌ๐ง London', population: 8982000, isSeaside: false), | |
(name: '๐ฏ๐ต Tokyo', population: 13960000, isSeaside: true), | |
(name: '๐บ๐ธ New York', population: 8419000, isSeaside: true), | |
(name: '๐ฎ๐น Rome', population: 2873000, isSeaside: false), | |
(name: '๐ฆ๐บ Sydney', population: 5312000, isSeaside: true), | |
(name: '๐ฉ๐ช Berlin', population: 3769000, isSeaside: false), | |
(name: '๐ง๐ท Rio de Janeiro', population: 6712000, isSeaside: true), | |
(name: '๐ฉ๐ช Munich', population: 1500000, isSeaside: false), | |
(name: '๐ซ๐ท Nice', population: 343000, isSeaside: true), | |
}; | |
return MaterialApp( | |
title: 'City Selection Application', | |
theme: ThemeData( | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | |
useMaterial3: true, | |
), | |
home: ChangeNotifierProvider<CityService>( | |
create: (context) => CityService(initialCities), | |
builder: (context, child) => | |
const CitySelectionScreen(), | |
), | |
); | |
} | |
} | |
void main() { | |
runApp(const MyApp()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment