rx_bloc_cli 2.6.0
rx_bloc_cli: ^2.6.0 copied to clipboard
rx_bloc_cli that enables quick project setup including: flavors, localization [intl], state management [rx_bloc], routing [go_router], design system, analytics [firebase], tests
example/README.md
Table of contents #
- Getting Started
- Project structure
- Architecture
- Routing
- Adding a new feature
- Localization
- Analytics
- Http client
- Design system
- Golden tests
- Server
- Push notifications
- Social Logins
- Patrol integration tests
- Realtime communication
- Next Steps
Getting started #
Before you start working on your app, make sure you familiarize yourself with the structure of the generated project and the essentials that are included with it.
Note: The app contains features that request data from API endpoints hosted on a local server. For the app to function properly, make sure the local server is up and running. For more info, check out the server topic.
Project structure #
Path | Contains |
---|---|
lib/main.dart |
The production flavour of the app. |
lib/main_dev.dart |
The development flavour of the app. |
lib/main_staging.dart |
The staging flavour of the app. |
lib/base/ |
Common code used on more than one feature in the project. |
lib/base/app/ |
The root of the application and Environment configuration. |
lib/base/common_blocs/ |
Generally available BLoCs |
lib/base/common_mappers/ |
Generally available Mappers |
lib/base/common_services/ |
Generally available Services |
lib/base/common_ui_components/ |
Generally available Reusable widgets (buttons, controls etc) |
lib/base/data_sources/local/ |
Generally available local data sources, such as shared preferences, secured storage etc. |
lib/base/data_sources/remote/ |
Generally available remote data sources such as APIs. Here is placed all retrofit code. |
lib/base/data_sources/remote/interceptors/ |
Custom interceptors that can monitor, rewrite, and retry calls. |
lib/base/data_sources/remote/http_clinets/ |
Generally available http clients |
lib/base/di/ |
Application dependencies, available in the whole app |
lib/base/extensions/ |
Generally available extension methods |
lib/base/models/ |
The business models used in the application |
lib/base/repositories/ |
Generally available repositories used to fetch and persist models |
lib/base/theme/ |
The custom theme of the app |
lib/base/utils/ |
Generally available utils |
lib/feature_X/ |
Feature related classes |
lib/feature_X/blocs |
Feature related BLoCs |
lib/feature_X/di |
Feature related dependencies |
lib/feature_X/services/ |
Feature related Services |
lib/feature_X/ui_components/ |
Feature related custom widgets |
lib/feature_X/views/ |
Feature related pages and forms |
lib/lib_auth/ |
The OAuth2 (JWT) based authentication and token management library |
lib/lib_social_logins/ |
Authentication with Apple, Google and Facebook library |
lib/lib_permissions/ |
The ACL based library that handles all the in-app routes and custom actions as well. |
lib/lib_router/ |
Generally available router related classes. The main router of the app is lib/lib_router/routers/router.dart . |
lib/lib_router/routes |
Declarations of all nested pages in the application are located here |
Architecture #
For in-depth review of the following architecture watch this presentation.
Routing #
The routing throughout the app is handled by GoRouter.
You can use the IntelliJ RxBloC Plugin, which automatically does all steps instead of you, or to manualy add your route to the lib/lib_router/routes/routes.dart
. Once the route is added one of the following shell scripts bin/build_runner_build.sh
(or bin/build_runner_watch.sh
) needs to be executed.
The navigation is handled by the business layer lib/lib_router/bloc/router_bloc
so that every route can be protected if needed.
You can push, pop, goToLocation or go as follows
context.read<RouterBlocType>().events.push(MyNewRoute())
context.read<RouterBlocType>().events.pop(Object())
context.read<RouterBlocType>().events.goToLocation('Location')
or
context.read<RouterBlocType>().events.go(MyNewRoute())
Deep linking #
Your app is already configured to use deep links. Although you may still want to do some adjustments.
iOS
The configuration file can be found at ios/Runner/Info.plist
Under the CFBundleURLTypes
key there are two things you may want to change:
CFBundleURLName
unique URL used to distinguish your app from others that use the same scheme. The URL we build contains yourproject name
,organization name
anddomain name
you provided when setting up the project.CFBundleURLSchemes
the scheme name is yourorganisation name
followed byscheme
.
Example:
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>testapp.primeholding.com</string>
<key>CFBundleURLSchemes</key>
<array>
<string>primeholdingscheme</string>
</array>
</dict>
</array>
You can test the deep-links on iOS simulator by executing the following command
Production
xcrun simctl openurl booted primeholdingscheme://testapp.primeholding.com/deepLinks/1
Staging
xcrun simctl openurl booted primeholdingstagscheme://testappstag.primeholding.com/deepLinks/1
Development
xcrun simctl openurl booted primeholdingdevscheme://testappdev.primeholding.com/deepLinks/1
Android
The configuration file can be found at android/app/src/main/AndroidManifest.xml
There is a metadata tag flutter_deeplinking_enabled
inside <activity>
tag with the .MainActivity
name. You may want to change the host name which again contains your project name
, organization name
and domain name
you provided when the project was set up.
See Deep linking documentation at flutter.dev for more information.
Access Control List #
Your app supports ACL out of the box, which means that the access to every page can be controlled remotely by the corresponding API endpoint /api/permissions
Expected API Response structure/data for anonymous users.
Note: The anonymous users have access to the splash route, but not to the profile route.
{
'SplashRoute': true,
'ProfileRoute': false,
...
}
Expected structure/data for authenticated users.
Note: The authenticated users have access to the splash and profile route.
{
'SplashRoute': true,
'ProfileRoute': true,
...
}
You can use the IntelliJ RxBloC Plugin, which automatically does all steps instead of you, or to manualy add the permission for your route to the lib/lib_permissions/models/route_permissions.dart
.
Adding a new feature #
You can manually create new features as described above, but to speed up the product development you can use the IntelliJ RxBloC Plugin, which not just creates the feature structure but also integrates it to the prefered routing solution (auto_route, go_router or none)
If you decide to create your feature manually without using the plugin here is all necessary steps you should do to register this feature and to be able to use it in the application:
- Add your feature path in the
RoutesPath
class which resides underlib/lib_router/models/routes_path.dart
:
class RoutesPath {
static const myNewFeature = ‘my-new-feature’;
...
}
- Add you feature permission name in the
RoutePermissions
class which resides underlib/lib_permissions/models/route_permissions.dart
:
class RoutePermissions {
static const myNewFeature = MyNewFeatureRoute’;
...
}
- Next step is to declare the new features as part of the
RouteModel
enumeration which resides underlib/lib_router/models/route_model.dart
:
enum RouteModel {
myNewFeature(
pathName: RoutesPath.myNewFeature
fullPath: '/my-new-feature',
permissionName: RoutePermissions.myNewFeature,
),
...
}
- As a final step the route itself should be created. All routes are situated under
lib/lib_router/routes/
folder which contains different route files organised by the application flow. If the new route doesn’t fit the existing application flows it can be added to theroutes.dart
file which is the default file used by the IntelliJ plugin.
@TypedGoRoute<MyFeatureRoute>(path: RoutesPath.myNewFeature)
@immutable
class MyFeatureRoute extends GoRouteData implements RouteDataModel {
const MyFeatureRoute();
@override
Page<Function> buildPage(BuildContext context, GoRouterState state) =>
MaterialPage(
key: state.pageKey,
child: const MyFeaturePage(),
);
@override
String get permissionName => RouteModel.myNewFeature.permissionName;
@override
String get routeLocation => location;
}
Now the new route can be navigated by calling one of the RouterBloc
events (go(...)
, push(...)
).
Example:
context.read<RouterBlocType>().go(const MyFeatureRoute())
For more information you can refer to the official GoRouter and GoRouterBuilder documentation.
Localization #
Your app supports localization out of the box.
You define localizations by adding a translation file in the lib/l10n/arb/[language_code].arb
directory. The language_code
represents the code of the language you want to support (en
, zh
,de
, ...). Inside that file, in JSON format, you define key-value pairs for your strings. Make sure that all your translation files contain the same keys!
If there are new keys added to the main translation file they can be propagated to the others by running the bin/sync_translations.py
script. This script depends on the pyyaml
library. If your python distribution does not include it you can install it by running pip3 install pyyaml
.
Upon rebuild, your translations are auto-generated inside lib/assets.dart
. In order to use them, you need to import the l10n.dart
file from lib/l10n/l10n.dart
and then access the translations from your BuildContext via context.l10n.someTranslationKey
or context.l10n.featureName.someTranslationKey
.
Analytics #
Firebase analytics track how your app is used. Analytics are available for iOS, Android and Web and support flavors.
Before you start using analytics, you need to add platform specific configurations:
- The
iOS
configuration files can be found atios/environments/[flavor]]/firebase/GoogleService-Info.plist
- For
Android
the configuration files are located atandroid/app/src/[flavor]/google-services.json
- All
Web
analytics configurations can be found insidelib/base/app/config/firebase_web_config.js
Every flavor represents a separate Firebase project that will be used for app tracking. For each flavor, based on the targeted platforms you'll have to download the configuration files and place them in the appropriate location mentioned above.
Note: When ran as development
flavor, .dev
is appended to the package name. Likewise, .stag
is appended to the package name when using staging
flavor. If using separate analytics for different flavors, make sure you specify the full package name with the correct extension (for instance: com.companyname.projectname.dev
for the dev
environment).
Http client #
Your project has integrated HTTP-client, using dio and retrofit. That helps you to easily communicate with remote APIs and use interceptors, global configuration, form fata, request cancellation, file downloading, timeout etc.
To use its benefits you should define a data model in lib/base/models/
, using json_annotation and json_serializable. Define your remote data source in folder lib/base/data_sources/remote/
with methods and real Url, using retrofit. In your dependencies class (for example: lib/feature_counter/di/counter_dependencies.dart
) specify which data source you are going to use in every repository.
JWT-based authentication and token management is supported out of the box.
Design system #
A design system is a centralized place where you can define your app`s design. This includes typography, colors, icons, images and other assets. It also defines the light and dark themes of your app. By using a design system we ensure that a design change in one place is reflected across the whole app.
To access the design system from your app, you have to import it from the following locationlib/app/base/theme/design_system.dart'
. After that, you can access different parts of the design system by using the BuildContext (for example: context.designSystem.typography.headline1
or context.designSystem.icons.someIcon
).
Golden tests #
A golden test lets you generate golden master images of a widget or screen, and compare against them so you know your design is always pixel-perfect and there have been no subtle or breaking changes in UI between builds. To make this easier, we employ the use of the golden_toolkit package.
To get started, you just need to generate a list of LabeledDeviceBuilder
and pass it to the runGoldenTests
function. That's done by calling generateDeviceBuilder
with a label, the widget/screen to be tested, as well as a Scenario
. They provide an optional onCreate
function which lets us execute arbitrary behavior upon testing. Each DeviceBuilder
will have two generated golden master files, one for each theme.
Due to the way fonts are loaded in tests, any custom fonts you intend to golden test should be included in pubspec.yaml
In order for the goldens to be generated, we have provided VS Code and IDEA run configurations, as well as an executable bin/generate_goldens.sh
. The golden masters will be located in goldens/light_theme
and goldens/dark_theme
. The failures
folder is used in case of any mismatched tests.
Server #
Your app comes with a small preconfigured local server (written in Dart) that you can use for testing purposes or even expand it. It is built using shelf, shelf_router and shelf_static. The server comes with several out-of-the-box APIs that work with the generated app.
In order to run the server locally, make sure to run bin/start_server.sh
. The server should be running on http://0.0.0.0:8080
, if not configured otherwise.
Some of the important paths are:
Path | Contains |
---|---|
bin/server/ |
The root directory of the server |
bin/server/start_server.dart |
The main entry point of the server app |
bin/server/config.dart |
All server-related configurations and secrets are located here |
bin/server/controllers/ |
All controllers are located here |
bin/server/models/ |
Data models are placed here |
bin/server/repositories/ |
Repositories that are used by the controllers reside here |
Note: When creating a new controller, make sure you also register it inside the _registerControllers()
method in start_server.dart
.
Push notifications #
Firebase Cloud Messaging (FCM) allows your integrating push notifications in your very own app. You can receive notifications while the app is in the foreground, background or even terminated. It even allows for event callbacks customizations, such when the app is opened via a notification from a specific state. All customizable callbacks can be found inside lib/base/app/initialization/firebase_messaging_callbacks.dart
.
In order to make the notifications work on your target platform, make sure you first add the config file in the proper location (as described in the Analytic section). For Web you also need to specify the vapid
key inside lib/base/app/config/app_constants.dart
and manually add the firebase web configuration to web/firebase-messaging-sw.js
(for more info refer to this link).
Note: On Android, FCM doesn't display heads-up notifications (notifications when the app is in foreground) by default. To display them while in app, we use a custom package called flutter_local_notifications . This package also provides a way of customizing your notification icon which you can find at the android/src/main/res/drawable
directory (supported types are .png
and .xml
).
Note: Since the app comes with a local server which can send notifications on demand, before using this feature, you need to create a server key for cloud messaging from the Firebase Console. Then you have to assign it to the firebasePushServerKey
constant located inside the bin/server/config.dart
file.
Social Logins Library #
Allows you to authenticate users in your app with Apple, Google and Facebook.
Apple Authentication
It uses the sign_in_with_apple package.
In order to make it work, fulfill the requirements described in its documentation.
Supports iOS.
Google Authentication
Google authentication uses google_sign_in package.
Follow the package documentation for registering your application and downloading Google Services file.(GoogleService-Info.plist/google-services.json)
Android:
For android integration you will need to copy google-services.json file to android/app/src/{name_of_the_environment}/
iOS:
For iOS integration you will need to copy GoogleService-Info.plist file to ios/environments/{name_of_the_environment}/firebase/
and copy reversed_client_id from GoogleService-Info.plist to ios/Flutter/{name_of_the_environment}.xcconfig file
For any other configurations refer to the google_sign_in package.
Facebook Authentication
Facebook authentication uses flutter_facebook_auth package.
Step 1:
In order to make it work you must register your app in facebook developer console.
Step 2:
There you will find your app_id, client_token and app_name.
Step 3:
3.1 Android:
Edit android/app/build.gradle, paste parameters from step 2 in
productFlavors{
name_of_the_enviroment{
dimension "default"
applicationIdSuffix ""
versionNameSuffix ""
resValue "string", "facebook_app_id", "insert_facebook_app_id_here"
resValue "string", "facebook_client_token", "insert_client_token_here"
}
}
3.2 iOS:
Edit ios/Flutter/(flavor-name).xcconfig and paste parameters from step 2.
Note:
Some requirements to be able to run application with this version of facebook auth is
- flutter_secure_storage package must be at least 8.0.0 version
- for iOS in Podfile platform must be at least 12
- for Android minSdkVersion must be at least 21.
All additional info about package and better explanation how to implement you can find in documentation flutter_facebook_auth_documentation.
Patrol Integration Tests #
The application comes with patrol package preconfigured for both Android and iOS. Patrol allows developers to use native automation and custom finders to write integration tests faster.
To run patrol integration tests install patrol_cli package. This package enables applications to use native automation features
Running the Tests
To run a test type a command patrol test --flavor flavor_name
, or use one of the preconfigured shell scripts provided within Android Studio
Realtime Communication #
Provides base datasource, repository, service and utility classes for establishing a SSE connection.
Register the classes into the DI system and configure the SSE endpoint by passing it as a parameter to SseRemoteDataSource
.
After this is done the event stream exposed by SseService
can be used by any BLoC.
Next Steps #
- Define the branching strategy that the project is going to be using.
- Define application-wide loading state representation. It could be a progress bar, spinner, skeleton animation or a custom widget.