A modern Flutter app for learning, sharing, and live collaboration.
WeBuddhist App is designed to help users learn, live, and share knowledge interactively. It features a beautiful UI, supports both light and dark themes, and is built with best Flutter practices.
- Light and Dark theme support with toggle
- Modern Flutter architecture
- Easy to customize and extend
git clone https://github.com/your-username/flutter_pecha.git
cd flutter_pechaflutter pub getCreate environment files from the template:
cp .env.example .env.dev
cp .env.example .env.staging
cp .env.example .env.prodEdit each file with the appropriate values for that environment.
Android
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor staging -t lib/main_staging.dart
flutter run --flavor prod -t lib/main_prod.dartBuild APK:
flutter build apk --flavor dev -t lib/main_dev.dart
flutter build apk --flavor staging -t lib/main_staging.dart
flutter build apk --flavor prod -t lib/main_prod.dartBuild App Bundle:
flutter build appbundle --flavor prod -t lib/main_prod.dartiOS
flutter run --flavor dev -t lib/main_dev.dart
flutter run --flavor staging -t lib/main_staging.dart
flutter run --flavor prod -t lib/main_prod.dartBuild IPA:
flutter build ios --flavor prod -t lib/main_prod.dartNote:
- For iOS, ensure you have Xcode installed and have granted the necessary permissions (see Flutter macOS setup).
- For Android, ensure Android Studio and an emulator/device are set up.
The app uses Flutter's built-in internationalization (l10n) with ARB files for translations.
Localization files location:
lib/core/l10n/
To add a new translation string:
- Add the key to all ARB files (
app_en.arb,app_bo.arb,app_zh.arb):
// app_en.arb
"my_new_key": "My English text",
// app_bo.arb
"my_new_key": "My Tibetan text",
// app_zh.arb
"my_new_key": "My Chinese text",- Generate localization files:
flutter gen-l10n- Use in your widget:
import 'package:flutter_pecha/core/extensions/context_ext.dart';
// Access translation via context.l10n
Text(context.l10n.my_new_key)For translations with parameters:
// app_en.arb
"greeting": "Hello {name}",
"@greeting": {
"placeholders": {
"name": {"type": "String"}
}
}Usage:
Text(context.l10n.greeting('John'))This project follows Clean Architecture principles with clear separation of concerns across three main layers.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PRESENTATION LAYER β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UI Components (Screens, Widgets) β β
β β State Management (Riverpod Notifiers/Providers) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β depends on
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DOMAIN LAYER β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Entities (Business Objects) β β
β β Use Cases (Business Logic) β β
β β Repository Interfaces (Contracts) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β depends on
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DATA LAYER β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Repository Implementations β β
β β Data Sources (API, Local Storage) β β
β β Models (DTOs for serialization) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β depends on
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EXTERNAL SERVICES β
β (Auth0, Firebase, HTTP, Storage, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
lib/
βββ core/ # Shared infrastructure & utilities
β βββ config/ # App configuration
β βββ di/ # Dependency injection
β βββ error/ # Error handling (Failures)
β βββ network/ # Network utilities
β βββ services/ # External service integrations
β βββ storage/ # Local storage utilities
β βββ theme/ # App theming
β
βββ features/ # Feature-based modules
β βββ auth/ # β Authentication feature example
β βββ domain/ # Business logic (no dependencies)
β βββ data/ # Data implementation
β βββ presentation/ # UI & state management
β
βββ shared/ # Cross-cutting concerns
βββ data/ # Shared data layer utilities
βββ domain/ # Shared domain logic
βββ presentation/ # Shared UI components
βββ widgets/ # Reusable widgets
Here's how authentication follows clean architecture through all layers:
Pure business logic with no framework dependencies
domain/
βββ entities/
β βββ user.dart # User business entity
βββ repositories/
β βββ auth_repository.dart # Repository interface (contract)
βββ usecases/
βββ login_usecase.dart # Login business logic
βββ get_current_user_usecase.dart
βββ logout_usecase.dart
Key points:
Userentity: Pure Dart class with business propertiesAuthRepository: Abstract interface defining data operationsLoginUseCase: Orchestrates login logic, returnsEither<Failure, User>
Implements domain contracts, handles external dependencies
data/
βββ models/
β βββ user_model.dart # DTO for JSON serialization
βββ datasources/
β βββ auth_remote_datasource.dart # API/Service calls
βββ repositories/
βββ auth_repository_impl.dart # Implements AuthRepository
Key points:
UserModel: Handles JSON β Dart conversionAuthRemoteDataSource: Makes actual API callsAuthRepositoryImpl: Implements domain interface, uses datasource
UI and state management
presentation/
βββ providers/
β βββ auth_notifier.dart # State management
β βββ auth_providers.dart # DI setup
β βββ use_case_providers.dart # Use case providers
βββ screens/
β βββ login_page.dart # Login UI
βββ widgets/
βββ login_form.dart # Reusable form widget
Key points:
AuthNotifier: Manages auth state, calls use casesauthProviders: Wire up dependencies using RiverpodLoginPage: UI that consumes state via providers
| Layer | Responsibility | Dependencies |
|---|---|---|
| Domain | Business rules & logic | None (pure Dart) |
| Data | Data sources & persistence | Domain, external services |
| Presentation | UI & state management | Domain (via use cases) |
- Testable: Each layer can be unit tested independently
- Maintainable: Changes in one layer don't break others
- Scalable: Easy to add new features following same pattern
- Flexible: Swap implementations (e.g., change API) without affecting business logic
Dio is a powerful HTTP client for Dart. Think of it as a supercharged http package with built-in support for interceptors, retries, timeout handling, and request transformation.
| Feature | http package |
Dio |
|---|---|---|
| Interceptors | No | Yes (we use this heavily) |
| Global configuration | Limited | Full |
| Automatic retries | Manual | Built-in |
Location: lib/core/network/dio_client.dart
class DioClient {
DioClient({
required AuthInterceptor authInterceptor,
required RetryInterceptor retryInterceptor,
// ... other interceptors
}) : _dio = Dio(options) {
// Interceptor order is critical
_dio.interceptors.addAll([
authInterceptor, // 1. Add auth headers first
cacheInterceptor, // 2. Check cache
retryInterceptor, // 3. Handle 401 & network errors
errorInterceptor, // 4. Convert errors
loggingInterceptor, // 5. Log final result
]);
}
}Why this order? Each interceptor processes the request in sequence. Auth must run first to add tokens before the request goes out.
Request: Auth β Cache β Retry β Error β Log β Server
Response: Log β Error β Retry β Cache β Auth β UI
What it does: Checks if the API endpoint requires authentication, adds the auth token.
// lib/core/network/interceptors/auth_interceptor.dart
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// Only add auth for protected routes
if (ProtectedRoutes.isProtected(options.path)) {
final token = await _tokenProvider.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
}
handler.next(options); // Pass to next interceptor
}Key point: Uses TokenProvider abstraction so we can swap token sources without changing this code.
What it does: Handles 401 (token expired) errors by refreshing the token and retrying the request.
// lib/core/network/interceptors/retry_interceptor.dart
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Handle 401 - Token expired
if (err.response?.statusCode == 401) {
if (_isRefreshing) {
// Already refreshing, queue this request
_refreshQueue.add(_RetryRequest(err, handler));
return;
}
_isRefreshing = true;
final newToken = await _authService.refreshIdToken();
// Retry all queued requests with new token
for (final request in _refreshQueue) {
request.error.requestOptions.headers['Authorization'] = 'Bearer $newToken';
// Retry the request...
}
}
// Retry network errors with exponential backoff
if (_shouldRetry(err)) {
await Future.delayed(Duration(milliseconds: 1000 * (1 << retryCount)));
// Retry...
}
}OAuth 2.0 is an authorization framework that lets users grant limited access to their accounts without sharing passwords.
Real-world analogy: Like giving a valet key to your car - it can only drive the car, not open the trunk.
- Security: User never shares password with your app
- Control: User can revoke access anytime
- Standardization: Industry-wide protocol
- Social Login: Leverage existing accounts
βββββββββββββββ βββββββββββββββ
β User β β Auth0 Server β
β (Resource β β β
β Owner) β β β
ββββββββ¬βββββββ ββββββββ¬βββββββ
β β
β 1. Tap "Login with Google" β
βββββββββββββββββββββββββββββββββΊβ
β β
β 2. Show Google login page β
ββββββββββββββββββββββββββββββββββ€
β β
β 3. User authenticates β
βββββββββββββββββββββββββββββββββΊβ
β β
β 4. Return tokens β
ββββββββββββββββββββββββββββββββββ€
β β
β (Access Token, ID Token, β
β Refresh Token) β
Location: lib/features/auth/auth_service.dart
class AuthService {
final Auth0 _auth0 = Auth0('YOUR_DOMAIN', 'YOUR_CLIENT_ID');
Future<Credentials?> loginWithGoogle() async {
final credentials = await _auth0.webAuthentication(scheme: 'org.pecha.app')
.login(
useHTTPS: true,
parameters: {"connection": "google-oauth2"},
scopes: {"openid", "profile", "email", "offline_access"},
);
// Auth0 SDK handles PKCE automatically
// Credentials contain: accessToken, idToken, refreshToken, expiresIn
await _auth0.credentialsManager.storeCredentials(credentials);
return credentials;
}
}What scopes mean:
openid: Enables OIDC protocolprofile: Access to user profile dataemail: Access to user emailoffline_access: Enables refresh tokens
Step 1: UI - Login Button
// lib/features/auth/presentation/widgets/auth_buttons.dart
ElevatedButton(
onPressed: () {
ref.read(authProvider.notifier).login(connection: 'google-oauth2');
},
child: Text('Login with Google'),
)Step 2: AuthNotifier - State Management
// lib/features/auth/presentation/providers/auth_notifier.dart
Future<void> login({String? connection}) async {
state = state.copyWith(isLoading: true);
final result = await _loginUseCase(LoginParams(connection: connection));
result.fold(
(failure) => state = state.copyWith(errorMessage: failure.message),
(credentials) => _handleSuccessfulLogin(credentials),
);
}Step 3: UseCase - Orchestration
// lib/features/auth/domain/usecases/login_usecase.dart
Future<Either<Failure, AuthCredentials>> call(LoginParams params) async {
switch (params.connection) {
case 'google-oauth2':
return await _repository.loginWithGoogle();
case 'apple':
return await _repository.loginWithApple();
}
}Step 4: Repository - Data Layer
// lib/features/auth/data/repositories/auth_repository_impl.dart
Future<Either<Failure, AuthCredentials>> loginWithGoogle() async {
try {
final credentials = await _authService.loginWithGoogle();
return Right(_toAuthCredentials(credentials));
} catch (e) {
return Left(AuthenticationFailure('Login failed'));
}
}Step 5: AuthService - Auth0 Integration
// lib/features/auth/auth_service.dart
Future<Credentials?> loginWithGoogle() async {
return _loginWithConnection('google-oauth2');
}
Future<Credentials?> _loginWithConnection(String connection) async {
final credentials = await _auth0.webAuthentication(scheme: 'org.pecha.app')
.login(
useHTTPS: true,
parameters: {"connection": connection},
scopes: {"openid", "profile", "email", "offline_access"},
);
await _auth0.credentialsManager.storeCredentials(credentials);
return credentials;
}Location: lib/core/config/router/go_router.dart
final goRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
refreshListenable: GoRouterRefreshStream(ref.watch(authProvider.notifier).stream),
redirect: (context, state) async {
final authState = ref.watch(authProvider);
// Unauthenticated trying to access protected route
if (!authState.isLoggedIn && RouteConfig.isProtectedRoute(currentPath)) {
return RouteConfig.login; // Redirect to login
}
// Authenticated user on login page
if (authState.isLoggedIn && currentPath == RouteConfig.login) {
return RouteConfig.home; // Redirect to home
}
return null; // No redirect
},
);
});How it works:
- Router watches
authProviderfor state changes - When auth state changes, router re-evaluates redirect logic
- Automatically redirects based on auth status
// lib/features/auth/presentation/providers/auth_notifier.dart
Future<void> logout() async {
// 1. Clear credentials from storage
await _localLogoutUseCase(const NoParams());
// 2. Clear user data
await ref.read(userProvider.notifier).clearUser();
// 3. Update state
state = state.copyWith(isLoggedIn: false);
// 4. Router automatically redirects to login
}// lib/features/auth/presentation/providers/auth_notifier.dart
AuthNotifier(...) : super(const AuthState(isLoading: true)) {
_restoreLoginState(); // Runs immediately on creation
}
Future<void> _restoreLoginState() async {
// 1. Check for valid credentials
final hasCredentials = await _hasValidCredentialsUseCase();
if (hasCredentials) {
// 2. Restore user data
state = state.copyWith(isLoggedIn: true, isLoading: false);
ref.read(userProvider.notifier).initializeUser();
} else {
// 3. Check for guest mode
final isGuest = await _isGuestModeUseCase();
state = state.copyWith(isLoggedIn: isGuest, isGuest: isGuest, isLoading: false);
}
}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTHENTICATION & API CALL FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
APP LAUNCH
β
βΌ
βββββββββββββββββββββββββββ
β AuthNotifier created β
β (isLoading: true) β
βββββββββββββ¬ββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Check stored creds β
β (CredentialsManager) β
βββββββββββββ¬ββββββββββββββ
β
βββββββββββββ΄ββββββββββββ
β β
Has Creds No Creds
β β
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ
β isLoggedIn=true β β Check guest mode β
β isLoading=false β ββββββββββ¬βββββββββ
ββββββββββ¬βββββββββ β
β βββββββββ΄ββββββββ
β Was Guest? Not Guest
β β β
βΌ βΌ βΌ
βββββββββββββββ ββββββββββββ ββββββββββββ
β Show Home β βShow Home β βShow Loginβ
ββββββββ¬βββββββ ββββββββ¬ββββ ββββββββββββ
β β
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββββββ
β USER TAPS LOGIN BUTTON β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β AuthNotifier.login() called β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β LoginUseCase called β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β AuthRepository.loginWithGoogle() β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β AuthService.loginWithGoogle() β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Auth0 Web Auth opens β
β (PKCE flow handled by SDK) β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β User authenticates with Google β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Credentials returned β
β (access, id, refresh tokens) β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Stored in CredentialsManager β
β (Keychain/Keystore) β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β AuthNotifier state updated β
β (isLoggedIn: true) β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β GoRouter redirects to Home β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β User fetches their plans β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β UserPlansNotifier.fetchPlans() β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β GetPlansUseCase called β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Repository.getUserPlans() β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β DataSource calls Dio.get() β
β URL: /users/me/plans β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β REQUEST INTERCEPTOR CHAIN β
β β
β 1. AuthInterceptor β
β - Path is protected? YES β
β - Get token from Provider β
β - Provider calls AuthService β
β - AuthService checks expiry β
β - If expired, refreshes β
β - Returns valid token β
β - Adds Authorization header β
β β
β 2. CacheInterceptor β
β - Not in cache, proceed β
β β
β 3. RetryInterceptor β
β - No error, proceed β
β β
β 4. Send to server β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β SERVER RESPONSE: 200 OK β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β RESPONSE INTERCEPTOR CHAIN β
β β
β 1. RetryInterceptor β
β - No 401, proceed β
β β
β 2. CacheInterceptor β
β - Store in cache β
β β
β 3. ErrorInterceptor β
β - No error, proceed β
β β
β 4. LoggingInterceptor β
β - Log success β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β DataSource parses JSON β
β Returns List<UserPlanModel> β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Repository maps to entities β
β Returns Either<Failure, Plans> β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β UseCase returns Either β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Notifier folds Either β
β Updates state with data β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β UI rebuilds with plans data β
ββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 401 TOKEN EXPIRED FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
API Request with expired token
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Server returns 401 Unauthorized β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β RetryInterceptor.onError() β
ββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Check: Has valid credentials? β
ββββββββββββββββ¬ββββββββββββββββββββ
β
ββββββββββ΄βββββββββ
β β
YES NO
β β
βΌ βΌ
ββββββββββββββββ ββββββββββββββββ
β Check if β β Pass error β
β already β β through β
β refreshing? β ββββββββββββββββ
ββββββββ¬ββββββββ
β
βββββββ΄ββββββ
β β
Refreshing Not refreshing
β β
βΌ βΌ
βββββββββββ ββββββββββββββββββββ
β Queue β β Set refreshing = β
β request β β true β
βββββββββββ ββββββββββ¬ββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Call AuthService β
β .refreshIdToken() β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Auth0 API renews β
β using refresh token β
ββββββββββββ¬ββββββββββββ
β
ββββββββββ΄βββββββββ
β β
Success Failure
β β
βΌ βΌ
βββββββββββββββββ ββββββββββββββββββ
β Store new β β onAuthExpired β
β credentials β β callback β
βββββββββ¬ββββββββ β β Logout β
β ββββββββββββββββββ
βΌ
βββββββββββββββββ
β Retry queued β
β requests β
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββ
β Retry originalβ
β request β
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββ
β Set refreshingβ
β = false β
βββββββββββββββββ
Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
Table of Contents Screen
- Browse through organized text content lists with ease
- View all available versions of each text in one convenient location
Reader Screen
- Immersive reading experience with optimized formatting