This is a boilerplate flutter project created using Riverpod and GetIt. It currently support only Mobile (Tablet and Phone) with both production and staging environment capabilities. You can clone the project using the link below:
Getting Started
The Boilerplate contains the minimal implementation required to create a new project. This repository is preloaded with some basic app architecture that can be expanded to fit in larger project. The purpose of this repository is to help reduce setup and development time and avoid re-writing same code pattern for every app to be created.
Installation
Please note: This repository requires Flutter to be installed to your development machine. After that is done, follow the below steps.
Step 1:
Download or clone this repo by using the link below:
https://github.com/peterewanfo/flutter_riverpod_boilerplate_project.git
Step 2:
Go to project root and execute the following command in console to get the required dependencies:
flutter pub get
Packages Used
This repository makes use of the following pub packages:
Package | Version | Usage |
Hooks Riverpod | ^1.0.3 | State Management |
Flutter Hooks | ^0.18.2+1 | Increase the code-sharing between widgets by removing duplicates. |
Flutter Lints | ^1.0.0 | To encourage good coding practices. |
Flutter Screenutil | ^5.0.0+2 | For adapting screen and font size |
GetIt | ^7.1.3 | For accessing service objects/Views/AppModels |
Connectivity Plus | ^2.3.9 | To discover network connectivity |
Dartz | ^0.10.1 | For easy and safe error handling with functional programming stule in Dart |
Shared Preference | ^6.0.0 | To store data as key/value pairs |
Flutter Secure Storage | ^2.0.7 | To store data in secure storage |
Flutter dotenv | ^5.0.2 | To manage/read .env files |
Pretty Dio Logger | ^1.1.1 | A Dio interceptor that logs network calls in a pretty, easy to read format |
Dio | ^4.0.4 | Http Client for Dart |
Another Flushbar | ^1.12.29 | To substitute toasts and snackbars and introduce more customization when notifying your user. |
Change App Package Name | ^1.1.0 | To change app package name with single command. It makes the process very easy and fast. |
Removing unwanted packages
If any package is not needed, then removing it from pubspec.yaml file as well as all imports and uses should be enough.
Changing the package and app name
use the Change App Package Name the package is already included in this boilerplate project. Simply run this command
flutter pub run change_app_package_name:main com.new.package.name
where "com.new.package.name" is your desired backage name for the project. For other information on how to do this, kindly visit this Stackoverflow issues
Boilerplate Features:
- Splash
- Login
- Signup
- Home
- Routing
- Dio
- Database
- Riverpod
- Validation
- Loggin
- Dependency Injection
- Connectivity
Up-Coming Feature:
- Widget and Unit Test Support
- Robust Example project
Folder Structure
app-base-directory/
| - android
| - assets
| - build
| - ios
| - lib
| - test
| - .env
| - .env_prod
Assets
This contains static image resources and fonts used in the application
This is what the assets
file structure looks like
assets/
| - fonts
| - images
Lib
Let's see a detailed view of the lib folder
lib/
| - data
| - handlers
| - models
| - presentation
| - utils
| - app.dart
| - main.prod.dart
| - main.dart
Here is a brief description of what is contained in each folder
1: data - includes directories for network calls and shared preferences
2: handlers - contains navigation handler and dialog manager/handler for managing dialogs and application navigation at a global level.
3: models - contains data models of your application
4: presentation - contains your application UI(Views), ViewModels, custom styles, defined routes and custom designed widgets.
5: app.dart - in here we load the application and set flag for staging or production based on the current active app flavour
6. main.dart - this is the app main lancher and uses the staging app flavour
7. main.prod.dart - this is the production main lancher and uses the production app flavour
for more explanation on app flavour and it is setup in a project, checkout this youtube video or this article
Now let's dive deep exhausively into each folder and see what each entails.
Data
This contains the data layer of your project. It is home to all your application business logic. From here, your application gets all resources it needs to best serve the user, this includes network resources (api calls) and local resources (shared preferences or secure storage) as prefered.
This is what the data
file structure looks like
data/
| - config
| - base_api.dart
| - services
| - local
__local.dart
| - implementations
| - secure_storage_impl.dart
| - shared_preference_impl.dart
| - repositories
| - secure_storage_repository.dart
| - shared_preference_repository.dart
| - remote
__remote.dart
| - implementations
| - user_repository_impl.dart
| - repositories
| - user_repository.dart
Like you have noticed, this repository provides implementation for both Flutter secure storage and shared preference, while both does thesame task of keeping data to the device, the former encrypts data stored, the latter doesnt. You can check this article on how both works and decide which to use, like said earlier both implementations are available in this repository, ready for you to decide.
Handlers
This contains manager/handler for managing dialogs and application navigation at a global level. with this, we're making provisions to enable navigations and dialog pop-up from anywhere in our application either in views/viewmodel/custom methods e.t.c. Learn more about this from these amazing blog posts
- Blog post 1
- Blog post 2
Special thanks to the creators.
This is what thehandlers
file structure looks like
handlers/
| - __handlers.dart
| - dialog_handler.dart
| - navigation_handler.dart
File naming convention to note
__folder_name.dart
: In every folder, there is a file named__folder_name.dart
. The purpose of this file is to enable export multiple dart files from just one. This is extensively used in the project for example in thehandlers
folder above, there exist the file named__handlers.dart
sample code:__handlers.dart
export dialog_handler.dart;
export navigation_handler.dart;
now importing __handlers.dart
also imports dialog_handler.dart
and navigation_handler.dart
For more explanations and examples on how this works, checkout this article
Models
On here is where all model classes for our application is created. It can be further split into folders like api
, just to differentiate model classes created.
Presentation
This contains the following sub folders custom_designs
, routes
, style
, view_models
, views
. This is what the presentation
file structure looks like
Here is a brief description of what is contained in each folder
1: custom_designs - This contains all custom and shared widgets.
2: routes - This contains two files routes.dart
and route_generator.dart
. The former contains all routes to your application while the latter, contains custom route generator callback for all routes in your application.
The structure looks like this
presentation/
| - routes
| - __routes.dart
| - route_generator.dart
| - routes.dart
Sample code routes.dart
class Routes {
static const splashScreenView = "/splashScreenView";
static const signupView = "/signupView";
static const loginView = "/loginView";
}
Sample code route_generator.dart
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case Routes.splash_screen:
return _getPageRoute(SplashScreen(), settings);
default:
return _getPageRoute(_errorPage());
}
static CupertinoPageRoute _getPageRoute(
Widget child, [
RouteSettings settings = const RouteSettings(),
bool? isfullScreenDialog = false,
]
) => CupertinoPageRoute(
builder: (context) => child,
fullscreenDialog: isfullScreenDialog ?? false,
settings: RouteSettings(
name: settings.name,
arguments: settings.arguments,
),
);
static Widget _errorPage({String message = "Error! Page not found"}) =>
kDebugMode
? Scaffold(
appBar: AppBar(
title: const Text(
'Page not found',
style: TextStyle(color: Colors.red),
)),
body: Center(
child: Text(
message,
style: const TextStyle(color: Colors.red),
),
),
)
: const SizedBox();
3: style - This contains application styling including app_theme.dart
, custom_colors.dart
The structure looks like this
presentation/
| - style
| - __style.dart
| - app_theme.dart
| - custom_colors.dart
| - custom_style.dart
4: view_models - In reference to an amazing article by Jitesh Mohite on "Flutter: MVVM Architecture". ViewModel is the mediator between View and Model. It's from here we handle all user events, fetch data required and notify the view. If you wish to learn more about MVVM, you can checkout this amazing article "Flutter: MVVM Architecture"
5: views - This directory contains all the UI of your application.
Handling Mobile and Tablet view compatibility
For View responsiveness across mobile and tablet, we use Layout builder to determine which view version to launch based on device width. contained in this boilerplate is a custom layout builder to handle this functionality for all views.
sample code - responsive_laout.dart
import 'package:flutter/material.dart';
class ResponsiveLayout extends StatelessWidget {
final Widget? tablet;
final Widget mobile;
const ResponsiveLayout({
Key? key,
required this.mobile,
this.tablet,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraint) {
if (constraint.maxWidth <= 488) return mobile;
return tablet ?? mobile;
},
);
}
}
- To use this functionality, other views can then be structured like below.
Inviews/ | - login | - login_mobile_view.dart | - login_tablet_view.dart | - login_view.dart
login_view.dart
, we importresponsive_laout.dart
and pass as parameters the different views(login_mobile_view.dart
andlogin_tablet_view.dart
)
sample code - login_view.dart
import 'package:flutter/material.dart';
import 'package:boilerplate_project/presentation/custom_designs/responsive_layout.dart';
import 'package:boilerplate_project/presentation/views/login_mobile_view.dart';
import 'package:boilerplate_project/presentation/views/login_tablet_view.dart';
class LoginView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
mobile: LoginMobileView(),
tablet: LoginTabletView(),
);
}
}
Utils
This direcctory is used as the helper folder of the application. It contains the following files:
1: api_endpoints.dart
: Here, you keep all api endpoints called in your application.
sample code - api_endpoints.dart
class ApiEndpoints {
static const login = "/login";
static const signUp = "/sign_up";
}
2: app_strings.dart
: Here we keep string constants used in your application. It could either be keys to shared preferences e.t.c.
sample code - app_strings.dart
class AppStrings {
AppStrings._();
//SHARE PREFERENCE STRINGS
static const accessTokenPref = "access_token_pref";
static const refreshTokenPref = "refresh_token_pref";
}
3: connection_status.dart
: This helper file contains class that tells if your application has internet connection.
sample code - connection_status.dart
import 'package:connectivity/connectivity.dart';
class ConnectionStatus {
static Future<bool> isConnected() async {
var connectionResult = await (Connectivity().checkConnectivity());
if (connectionResult == ConnectivityResult.mobile)
return true;
else if (connectionResult == ConnectivityResult.wifi)
return true;
else {
return false;
}
}
}
4: enums.dart
: This file contains all enums used in your applications.
5: extensions.dart
: This file contains extensions to add functionality to widgets and libraries preventing code repitition.
6: validators.dart
: This file contain custom form validators used in your application.
7: locator.dart
: This file contains the configuration of getIt service locator used in this boilerplate.
More Explanation - In here we register shared preferences and custom app flavour as singleton and other classes used as lazy singleton. The purpose of lazy singleton is for initializing resources at the time of the first request instead at the time of declaration.
sample code - locator.dart
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
GetIt locator = GetIt.instance;
Future<void> setupLocator({
String baseApi = "",
AppFlavor flavor = AppFlavor.debug,
}) async {
locator.registerSingleton<AppFlavor>(flavor);
final sharedPreferences = await SharedPreferences.getInstance();
locator.registerSingleton(sharedPreferences);
//Local storage
locator.registerLazySingleton<SecureStorage>(
() => SecureStorageImpl(),
);
locator.registerLazySingleton<LocalCache>(
() => LocalCacheImpl(
sharedPreferences: locator(),
storage: locator(),
),
);
//Handlers
locator.registerLazySingleton<NavigationHandler>(
() => NavigationHandlerImpl(),
);
locator.registerLazySingleton<DialogHandler>(
() => DialogHandlerImpl(),
);
}
8: logger.dart
: From here, we toggle logs 'on' when in debug mode and off when in production
sample code - logger.dart
import 'dart:developer' as dev;
class AppLogger {
AppLogger._();
static bool _showLogs = false;
static bool get showLogs => _showLogs;
static void setLogger({required bool showLogs}) {
_showLogs = showLogs;
}
static void log(Object? e) {
if (_showLogs) dev.log("$e");
}
}
9: env_config.dart
: This contains method to load different environment variables used in your application. These environment file (.env
and .env_prod
) is located in the application project folder. These files are added to gitignore so you should create them manually after cloning this boilerplate project.
sample code - env_config.dart
import 'package:boilerplate_project/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
Future<void> loadEnvFile({String path = ".env"}) async {
try {
await dotenv.load(fileName: path);
} catch (e) {
AppLogger.log(e);
}
}
Future<void> loadProdEnvFile({String path = ".env_prod"}) async {
try {
await dotenv.load(fileName: path);
} catch (e) {
AppLogger.log(e);
}
}
Contribution
If you wish to contribute to this boilerplate project, please feel free to submit an issue and/or pull request.
Thanks for your time.