ExoSnip Code Snippets

Flutter Snippets

← Back to Home

About Flutter

Flutter lets you build mobile, desktop, and web apps from one Dart codebase. This track covers project setup, core widgets, async data loading, and a simple testing loop.

Create a New Flutter App

Generate a starter app, enter the project, and run it on a connected simulator or device.

flutter create habit_flow
cd habit_flow
flutter pub add http
flutter run

Recommended Project Structure

Split features, data, and widgets early so the app stays navigable as screens grow.

lib/
  main.dart
  app.dart
  features/
    habits/
      data/
        habit_service.dart
      models/
        habit.dart
      presentation/
        habit_list_screen.dart
        widgets/
          habit_tile.dart
  shared/
    theme/
      app_theme.dart
    widgets/
      app_scaffold.dart
test/
  widget_test.dart

Bootstrap MaterialApp (lib/main.dart)

Keep app-level theme and routes near the entrypoint so every screen inherits the same setup.

import 'package:flutter/material.dart';

void main() {
  runApp(const HabitFlowApp());
}

class HabitFlowApp extends StatelessWidget {
  const HabitFlowApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Habit Flow',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const HabitListScreen(),
    );
  }
}

class HabitListScreen extends StatelessWidget {
  const HabitListScreen({super.key});

  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: Text('Your habits go here')),
    );
  }
}

Reusable Stateless Widget

Move repeated UI into small widgets with explicit inputs instead of rebuilding layout inline.

import 'package:flutter/material.dart';

class HabitTile extends StatelessWidget {
  const HabitTile({
    super.key,
    required this.name,
    required this.streak,
    required this.onTap,
  });

  final String name;
  final int streak;
  final VoidCallback onTap;

  
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(name),
      subtitle: Text('Streak: $streak days'),
      trailing: const Icon(Icons.chevron_right),
      onTap: onTap,
    );
  }
}

Form State with StatefulWidget

Keep small, screen-local input state inside a StatefulWidget before reaching for heavier state libraries.

import 'package:flutter/material.dart';

class AddHabitScreen extends StatefulWidget {
  const AddHabitScreen({super.key});

  
  State<AddHabitScreen> createState() => _AddHabitScreenState();
}

class _AddHabitScreenState extends State<AddHabitScreen> {
  final _controller = TextEditingController();

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _submit() {
    final name = _controller.text.trim();
    if (name.isEmpty) return;
    Navigator.pop(context, name);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Add Habit')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(controller: _controller),
            const SizedBox(height: 12),
            ElevatedButton(onPressed: _submit, child: const Text('Save')),
          ],
        ),
      ),
    );
  }
}

Model JSON Data with a Dart Class

Parsing at the edge keeps your widgets focused on rendering instead of data cleanup.

class Habit {
  const Habit({
    required this.id,
    required this.name,
    required this.streak,
  });

  final String id;
  final String name;
  final int streak;

  factory Habit.fromJson(Map<String, dynamic> json) {
    return Habit(
      id: json['id'] as String,
      name: json['name'] as String,
      streak: json['streak'] as int,
    );
  }

  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'streak': streak,
      };
}

Fetch Remote Data in a Service

Wrap network calls in a service so screens depend on a clear API instead of raw HTTP details.

import 'dart:convert';
import 'package:http/http.dart' as http;

class HabitService {
  Future<List<Habit>> fetchHabits() async {
    final response = await http.get(Uri.parse('https://example.com/api/habits'));

    if (response.statusCode != 200) {
      throw Exception('Failed to load habits');
    }

    final data = jsonDecode(response.body) as List<dynamic>;
    return data
        .map((item) => Habit.fromJson(item as Map<String, dynamic>))
        .toList();
  }
}

Render Async Data with FutureBuilder

FutureBuilder is a simple starting point for one-shot loading states and error handling.

class HabitListScreen extends StatelessWidget {
  HabitListScreen({super.key});

  final HabitService service = HabitService();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Habits')),
      body: FutureBuilder<List<Habit>>(
        future: service.fetchHabits(),
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text(snapshot.error.toString()));
          }

          final habits = snapshot.data ?? const [];
          return ListView.builder(
            itemCount: habits.length,
            itemBuilder: (context, index) {
              final habit = habits[index];
              return HabitTile(
                name: habit.name,
                streak: habit.streak,
                onTap: () {},
              );
            },
          );
        },
      ),
    );
  }
}

Push to a Named Route

Start with Navigator APIs directly before adopting a routing package for larger apps.

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HabitListScreen(),
    '/add-habit': (context) => const AddHabitScreen(),
  },
)

FloatingActionButton(
  onPressed: () => Navigator.pushNamed(context, '/add-habit'),
  child: const Icon(Icons.add),
)

Widget Test for a Screen

Test visible behavior first so refactors do not break user-facing flows silently.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('shows add habit button', (tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: null,
            child: Icon(Icons.add),
          ),
        ),
      ),
    );

    expect(find.byIcon(Icons.add), findsOneWidget);
  });
}

Next Learning Upgrades

Once the basics are comfortable, expand into production-ready Flutter patterns.

1. Add freezed or sealed classes for richer state models.
2. Add Riverpod or Bloc when local widget state stops scaling.
3. Add Dio interceptors for auth, retries, and logging.
4. Add integration tests for navigation and forms.
5. Add offline caching with Hive or Drift.