Your First Flutter App — The Counter
When you run flutter create my_app, the generated app is a counter — the same “Hello World” that React tutorials use to introduce useState. The logic is identical; the syntax is different. This lesson walks through both side by side so you see exactly where your React knowledge maps.
The React counter (your baseline)
Section titled “The React counter (your baseline)”import { useState } from 'react';
export default function Counter() { const [count, setCount] = useState(0);
return ( <div style={{ textAlign: 'center', marginTop: 60 }}> <h2>Count: {count}</h2> <button onClick={() => setCount(c => c + 1)}> Increment </button> </div> );}// Flutter counter — the default generated appimport 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return const MaterialApp(home: CounterPage()); }}
// StatefulWidget = component with useStateclass CounterPage extends StatefulWidget { const CounterPage({super.key});
@override State<CounterPage> createState() => _CounterPageState();}
class _CounterPageState extends State<CounterPage> { int _count = 0; // ≈ const [count, setCount] = useState(0)
void _increment() { setState(() { // ≈ setCount(c => c + 1) _count++; }); }
@override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Count: $_count', style: const TextStyle(fontSize: 28)), const SizedBox(height: 16), ElevatedButton( onPressed: _increment, child: const Text('Increment'), ), ], ), ), ); }}The full default app — runnable
Section titled “The full default app — runnable”The app below is the default Flutter counter with minor polish. Run it, press the button, and watch the count update — exactly like the React version.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Counter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const CounterPage(title: 'Flutter Counter'),
);
}
}
class CounterPage extends StatefulWidget {
final String title;
const CounterPage({super.key, required this.title});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.displayMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}Where setState falls short (teaser)
Section titled “Where setState falls short (teaser)”setState works fine for local UI state — exactly like useState. But when state needs to be shared across widgets, Flutter developers reach for Riverpod (or Provider/Bloc) — the same reason React developers move from useState to Zustand or Redux for shared state. The State & Lifecycle module covers this in depth.