Skip to content
· 8 min read · 0 views

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:

TypeWhen to UseRebuilds
StatelessWidgetStatic UI, no internal stateWhen parent rebuilds
StatefulWidgetDynamic UI, internal state changesWhen 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();
  },
)

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');
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
GrandparentParentChildGrandchildButton

You need a way to share state without manually passing it through every constructor.

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

MethodRebuildsUse When
context.watch<T>()Yes, on changeDisplaying state in build()
context.read<T>()NoCalling methods (buttons, callbacks)
context.select<T, R>()Yes, on specific changeOptimizing rebuilds
Provider.of<T>(context, listen: false)NoSame 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

FeatureProviderRiverpod
Compile-time safetyNoYes
Depends on widget treeYesNo
Learning curveLowerMedium
DevTools supportBasicAdvanced
Recommended for new appsYesYes (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

Featurehttpdio
SizeSmallLarger
InterceptorsNoYes
Request cancellationNoYes
File download/uploadManualBuilt-in
Timeout configPer-requestGlobal + per-request
FormDataManualBuilt-in
Good forSimple APIsComplex 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

TopicKey Concept
WidgetEverything is a widget
StatelessWidgetStatic UI
StatefulWidgetDynamic UI with setState()
NavigationNavigator or go_router
ProviderState management via ChangeNotifier
RiverpodCompile-time safe state management
httpSimple HTTP client
dioFeature-rich HTTP client
JSONfromJson() / 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

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.

Discussion