
How to structure your Flutter projects
When creating a new flutter project, you might find yourself confused with how you are supposed to organize your app architecture and to figure out where certain pieces of code should go.
If you have been following a lot of tutorials to learn Flutter, you’ll quickly realize that they all have very messy ways of organizing their code to quickly develop their code and teach you the topic at hand.
Now in most cases this is fine, but say you are building a large scale project or trying to convert an already existing one; You don’t want to end up putting all of your code in the /lib
directory! And by wasting too much time figuring this out, you’ll slow yourself down on development and confuse yourself on where everything goes.
So in this tutorial we’re going to look at an effective way of organizing your flutter project.
This starts off with your app architecture. This is heavily influenced by what state management tool you decide to use, however many of the practices do remain the same. The end goal is to separate your business logic (app functionality/what the user doesn’t see) from your UI (What the user sees/interacts with).
The architecture used in this tutorial is based on the Riverpod state management tool created by Remi Rousselet.
According to pub.dev, “Riverpod is a Declarative programming for Flutter state management. It allows you to write business logic like Stateless widgets, easily implement ‘pull to refresh’ and more and it enhances compiler, provides lint rules, etc. Learn more at https://riverpod.dev/“.
Note: Some of the syntax in the code examples are autogenerated from Riverpod Generator.
Getting Started
Now that we’ve cleared the introductory things out of the way, let’s dive into how we can structure our project! Open up a new terminal window and run the following command to get your project set up:
flutter create .
Once that’s done running, we’ll have the following folder structure:
‣ .dart_tool
‣ .idea
‣ android
‣ ios
‣ lib
‣ linux
‣ macos
‣ test
‣ web
‣ windows
.gitignore
.metadata
analysis_options.yaml
(app_name).iml
pubspec.lock
pubspec.yaml
README.md
For the purposes of this tutorial we’re going to focus on the folder structure of the /lib
directory (where all the magic happens).
Moving to /src folder
The first thing you can go ahead and do is create a folder under the /lib
directory called /src
. This is where all the functionality of the app will live. Other than /src
the only thing you want inside of /lib
is main.dart
.
/config folder
The first folder we are going to create will be called /config
. This folder is going to contain data that will determine the way our app behaves; Essentially general configuration for your app. You can think of it like the settings screen of an app but this time for your code.
Constants
We are now going to add some more folders and files in here. Create the following 2 files: constants.dart
and environment_keys.dart
. Fairly self explanatory files. Think of constants.dart
as a file that simply contains all immutable global variables in your app.
Here’s an example:
// src/config/constants.dart
// App Configurations
// Dart imports:
// Flutter imports:
import 'package:flutter/material.dart';
// Project imports:
import 'package:clubology/src/config/themes/system_color.dart';
// Package imports:
const String kAppTitle = 'Clubology';
const String kAppVersionNumber = 'v1.3.3 (37)';
const String kFontFamily = 'General Sans';
const String kAppIconImage = 'assets/icons/app_icon.png';
const String kSplashScreenImage = 'assets/images/splash_screen.png';
// App Padding Configurations
const double kXXXXLPadding = 48.0;
const double kXXXLPadding = 40.0;
const double kXXLPadding = 32.0;
const double kXLPadding = 28.0;
const double kLargePadding = 24.0;
const double kDefaultPadding = 16.0;
const double kMediumPadding = 12.0;
const double kSmallPadding = 10.0;
const double kXSPadding = 8.0;
const double kXXSPadding = 6.0;
const double kXXXSPadding = 4.0;
// App BorderRadii Configurations
const double kDefaultBorderRadius = 20.0;
const double kXSBorderRadius = 4.0;
const double kSmallBorderRadius = 6.0;
const double kMediumBorderRadius = 8.0;
const double kLargeBorderRadius = 12.0;
const double kMaxBorderRadius = 999.0;
// Icon Paths
const String kLogoImagePath = 'assets/icons/logo.png';
const String kLogoLargeImagePath = 'assets/icons/logo_large.png';
const String kAltLogoImagePath = 'assets/icons/alt_logo.png';
const String kStoreIconImagePath = 'assets/icons/store_icon.png';
const String kAppIconImagePath = 'assets/icons/app_icon.png';
const String kAndroidAppIconImagePath = 'assets/icons/android_app_icon.png';
const String kProfilePicture = 'assets/icons/pfp.png';
// Image Paths
const String kSplashScreenImagePath = 'assets/images/splash_screen.png';
// Theme
const String kLightModeImagePath = 'assets/images/light_mode_image.png';
const String kDarkModeImagePath = 'assets/images/dark_mode_image.png';
const String kNormalCaseImagePath = 'assets/images/normal_case_image.png';
const String kLowercaseImagePath = 'assets/images/lowercase_image.png';
// Club Banners
const String kClubBannerOne = 'assets/images/club_banner_one.png';
const String kClubBannerTwo = 'assets/images/club_banner_two.png';
const String kClubBannerThree = 'assets/images/club_banner_three.png';
const String kClubBannerFour = 'assets/images/club_banner_four.png';
const List<String> kClubBannerImages = [kClubBannerOne, kClubBannerTwo, kClubBannerThree, kClubBannerFour];
// Scripts
const String kCronParserJs = 'scripts/cron_parser/out.js';
// Shadows
List<BoxShadow> kMediumBoxShadow = [
BoxShadow(offset: const Offset(0, 4), spreadRadius: -1, blurRadius: 6, color: SystemColor.black.withOpacity(0.1)),
BoxShadow(offset: const Offset(0, 2), spreadRadius: -2, blurRadius: 4, color: SystemColor.black.withOpacity(0.1)),
];
// Borders
BoxBorder kDefaultBoxBorder = Border.all(color: SystemColor.gray300, width: 1);
BoxBorder kActiveBoxBorder = Border.all(color: SystemColor.primary500, width: 1);
// General
final gradeLevels = <String>[
'Not Set',
'Elementary School',
'Middle School',
'High School Freshman (Grade 9, Year 10)',
'High School Sophomore (Grade 10, Year 11)',
'High School Junior (Grade 11, Year 12)',
'High School Senior (Grade 12, Year 13)',
'College Undergraduate',
'College Graduate',
];
Map<String, String> bearerTokenAuthorizationHeader(String token) => {
'Authorization': 'Bearer $token',
};
extension DateOnlyCompare on DateTime {
bool isSameDate(DateTime other) {
return year == other.year && month == other.month && day == other.day;
}
}
In here we have UI related constants as well as general data related constants and app configurations.
Environment Keys
Now for the environment_keys.dart
file, think of it exactly like a .env
file if you’ve worked with them. It will contain all your public, private, secret keys and other URLs and data of that sort. Using .env
is not a common design pattern in flutter, so we improvise by using an actual .dart
file. Essentially works like the constants.dart
file in the sense that you’re creating a bunch of immutable values.
WARNING: Make sure you add this file to your .gitignore
. Flutter will not automatically add this because it’s a custom made file so make sure you do this before committing.
Themes
Notice how /themes
is a folder? Incase you want to include dark mode in your app, you would want to create 2 separate theme files. In here I also like to keep my typography variables/text theme as well as my colors. Essentially the design system of your app will live here.
▾ themes
light_theme.dart
dark_theme.dart
text_theme.dart
source_colors.dart
Exceptions
If you are building an app that requires API endpoint access, then it’s definitely a good idea to create a file to house the different types of exceptions/errors that you may encounter. It’s usually a good idea to split them up based on the API so that’s why /exceptions
should be a folder.
▾ exceptions
api_exceptions.dart
auth_exceptions.dart
This leaves our config folder looking like the following:
▾ config
▾ exceptions
api_exceptions.dart
auth_exceptions.dart
▾ themes
light_theme.dart
dark_theme.dart
text_theme.dart
source_colors.dart
constants.dart
environment_keys.dart
Feel free to put other immutable files that hold values and functions here as well!
/models folder
This folder is, as the name suggests, a folder to house all of your models/classes. They will contain methods to convert JSON data to usable classes that you can utilize inside of your app as well.
Here’s an example of what the folder may look like:
▾ models
club.dart
member.dart
...
And here’s what one of your classes might look like:
// src/models/club.dart
// Dart imports:
import 'dart:convert';
// Flutter imports:
import 'package:flutter/foundation.dart';
// Package imports:
import 'package:event_calendar/event_calendar.dart';
import 'package:intl/intl.dart';
class CbClubModel {
String id;
String name;
String code;
String? schoolId;
String description;
String qrCode;
List<Map<String, dynamic>> externalResources;
String banner;
String meetingSchedule;
bool nextMeeting;
int joinStatus;
bool clubCodeVisibleToMembers;
bool bannerIsTiled;
String dateCreated;
String dateLastModified;
CbClubModel({
required this.id,
required this.name,
required this.code,
this.schoolId,
required this.description,
required this.qrCode,
required this.externalResources,
required this.banner,
required this.meetingSchedule,
required this.nextMeeting,
required this.joinStatus,
required this.clubCodeVisibleToMembers,
required this.bannerIsTiled,
required this.dateCreated,
required this.dateLastModified,
});
String get meetingScheduleString {
final ms = meetingSchedule;
if (ms.isEmpty) return ms;
if (ms == 'ond') {
return 'Meetings will occur On Demand';
} else {
final mtng = Event.fromJson(json.decode(ms) as Map<String, dynamic>);
final preferredDateFormat = DateFormat('EEE, MMM d, y @ ${DateFormat.jm().pattern}');
String intrvTxt() {
switch (mtng.recurrenceRule!.frequency) {
case Frequency.daily:
return 'day(s)';
case Frequency.weekly:
return 'week(s)';
case Frequency.monthly:
return 'month(s)';
default:
return 'day(s)';
}
}
String intrContTxt() {
final intrv = mtng.recurrenceRule!.interval;
if (intrv == 1) {
return '';
} else {
return '$intrv ';
}
}
String daysOfTheWeekTxt() {
final days = <String>[];
for (var e in mtng.recurrenceRule!.byDay!) {
final d = e.toString();
days.add('${d.substring(d.indexOf('.') + 1)}s');
}
return days.join(', ');
}
daysOfTheWeekTxt();
String timeZoneExtension() {
if (mtng.currentDate!.toLocal().timeZoneName != mtng.currentDate!.timeZoneName) {
return ' (${preferredDateFormat.format(mtng.startDate!).substring(preferredDateFormat.format(mtng.startDate!).indexOf('@') + 1).trimLeft()} ${mtng.startDate!.timeZoneName})';
} else {
return '';
}
}
return 'Meetings will occur every ${intrContTxt()}${intrvTxt()} at${preferredDateFormat.format(mtng.startDate!.toLocal()).substring(preferredDateFormat.format(mtng.startDate!.toLocal()).indexOf('@') + 1)}${timeZoneExtension()} on ${daysOfTheWeekTxt()}';
}
}
CbClubModel copyWith({
String? id,
String? name,
String? code,
String? schoolId,
String? description,
String? qrCode,
List<Map<String, dynamic>>? externalResources,
String? banner,
String? meetingSchedule,
bool? nextMeeting,
int? joinStatus,
bool? clubCodeVisibleToMembers,
bool? bannerIsTiled,
String? dateCreated,
String? dateLastModified,
}) {
return CbClubModel(
id: id ?? this.id,
name: name ?? this.name,
code: code ?? this.code,
schoolId: schoolId ?? this.schoolId,
description: description ?? this.description,
qrCode: qrCode ?? this.qrCode,
externalResources: externalResources ?? this.externalResources,
banner: banner ?? this.banner,
meetingSchedule: meetingSchedule ?? this.meetingSchedule,
nextMeeting: nextMeeting ?? this.nextMeeting,
joinStatus: joinStatus ?? this.joinStatus,
clubCodeVisibleToMembers: clubCodeVisibleToMembers ?? this.clubCodeVisibleToMembers,
bannerIsTiled: bannerIsTiled ?? this.bannerIsTiled,
dateCreated: dateCreated ?? this.dateCreated,
dateLastModified: dateLastModified ?? this.dateLastModified,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'code': code,
'schoolId': schoolId,
'description': description,
'qrCode': qrCode,
'externalResources': externalResources,
'banner': banner,
'meetingSchedule': meetingSchedule,
'nextMeeting': nextMeeting,
'joinStatus': joinStatus,
'clubCodeVisibleToMembers': clubCodeVisibleToMembers,
'bannerIsTiled': bannerIsTiled,
'dateCreated': dateCreated,
'dateLastModified': dateLastModified,
};
}
Map<String, dynamic> toMapWithUser(String userId) {
return {
'id': id,
'userId': userId,
'name': name,
'code': code,
'schoolId': schoolId,
'description': description,
'qrCode': qrCode,
'externalResources': externalResources,
'banner': banner,
'meetingSchedule': meetingSchedule,
'nextMeeting': nextMeeting,
'joinStatus': joinStatus,
'clubCodeVisibleToMembers': clubCodeVisibleToMembers,
'bannerIsTiled': bannerIsTiled,
'dateCreated': dateCreated,
'dateLastModified': dateLastModified,
};
}
factory CbClubModel.fromMap(Map<String, dynamic> map) {
return CbClubModel(
id: map['_id'] ?? '',
name: map['name'] ?? '',
schoolId: map['schoolId'],
code: map['code'] ?? '',
description: map['description'] ?? '',
qrCode: map['qrCode'] ?? '',
externalResources: List<Map<String, dynamic>>.from(map['externalResources']),
banner: map['banner'] ?? '',
meetingSchedule: map['meetingSchedule'] ?? '',
nextMeeting: map['nextMeeting'] ?? true,
joinStatus: map['joinStatus']?.toInt() ?? 0,
clubCodeVisibleToMembers: map['clubCodeVisibleToMembers'] ?? true,
bannerIsTiled: map['bannerIsTiled'] ?? false,
dateCreated: map['dateCreated'] ?? '',
dateLastModified: map['dateLastModified'] ?? '',
);
}
factory CbClubModel.blank() {
return CbClubModel(
id: '',
name: '',
schoolId: '',
code: '',
description: '',
qrCode: '',
externalResources: [],
banner: '',
meetingSchedule: '',
nextMeeting: true,
joinStatus: 0,
clubCodeVisibleToMembers: true,
bannerIsTiled: false,
dateCreated: '',
dateLastModified: '',
);
}
String toJson() => json.encode(toMap());
String toJsonWithUser(String userId) => json.encode(toMapWithUser(userId));
factory CbClubModel.fromJson(String source) => CbClubModel.fromMap(json.decode(source));
String toString() {
return 'CbClub(id: $id, name: $name, code: $code, schoolId: $schoolId, description: $description, qrCode: $qrCode, externalResources: $externalResources, banner: $banner, meetingSchedule: $meetingSchedule, nextMeeting: $nextMeeting, joinStatus: $joinStatus, clubCodeVisibleToMembers: $clubCodeVisibleToMembers, bannerIsTiled: $bannerIsTiled, dateCreated: $dateCreated, dateLastModified: $dateLastModified)';
}
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CbClubModel &&
other.id == id &&
other.name == name &&
other.code == code &&
other.schoolId == schoolId &&
other.description == description &&
other.qrCode == qrCode &&
listEquals(other.externalResources, externalResources) &&
other.banner == banner &&
other.meetingSchedule == meetingSchedule &&
other.nextMeeting == nextMeeting &&
other.joinStatus == joinStatus &&
other.clubCodeVisibleToMembers == clubCodeVisibleToMembers &&
other.bannerIsTiled == bannerIsTiled &&
other.dateCreated == dateCreated &&
other.dateLastModified == dateLastModified;
}
int get hashCode {
return id.hashCode ^
name.hashCode ^
code.hashCode ^
schoolId.hashCode ^
description.hashCode ^
qrCode.hashCode ^
externalResources.hashCode ^
banner.hashCode ^
meetingSchedule.hashCode ^
nextMeeting.hashCode ^
joinStatus.hashCode ^
clubCodeVisibleToMembers.hashCode ^
bannerIsTiled.hashCode ^
dateCreated.hashCode ^
dateLastModified.hashCode;
}
}
You can always put in custom methods and values in here. I’d try to avoid this pattern in some cases and perform the functionality of files such as intrContTxt()
inside of your custom backend API, unless you’re retrieving the information from another source. Try to make this file as simple and as consistent as possible whilst having the necessary functions.
/services folder
If your app requires any external API/internet/local API functionality, you want to put it in here. The services folder will be responsible for managing that.
Once again you will need subdivided folders to manage the different types of services you’re implementing. If you need a folder for local storage services, then create a folder called /storage_services
, if you need one for authentication providers (or auth in general) then create one called /auth_services
.
Here’s what the structure should look like:
▾ services
▾ authentication_services
apple_sign_in_service.dart
google_sign_in_service.dart
email_password_sign_in_service.dart
...
▾ database_services
club_service.dart
member_service.dart
...
dio_provider.dart
...
If you have a custom backend API setup for your app, for example, an express.js server, then you will want to create a /database_services
folder. In this folder, for each endpoint, create a file for it and add the functions needed to access each endpoint.
Here’s an example file:
// src/services/database_services/club_service.dart
// Dart imports:
import 'dart:async';
// Package imports:
import 'package:dio/dio.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
// Project imports:
import 'package:clubology/src/config/constants.dart';
import 'package:clubology/src/config/exceptions/api_exceptions.dart';
import 'package:clubology/src/models/club.dart';
import 'package:clubology/src/models/club_announcement.dart';
import 'package:clubology/src/models/club_busy_date.dart';
import 'package:clubology/src/models/club_event.dart';
import 'package:clubology/src/models/club_event_going.dart';
import 'package:clubology/src/models/club_event_interested.dart';
import 'package:clubology/src/models/club_event_not_going.dart';
import 'package:clubology/src/models/club_poll.dart';
import 'package:clubology/src/models/club_poll_option.dart';
import 'package:clubology/src/models/club_poll_vote.dart';
import 'package:clubology/src/models/club_post.dart';
import 'package:clubology/src/models/club_resource.dart';
import 'package:clubology/src/models/member.dart';
import 'package:clubology/src/models/request.dart';
import 'package:clubology/src/models/user.dart';
import 'package:clubology/src/services/authentication_services/firebase_service.dart';
import 'package:clubology/src/services/dio_provider.dart';
import 'package:clubology/src/ui/views/club_views/club_polls_view/club_create_poll_service.dart';
part 'club_service.g.dart';
class CbClubRepository extends _$CbClubRepository {
/// Finds Club by ID
Future<CbClubModel> getClubById(GetClubByIdRef ref, {required String id}) async {
try {
final token = await ref.watch(firebaseAuthProvider).currentUser!.getIdToken();
final response =
await ref.watch(dioProvider(headers: bearerTokenAuthorizationHeader(token), subDomain: '/clubs')).get(
'/$id/club',
);
final result = Map<String, dynamic>.from(response.data);
final club = CbClubModel.fromMap(result);
await FirebaseAnalytics.instance.logEvent(
name: "getClubById",
parameters: {
"club_id": id,
},
);
return club;
} on DioError catch (e) {
throw APIExceptions.fromDioError(e);
}
}
/// Joins Club
Future<int> joinClub(JoinClubRef ref, {required CbMemberModel member}) async {
try {
final token = await ref.watch(firebaseAuthProvider).currentUser!.getIdToken();
final response = await ref
.watch(dioProvider(headers: bearerTokenAuthorizationHeader(token), subDomain: '/clubs'))
.post('/join', data: member.toMap());
return response.statusCode ?? 500;
} on DioError catch (e) {
throw APIExceptions.fromDioError(e);
}
}
// ...
It’s also a highly recommended practice to use another external package to perform your GET, POST, PUT, DELETE, etc. operations. The most popular package to do this is dio. According to their site, Dio is “a powerful HTTP networking package, supports Interceptors, Aborting and canceling a request, Custom adapters, Transformers, etc.”
Here’s my configuration for Dio that you can use in your code as well! It comes with a JSON Parser, Transformer and automatic Caching!
// src/services/dio_provider.dart
// Dart imports:
import 'dart:async';
import 'dart:convert';
// Flutter imports:
import 'package:flutter/foundation.dart';
// Package imports:
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sentry_dio/sentry_dio.dart';
// Project imports:
import 'package:clubology/src/config/environment_keys.dart';
// import 'package:pretty_dio_logger/pretty_dio_logger.dart';
part 'dio_provider.g.dart';
class DioTransformer extends DefaultTransformer {
DioTransformer() : super(jsonDecodeCallback: _parseJson);
}
// Must be top-level function
_parseAndDecode(String response) {
return jsonDecode(response);
}
_parseJson(String text) {
return compute(_parseAndDecode, text);
}
Dio dio(DioRef ref, {Map<String, dynamic>? headers, String? subDomain}) {
final dio = Dio(BaseOptions(baseUrl: '$kBaseAPIURL${subDomain ?? ''}', headers: headers))
..transformer = DioTransformer();
if (kReleaseMode) {
dio.addSentry(captureFailedRequests: true, networkTracing: true);
} else {
// dio.interceptors.add(
// PrettyDioLogger(
// requestHeader: true,
// requestBody: true,
// responseBody: true,
// responseHeader: true,
// error: true,
// compact: true,
// ),
// );
}
return dio;
}
extension AutoDisposeRefCache on AutoDisposeRef {
// keeps the provider alive for [duration] since when it was first created
// (even if all the listeners are removed before then)
void cacheFor(Duration duration) {
final link = keepAlive();
final timer = Timer(duration, () => link.close());
onDispose(() => timer.cancel());
}
}
/ui folder
Now here’s where the real fun begins. We successfully separated our business logic from our screens through all the previous folders. Now we our going to create our screens and components.
Note: If you’d like, you can move the /themes
folder in here.
Components
We are going to separate everything into 2 folders, /components
and /screens
. The /components
folder will simply house all of the reusable widgets that you use across multiple screens througout your app. You can put custom UI components here or even replace the default material or cupertino widgets provided by Flutter in here as well!
Here’s an example of what it may look like:
▾ components
primary_button.dart
show_club_bottom_sheet.dart
action_button.dart
system_text_input.dart
close_button.dart
spinner.dart
...
Views
Note: View and Screen are the same.
We will separate all of our views into folders. Inside of each folder will contain, not only the screen, but the components that are specifically for the screen.
It’s a good practice to create another subfolder inside of the view folder called /components
to distinguish where the actual view is and the components. It will make your imports a bit neater and helps you easily find your files. This is a good practice but you don’t have to do it if you don’t believe your app/code needs it.
I also recommend naming the components with a naming convention that includes the name of the screen in some way, shape or form, whether that’s an abbreviation or the name of it itself. Once again, entirely up to you.
Another practice that, once again, you don’t have to do but will lead to better organization, is creating a general view, and then subfolders for each subview inside of the screen.
For example, let’s say we have a news app. Suppose we tap on an article and then the app performs a GET request to retrieve all of the article’s details. After this GET request completes, it navigates to a screen called article_view.dart
. On this article screen, we have a button that goes to a screen that gives “metadata” or extra details about the article such as exact time it was posted, reporter name and title, etc. This screen and information cannot be found in any other kind of way throughout the app unless you go through the article itself.
In this case, you can make a subfolder inside of /article_views
called article_views/article_info_view
and in there you can put article_info_view.dart
in there. Then from article_view.dart
, you can call Navigator.push(...)
in the button to navigate to article_info_view.dart
. Make sure you import it first of course.
Here’s an example of what the views folder structure could look like:
▾ views
▾ club_views
▾ components
club_view_app_bar.dart
club_view_show_club_details_bottom_sheet.dart
...
▾ club_settings_view
▾ components
club_settings_view_app_bar.dart
club_settings_view.dart
▾ club_announcement_view
club_announcement_view.dart
club_view.dart
▾ settings_view
▾ components
sv_app_bar.dart
sv_card.dart
settings_view.dart
▾ home_view
▾ components
app_bar.dart
home_view.dart
▾ welcome_view
welcome_view.dart
...
Final Project Structure
‣ .dart_tool
‣ .idea
‣ android
‣ ios
▾ lib
▾ src
▾ config
▾ exceptions
api_exceptions.dart
auth_exceptions.dart
▾ themes
light_theme.dart
dark_theme.dart
text_theme.dart
source_colors.dart
constants.dart
environment_keys.dart
...
▾ models
club.dart
user.dart
...
▾ services
▾ authentication_services
apple_sign_in_service.dart
google_sign_in_service.dart
email_password_sign_in_service.dart
...
▾ database_services
club_service.dart
member_service.dart
...
dio_provider.dart
...
▾ ui
▾ components
primary_button.dart
show_club_bottom_sheet.dart
action_button.dart
system_text_input.dart
close_button.dart
spinner.dart
...
▾ views
▾ club_views
▾ components
club_view_app_bar.dart
club_view_show_club_details_bottom_sheet.dart
...
▾ club_settings_view
▾ components
club_settings_view_app_bar.dart
club_settings_view.dart
▾ club_announcement_view
club_announcement_view.dart
club_view.dart
▾ settings_view
▾ components
sv_app_bar.dart
sv_card.dart
settings_view.dart
▾ home_view
▾ components
app_bar.dart
home_view.dart
▾ welcome_view
welcome_view.dart
...
main.dart
‣ linux
‣ macos
‣ test
‣ web
‣ windows
.gitignore
.metadata
analysis_options.yaml
(app_name).iml
pubspec.lock
pubspec.yaml
README.md
Conclusion
In conclusion, structuring your Flutter projects effectively is crucial for maintaining a clean codebase and ensuring the future scalability of your applications. By separating your business logic from your UI components and organizing your files and folders logically, you can enhance the readability and maintainability of your code. Remember, a well-structured project not only benefits you but also aids in collaboration with others.
I hope this article was of use to you! If it was, be sure to read my other articles on my blog for more tutorials, experiences, and guides at www.carltonaikins.com.
In God we trust 🙏🏾