Credits: https://unsplash.com/photos/OqtafYT5kTw

Road to Production-Ready Application — I (Code Structure in Flutter)

Samveg Thaker
Zenithec Techware
Published in
8 min readFeb 14, 2021

--

In this section, we assume that you are already familiar with the basics of Flutter. In case you are new or a beginner at Flutter Application Development, please go through my blog — Starting with Flutter.

As a developer, when you start to write an application, the 1st thing that comes to your mind is the folder hierarchy referred to as the Code Structure (not the architecture, which is entirely different). That is what we will talk about in this article.

I researched a couple of fold structures online and thought of mixing them to follow a hybrid and improved code structure. Feel free to improvise further on my code structure.

Default Code Structure

So when you 1st create a Flutter app, the default structure have

  1. .dart_tool
  2. .idea
  3. android
  4. build
  5. gen
  6. homepage
  7. ios
  8. lib
  9. External Libraries
  10. Scratches and Console
  11. pubspec.lock, pubspec.yaml etc

All the files in the image below (except assets, homepage_top).

The default file structure of a Flutter Application

Now let’s discuss one by one the uses and how to go further. Few things that we will add and why and how will be discussed in the order the above pic mentions.

android

Now, this contains all the code for the native android. Now inside android, you can find the below architecture (by default it is like this and don’t alter until and unless you have some specific need).

File Structure

Now here we will be using 2 files when we want to sign the apk for release. That will be covered in the coming blogs.

  1. key.properties
  2. build.gradle(The one inside the app and not the one which is immediately inside android)
Location of the build.gradle and google-services.json to be used

If you want to use some API's and add the google-services.json, it is inside the app as shown above.

asset

Now let’s discuss why, how and what is this folder needed for?

Why?

Any static image, gif or loaders will be added here. Adding here directly won’t enable us to access it directly. We will let the pubspec.yaml know that assets will be used from this folder as below:

# The following section is specific to Flutter.
flutter:

# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true

# To add assets to your application, add an assets section, like this:
assets:
- assets/
- assets/launcher/

How?

So we go to the parent directory -> right-click on it -> click on New -> choose Directory as shown below:

Creating the asset folder

What?

Assets can be a

  1. Picture in jpg, jpeg, png or webp format
  2. Gif, etc

It is recommended to have assets differently for Android and iOS. To read in detail about that, please read about that here. The official docs give an in-depth idea of best practices and everything.

ios

This is again the auto-generated directory, which should not be changed until and unless required specifically. The structure is usually as shown below:

ios directory structure

As you can see GoogleService-Info.plist, but is always recommended not to add this from here but use Xcode(In case you don’t have a MacBook no need to worry). Following the Official Firebase Docs would make this really smooth.

lib

The most important code or basically the flutter code will lie here. So let’s see this in detail. I have customised the sub-directories so that we can keep things well-fragmented. Below is the file structure for my lib directory:

The file structure of lib

When you start with the starter project you will only see the main.dart.

UI

Now, start by making a Directory as suggested in the assets section and name it UI. Here we will be keeping all the stateful and stateless widgets. It will look like below

Structure of ui

It has the following:

  1. common_widgets

This contains all the common widgets. That may be the Primary Card that you might be using, or a Primary Button or a Secondary Button. If it is customised and has to be used multiple times(same design but different content in the different screens) then it is recommended to keep it in the common widgets. Below are some examples for common_widgets

Examples of common_widgets

The code for a custom elevation(so that we can see shadow) for any widgets can be found below:

import 'package:flutter/material.dart';

class CustomElevation extends StatelessWidget {
final Widget child;
final double height;
final Color shadowColor;

CustomElevation({@required this.child, @required this.height, this.shadowColor})
: assert(child != null);

@override
Widget build(BuildContext context) {
return Container(
height: this.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(this.height / 2)),
boxShadow: <BoxShadow>[
BoxShadow(
color: shadowColor,
blurRadius: height / 5,
offset: Offset(0, height / 10),
),
],
),
child: this.child,
);
}
}

