Flutter Intro: Widget, State Management, & Networking
A practical introduction to Flutter development covering the widget tree, state management patterns with Provider and Riverpod, and networking with http and dio packages.
// table of contents (27 sections)
What is Flutter?
Flutter is Google’s UI toolkit for building natively compiled applications from a single codebase. It targets mobile (iOS & Android), web, and desktop.
The core idea: everything is a widget.
MaterialApp
└── Scaffold
├── AppBar
│ └── Text("My App")
└── Body
├── ListView
│ ├── ListTile
│ ├── ListTile
│ └── ...
└── FloatingActionButton
Flutter doesn’t use platform native components. It renders every pixel with its own rendering engine (Skia/Impeller), which gives you pixel-perfect control across platforms.
Widget Basics
Stateless vs Stateful
This is the first thing you learn in Flutter, and it matters:
| Type | When to Use | Rebuilds |
|---|---|---|
StatelessWidget | Static UI, no internal state | When parent rebuilds |
StatefulWidget | Dynamic UI, internal state changes | When setState() is called |
// StatelessWidget — UI that doesn't change
class GreetingCard extends StatelessWidget {
final String name;
const GreetingCard({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Hello, $name!'),
),
);
}
}
// StatefulWidget — UI that changes over time
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Increment'),
),
],
);
}
}
Common Widgets
// Layout
Column(children: [...]) // Vertical stack
Row(children: [...]) // Horizontal stack
Stack(children: [...]) // Layered widgets
ListView.builder(...) // Scrollable list
GridView.count(...) // Grid layout
Padding(padding: ..., child: ...)
Container(...) // Box with decoration
// Display
Text('Hello')
Image.asset('assets/photo.png')
Icon(Icons.star)
// Input
TextField(...)
ElevatedButton(...)
GestureDetector(...)
Layout System
Flutter uses a box constraint model. Widgets receive constraints from their parent, decide their size, and tell the parent.
Flexbox-like Layout with Column & Row
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(flex: 2, child: Header()),
Expanded(flex: 3, child: Content()),
Expanded(flex: 1, child: Footer()),
],
)
Responsive Design
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return TabletLayout();
}
return MobileLayout();
},
)
Navigation
Flutter 2.0+ uses the Navigator 2.0 API, but for most apps the declarative routing with go_router is the preferred approach.
Basic Navigation
// Push a new route
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);
// Go back
Navigator.pop(context);
Named Routes (in MaterialApp)
MaterialApp(
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/settings': (context) => SettingsPage(),
},
);
// Navigate
Navigator.pushNamed(context, '/detail');
go_router (Recommended)
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/detail/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
},
),
],
);
// Use in MaterialApp
MaterialApp.router(routerConfig: router);
State Management
This is where most beginners get confused. The key question: how do different parts of your app share data?
The Problem
// Bad: passing callbacks through 5 levels of widgets
Grandparent → Parent → Child → Grandchild → Button
You need a way to share state without manually passing it through every constructor.
Provider (Most Popular)
Provider is the most widely used state management solution in Flutter. It uses InheritedWidget under the hood but with a much simpler API.
// 1. Create a ChangeNotifier
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Triggers rebuild
}
}
// 2. Wrap your app with ChangeNotifierProvider
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterNotifier(),
child: MyApp(),
),
);
}
// 3. Consume the state
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterNotifier>();
return Text('Count: ${counter.count}');
}
}
// 4. Modify the state
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.read<CounterNotifier>();
return ElevatedButton(
onPressed: counter.increment,
child: Text('Add'),
);
}
}
Key Provider Methods
| Method | Rebuilds | Use When |
|---|---|---|
context.watch<T>() | Yes, on change | Displaying state in build() |
context.read<T>() | No | Calling methods (buttons, callbacks) |
context.select<T, R>() | Yes, on specific change | Optimizing rebuilds |
Provider.of<T>(context, listen: false) | No | Same as read() |
Riverpod (Modern Alternative)
Riverpod is the successor to Provider, built by the same author. It’s compile-time safe and doesn’t depend on the widget tree.
// 1. Create a provider
final counterProvider = StateProvider<int>((ref) => 0);
// For more complex state
final todoListProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
return TodoNotifier();
});
// 2. Use in widget (ConsumerWidget instead of StatelessWidget)
class CounterDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
// 3. Modify state
class IncrementButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('Add'),
);
}
}
Provider vs Riverpod
| Feature | Provider | Riverpod |
|---|---|---|
| Compile-time safety | No | Yes |
| Depends on widget tree | Yes | No |
| Learning curve | Lower | Medium |
| DevTools support | Basic | Advanced |
| Recommended for new apps | Yes | Yes (preferred) |
Networking
Most apps need to talk to an API. Flutter uses Dart’s http package or the more feature-rich dio package.
Using http Package
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<User>> fetchUsers() async {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
);
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
Using dio Package
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
// GET request
Future<List<User>> fetchUsers() async {
final response = await dio.get('/users');
return (response.data as List)
.map((json) => User.fromJson(json))
.toList();
}
// POST request
Future<User> createUser(Map<String, dynamic> data) async {
final response = await dio.post('/users', data: data);
return User.fromJson(response.data);
}
http vs dio
| Feature | http | dio |
|---|---|---|
| Size | Small | Larger |
| Interceptors | No | Yes |
| Request cancellation | No | Yes |
| File download/upload | Manual | Built-in |
| Timeout config | Per-request | Global + per-request |
| FormData | Manual | Built-in |
| Good for | Simple APIs | Complex apps |
Combining with State Management
The real power comes from combining networking with state management:
class UserNotifier extends ChangeNotifier {
List<User> _users = [];
bool _isLoading = false;
String? _error;
List<User> get users => _users;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> loadUsers() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_users = await fetchUsers();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
Then in your widget:
class UserListPage extends StatefulWidget {
@override
State<UserListPage> createState() => _UserListPageState();
}
class _UserListPageState extends State<UserListPage> {
@override
void initState() {
super.initState();
context.read<UserNotifier>().loadUsers();
}
@override
Widget build(BuildContext context) {
final notifier = context.watch<UserNotifier>();
if (notifier.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (notifier.error != null) {
return Center(child: Text('Error: ${notifier.error}'));
}
return ListView.builder(
itemCount: notifier.users.length,
itemBuilder: (context, index) {
final user = notifier.users[index];
return ListTile(title: Text(user.name));
},
);
}
}
JSON Serialization
Most APIs return JSON. You need to convert between JSON maps and Dart objects.
Manual Serialization
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
// From JSON (API → Dart)
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
// To JSON (Dart → API)
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
With code generation (json_serializable)
For larger projects, use json_annotation + build_runner:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Run flutter pub run build_runner build to generate the code.
Quick Reference
| Topic | Key Concept |
|---|---|
| Widget | Everything is a widget |
| StatelessWidget | Static UI |
| StatefulWidget | Dynamic UI with setState() |
| Navigation | Navigator or go_router |
| Provider | State management via ChangeNotifier |
| Riverpod | Compile-time safe state management |
| http | Simple HTTP client |
| dio | Feature-rich HTTP client |
| JSON | fromJson() / toJson() pattern |
What’s Next?
With widgets, state management, and networking covered, the next step is learning about testing your Flutter apps and scheduling tasks.
Bismillah, happy Fluttering! 🚀
You might also like
Flutter State Management Best Practices 2026
Master Flutter state management with practical best practices. Compare Provider, Riverpod, BLoC, and GetX with real code examples and architecture patterns.
Dart Deep Dive: Understanding the Programming Language for Flutter
A comprehensive guide to Dart programming for Flutter development, covering variables, data types, control flow, OOP, collections, null safety, and async programming.
Flutter Fundamental: Testing & Scheduler
Learn how to test Flutter apps with unit, widget, and integration tests, plus scheduling background tasks with timers, WorkManager, and alarm managers.
More Posts
API Gateway Patterns: The Front Door to Your Microservices
Web Components 2026: Building Framework-Agnostic UI Libraries
Building Autonomous AI Workflows with LangGraph: A Practical Guide
Building Type-Safe APIs with tRPC in 2026: Full-Stack TypeScript Without Schemas
Database Connection Pooling: Patterns for High-Performance Applications
Prompt Caching: Reduce LLM Costs by 90% with Smart Context Management
Enjoyed This Post?
Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.
