A professional portfolio web application built with Flutter, showcasing projects, skills, experience, and blog posts. Built with Clean Architecture and BLoC pattern for maintainability and scalability.
- Getting Started
- Architecture
- Project Structure
- Key Technologies
- Features
- Theming
- Firebase Setup
- Testing
- Building
- Additional Documentation
- Contributing
- Contact
This is a Flutter web application that can also be built for desktop and mobile platforms.
- Flutter SDK (latest stable version)
- Dart SDK (comes with Flutter)
- Firebase account (for backend data)
# Clone the repository
git clone <repository-url>
# Navigate to project directory
cd Portfolio
# Get dependencies
flutter pub get
# (Optional) Create .env file with Firebase credentials for local development
echo "FIREBASE_EMAIL=your-email@example.com" > .env
echo "FIREBASE_PASSWORD=your-password" >> .env
# Run the application with credentials
flutter run -d chrome --dart-define-from-file=.env # For web
flutter run --dart-define-from-file=.env # For desktop/mobile
# Or run without credentials (uses stub config)
flutter run -d chrome # For web
flutter run # For desktop/mobileThis project follows Clean Architecture with clear separation of concerns across three layers:
┌──────────────────────────────────────────────┐
│ Presentation Layer │
│ - UI (lib/main/ui/) │
│ - BLoC State Management │
│ - Responsive Layouts (Desktop/Tablet/Mobile)│
└──────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ Domain Layer │
│ - Models (lib/main/domain/model/) │
│ - Use Cases (lib/main/domain/usecases/) │
│ - Repository Interfaces (domain/repositories/)│
└──────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────┐
│ Data Layer │
│ - Repository Implementations │
│ - Data Sources (Remote/Local) │
│ - Mappers (DTO ↔ Domain) │
│ - Local: SQLite (lib/main/data/local/) │
│ - Remote: Firebase (lib/main/data/remote/) │
└──────────────────────────────────────────────┘
- UI Components: Flutter widgets organized by feature (blog, portfolio, resume, contact, etc.)
- State Management: BLoC pattern for predictable state handling
- Responsive Design: Separate layouts for desktop, tablet, and mobile
- Navigation: Go Router for declarative routing
Key UI Modules:
blog/- Blog posts displayportfolio/- Project showcaseresume/- Work experience and skillscontact/- Contact form and informationadmin/- Admin panel for content managementcomponents/- Reusable UI components
- Models (
model/): Pure Dart entities representing business objects (Post, Project, Position, Skill, etc.) - Use Cases (
usecases/): Business logic operations (GetPosts, GetProjects, CreateProject, etc.) - Repository Interfaces (
repositories/): Abstract contracts for data access
Key Models:
Post- Blog post entityProject- Portfolio project entityPosition- Work experience entitySkill- Professional skill entityEducation- Education/certification entityPersonalInfo- Personal information and contacts
- Repository Implementations (
repository/): Concrete implementations of domain repositories - Data Sources:
remote/- Firebase Realtime Database integrationlocal/- SQLite for offline caching
- Mappers (
mapper/): Convert between DTOs and domain models - Utils: Helper functions for data operations
Key Repositories:
PostRepositoryImpl- Blog post data managementProjectRepositoryImpl- Portfolio project data managementPositionRepositoryImpl- Work experience data management
lib/
├── main.dart # Application entry point
├── firebase_options.dart # Firebase configuration
├── core/ # Core infrastructure
│ ├── config/ # Configuration management
│ ├── logger/ # Logging (Debug, Crashlytics, Release)
│ └── platform/ # Platform-specific utilities
├── main/
│ ├── di/ # Dependency Injection
│ │ └── service_locator.dart # GetIt setup
│ ├── domain/ # Domain Layer (Business Logic)
│ │ ├── model/ # Domain entities
│ │ ├── usecases/ # Business use cases
│ │ └── repositories/ # Repository interfaces
│ ├── data/ # Data Layer
│ │ ├── repository/ # Repository implementations
│ │ ├── remote/ # Firebase data sources
│ │ ├── local/ # SQLite data sources
│ │ ├── mapper/ # DTO ↔ Domain mappers
│ │ └── utils/ # Data utilities
│ ├── ui/ # Presentation Layer
│ │ ├── components/ # Reusable UI components
│ │ ├── responsive/ # Responsive layouts
│ │ ├── blog/ # Blog feature
│ │ ├── portfolio/ # Portfolio feature
│ │ ├── resume/ # Resume/CV feature
│ │ ├── contact/ # Contact feature
│ │ ├── admin/ # Admin panel
│ │ └── menu/ # Navigation menu
│ └── mixins/ # Reusable mixins
└── utils/ # Global utilities
├── colors.dart # Color palette
├── theme.dart # Theme definitions
├── theme_provider.dart # Theme state management
├── env_config.dart # Environment configuration
└── ...
- Flutter: Cross-platform UI framework (web, mobile, desktop)
- Dart: Programming language
- Clean Architecture: Separation of concerns across layers
- BLoC Pattern: Predictable state management with
flutter_bloc - Repository Pattern: Abstract data access
- Use Case Pattern: Encapsulated business logic
- Dependency Injection:
GetItfor service location
- Firebase:
- Realtime Database (remote data storage)
- Authentication (user management)
- Crashlytics (error tracking)
- SQLite (
sqflite): Local data caching - SharedPreferences: Local settings storage
- Go Router: Declarative routing
- Provider: Theme management
- Responsive Design: Adaptive layouts for all screen sizes
- Animated Text Kit: Text animations
- Cached Network Image: Image caching and optimization
- BLoC Test: Testing BLoC state management
- Flutter Test: Unit and widget testing
- Flutter Lints: Code quality and style enforcement
- Firebase Crashlytics: Production error monitoring
The application strictly follows the Dependency Rule: dependencies only point inward (toward the domain layer).
Presentation → Domain ← Data
↓ ↑ ↑
BLoCs Use Cases Repositories
↓ ↑ ↑
UI Entities Data Sources
Follow these steps to add a new feature following Clean Architecture:
Create a model in lib/main/domain/model/:
class User {
final String id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
}Define the contract in lib/main/domain/repositories/:
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> saveUser(User user);
}Implement business logic in lib/main/domain/usecases/:
class GetUserUseCase {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<User> call(String id) async {
return await repository.getUser(id);
}
}Create implementation in lib/main/data/repository/:
class UserRepositoryImpl implements UserRepository {
final RemoteDataSource remoteDataSource;
final LocalDataSource localDataSource;
UserRepositoryImpl(this.remoteDataSource, this.localDataSource);
@override
Future<User> getUser(String id) async {
try {
// Try remote first
final dto = await remoteDataSource.getUser(id);
final user = UserMapper.fromDto(dto);
// Cache locally
await localDataSource.saveUser(user);
return user;
} catch (e) {
// Fallback to local
return await localDataSource.getUser(id);
}
}
}Add to lib/main/di/service_locator.dart:
// Data sources
locator.registerLazySingleton<UserRemoteDataSource>(
() => UserRemoteDataSourceImpl(database: locator()),
);
// Repositories
locator.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(locator(), locator()),
);
// Use cases
locator.registerLazySingleton(() => GetUserUseCase(locator()));Implement state management in lib/main/ui/<feature>/bloc/:
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserUseCase getUserUseCase;
UserBloc(this.getUserUseCase) : super(UserInitial()) {
on<LoadUser>(_onLoadUser);
}
Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final user = await getUserUseCase(event.id);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}Create widgets in lib/main/ui/<feature>/:
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => locator<UserBloc>()..add(LoadUser('123')),
child: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) return CircularProgressIndicator();
if (state is UserLoaded) return UserWidget(state.user);
if (state is UserError) return ErrorWidget(state.message);
return Container();
},
),
);
}
}- Responsive Design: Adaptive layouts for mobile, tablet, and desktop screens
- Light & Dark Themes: System-aware theme with manual toggle option
- Project Portfolio: Showcase of projects with images, descriptions, and links
- Work Experience: Timeline of professional positions and roles
- Skills Showcase: Visual representation of technical skills and competencies
- Blog Integration: Dynamic blog posts from Firebase
- Certifications: Education and professional certifications
- Contact Information: Social media links and contact details
- Smooth Animations: Modern UI with fluid transitions
- Content Management: Admin panel for managing portfolio content
- Project CRUD: Create, read, update, delete projects
- Real-time Updates: Changes sync immediately via Firebase
- Authentication: Secure Firebase authentication for admin access
- Offline Support: SQLite caching for offline functionality
- Error Tracking: Firebase Crashlytics for production monitoring
- Optimized Performance: Image caching and lazy loading
- SEO-Friendly: Proper meta tags and routing for web
- Progressive Web App: Installable PWA with splash screens
The application supports light and dark themes with automatic system detection.
- System Theme Detection: Automatically follows device theme preference
- Manual Toggle: Theme switcher in the navigation panel
- Responsive: Optimized font sizes for desktop and mobile
- Persistent: Theme preference saved using SharedPreferences
Dark Theme (Default):
- Background:
#212428 - Text:
#C4CFDE - Accent:
#F9004D
Light Theme:
- Background:
#DEDBD7 - Text:
#3C3021 - Accent:
#F9004D
// Get current theme
final isDark = Theme.of(context).brightness == Brightness.dark;
// Use theme-aware colors
Container(
color: isDark ? UIColors.backgroundColor : UIColors.lightBackgroundColor,
)
// Or use theme properties directly
Text(
'Hello',
style: Theme.of(context).textTheme.bodyLarge,
)Edit theme definitions in lib/utils/theme.dart:
desktopTheme/phoneTheme- Dark themesdesktopLightTheme/phoneLightTheme- Light themes
Add custom colors in lib/utils/colors.dart.
-
Create Firebase Project
- Go to Firebase Console
- Create a new project or use an existing one
-
Enable Services
- Realtime Database: For storing portfolio data
- Authentication: For admin panel access
- Crashlytics: For error tracking (optional but recommended)
-
Configure Flutter App
# Install FlutterFire CLI dart pub global activate flutterfire_cli # Configure Firebase for your project flutterfire configure
This generates
firebase_options.dartwith your Firebase configuration. -
Set Up Authentication
- Go to Firebase Console → Authentication
- Enable Email/Password sign-in method
- Create an admin user for accessing the admin panel
Set up your Realtime Database with this structure:
{
"posts": {
"post_id_1": {
"id": "post_id_1",
"title": "Blog Post Title",
"description": "Post description",
"imageUrl": "https://...",
"link": "https://...",
"createdAt": 1234567890
}
},
"projects": {
"project_id_1": {
"id": "project_id_1",
"title": "Project Name",
"description": "Project description",
"imageUrl": "assets/projects/project.jpg",
"githubUrl": "https://github.com/...",
"technologies": ["Flutter", "Firebase"],
"order": 0
}
},
"positions": {
"position_id_1": {
"id": "position_id_1",
"company": "Company Name",
"position": "Job Title",
"description": "Job description",
"startDate": "2020-01",
"endDate": "2023-12",
"icon": "assets/icons/company.png"
}
}
}For local development with Firebase Authentication:
-
Create a
.envfile in the project root:FIREBASE_EMAIL=your-admin@example.com FIREBASE_PASSWORD=your-secure-password
-
Run with environment variables:
flutter run --dart-define-from-file=.env
Security Note: Never commit .env files to version control. The file is already in .gitignore.
# Run all tests
flutter test
# Run tests with coverage
flutter test --coverage# Development build
flutter run -d chrome
# Production build
flutter build web --release
# With Firebase credentials
flutter build web --release --dart-define-from-file=.env# macOS
flutter build macos --release
# Windows
flutter build windows --release
# Linux
flutter build linux --release# Android
flutter build apk --release # APK for testing
flutter build appbundle --release # App Bundle for Play Store
# iOS
flutter build ios --release# Build with .env file
flutter build web --dart-define-from-file=.env
# Or pass individual variables
flutter build web --dart-define=FIREBASE_EMAIL=admin@example.comThis project includes automated CI/CD pipeline for distributing iOS builds to TestFlight using GitHub Actions and Fastlane.
The CI pipeline automatically:
- Builds and signs the iOS app on push to
developbranch - Uploads the build to TestFlight via App Store Connect API
- Supports manual workflow dispatch with custom release notes
- Provides a stable public TestFlight link for long-term distribution
You must configure the following secrets in your GitHub repository (Settings → Secrets and variables → Actions → New repository secret):
| Secret Name | Description | How to Obtain |
|---|---|---|
APP_STORE_CONNECT_KEY_ID |
API Key ID (e.g., AB12CD34EF) |
App Store Connect → Users and Access → Keys |
APP_STORE_CONNECT_ISSUER_ID |
Issuer ID (UUID format) | App Store Connect → Users and Access → Keys (top right) |
APP_STORE_CONNECT_KEY_P8 |
Private key content (.p8 file) |
Download when creating the key (only shown once!) |
Creating App Store Connect API Key:
- Go to App Store Connect
- Navigate to Users and Access → Keys tab (under Integrations)
- Click Generate API Key or + button
- Enter a name (e.g., "GitHub Actions CI")
- Select Access: Choose App Manager or Developer role
- Click Generate
- Important: Download the
.p8file immediately - you cannot download it again! - Copy the Key ID and Issuer ID from the page
Adding the P8 key to GitHub:
# On macOS/Linux, copy the entire content of the .p8 file:
cat AuthKey_AB12CD34EF.p8 | pbcopy # macOS
cat AuthKey_AB12CD34EF.p8 # Linux (copy manually)
# Then paste it as the APP_STORE_CONNECT_KEY_P8 secret value in GitHub| Secret Name | Description | How to Obtain |
|---|---|---|
IOS_DISTRIBUTION_CERT_BASE64 |
Base64-encoded distribution certificate (.p12) |
Export from Xcode or Keychain Access |
IOS_DISTRIBUTION_CERT_PASSWORD |
Password for the .p12 certificate |
Set when exporting the certificate |
Exporting Distribution Certificate:
-
Option A: From Xcode
- Open Xcode → Preferences → Accounts
- Select your Apple ID → Select Team → Manage Certificates
- Right-click Apple Distribution certificate → Export
- Save as
.p12and set a password - Convert to base64:
base64 -i distribution_certificate.p12 | pbcopy # macOS base64 -w 0 distribution_certificate.p12 # Linux
-
Option B: From Keychain Access (macOS)
- Open Keychain Access app
- Select login keychain → My Certificates
- Find your Apple Distribution certificate
- Right-click → Export "Apple Distribution: ..."
- Save as
.p12format with a password - Convert to base64:
base64 -i YourCertificate.p12 | pbcopy
| Secret Name | Description | How to Obtain |
|---|---|---|
IOS_PROVISION_PROFILE_BASE64 |
Base64-encoded provisioning profile (.mobileprovision) |
Apple Developer Portal |
Creating and Exporting Provisioning Profile:
- Go to Apple Developer Portal
- Click + to create a new profile
- Select App Store distribution type
- Select your App ID (
com.insearching.portfolio) - Select your Distribution Certificate
- Enter a name (e.g., "Portfolio AppStore")
- Click Generate and download the
.mobileprovisionfile - Convert to base64:
base64 -i Portfolio_AppStore.mobileprovision | pbcopy # macOS base64 -w 0 Portfolio_AppStore.mobileprovision # Linux
| Secret Name | Description | Default |
|---|---|---|
KEYCHAIN_PASSWORD |
Password for temporary keychain in CI | Auto-generated UUID |
After your first successful build is uploaded and processed:
- Go to App Store Connect
- Select your app → TestFlight tab
- In the left sidebar, under External Testing, click on a group or create a new one
- Click Add Build and select your uploaded build
- Fill in the Test Details (What to Test)
- Click Submit for Review (first time only - required for external testing)
- Once approved, click Public Link in the group
- Click Enable Public Link
- Choose Open to Anyone or configure filters
- Copy the public link (format:
https://testflight.apple.com/join/XXXXXXXX)
Important Notes:
- The public link remains stable across builds
- Users can install new builds automatically through TestFlight
- Each build expires after 90 days, but the link doesn't
- You must keep uploading new builds to maintain availability
- External testing requires Apple's review for the first build only
The workflow automatically runs when you push to develop branch and changes include:
- iOS code (
ios/**) - Flutter code (
lib/**) - Dependencies (
pubspec.yaml,pubspec.lock) - Workflow or Fastlane files
git checkout develop
git add .
git commit -m "feat: Add new feature for iOS"
git push origin developYou can also trigger the workflow manually with custom parameters:
- Go to your GitHub repository
- Click Actions tab
- Select Deploy to Firebase Hosting on merge workflow
- Click Run workflow button
- (Optional) Enter custom Release notes
- (Optional) Override the Scheme name (default:
Runner) - Click Run workflow
Via GitHub CLI:
# With default changelog
gh workflow run firebase-hosting-merge.yml
# With custom changelog
gh workflow run firebase-hosting-merge.yml \
-f changelog="Fixed critical bug in authentication flow"
# With custom scheme
gh workflow run firebase-hosting-merge.yml \
-f scheme="Runner" \
-f changelog="Performance improvements and bug fixes"In GitHub Actions:
- Go to Actions tab in your repository
- Click on the latest Deploy to Firebase Hosting on merge workflow run
- Monitor real-time logs for each step
- Download build artifacts (IPA, dSYM) from the workflow run
In App Store Connect:
- Go to App Store Connect
- Select your app → TestFlight tab
- Check iOS builds section
- Wait for Apple to process the build (typically 5-15 minutes)
- Once status shows ✅, the build is ready for testing
Solution:
- Verify
IOS_DISTRIBUTION_CERT_BASE64is correctly base64-encoded - Ensure
IOS_DISTRIBUTION_CERT_PASSWORDmatches the certificate password - Check that the certificate is a Distribution certificate (not Development)
- Verify the certificate hasn't expired in Apple Developer Portal
Solution:
- Ensure
IOS_PROVISION_PROFILE_BASE64is correctly base64-encoded - Verify the provisioning profile:
- Type is App Store (not Ad Hoc or Development)
- App ID matches
com.insearching.portfolio - Certificate matches your distribution certificate
- Profile hasn't expired
- Re-download and re-encode the provisioning profile if needed
Solution:
- Verify all three API key secrets are correct:
APP_STORE_CONNECT_KEY_IDAPP_STORE_CONNECT_ISSUER_IDAPP_STORE_CONNECT_KEY_P8(must be complete.p8file content)
- Ensure the API key has App Manager or Developer access
- Check the API key hasn't been revoked in App Store Connect
Common reasons:
- Apple is still processing the build (wait 5-15 minutes)
- Build has the same version/build number as existing build
- Build was uploaded but needs compliance information
- Check email for messages from Apple about the build
Solution:
- Increment version or build number in
pubspec.yaml:version: 1.0.1+2 # Increment build number (+2)
- Check App Store Connect for any required compliance responses
- Verify App Store Connect API key has proper permissions
If Apple asks about encryption:
- Most apps need to add
ITSAppUsesNonExemptEncryption = NOtoInfo.plist - Or respond to the compliance questions in App Store Connect
Test the Fastlane setup locally before pushing to CI:
# Install dependencies
bundle install
# Set environment variables (create a .env.local file)
export APP_STORE_CONNECT_KEY_ID="your_key_id"
export APP_STORE_CONNECT_ISSUER_ID="your_issuer_id"
export APP_STORE_CONNECT_KEY_P8="$(cat path/to/AuthKey_XXXXXX.p8)"
export IOS_WORKSPACE="$(pwd)/ios/Runner.xcworkspace"
export IOS_SCHEME="Runner"
export CHANGELOG="Test build from local machine"
# Run build only (no upload)
bundle exec fastlane ios build_only
# Run full beta lane (build + upload)
bundle exec fastlane ios beta✅ Do:
- Store all sensitive data in GitHub Secrets
- Use App Store Connect API key (avoids 2FA issues)
- Regularly rotate certificates and provisioning profiles
- Use strong passwords for certificate
.p12files - Enable branch protection on
developbranch
❌ Don't:
- Commit certificates, provisioning profiles, or API keys to the repository
- Share API keys or certificates publicly
- Use personal Apple ID credentials in CI
- Store secrets in code or configuration files
- ✅ Automatic code signing with temporary keychain
- ✅ App Store Connect API authentication (no 2FA required)
- ✅ Build artifacts uploaded to GitHub (IPA, dSYM)
- ✅ Automatic cleanup of sensitive files
- ✅ Detailed logging and error messages
- ✅ Job summaries with next steps
- ✅ Configurable via workflow inputs
- ✅ Caching for Ruby gems and Flutter SDK
- GitHub Actions provides 2,000 free macOS minutes/month for private repositories
- Each build typically takes 15-25 minutes
- Monitor usage in Settings → Billing → Actions usage
- Consider triggering builds only on significant changes
When contributing to this project:
- Follow Clean Architecture: Maintain clear separation between layers
- Use BLoC Pattern: All state management must use BLoC
- Write Tests: Add unit tests for use cases and BLoCs
- Follow Style Guide: Use Flutter/Dart conventions and linting rules
- Document Changes: Update relevant documentation
- Add Type Safety: Use strong typing and avoid
dynamicwhere possible
# Run linter
flutter analyze
# Format code
flutter format .
# Run tests
flutter testThis project includes detailed documentation for specific topics:
- CRASHLYTICS_SETUP.md: Firebase Crashlytics integration for error tracking
- VERSION_MANAGEMENT.md: Managing Flutter SDK and app versions
- scripts/README.md: Utility scripts for Firebase data management
- QUICKSTART_TESTFLIGHT.md: ⚡ Quick start guide (30 minutes to first build!)
- IOS_TESTFLIGHT_SETUP.md: Complete implementation details and reference
- .github/SECRETS_CHECKLIST.md: GitHub secrets configuration checklist
- fastlane/README_SETUP.md: Local Fastlane setup and testing guide
- Email: hrabas.serhii@gmail.com
- GitHub: insearching
Built with ❤️ using Flutter and Clean Architecture