2. screens

This consists of the actual screens. Screens are usually a big feature, for example — Login, Register, Feeds, Profile etc. Make sure only the widget tree and some UI manipulating functions shall be present in the UI directory. NO API calls or Database operations are to be performed.

3. styles

Last and the final is the styles. We will be using a particular set of colours and text style throughout our applications(that’s what is recommended), so instead of defining the colours hex code or font style, size and weight etc, we define it once and reuse every time we want. This will be discussed later in the blog that will have all the best practises of flutter.

Contents of styles

Code for TextThemes.dart can be found below:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_html/style.dart';
import 'Colors.dart';

abstract class TextThemes {

//Texts

static const TextStyle textTopForumSubHeading = TextStyle(
fontFamily: 'Helvetica',
color: colorGreyLow,
fontSize: 8.0,
fontWeight: fontWeightBold,
);

static const TextStyle textTopForumDetailDescription = TextStyle(
fontFamily: 'Helvetica',
color: colorGreyDark2,
fontSize: 9.0,
fontWeight: fontWeightRegular,
// letterSpacing: 0.42,
height: 1.2
);

static const TextStyle textAvasarLogo = TextStyle(
fontFamily: 'Helvetica',
color: colorAvasarLogoHeading,
fontSize: 26.0,
fontWeight: fontWeightMedium,
letterSpacing: 0.60
);

//Font Weights
static const
FontWeight fontWeightLight = FontWeight.w300;
static const FontWeight fontWeightRegular = FontWeight.w400;
static const FontWeight fontWeightMedium = FontWeight.w500;
static const FontWeight fontWeightSemiBold = FontWeight.w600;
static const FontWeight fontWeightBold = FontWeight.w700;
static const FontWeight fontWeightExtraBold = FontWeight.w800;
static const FontWeight fontWeightHeavy = FontWeight.w900;


}

I have trimmed since we are using a couple of styles. Feel free to add as you find fit.

Colors.dart can be used to use colours with hex code as shown below:

import 'package:flutter/material.dart';

//Add All colors here
const
colorSponsorText = const Color(0xff0E4D92);
const colorGrey = const Color(0xffABB4BD);
const colorBlack = const Color(0xff1D2029);
const colorPrimary = const Color(0xff197FBA);
const colorShadowPrimaryButton = const Color(0x69197FBA);
const colorDisabledButton = const Color(0xffD9DDE2);
const colorOceanBlue = const Color(0xff0B4F90);
const colorShadowSearchCard = const Color(0x71abb4bd);
.
.
.
.
const colorAvasarLogoHeading = const Color(0xff103558);

utils

utils contains Helper classes which have common functions that can be used throughout the applications and the constants. Shared Preference can be added here as well.

Structure of utils

Talking about constants, we have

  1. AppConstants.dart— to have certain error messages, Shared Preference keys, number of tabs and/or anything that might be constant(not to be used in UI).
import 'package:shared_preferences/shared_preferences.dart';//Integer Constantsconst int SPLASH_TIME = 250;
const int TAB_INDEX_FEEDS = 0;
const int TAB_INDEX_COUNSELLORS = 1;
const int TAB_INDEX_HOME = 2;
const int TAB_INDEX_FORUM = 3;
const int TAB_INDEX_PROFILE = 4;

//String Constants
const String SPONSOR = "Powered by Zenithec Techware Pvt. Ltd.";
const
String OS = 'ANDROID';
const String INTERNET_ERROR = 'Please check your Internet connection';
const String EMPTY_TITLE_POST_ERROR = 'Please fill the title';
.
.
const
String EMPTY_DESCRIPTION_POST_ERROR = 'Please fill the description';

