Introduction
When you start learning Flutter, the very first file you encounter is main.dart. This file is the entry point of every Flutter application. It contains the main() function and the root runApp() call that launches your app.
For beginners, it is tempting to put all of your application logic, UI code, and widgets inside main.dart. After all, it works for small projects and helps you quickly see results. However, as your application grows, putting everything in a single file leads to a giant, unmanageable, and messy codebase.
The recommended best practice in Flutter development is simple: don’t put all your code in main.dart. Instead, structure your code by splitting it into screens, widgets, and separate files. This makes your project modular, clean, and scalable.
Why You Should Not Put All Code in main.dart
1. Readability Issues
A single file with hundreds or thousands of lines of code becomes difficult to read and navigate. Developers spend more time scrolling than actually coding.
2. Hard to Maintain
If everything is inside main.dart, making a small change (like updating the login button style) means digging through irrelevant parts of the app. Maintenance becomes painful.
3. Difficult to Reuse Widgets
Reusable widgets like buttons, forms, or cards should be placed in separate files. If they remain inside main.dart, you cannot easily reuse them across different parts of the project.
4. Poor Collaboration
In team projects, if all code is in one file, multiple developers end up working on the same file. This causes merge conflicts in version control systems like Git.
5. Scalability Problems
A small app might survive with a single file, but as soon as you add multiple screens, state management, API calls, and navigation, the single-file approach collapses.
What Belongs in main.dart
The purpose of main.dart is to act as the entry point of your application. It should contain:
- The
main()function. - The root
runApp()call. - A root widget (usually a
MaterialApporCupertinoApp). - Basic app-level configuration such as theme, routes, and navigation.
Example:
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Best Practice Example',
theme: ThemeData(primarySwatch: Colors.blue),
home: LoginScreen(),
routes: {
'/home': (context) => HomeScreen(),
'/login': (context) => LoginScreen(),
},
);
}
}
Notice how main.dart is clean, short, and easy to read. It delegates actual screens and widgets to other files.
Splitting Code into Screens
In Flutter, screens (or pages) represent different parts of the user interface. For example:
home_screen.dart→ For the home page of your app.login_screen.dart→ For the login page.profile_screen.dart→ For the user profile page.
Each screen should be stored in its own file inside a screens/ directory.
Example: screens/login_screen.dart
import 'package:flutter/material.dart';
import '../widgets/custom_button.dart';
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
decoration: InputDecoration(labelText: 'Email'),
),
TextField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
SizedBox(height: 20),
CustomButton(
text: 'Login',
onPressed: () {
Navigator.pushNamed(context, '/home');
},
),
],
),
),
);
}
}
Splitting Code into Widgets
Widgets are the building blocks of Flutter applications. If you find yourself repeating the same code across multiple screens, extract that part into a separate widget.
Example: widgets/custom_button.dart
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),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
);
}
}
Now you can use CustomButton across different screens without duplicating code.
Example Project Structure
Here’s how a simple Flutter project should be organized:
lib/
main.dart
screens/
home_screen.dart
login_screen.dart
widgets/
custom_button.dart
This structure separates responsibilities:
main.dart→ App entry point and configuration.screens/→ UI screens for the app.widgets/→ Reusable UI components.
Benefits of Splitting Code
1. Clean and Readable Code
Each file has a clear purpose. Developers can quickly find the file they need without scrolling endlessly.
2. Easier Debugging
If there is an error in login_screen.dart, you know exactly where to look. No need to search through a 1000-line main.dart.
3. Better Collaboration
Multiple developers can work on different screens simultaneously without conflicts.
4. Code Reusability
Widgets like CustomButton or CustomCard can be reused across the entire app, saving time and reducing duplication.
5. Scalability
As your app grows, you can continue adding screens and widgets without cluttering the main file.
Advanced Project Organization
For larger apps, developers often follow advanced folder structures:
lib/
main.dart
screens/
home/
home_screen.dart
home_view_model.dart
login/
login_screen.dart
login_view_model.dart
widgets/
custom_button.dart
custom_text_field.dart
models/
user.dart
services/
auth_service.dart
utils/
validators.dart
This approach uses:
- Screens → UI pages.
- Widgets → Reusable UI elements.
- Models → Data structures (e.g., User, Product).
- Services → Logic for APIs, authentication, and storage.
- Utils → Helper functions.
Such a structure keeps even very large projects maintainable.
Case Study: Bad vs Good Practice
Bad Example – One Giant main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Login')),
body: Column(
children: [
TextField(decoration: InputDecoration(labelText: 'Email')),
TextField(decoration: InputDecoration(labelText: 'Password')),
ElevatedButton(
onPressed: () {},
child: Text('Login'),
),
],
),
),
));
}
Problems:
- All code is inside
main.dart. - No reusability of widgets.
- Hard to scale beyond a single screen.
Good Example – Modular Code
main.dart:
import 'package:flutter/material.dart';
import 'screens/login_screen.dart';
import 'screens/home_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Modular App',
theme: ThemeData(primarySwatch: Colors.blue),
home: LoginScreen(),
routes: {
'/home': (context) => HomeScreen(),
},
);
}
}
login_screen.dart:
import 'package:flutter/material.dart';
import '../widgets/custom_button.dart';
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Column(
children: [
TextField(decoration: InputDecoration(labelText: 'Email')),
TextField(decoration: InputDecoration(labelText: 'Password')),
CustomButton(
text: 'Login',
onPressed: () {
Navigator.pushNamed(context, '/home');
},
),
],
),
);
}
}
Best Practices for Splitting Code
- Follow meaningful file names – e.g.,
home_screen.dartinstead ofscreen1.dart. - Create a
screens/folder – store each screen in its own file. - Use a
widgets/folder – store reusable UI components. - Avoid duplication – extract repeated UI into widgets.
- Keep
main.dartclean – only entry logic and app setup. - Plan for scalability – organize project folders so that future growth is easy.
Frequently Asked Questions
Can I keep everything in main.dart for small apps?
Yes, for very small prototypes it is fine. But even then, splitting improves readability.
Does splitting code affect app performance?
No, it only affects code organization. The compiled app runs the same.
Should I create separate folders for models and services?
Yes, if your app involves APIs or complex logic, separating models and services makes the project more maintainable.
What about state management?
When you use providers, bloc, or riverpod, it is best to organize state files separately from UI code.
Leave a Reply