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:
- main.dart: The entry point of the Flutter app.
- 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.
- utils: General utility files like constants, themes, helper functions.
- models: Contains data models, typically Plain Old Dart Objects (PODO) that might be used to represent the shape of JSON data, database rows, etc.
- providers: This is where all the Providers for state management using the provider package will reside.
- services: Services like API calls, database handlers, authentication, etc.
- 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.
- widgets: Common or reusable widgets for the entire app.
-
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.