DEV Community

Seif Almotaz Bellah
Seif Almotaz Bellah

Posted on

Flutter Project Architecture: Organizing for Clarity and Efficiency

Hey Flutter devs! ๐Ÿ‘‹ Let's dive into an approach for organizing your Flutter project to ensure clean and maintainable code. I'll guide you through my preferred project structure, naming conventions, and some useful tips.

Project Structure Unveiled

Let's start with the breakdown of my project structure:

- core
  -- api
  -- models
  -- helpers
  -- utils
  -- constants
- pages
- services
- components (shared among all pages)
Enter fullscreen mode Exit fullscreen mode

Pages Unveiled

Maintaining order is key, especially with multiple pages. Take a user CRUD as an example, organize it like this:

pages/
|-- users/
    |-- update/
        |-- update.dart (UI widget)
        |-- controller.dart (GetX controller)
        |-- components (page-specific components)
Enter fullscreen mode Exit fullscreen mode

Pay attention to naming! For the widget file, I go for UsersUpdatePage, making it crystal clear that it's about updating users. This aids in autocomplete, making your coding life a breeze.

Controller Chronicles

Always keep logic away from UI. In the controller.dart file, meet the UsersUpdateController. Functions like onPressed and onTap find their home here.

Components Realm

For components specific to a page, create a folder within the page, like so:

pages/
|-- users/
    |-- update/
        |-- components/ (components specific to this page)
Enter fullscreen mode Exit fullscreen mode

For shared components used across all pages, maintain a separate folder:

components/ (shared components used across all pages)
Enter fullscreen mode Exit fullscreen mode

Services Hub

Global controllers or providers, like AuthProvider, find their spot here. Stick to the same naming conventions and logic separation as in the pages.

Constants Abode

For constants, such as API base URLs or color themes, create a folder. Keep it tidy and organized.

Coding Hacks

  1. Global Storage: Using GetStorage or SharedPreferences, create a class. It provides a readable way to handle keys and avoids typos.
class StorageProvider {
     static String? get userToken => getStorage.read<String>('userToken');
     static set userToken(String? token) => getStorage.write('userToken', token);
     static void removeUserToken() => getStorage.remove('userToken');
}
Enter fullscreen mode Exit fullscreen mode
  1. Models Folder: Keep models like UserModel in the models folder. If using JsonSerializable, organize generated code in a generated folder, simply by adding this code to your build.yaml file:
targets:
  $default:
    builders:
      source_gen:combining_builder:
        options:
          build_extensions:
            "^lib/core/models/{{}}.dart": "lib/core/models/generated/{{}}.g.dart"
Enter fullscreen mode Exit fullscreen mode

Logic in Controllers: The Saga of Separation

One of the core principles in software development is the separation of concerns. In Flutter, clean separation of UI and logic is achieved by placing logic in controllers.

1. UI in UsersUpdatePage:

Focus solely on the presentation layer when designing the UI in the UsersUpdatePage widget. Craft visual elements, arrange widgets, and define the layout. Keep complex logic away.

class UsersUpdatePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Update User'),
      ),
      body: Column(
        // UI elements here
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Functions in Controller:

For functions related to user updates, create a corresponding controller, like UsersUpdateController. Move functions like onPressed or onTap here.

class UsersUpdateController extends GetxController {
  // Controller logic here

  void updateUser() {
    // Logic for updating user
    // Access data, call APIs, perform validations, etc.
  }

  // Other functions related to the update process
}
Enter fullscreen mode Exit fullscreen mode

3. Benefits of Separation:

  • Readability and Maintainability: UI remains clean. Developers grasp and modify the presentation layer without being overwhelmed by complex business logic.

  • Reusability: Controllers can be reused across multiple pages or components, making it easy to maintain and scale your project.

  • Testing: Separating logic into controllers facilitates unit testing.

4. Example: Using the Controller in UI:

class UsersUpdatePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final UsersUpdateController _controller = UsersUpdateController();
    return Scaffold(
      appBar: AppBar(
        title: Text('Update User'),
      ),
      body: Column(
        children: [
          // UI elements here
          ElevatedButton(
            onPressed: _controller.updateUser,
            child: Text('Update'),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach keeps your codebase organized, modular, and easy to maintain, crucial for scalability.

Private Variables: Mastering Control and Modularity

In Flutter, managing your application's state is vital, and as projects grow, controlling your variables becomes essential. Private variables and accessor functions (getters and setters) play a significant role in achieving an organized and maintainable codebase.

1. Why Private Variables?

Private variables, denoted by an underscore _, are not accessible outside the declaring class. This encapsulation ensures that the internal state of your controller is not directly manipulated from other parts of your codebase.

2. Getter and Setter Functions:

Use getter and setter functions to interact with private variables. This adds control over how data is accessed and modified.

class UsersUpdateController extends GetxController {
  // Private variable
  String _userName = '';

  // Getter
  String get userName => _userName;

  // Setter
  set userName(String name) => _userName = name;

  // Other functions can now access _userName via the getter and setter
}
Enter fullscreen mode Exit fullscreen mode

3. Benefits of Encapsulation:

  • Controlled Access: Private variables restrict direct access, allowing you to manage when and how data is modified.

  • Data Validation: Setter functions enable you to add validation logic.

  • Centralized Logic: All logic related to a specific variable is centralized within the controller.

4. Example Usage:

class UsersUpdatePage extends StatelessWidget {
  final UsersUpdateController _controller = UsersUpdateController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Update User'),
      ),
      body: Column(
        children: [
          // UI elements here
          TextField(
            onChanged: (newName) => _controller.userName = newName,
            // Use _controller.userName to access the variable indirectly
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

By adopting this approach, you maintain control over your variables, ensure data consistency, and enhance the modularity of your Flutter project.

Widget Naming Odyssey: Clarity and Consistency

1. Page Widgets:

For pages, follow a clear and descriptive naming convention. Combine the feature name, action, and "Page."

class UsersUpdatePage extends StatelessWidget {
  // Widget code
}
Enter fullscreen mode Exit fullscreen mode

2. Component Widgets:

For reusable components within a page, keep names concise but descriptive.

class UserNameInput extends StatelessWidget {
  // Widget code
}
Enter fullscreen mode Exit fullscreen mode

Controller Naming Expedition: Organization and Readability

1. Page Controllers:

Follow a similar pattern for page controllers, incorporating the feature and action.

class UsersUpdate

Controller extends GetxController {
  // Controller logic
}
Enter fullscreen mode Exit fullscreen mode

2. Component Controllers:

If you have controllers specifically for components, consider appending "Controller" to the component's name.

class UserNameInputController extends GetxController {
  // Controller logic
}
Enter fullscreen mode Exit fullscreen mode

Components Naming Journey: Specificity and Reusability

1. Page-Specific Components:

For components specific to a page, keep the naming within the page's context.

pages/
|-- users/
    |-- update/
        |-- components/
            |-- user_name_input.dart
Enter fullscreen mode Exit fullscreen mode

2. Shared Components:

If a component is shared among multiple pages, consider a more general name.

components/
    |-- custom_button.dart
Enter fullscreen mode Exit fullscreen mode

Key Takeaways:

  1. Descriptive and Consistent: Maintain a consistent and descriptive naming convention for widgets, controllers, and components.

  2. Combine Feature and Action: Incorporate both the feature and the action into the names for better clarity.

  3. Contextual Naming: Consider the context of components, especially when organizing them within specific pages or making them reusable.

  4. Readability is Key: Prioritize readability over brevity. Clear names make code more accessible and understandable.

Conclusion

By keeping it simple, following consistent naming conventions, and separating concerns, your Flutter project will be clean, maintainable, and a joy to work on. Happy coding! ๐Ÿš€

This article is written with a help of AI for organizing the article context โœŒ๏ธ.

Top comments (0)