Don’t Put All Code in main.dart

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:

  1. The main() function.
  2. The root runApp() call.
  3. A root widget (usually a MaterialApp or CupertinoApp).
  4. 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

  1. Follow meaningful file names – e.g., home_screen.dart instead of screen1.dart.
  2. Create a screens/ folder – store each screen in its own file.
  3. Use a widgets/ folder – store reusable UI components.
  4. Avoid duplication – extract repeated UI into widgets.
  5. Keep main.dart clean – only entry logic and app setup.
  6. 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.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *