bugstweet 0.9.7
bugstweet: ^0.9.7 copied to clipboard
Testers report bugs from inside your Flutter app as GitHub issues an AI coding agent can fix. Tester builds only: store builds never contain it.
example/lib/main.dart
// The sample app: a tiny health tracker with two planted bugs.
//
// This is the "store build" entry point. It knows nothing about bugsTweet.
// See .bugstweet/tester.dart for the tester build.
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
void main() => runApp(const SampleApp());
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Sample Health',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFFB93A63),
scaffoldBackgroundColor: const Color(0xFFF7F5F2),
),
darkTheme: ThemeData(colorSchemeSeed: const Color(0xFFB93A63), brightness: Brightness.dark),
// English and Arabic (right to left).
supportedLocales: const [Locale('en'), Locale('ar')],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
routes: {
'/': (_) => const HomeShell(),
'/dose': (_) => const DoseDetailScreen(),
},
);
}
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _tab = 0;
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(child: _tab == 0 ? const TodayScreen() : const VitalsScreen()),
bottomNavigationBar: NavigationBar(
selectedIndex: _tab,
onDestinationSelected: (i) => setState(() => _tab = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.today_outlined), label: 'Today'),
NavigationDestination(icon: Icon(Icons.favorite_outline), label: 'Vitals'),
],
),
);
}
/// Whether today's Bisoprolol dose is marked as taken. Public so the tester
/// build can name it in reports (see .bugstweet/tester.dart).
bool bisoprololTaken = false;
class TodayScreen extends StatefulWidget {
const TodayScreen({super.key});
@override
State<TodayScreen> createState() => _TodayScreenState();
}
class _TodayScreenState extends State<TodayScreen> {
bool _taken = false;
@override
Widget build(BuildContext context) => ListView(
padding: const EdgeInsets.all(16),
children: [
const ScreenHeader(title: 'Today'),
NextDoseCard(
taken: _taken,
onTaken: () => setState(() => _taken = bisoprololTaken = true),
),
const SizedBox(height: 16),
const Text('ALL DOSES TODAY', style: TextStyle(fontSize: 11, letterSpacing: .8, color: Colors.black54)),
const SizedBox(height: 6),
Card(
child: Column(
children: [
const DoseRow(time: '08:00', name: 'Aspirin 75 mg', status: 'Taken'),
// Planted bug 2: a long medicine name that overflows its row.
const DoseRow(time: '13:00', name: 'Atorvastatin calcium 20 mg film-coated tablet', status: 'Due'),
DoseRow(time: '21:00', name: 'Bisoprolol 2.5 mg', status: _taken ? 'Taken' : 'Due'),
],
),
),
const SizedBox(height: 16),
const Text('THIS WEEK', style: TextStyle(fontSize: 11, letterSpacing: .8, color: Colors.black54)),
const SizedBox(height: 6),
// A long list, so the page scrolls (for the long shot).
for (final day in const ['Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue'])
Card(
child: Column(
children: [
DoseRow(time: '$day 08:00', name: 'Aspirin 75 mg', status: 'Planned'),
DoseRow(time: '$day 13:00', name: 'Atorvastatin 20 mg', status: 'Planned'),
DoseRow(time: '$day 21:00', name: 'Bisoprolol 2.5 mg', status: 'Planned'),
],
),
),
],
);
}
class ScreenHeader extends StatelessWidget {
const ScreenHeader({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('TUESDAY, 14 OCTOBER', style: TextStyle(fontSize: 11, letterSpacing: .8, color: Colors.black54)),
Text(title, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700)),
],
),
);
}
class NextDoseCard extends StatelessWidget {
const NextDoseCard({super.key, required this.taken, required this.onTaken});
final bool taken;
final VoidCallback onTaken;
@override
Widget build(BuildContext context) => Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('NEXT DOSE', style: TextStyle(fontSize: 11, letterSpacing: .8, color: Colors.black54)),
const SizedBox(height: 4),
const Text('Bisoprolol 2.5 mg', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
// Planted bug 1: a dose time that does not exist (should be 21:00).
const Text('Due at 25:00', style: TextStyle(color: Color(0xFFB93A63), fontWeight: FontWeight.w600)),
const SizedBox(height: 10),
if (taken)
const Text('Marked as taken', style: TextStyle(color: Color(0xFF2F7D4F), fontWeight: FontWeight.w600))
else
FilledButton(onPressed: onTaken, child: const Text('Mark as taken')),
],
),
),
);
}
class DoseRow extends StatelessWidget {
const DoseRow({super.key, required this.time, required this.name, required this.status});
final String time;
final String name;
final String status;
@override
Widget build(BuildContext context) => InkWell(
onTap: () => Navigator.of(context).pushNamed('/dose', arguments: name),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Row(
children: [
Text(time, style: const TextStyle(fontFamily: 'monospace', color: Colors.black54)),
const SizedBox(width: 12),
// No Expanded on purpose: the long name overflows the row.
Text(name, softWrap: false),
const Spacer(),
Text(status, style: TextStyle(color: status == 'Taken' ? const Color(0xFF2F7D4F) : Colors.black54)),
],
),
),
);
}
class DoseDetailScreen extends StatelessWidget {
const DoseDetailScreen({super.key});
@override
Widget build(BuildContext context) {
final name = ModalRoute.of(context)?.settings.arguments as String? ?? 'Dose';
return Scaffold(
appBar: AppBar(title: Text(name)),
body: const Padding(
padding: EdgeInsets.all(16),
child: Text('Dose details would be here.'),
),
);
}
}
class VitalsScreen extends StatefulWidget {
const VitalsScreen({super.key});
@override
State<VitalsScreen> createState() => _VitalsScreenState();
}
class _VitalsScreenState extends State<VitalsScreen> {
String? _chartError;
final _note = TextEditingController(text: 'Dizzy after the 9 pm dose');
@override
void dispose() {
_note.dispose();
super.dispose();
}
void _loadChart() {
try {
// Planted error: the chart data is missing for one day.
throw StateError('HeartRateChart: no readings found for 13 Oct (value was null)');
} on StateError catch (e, stack) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: stack, library: 'sample vitals'));
setState(() => _chartError = "Couldn't load chart");
}
}
@override
Widget build(BuildContext context) => ListView(
padding: const EdgeInsets.all(16),
children: [
const ScreenHeader(title: 'Vitals'),
HeartRateCard(error: _chartError, onLoad: _loadChart),
const SizedBox(height: 12),
const Card(
child: ListTile(title: Text('Blood pressure'), subtitle: Text('118/76 mmHg'), trailing: Text('See history')),
),
const SizedBox(height: 12),
// A text field: bugsTweet blurs it in screenshots automatically.
TextField(
controller: _note,
decoration: const InputDecoration(labelText: 'Note for your doctor', border: OutlineInputBorder()),
),
],
);
}
class HeartRateCard extends StatelessWidget {
const HeartRateCard({super.key, required this.error, required this.onLoad});
final String? error;
final VoidCallback onLoad;
@override
Widget build(BuildContext context) => Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('RESTING HEART RATE', style: TextStyle(fontSize: 11, letterSpacing: .8, color: Colors.black54)),
const Text('72 bpm', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700)),
const SizedBox(height: 8),
Container(
height: 80,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFD9B9C4)),
borderRadius: BorderRadius.circular(10),
),
child: error != null
? Text(error!, style: const TextStyle(color: Color(0xFFB93A63)))
: TextButton(onPressed: onLoad, child: const Text('Load chart')),
),
],
),
),
);
}