stripes
17 min read

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 🙏🏾


Blog