Customization Guide

Make the app your own with branding and feature customization

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.

Important: Changing the Bundle ID/Package Name will create a completely new app from the store's perspective. Users of the original app will need to install the new version separately.

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
What it does:
  • 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

  1. Go to Firebase Console
  2. Create a new Firebase project or use an existing one
  3. Add new Android app with your new package name
  4. Add new iOS app with your new bundle ID
  5. 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:

Example: If your company is "Acme Church" and the app is "Parish Connect", a good identifier would be com.acmechurch.parishconnect

App Logo & Icons

The app uses a single logo file that appears in multiple locations:

Quick Method: Change Logo with One Command

Recommended: This is the easiest way to change the app logo and launcher icons across all platforms.

Step 1: Prepare Your Logo

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
That's it! This command automatically generates all launcher icons for Android, iOS, Web, Windows, and macOS from your single logo file.

Step 4: Verify Changes

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:

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:

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
Note: The quick method above is recommended as it handles all platforms automatically and ensures consistent sizing.

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:

  1. Open ios/Runner.xcworkspace
  2. Go to Runner → Assets.xcassets → LaunchImage
  3. 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

  1. Add font files to packages/possas_ds/assets/fonts/
  2. 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
  3. Update fontFamily in 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:

  1. Deployment - Deploy your customized app
  2. FAQ - Common questions and troubleshooting