// SharedPreferences Key
const
String KEY_LOGIN = 'keyLogin';
const String KEY_ACCESS_TOKEN = 'keySomeToken';
const String KEY_REFRESH_TOKEN = 'keySomeToken';
const String KEY_USER_ROLE = 'keySomeRole';
const String KEY_USER_SELECTED = 'isUserSelected';
const String KEY_USER_AUTHOR_SKIPPED = 'isAuthSkipped';
const String KEY_SERVER_USER_ID = "serverUserId";
const String KEY_USER_FIRST_NAME = 'firstName';
const String KEY_USER_LAST_NAME = 'lastName';
.
.
const String KEY_USER_COLLEGE_NAME = 'collegeName';

//Feed Type
const String FEED_CATEGORY_STORY = "STORY";
const String FEED_CATEGORY_BLOG = "BLOG";
const String FEED_CATEGORY_VIDEO = "VIDEO";

const String ROOT_TYPE_FORUM = 'FORUM';
const String ROOT_TYPE_COMMENT = 'COMMENT';

//Web view Urls
const String PRIVACY_POLICY_URL = 'http://www.avasar.life/privacy_policy';
const String TERMS_AND_CONDITIONS_URL = 'http://www.avasar.life/terms_and_conditions';

2. HttpConstants.dart — All API call-related constants. Usually BASE URLs and the routes.

//Base URL
const
String BASE_URL = 'https://subdomain.domain';

//GET Request URL's
const
String APP_UPDATE_URL = 'path/some_more_path';
const String FEEDS_URL = 'path/some_more_path';
const String TAGS_URL = 'path/some_more_path';
.
.
const String EXPERIENCES_URL = 'path/some_more_path';


//POST Request URL's
const
String LOGIN_URL = 'path/some_more_path';
const String REGISTER_URL = 'path/some_more_path';
const String REQUEST_OTP_URL = 'path/some_more_path';
.
.
const String REPORT_URL = 'path/some_more_path';

3. UIConstants.dart — is used to keep all the values that are seen in the UI of the app, that might be the button text or text field hints. Code is similar to AppConstants.dart

main

This is the started code and you can put all the routes and routing information here. You can also initialise the DB, Shared Prefs, Firebase and any other such services.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
.
.
import
'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';



void main() {
runApp(App());
}

class App extends StatefulWidget {
@override
_AppState createState() => _AppState();
}

class _AppState extends State<App> {

final
FirebaseMessaging _firebaseMessaging = FirebaseMessaging();

void initialiseSharedPref() async{

bool userSelected = await SharedPrefs.getUserSelected();
if (userSelected == null && userSelected)
SharedPreferences.setMockInitialValues({});
}

@override
void initState() {
super.initState();

initialiseSharedPref();

final
Completer<Map<String, dynamic>> completer =
Completer<Map<String, dynamic>>();
//Firebase setup
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
completer.complete(message);
},
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
},
);
_firebaseMessaging.requestNotificationPermissions(
const IosNotificationSettings(
sound: true, badge: true, alert: true, provisional: true));
_firebaseMessaging.onIosSettingsRegistered
.listen((IosNotificationSettings settings) {
print("Settings registered: $settings");
});

}

@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);

return MaterialApp(

initialRoute: '/splashscreen',
routes: {
'/splashscreen': (context) => SplashScreen(),
'/register': (context) => Register(),
'/login': (context) => Login(),
'/otp': (context) => OtpVerification(),
'/home': (context) => Home(),
'/userSelection': (context) => UserSelection(),
'/counsellorDetail': (context) => CounsellorDetails(),
'/feeddetail': (context) => FeedDetail(),
'/feedlisting':(context)=>FeedsListing(),
'/forumDetails':(context)=>ForumDetails(),
'/forumListing' : (context) => ForumListing()
},
);
}
}

I hope this made a few things clear about how you can keep your flutter code clean. Let me know if you have any doubts in the comments below or you can reach out to me on LinkedIn.

Also, if you want to check out the application I made using this code, you can download it here and share this with your friends.

https://play.google.com/store/apps/details?id=com.avasar.life.suicide.prevention.student.avsar.avasarlife

--

--

Samveg Thaker
Zenithec Techware

Entrepreneur | SE at Microsoft | Ex-DSC Lead | Winner of SIH2019 | Android Developer