Skip to content

Instantly share code, notes, and snippets.

@evaisse
Last active June 30, 2025 13:59
Show Gist options
  • Save evaisse/da370f456b9d71723cefebab7e474eea to your computer and use it in GitHub Desktop.
Save evaisse/da370f456b9d71723cefebab7e474eea to your computer and use it in GitHub Desktop.
Describing usage of context.select/read/watch etc
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