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
-
Global Storage: Using
GetStorage
orSharedPreferences
, 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');
}
-
Models Folder: Keep models like
UserModel
in the models folder. If usingJsonSerializable
, organize generated code in agenerated
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"
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
),
);
}
}
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
}
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'),
),
],
),
);
}
}
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
}
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
),
],
),
);
}
}
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
}
2. Component Widgets:
For reusable components within a page, keep names concise but descriptive.
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.
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.
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:
Descriptive and Consistent: Maintain a consistent and descriptive naming convention for widgets, controllers, and components.
Combine Feature and Action: Incorporate both the feature and the action into the names for better clarity.
Contextual Naming: Consider the context of components, especially when organizing them within specific pages or making them reusable.
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)