A Flutter template application showcasing - Clean architecture, Responsive design, State management, Decoupled widgets using the connector pattern, Dependency Injection, Widget and Unit testing, Navigation, Localization, Continuous Integration and Continuous Deployment.
We’re always looking for people who value their work, so come and join us. We are hiring!
Clone the repo and run the following commands to setup the project.
flutter pub get
scripts/generate-all.sh
Read the scripts documentation to learn about all the scrips used in the project.
The architecture of the template facilitates separation of concerns and avoids tight coupling between it's various layers. The goal is to have the ability to make changes to individual layers without affecting the entire app. This architecture is an adaptation of concepts from The Clean Architecture
.
The architecture is separated into the following layers
lib/presentation
: All UI and state management elements like widgets, pages and view models.lib/navigation
: navigators to navigate between destinations.lib/interactor
: provides feature specific functionality.lib/domain
: use cases for individual pieces of work.lib/repository
: repositories to manage various data sources.lib/services
: services provide access to external elements such as databases, apis, etc.
Each layer has a di
directory to manage Dependency Injection for that layer.
The layers presentation
, domain
and services
each have an entity
directory.
lib/presentation/entity
: Classes that model the visual elements used by the widgets.lib/domain/entity
: Model classes for performing business logic manipulations. They act as an abstraction to hide the local and remote data models.lib/services/entity
: Contains local models (data classes for the database) and remote models (data classes for the api).
- Presentation entities are prefixed with
UI
(eg: UICity). - Domain entities do not have any prefix. (eg: City).
- Service entities are of 2 types:
- Local / Database entities are prefixed with
Local
(eg: LocalCity). - Remote / API entities are prefixed with
Remote
(eg: RemoteCity).
- Local / Database entities are prefixed with
Apart from the main layers, the template has
lib/foundation
: Extensions on primitive data types, loggers, global type alias etc.lib/flavors
: Flavor i.e. Environment related classes.lib/entrypoints
: Target files for flutter to run for each flavor.lib/app.dart
: App initialization code.
The presentation layer houses all the visual components and state management logic.
The base
directory has all the reusable and common elements used as building blocks for the UI like common widgets, app theme data, exceptions, base view models etc.
State Management is done using the riverpod
along with state_notifier
. The class that manages state is called the View Model
.
Each View Model
is a subclass of the BaseViewModel
. The BaseViewModel
is a StateNotifier
of ScreenState
. Along with the ScreenState it also exposes a stream of Effect
.
Implementations of the BaseViewModel can also choose to handle Intents
.
ScreenState
encapsulates all the state required by a Page
. State is any data that represents the current situation of a Page.
For example, the HomeScreenState
holds the state required by the HomePage
.
Effects
are events that take place on a page that are not part of the state of the screen. These usually deal with UI elements that are not part of the widget tree.
Showing a snackbar or hiding the keyboard are examples of an effect.
Intent is any action that takes place on a page. It may or may not be user initiated.
SearchScreenIntent
has the actions that can happen on the SearchPage
.
A page is a widget that the navigator can navigate to. It should return the BasePage
widget.
The BasePage
creates the structure for the page, initialises the ViewModel
and provides the view model in the widget tree so that all the children have access to it. It also listens to the effects from the view model and notifies the page about it.
Each page accepts the Screen
object as input.
Each destination has a widgets
directory. It holds all the widgets that appear on a Page
excluding the page itself.
Each widget the requires access to data from the view model it split into two dart files. The connector widget communicates with the view model, and the content widget has the actual UI. The connector widget passes all the required data to the content widget. Thus the content widget never depends on the state managent solution used. This helps in easy replacement of state management solution if needed and also makes it easier to test widgets.
A Screen
is a class that represents a Page
in the context of navigation. It holds the path
used by the navigator to navigate to a Page
and also holds any arguments required to navigate to that Page
.
The template comes with built-in support for 3 flavors. Each flavor uses a different main.dart
file.
- Dev -
main_dev.dart
- QA -
main_qa.dart
- Prod -
main_prod.dart
You can setup any environment specific values in the respective main.dart
files.
To run a specific flavor you need to specify the flavor and target file.
flutter run --flavor qa -t lib/entrypoints/main_qa.dart
To avoid specifying all the flags every time, use the run.sh
script
Read the scripts documentation to learn about all the scrips used in the project.
The Flutter Template contains:
- A
Flutter
application. - Built-in support for 3
flavors
-dev
,qa
andprod
. - A
reactive base architecture
for your application. Riverpod
along withstate_notifier
for state management.Drift
as local database for storage.Dio
for making API calls.Freezed
for data class functionality.Get It
for dependency injection.Flutter Lints
for linting.
The template contains an example (displaying weather data) with responsive widgets, reactive state management, offline storage and api calls.
The template was build using dart null safety. Dart 2.12 or greater and Flutter 2 or greater is required.
Dart 2.15 or greater and Flutter 2.10 or greater is recommended.
Follow this guide to setup your flutter environment based on your platform.
The Flutter template comes with built-in support for CI/CD using Github Actions.
The CI
workflow performs the following checks on every pull request:
- Lints the code with
flutter analyze
. - Runs tests using
flutter test
. - Build the android app.
- Build the ios app.
The CD
workflow performs the following actions:
- Bump the build number by 1.
- Build a signed release apk.
- Upload apk to the app center.
- Upload apk as artifact to release tag.
- Build a signed iOS app.
- Upload ipa to testflight.
- Upload the ipa as an artifact to release the tag.
- Commit the updated version to git.
For the android CD workflow to run, we need to perform the following setup steps:
- Follow these instructions to generate an upload keystore. Note down the
store password
,key alias
andkey password
. You will need these in later steps. - Use
openssl
to convert thejks
file toBase64
.
openssl base64 < flutter_template_keystore.jks | tr -d '\n' | tee flutter_template_keystore_encoded.txt
- Store the
base64
output onGithub Secrets
with the key nameKEYSTORE
. - Save the
store password
in github secrets with the key nameRELEASE_STORE_PASSWORD
. - Save the
key alias
in github secrets with the key nameRELEASE_KEY_ALIAS
. - Save the
key password
in github secrets with the key nameRELEASE_KEY_PASSWORD
. - Create a distribution on app center and get the upload key. You can get it from appcenter.ms/settings.
- Save the app center upload key on github secrets with key name
APP_CENTER_TOKEN
.
For the IOS job in the cd.yml
to run, you first need to have a valid Apple Developer Account.If you don't have it yet, please create one before proceeding further
We will divide the guide into steps so that it is easier to understand
- Register your
Bundle ID
. You can view the official Flutter guide here
CAUTION: Apple doesn't allow underscore in the bundle identifier. Read about valid identifiers here
- Create an application on the AppStore Connect Portal. Check out the official guide here
- Create a
Distribution Certificate
for your machine locally once. You can refer to this guide. Download the.p12
file for use later. Remember the password used to create this certificate as we will need this later - Create a
Provisioning Profile
for yourBundle ID
you registered above. You can refer to this guide. Download the profile for use later.
- In the following template
- Replace
BUNDLE ID
with yourBundle Identifier
(You got that already from Step 1) - Replace
PROVISIONING PROFILE NAME
with your Provisioning Profile Name (You already created one in Step 2, use that) - Replace
TEAM_ID
with your team id. Look at this answer on "How to find your Team ID"
- Replace
Click to View Template
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>generateAppStoreInformation</key>
<false/>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>BUNDLE-ID</key>
<string>PROVISION PROFILE NAME</string>
</dict>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>TEAM_ID</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
- Create a new file called
options.plist
and save the above contents in that file
- Read the official guide to create an app specific password and remember it(;P)
- The pipeline uses this password to upload an ipa to testflight
- Add the following keys to Github Secrets
BUILD_CERTIFICATE_BASE64
: The base64 of the p12 file we generated(Step 2)P12_PASSWORD
: The password of the p12 certificate generated above in Step 2BUILD_PROVISION_PROFILE_BASE64
: The provisioning profile in base64(Step 2)KEYCHAIN_PASSWORD
: The password used to store the keychain in the local keystore of the Github Runner(Any random value)IOS_PLIST
: The options.plist file needed to make an ipa out of the xcarchive generated by flutter(Step 3)APPSTORE_PASSWORD
: The password passed to altool to upload the ipa to the store(Step 4)
- To generate a base64 string, use the following command, replacing
FILENAME
with your filename
openssl base64 < FILENAME | tr -d '\n' | tee ENCODED_FILENAME.txt
- If the branches that you will be running CD on are protected, you will need to use a
Personal Access Token (PAT)
to commit the version changes. - After creating the
PAT
, exclude the account that the token belongs to from thebranch protection rules
. - Save the token in github secrets and update the key name in the
cd.yml
file under eachcheckout
action. - Since our
CD
workflow is triggered on a push, and we create a new commit in the workflow itself, the commit message created by theCD
workflow includes[skip ci]
tag so that the workflow does not end up in an infinite loop.Read more about this here
If you do not plan to use the CD workflow on protected branches, you can remove the token part from the checkout actions.
- Flutter apps might have issues on some android devices with variable refresh rate where the app is locked at 60fps instead of running at the highest refresh rate. This might make your app look like it is running slower than other apps on the device. To fix this the template uses the
flutter_displaymode
package. The template sets the highest refresh rate available. If you don't want this behaviour you can remove the lines 40 to 46 inapp.dart
.Link to frame rate issue on flutter
.
- Additionally look into the Issues for the repository for some commonly faced problems while setup