Branding Customization
Customize the app to match your organization's branding.
App Name
Update the app name in multiple locations:
1. Environment Files
# .env.dev and .env.prod
APP_NAME=Your Church App
2. Android: android/app/src/main/AndroidManifest.xml
<application
android:label="${APP_NAME}"
...>
3. iOS: ios/Runner/Info.plist
<key>CFBundleDisplayName</key>
<string>$(APP_NAME)</string>
4. Web: web/index.html
<title>Your Church App</title>
Bundle ID / Package Name
Change the app's unique identifier for Android (Application ID) and iOS (Bundle ID). This is required when publishing your own version of the app to the stores.
Quick Method: Using CLI Tools
The project includes two CLI tools as dev dependencies to automate this process:
Option 1: change_app_package_name (Recommended)
Changes both Android package name and iOS bundle ID with a single command:
# Change for both platforms
dart run change_app_package_name:main com.yourcompany.yourapp
# Change only Android
dart run change_app_package_name:main com.yourcompany.yourapp --android
# Change only iOS
dart run change_app_package_name:main com.yourcompany.yourapp --ios
- Updates AndroidManifest.xml files (debug, release, profile)
- Updates build.gradle.kts (applicationId and namespace)
- Renames and reorganizes MainActivity directory structure
- Updates iOS Bundle Identifier in project.pbxproj
Option 2: rename
Alternative tool with similar functionality:
# Install globally (one-time)
dart pub global activate rename
# Change bundle ID for all platforms
dart pub global run rename setBundleId --value com.yourcompany.yourapp
# Change only for specific platforms
dart pub global run rename setBundleId --targets android --value com.yourcompany.yourapp
dart pub global run rename setBundleId --targets ios --value com.yourcompany.yourapp
After Changing Bundle ID
After running the CLI tool, you need to update Firebase configuration:
Step 1: Update Firebase Project
- Go to Firebase Console
- Create a new Firebase project or use an existing one
- Add new Android app with your new package name
- Add new iOS app with your new bundle ID
- Download the new configuration files
Step 2: Replace Firebase Configuration Files
# Android - Replace google-services.json
android/app/google-services.json
# iOS - Replace GoogleService-Info.plist
ios/Runner/GoogleService-Info.plist
ios/dev/GoogleService-Info.plist (if using flavors)
Step 3: Update firebase_options.dart
Run FlutterFire CLI to regenerate the configuration:
flutterfire configure
Step 4: Clean and Rebuild
flutter clean
flutter pub get
cd ios && pod install && cd ..
flutter run
Current Package Name
The app currently uses:
| Platform | Identifier |
|---|---|
| Android (applicationId) | br.com.douglaspossasdev.missaoapp |
| iOS (Bundle ID) | br.com.douglaspossasdev.missaoapp |
Naming Conventions
Follow these best practices for your new identifier:
- Format: Reverse domain notation (e.g.,
com.yourcompany.appname) - Characters: Only lowercase letters, numbers, and dots
- Length: Keep it reasonably short but descriptive
- Uniqueness: Must be unique across all apps in the store
com.acmechurch.parishconnect
App Logo & Icons
The app uses a single logo file that appears in multiple locations:
- Launcher Icons - Android, iOS, Web, Windows, macOS
- Login Pages - Mobile and Admin Web login screens
- Admin Sidebar - Header with logo and app title
- Splash Screen - Animated logo via Lottie
Quick Method: Change Logo with One Command
Step 1: Prepare Your Logo
- Format: PNG with transparency (recommended)
- Size: Minimum 1024x1024 pixels
- Aspect Ratio: Square for best results
Step 2: Replace the Logo File
# Replace this file with your logo:
assets/icons/app_logo.png
Step 3: Generate All Icons Automatically
flutter pub get
dart run flutter_launcher_icons
Step 4: Verify Changes
- Run the app on each platform to verify the launcher icons
- Check the login pages (mobile and admin web) for the logo
- Check the admin sidebar header for the logo
Configuration Details
The launcher icons configuration is in flutter_launcher_icons.yaml:
flutter_launcher_icons:
# Platforms
android: true
ios: true
# Main icon source (your logo file)
image_path: "assets/icons/app_logo.png"
# Android Adaptive Icons (Android 8.0+)
min_sdk_android: 21
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/icons/app_logo.png"
# Web
web:
generate: true
image_path: "assets/icons/app_logo.png"
background_color: "#FFFFFF"
theme_color: "#6750A4"
# Desktop
windows:
generate: true
image_path: "assets/icons/app_logo.png"
icon_size: 256
macos:
generate: true
image_path: "assets/icons/app_logo.png"
# iOS App Store requirement
remove_alpha_ios: true
Customizing Android Adaptive Icons
Android 8.0+ supports adaptive icons with separate foreground/background layers:
- Background: Solid color or image
- Foreground: Your logo centered
To change the background color, edit flutter_launcher_icons.yaml:
adaptive_icon_background: "#YOUR_COLOR"
Logo Locations in Code
The logo path is centralized in a constants file for easy maintenance:
// lib/app/core/constants/asset_paths.dart
static const String appLogo = 'assets/icons/app_logo.png';
This constant is used in:
lib/features/auth/presentation/pages/login_page.dart(Mobile login)lib/admin_features/auth/presentation/pages/admin_login_page.dart(Admin login)lib/core/widgets/admin_sidebar.dart(Admin sidebar header)
Manual Method (Advanced)
If you prefer to manually replace icons, here are the locations:
Android Icons
android/app/src/main/res/
├── mipmap-hdpi/ # 72x72 px
├── mipmap-mdpi/ # 48x48 px
├── mipmap-xhdpi/ # 96x96 px
├── mipmap-xxhdpi/ # 144x144 px
└── mipmap-xxxhdpi/ # 192x192 px
iOS Icons
Replace in ios/Runner/Assets.xcassets/AppIcon.appiconset/
Web Icons
web/
├── favicon.png
└── icons/
├── Icon-192.png
├── Icon-512.png
├── Icon-maskable-192.png
└── Icon-maskable-512.png
Splash Screen
Android Splash
Replace splash images in:
android/app/src/main/res/
├── drawable/
│ └── launch_background.xml
├── drawable-hdpi/
│ └── splash.png # Your logo
├── drawable-mdpi/
│ └── splash.png
├── drawable-xhdpi/
│ └── splash.png
├── drawable-xxhdpi/
│ └── splash.png
└── drawable-xxxhdpi/
└── splash.png
Edit launch_background.xml for background color:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splash_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash" />
</item>
</layer-list>
iOS Splash
Edit in Xcode:
- Open
ios/Runner.xcworkspace - Go to Runner → Assets.xcassets → LaunchImage
- Replace images for each device size
Or modify ios/Runner/Base.lproj/LaunchScreen.storyboard
Theme & Colors
Design System Tokens
The app uses a custom Design System package. Edit colors in:
packages/possas_ds/lib/src/tokens/colors.dart
/// Primary color palette
class PDSColors {
// Primary - Your main brand color
static const Color primary50 = Color(0xFFE3F2FD);
static const Color primary100 = Color(0xFFBBDEFB);
static const Color primary200 = Color(0xFF90CAF9);
static const Color primary300 = Color(0xFF64B5F6);
static const Color primary400 = Color(0xFF42A5F5);
static const Color primary500 = Color(0xFF2196F3); // Main primary
static const Color primary600 = Color(0xFF1E88E5);
static const Color primary700 = Color(0xFF1976D2);
static const Color primary800 = Color(0xFF1565C0);
static const Color primary900 = Color(0xFF0D47A1);
// Secondary - Accent color
static const Color secondary500 = Color(0xFF2C3E50);
// Semantic colors
static const Color success = Color(0xFF27AE60);
static const Color warning = Color(0xFFF39C12);
static const Color error = Color(0xFFE74C3C);
static const Color info = Color(0xFF3498DB);
// Neutral colors
static const Color white = Color(0xFFFFFFFF);
static const Color black = Color(0xFF000000);
static const Color grey100 = Color(0xFFF5F5F5);
static const Color grey200 = Color(0xFFEEEEEE);
static const Color grey300 = Color(0xFFE0E0E0);
static const Color grey400 = Color(0xFFBDBDBD);
static const Color grey500 = Color(0xFF9E9E9E);
static const Color grey600 = Color(0xFF757575);
static const Color grey700 = Color(0xFF616161);
static const Color grey800 = Color(0xFF424242);
static const Color grey900 = Color(0xFF212121);
}
Typography
Edit typography in:
packages/possas_ds/lib/src/tokens/typography.dart
class PDSTypography {
static const String fontFamily = 'Roboto';
static const TextStyle headlineLarge = TextStyle(
fontFamily: fontFamily,
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
);
static const TextStyle headlineMedium = TextStyle(
fontFamily: fontFamily,
fontSize: 28,
fontWeight: FontWeight.bold,
);
// ... more styles
}
Using Custom Fonts
- Add font files to
packages/possas_ds/assets/fonts/ - Update
packages/possas_ds/pubspec.yaml:flutter: fonts: - family: YourFont fonts: - asset: assets/fonts/YourFont-Regular.ttf - asset: assets/fonts/YourFont-Bold.ttf weight: 700 - asset: assets/fonts/YourFont-Light.ttf weight: 300 - Update
fontFamilyin typography tokens
Adding New Languages
Step 1: Create ARB File
Copy an existing file and translate:
cp lib/l10n/app_en.arb lib/l10n/app_es.arb
Step 2: Translate Strings
Edit the new ARB file:
{
"@@locale": "es",
"appName": "Missão",
"loginTitle": "Iniciar sesión",
"emailLabel": "Correo electrónico",
"passwordLabel": "Contraseña",
"loginButton": "Iniciar sesión",
"welcomeMessage": "¡Bienvenido, {name}!",
"@welcomeMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
}
}
Step 3: Generate Localizations
flutter gen-l10n
Step 4: Add Locale to App
Edit lib/main.dart:
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: const [
Locale('en'),
Locale('pt'),
Locale('es'), // Add new locale
],
)
Feature Customization
Enabling/Disabling Features
Use feature flags in your environment files:
# .env.prod
ENABLE_GOOGLE_SIGNIN=true
ENABLE_APPLE_SIGNIN=true
ENABLE_PARISH_SEARCH=true
ENABLE_EVENTS=true
ENABLE_NOTIFICATIONS=true
Conditional Feature Loading
// lib/core/config/feature_flags.dart
class FeatureFlags {
static bool get googleSignIn =>
const String.fromEnvironment('ENABLE_GOOGLE_SIGNIN', defaultValue: 'true') == 'true';
static bool get appleSignIn =>
const String.fromEnvironment('ENABLE_APPLE_SIGNIN', defaultValue: 'true') == 'true';
static bool get parishSearch =>
const String.fromEnvironment('ENABLE_PARISH_SEARCH', defaultValue: 'true') == 'true';
}
// Usage in UI
if (FeatureFlags.googleSignIn) {
GoogleSignInButton(),
}
Project Architecture
Clean Architecture Overview
Each feature follows Clean Architecture principles:
lib/features/YOUR_FEATURE/
├── domain/ # Business Logic Layer
│ ├── entities/ # Business objects
│ ├── repositories/ # Abstract interfaces
│ ├── usecases/ # Business rules
│ └── failures/ # Domain-specific errors
│
├── data/ # Data Layer
│ ├── datasources/ # Remote/Local data sources
│ ├── models/ # DTOs (Data Transfer Objects)
│ ├── mappers/ # Entity ↔ DTO converters
│ └── repositories/ # Repository implementations
│
└── presentation/ # UI Layer
├── viewmodels/ # State management (RxNotifier)
├── pages/ # Screen widgets
└── widgets/ # Reusable UI components
Adding a New Feature
Step 1: Create Domain Layer
// lib/features/new_feature/domain/entities/my_entity.dart
class MyEntity {
final String id;
final String name;
const MyEntity({required this.id, required this.name});
}
// lib/features/new_feature/domain/repositories/my_repository.dart
abstract class MyRepository {
Future<Result<MyEntity, Failure>> getById(String id);
}
// lib/features/new_feature/domain/usecases/get_my_entity_usecase.dart
class GetMyEntityUseCase {
final MyRepository _repository;
GetMyEntityUseCase(this._repository);
Future<Result<MyEntity, Failure>> call(String id) {
return _repository.getById(id);
}
}
Step 2: Create Data Layer
// lib/features/new_feature/data/models/my_entity_dto.dart
class MyEntityDto {
final String id;
final String name;
MyEntityDto({required this.id, required this.name});
factory MyEntityDto.fromJson(Map<String, dynamic> json) {
return MyEntityDto(
id: json['id'] as String,
name: json['name'] as String,
);
}
MyEntity toEntity() => MyEntity(id: id, name: name);
}
// lib/features/new_feature/data/datasources/my_datasource.dart
abstract class MyDataSource {
Future<MyEntityDto> getById(String id);
}
class MyDataSourceImpl implements MyDataSource {
final FirebaseFirestore _firestore;
MyDataSourceImpl(this._firestore);
@override
Future<MyEntityDto> getById(String id) async {
final doc = await _firestore.collection('my_collection').doc(id).get();
return MyEntityDto.fromJson(doc.data()!);
}
}
// lib/features/new_feature/data/repositories/my_repository_impl.dart
class MyRepositoryImpl implements MyRepository {
final MyDataSource _dataSource;
MyRepositoryImpl(this._dataSource);
@override
Future<Result<MyEntity, Failure>> getById(String id) async {
try {
final dto = await _dataSource.getById(id);
return Success(dto.toEntity());
} catch (e) {
return Error(Failure.unknown());
}
}
}
Step 3: Create Presentation Layer
// lib/features/new_feature/presentation/viewmodels/my_viewmodel.dart
class MyViewModel extends RxNotifier<MyViewState> {
final GetMyEntityUseCase _getMyEntityUseCase;
MyViewModel(this._getMyEntityUseCase);
@override
MyViewState initial() => MyViewState.initial();
Future<void> loadEntity(String id) async {
state = state.copyWith(isLoading: true);
final result = await _getMyEntityUseCase(id);
state = result.when(
success: (entity) => state.copyWith(isLoading: false, entity: entity),
error: (failure) => state.copyWith(isLoading: false, error: failure),
);
}
}
// lib/features/new_feature/presentation/pages/my_page.dart
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = context.watch<MyViewModel>();
return Scaffold(
appBar: AppBar(title: Text('My Feature')),
body: viewModel.state.isLoading
? Center(child: CircularProgressIndicator())
: Text(viewModel.state.entity?.name ?? 'No data'),
);
}
}
Step 4: Register in Dependency Injection
// lib/app/di/my_feature_module.dart
class MyFeatureModule {
static Future<void> init() async {
sl.registerFactory<MyDataSource>(
() => MyDataSourceImpl(sl<FirebaseFirestore>()),
);
sl.registerFactory<MyRepository>(
() => MyRepositoryImpl(sl<MyDataSource>()),
);
sl.registerFactory(
() => GetMyEntityUseCase(sl<MyRepository>()),
);
sl.registerFactory(
() => MyViewModel(sl<GetMyEntityUseCase>()),
);
}
}
Design System (Possas DS)
Available Components
The Design System includes pre-built components:
Buttons
PDSButton, PDSIconButton, PDSOutlinedButton
Inputs
PDSTextField, PDSSearchField, PDSDropdown
Cards
PDSCard, PDSListTile, PDSInfoCard
Dialogs
PDSDialog, PDSBottomSheet, PDSSnackbar
Navigation
PDSAppBar, PDSBottomNav, PDSDrawer
Feedback
PDSLoader, PDSEmptyState, PDSErrorView
Widgetbook Preview
View all components in the Widgetbook:
cd packages/possas_ds/example
flutter run -d chrome
Using Components
import 'package:possas_ds/possas_ds.dart';
// Button
PDSButton(
label: 'Submit',
onPressed: () {},
isLoading: false,
),
// Text Field
PDSTextField(
label: 'Email',
controller: _emailController,
keyboardType: TextInputType.emailAddress,
),
// Card
PDSCard(
child: Column(
children: [
Text('Card Title'),
Text('Card content'),
],
),
),
Next Steps
After customization:
- Deployment - Deploy your customized app
- FAQ - Common questions and troubleshooting