Flutter Project Architecture: Organizing for Clarity and Efficiency
Tags: flutter, dart, codequality
Published: 2023-12-30 16:36:57

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)
```

### 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)
```

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)
```

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

```
components/ (shared components used across all pages)
```

### 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.

```dart
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');
}
```

2. **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:
```yaml
targets:
  $default:
    builders:
      source_gen:combining_builder:
        options:
          build_extensions:
            "^lib/core/models/{{}}.dart": "lib/core/models/generated/{{}}.g.dart"
```

## 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.

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

#### 2. **Functions in Controller:**

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

```dart
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
}
```

#### 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:**

```dart
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'),
          ),
        ],
      ),
    );
  }
}
```

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.

```dart
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
}
```

#### 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:**

```dart
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
          ),
        ],
      ),
    );
  }
}
```

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."

```dart
class UsersUpdatePage extends StatelessWidget {
  // Widget code
}
```

#### 2. **Component Widgets:**

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

```dart
class UserNameInput extends StatelessWidget {
  // Widget code
}
```

## Controller Naming Expedition: Organization and Readability

#### 1. **Page Controllers:**

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

```dart
class UsersUpdate

Controller extends GetxController {
  // Controller logic
}
```

#### 2. **Component Controllers:**

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

```dart
class UserNameInputController extends GetxController {
  // Controller logic
}
```

## 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
```

#### 2. **Shared Components:**

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

```
components/
    |-- custom_button.dart
```

## 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 ✌️.