Setting Up a Structured Flutter App Using Provider: A Step-by-Step Guide

When structuring a Flutter project that uses the provider package for state management, it's crucial to ensure that your codebase is organized, scalable, and maintainable. Here's a suggested directory structure for a professional Flutter project using the Provider pattern:

lib/
|-- main.dart
|-- app.dart
|-- utils/
|   |-- constants.dart
|   |-- theme.dart
|-- models/
|   |-- user.dart
|   |-- order.dart
|-- providers/
|   |-- user_provider.dart
|   |-- order_provider.dart
|-- services/
|   |-- api_service.dart
|   |-- auth_service.dart
|-- screens/
|   |-- home/
|   |   |-- home_screen.dart
|   |   |-- home_viewmodel.dart
|   |-- profile/
|   |   |-- profile_screen.dart
|   |   |-- profile_viewmodel.dart
|-- widgets/
|   |-- custom_button.dart
|   |-- custom_textfield.dart
|-- config/
|   |-- routes.dart
|   |-- dependencies.dart
1lib/ 2|-- main.dart 3|-- app.dart 4|-- utils/ 5| |-- constants.dart 6| |-- theme.dart 7|-- models/ 8| |-- user.dart 9| |-- order.dart 10|-- providers/ 11| |-- user_provider.dart 12| |-- order_provider.dart 13|-- services/ 14| |-- api_service.dart 15| |-- auth_service.dart 16|-- screens/ 17| |-- home/ 18| | |-- home_screen.dart 19| | |-- home_viewmodel.dart 20| |-- profile/ 21| | |-- profile_screen.dart 22| | |-- profile_viewmodel.dart 23|-- widgets/ 24| |-- custom_button.dart 25| |-- custom_textfield.dart 26|-- config/ 27| |-- routes.dart 28| |-- dependencies.dart 29

Explanation:

  1. main.dart: The entry point of the Flutter app.
  2. app.dart: A separate file for the MaterialApp to keep the main.dart file clean. This is where you can set up theme, routes, initial screen, etc.
  3. utils: General utility files like constants, themes, helper functions.
  4. models: Contains data models, typically Plain Old Dart Objects (PODO) that might be used to represent the shape of JSON data, database rows, etc.
  5. providers: This is where all the Providers for state management using the provider package will reside.
  6. services: Services like API calls, database handlers, authentication, etc.
  7. screens: Each screen in the app gets its own sub-directory which might include:
    • _screen.dart: The widget file for the screen itself.
    • _viewmodel.dart: Logic and data that the screen needs, typically using ChangeNotifier.
  8. widgets: Common or reusable widgets for the entire app.
  9. config: Configuration-related files.

    • routes.dart: Defines named routes for navigation.
    • dependencies.dart: If you're using a dependency injection package like get_it, you can set up your service locators here.

When starting a new project, it's often helpful to use code generation tools or project templates to expedite the setup of such structures. This directory structure is just a suggestion; the "best" structure can vary depending on team preferences, project requirements, and other factors.

Creating an entire basic app with every file mentioned is a considerable task, but I'll provide you with a simplified version of each file to get you started. For simplicity, let's say our app displays a list of users.

1. main.dart

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

void main() => runApp(MyApp());
1import 'package:flutter/material.dart'; 2import 'app.dart'; 3 4void main() => runApp(MyApp()); 5

2. app.dart

import 'package:flutter/material.dart';
import 'config/routes.dart';
import 'screens/home/home_screen.dart';
import 'utils/theme.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider Demo',
      theme: appTheme,
      routes: routes,
      home: HomeScreen(),
    );
  }
}
1import 'package:flutter/material.dart'; 2import 'config/routes.dart'; 3import 'screens/home/home_screen.dart'; 4import 'utils/theme.dart'; 5 6class MyApp extends StatelessWidget { 7 @override 8 Widget build(BuildContext context) { 9 return MaterialApp( 10 title: 'Provider Demo', 11 theme: appTheme, 12 routes: routes, 13 home: HomeScreen(), 14 ); 15 } 16} 17

3. utils/constants.dart

const String API_ENDPOINT = "https://api.example.com";
1const String API_ENDPOINT = "https://api.example.com"; 2

4. utils/theme.dart

import 'package:flutter/material.dart';

final ThemeData appTheme = ThemeData(
  primarySwatch: Colors.blue,
);
1import 'package:flutter/material.dart'; 2 3final ThemeData appTheme = ThemeData( 4 primarySwatch: Colors.blue, 5); 6

