Road to Production-Ready Application — I (Code Structure in Flutter)
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
- .dart_tool
- .idea
- android
- build
- gen
- homepage
- ios
- lib
- External Libraries
- Scratches and Console
- pubspec.lock, pubspec.yaml etc
All the files in the image below (except assets, homepage_top).
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).
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.
- key.properties
- build.gradle(The one inside the app and not the one which is immediately inside android)
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:
What?
Assets can be a
- Picture in jpg, jpeg, png or webp format
- 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:
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:
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
It has the following:
- 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
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.
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.
Talking about constants, we have
- 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 Constantsconst 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 Typeconst 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 Urlsconst 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.