5. models/user.dart

class User {
  final String id;
  final String name;

  User({required this.id, required this.name});
}
1class User { 2 final String id; 3 final String name; 4 5 User({required this.id, required this.name}); 6} 7

6. providers/user_provider.dart

import 'package:flutter/material.dart';
import 'models/user.dart';
import 'services/api_service.dart';

class UserProvider with ChangeNotifier {
  List<User> _users = [];

  List<User> get users => _users;

  fetchUsers() async {
    _users = await ApiService.fetchUsers();
    notifyListeners();
  }
}
1import 'package:flutter/material.dart'; 2import 'models/user.dart'; 3import 'services/api_service.dart'; 4 5class UserProvider with ChangeNotifier { 6 List<User> _users = []; 7 8 List<User> get users => _users; 9 10 fetchUsers() async { 11 _users = await ApiService.fetchUsers(); 12 notifyListeners(); 13 } 14} 15

7. services/api_service.dart

import '../models/user.dart';
import '../utils/constants.dart';

class ApiService {
  static Future<List<User>> fetchUsers() async {
    // Normally we'd call the API here, but for simplicity, we'll return a dummy list.
    return [
      User(id: '1', name: 'John'),
      User(id: '2', name: 'Doe'),
    ];
  }
}
1import '../models/user.dart'; 2import '../utils/constants.dart'; 3 4class ApiService { 5 static Future<List<User>> fetchUsers() async { 6 // Normally we'd call the API here, but for simplicity, we'll return a dummy list. 7 return [ 8 User(id: '1', name: 'John'), 9 User(id: '2', name: 'Doe'), 10 ]; 11 } 12} 13

8. screens/home/home_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/user_provider.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users')),
      body: Consumer<UserProvider>(
        builder: (context, userProvider, child) {
          return ListView.builder(
            itemCount: userProvider.users.length,
            itemBuilder: (context, index) {
              return ListTile(title: Text(userProvider.users[index].name));
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Provider.of<UserProvider>(context, listen: false).fetchUsers(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}
1import 'package:flutter/material.dart'; 2import 'package:provider/provider.dart'; 3import '../../providers/user_provider.dart'; 4 5class HomeScreen extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return Scaffold( 9 appBar: AppBar(title: Text('Users')), 10 body: Consumer<UserProvider>( 11 builder: (context, userProvider, child) { 12 return ListView.builder( 13 itemCount: userProvider.users.length, 14 itemBuilder: (context, index) { 15 return ListTile(title: Text(userProvider.users[index].name)); 16 }, 17 ); 18 }, 19 ), 20 floatingActionButton: FloatingActionButton( 21 onPressed: () => Provider.of<UserProvider>(context, listen: false).fetchUsers(), 22 child: Icon(Icons.refresh), 23 ), 24 ); 25 } 26} 27

9. widgets/custom_button.dart (example widget)

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;

  CustomButton({required this.text, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(text),
    );
  }
}
1import 'package:flutter/material.dart'; 2 3class CustomButton extends StatelessWidget { 4 final String text; 5 final VoidCallback onPressed; 6 7 CustomButton({required this.text, required this.onPressed}); 8 9 @override 10 Widget build(BuildContext context) { 11 return ElevatedButton( 12 onPressed: onPressed, 13 child: Text(text), 14 ); 15 } 16} 17

10. config/routes.dart

import 'package:flutter/material.dart';
import '../screens/home/home_screen.dart';

final Map<String, WidgetBuilder> routes = {
  '/': (context) => HomeScreen(),
};
1import 'package:flutter/material.dart'; 2import '../screens/home/home_screen.dart'; 3 4final Map<String, WidgetBuilder> routes = { 5 '/': (context) => HomeScreen(), 6}; 7

11. services/auth_service.dart (example service)

class AuthService {
  static Future<bool> isAuthenticated() async {
    return true; // This is just a dummy function for the sake of example.
  }
}
1class AuthService { 2 static Future<bool> isAuthenticated() async { 3 return true; // This is just a dummy function for the sake of example. 4 } 5} 6

For brevity, some files such as profile_screen.dart, profile_viewmodel.dart, order.dart, order_provider.dart, and custom_textfield.dart were not included. Also, this is a very basic example, and in a real-world scenario, you'd have more comprehensive logic, error handling, more features, etc.

Remember to include the necessary dependencies (flutter/material.dart, provider, etc.) in each file and to integrate the provider package by adding it to your pubspec.yaml file.