Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions ANALYTICS_REFACTORING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Analytics Refactoring Summary

## Overview

This refactoring makes the `Analytics` class injectable throughout the Sharezone app, enabling proper testing and better dependency management.

## Problem

Previously, the app used `Analytics(getBackend())` in many places, which directly called `FirebaseAnalytics.instance`. This created several issues:

1. **Untestable**: The root `Sharezone` widget couldn't be tested in widget tests because it used a static `Analytics` instance
2. **Non-mockable**: Tests couldn't provide a mocked Analytics implementation
3. **Tight coupling**: Code was tightly coupled to Firebase implementation

## Solution

### Key Changes

#### 1. Sharezone Widget
- **Before**: Used static `Analytics analytics = Analytics(getBackend())`
- **After**: Accepts optional `Analytics? analytics` parameter
- Falls back to `blocDependencies.analytics` if not provided

#### 2. Authentication Gateways
Updated to accept optional Analytics parameter:
- `RegistrationGateway(collection, auth, {Analytics? analytics})`
- `LinkProviderGateway(userGateway, {Analytics? analytics})`

Both default to `Analytics(getBackend())` if not provided, maintaining backward compatibility.

#### 3. BLoC Factories
Updated factories to accept and pass Analytics:
- `AccountPageBlocFactory` now accepts and uses Analytics
- `EmailAndPasswordLinkBloc` accepts optional Analytics parameter

#### 4. Page-level Usage
Pages that need Analytics now get it from context:
- **AuthApp**: Provides Analytics via Provider for login/signup flows
- **SharezoneContext**: Available via BlocProvider for authenticated flows
- **LoginPage**: Uses lazy initialization with context.read<Analytics>()

#### 5. Centralized Creation
In `run_app.dart`:
- Create Analytics once: `final analytics = Analytics(getBackend())`
- Pass to both `RegistrationGateway` and `BlocDependencies`
- Reuse the same instance throughout the app

### Migration Path

All changes are backward compatible:
- Analytics parameters are optional with sensible defaults
- Existing code continues to work without modifications
- Tests can now provide mocked implementations

### Testing

Added `app/test/main/analytics_mocking_test.dart` demonstrating:
- Analytics can be instantiated with `NullAnalyticsBackend`
- All analytics methods work without errors when mocked
- This enables proper unit and widget testing

## Files Modified

### Core Files
- `app/lib/main/sharezone.dart` - Made Analytics injectable
- `app/lib/main/run_app.dart` - Centralized Analytics creation
- `app/lib/main/auth_app.dart` - Provide Analytics via Provider
- `app/lib/main/sharezone_bloc_providers.dart` - Use injected Analytics

### Authentication
- `lib/authentification/authentification_base/lib/src/gateways/registration_gateway.dart`
- `lib/authentification/authentification_base/lib/src/gateways/link_provider_gateway.dart`

### Account/Auth Pages
- `app/lib/account/account_page_bloc.dart`
- `app/lib/account/account_page_bloc_factory.dart`
- `app/lib/account/register_account_section.dart`
- `app/lib/auth/login_page.dart`
- `app/lib/auth/sign_in_with_qr_code_page.dart`
- `app/lib/auth/email_and_password_link_bloc.dart`

### Dashboard
- `app/lib/dashboard/widgets/dashboard_fab.dart`

### Tests
- `app/test/main/analytics_mocking_test.dart` (new)

## Benefits

1. **Testability**: Widgets and classes can now be tested with mocked Analytics
2. **Flexibility**: Easy to switch analytics implementations
3. **Better Architecture**: Dependencies are explicit rather than hidden
4. **No Breaking Changes**: All existing code continues to work

## Usage Examples

### Testing
```dart
final mockAnalytics = Analytics(NullAnalyticsBackend());

// Pass to widgets or classes that need it
final sharezone = Sharezone(
blocDependencies: deps,
analytics: mockAnalytics,
// ... other params
);
```

### Production
```dart
// Still works as before - defaults are used
final sharezone = Sharezone(
blocDependencies: deps,
// Analytics will be taken from blocDependencies
);
```

## Next Steps

Future improvements could include:
1. Making Analytics required parameter (breaking change)
2. Creating a dedicated Analytics service layer
3. Adding more comprehensive analytics testing
7 changes: 5 additions & 2 deletions app/lib/account/account_page_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ class AccountPageBloc extends BlocBase {
final LinkProviderGateway linkProviderGateway;
final UserGateway userGateway;

final _analytics = LinkProviderAnalytics(Analytics(getBackend()));
final LinkProviderAnalytics _analytics;

late Stream<UserView> userViewStream;

AccountPageBloc({
required this.userGateway,
required this.linkProviderGateway,
required this.globalKey,
}) {
Analytics? analytics,
}) : _analytics = LinkProviderAnalytics(
analytics ?? Analytics(getBackend()),
) {
final userStream = userGateway.userStream;
final authUserStream = userGateway.authUserStream;

Expand Down
6 changes: 4 additions & 2 deletions app/lib/account/account_page_bloc_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ import 'package:sharezone/util/api/user_api.dart';

class AccountPageBlocFactory extends BlocBase {
final UserGateway _userGateway;
final Analytics _analytics;

AccountPageBlocFactory(this._userGateway);
AccountPageBlocFactory(this._userGateway, this._analytics);

AccountPageBloc create(
GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey,
) {
return AccountPageBloc(
globalKey: scaffoldMessengerKey,
linkProviderGateway: LinkProviderGateway(_userGateway),
linkProviderGateway: LinkProviderGateway(_userGateway, analytics: _analytics),
userGateway: _userGateway,
analytics: _analytics,
);
}

Expand Down
7 changes: 3 additions & 4 deletions app/lib/account/register_account_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,9 @@ Future<void> showCredentialAlreadyInUseDialog(BuildContext context) async {
);

if (showInstruction != null && showInstruction && context.mounted) {
final LinkProviderAnalytics analytics = LinkProviderAnalytics(
Analytics(getBackend()),
);
analytics.logShowedUseMultipleDevicesInstruction();
final analytics = BlocProvider.of<SharezoneContext>(context).analytics;
final linkProviderAnalytics = LinkProviderAnalytics(analytics);
linkProviderAnalytics.logShowedUseMultipleDevicesInstruction();
Navigator.pushNamed(context, UseAccountOnMultipleDevicesInstructions.tag);
}
}
11 changes: 6 additions & 5 deletions app/lib/auth/email_and_password_link_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ class EmailAndPasswordLinkBloc extends BlocBase
final UserEditBlocGateway userEditBlocGateway;
final String initialName;

final LinkProviderAnalytics _analytics = LinkProviderAnalytics(
Analytics(getBackend()),
);
final LinkProviderAnalytics _analytics;

final _emailController = BehaviorSubject<String>();
final _passwordController = BehaviorSubject<String?>();
Expand All @@ -50,8 +48,11 @@ class EmailAndPasswordLinkBloc extends BlocBase
this.linkProviderGateway,
this.userEditBlocGateway,
this.initialName,
this.scaffoldMessengerKey,
) {
this.scaffoldMessengerKey, {
Analytics? analytics,
}) : _analytics = LinkProviderAnalytics(
analytics ?? Analytics(getBackend()),
) {
_nameController.sink.add(initialName);
}

Expand Down
27 changes: 22 additions & 5 deletions app/lib/auth/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,25 +90,42 @@ class LoginPage extends StatefulWidget {
}

class _LoginPageState extends State<LoginPage> {
late LoginBloc bloc;
LoginBloc? _bloc;
bool isLoading = false;
bool _isInitialized = false;

final passwordFocusNode = FocusNode();

@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isInitialized) {
final analytics = LoginAnalytics(context.read<Analytics>());
_bloc = LoginBloc(analytics);
_isInitialized = true;
}
}

@override
void initState() {
final analytics = LoginAnalytics(Analytics(getBackend()));
bloc = LoginBloc(analytics);
super.initState();
showTipCardIfIsAvailable(context);
// Defer the tip card initialization until after the first frame to ensure
// the context is fully available. This is necessary because
// showTipCardIfIsAvailable accesses context, which should not be done
// synchronously in initState.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
showTipCardIfIsAvailable(context);
}
});
}

@override
Widget build(BuildContext context) {
final flavor = context.read<Flavor>();
final showDebugLogins = kDebugMode && flavor == Flavor.dev;
return BlocProvider(
bloc: bloc,
bloc: _bloc!,
child: Scaffold(
body: Builder(
builder:
Expand Down
3 changes: 2 additions & 1 deletion app/lib/auth/sign_in_with_qr_code_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class SignInWithQrCodePage extends StatelessWidget {

@override
Widget build(BuildContext context) {
final analytics = context.read<Analytics>();
return Scaffold(
body: Builder(
builder: (context) {
Expand All @@ -39,7 +40,7 @@ class SignInWithQrCodePage extends StatelessWidget {
FirebaseFunctions.instanceFor(region: 'europe-west1'),
),
),
loginAnalytics: LoginAnalytics(Analytics(getBackend())),
loginAnalytics: LoginAnalytics(analytics),
crashAnalytics: getCrashAnalytics(),
),
child: _InnerSignInWithQrCodePage(),
Expand Down
4 changes: 3 additions & 1 deletion app/lib/dashboard/widgets/dashboard_fab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class _DashboardPageFAB extends StatelessWidget {
const _DashboardPageFAB();

Future<void> openDashboardFabSheet(BuildContext context) async {
final analytics = DashboardAnalytics(Analytics(getBackend()));
final analytics = DashboardAnalytics(
BlocProvider.of<SharezoneContext>(context).analytics,
);
analytics.logOpenFabSheet();

final fabResult = await showModalBottomSheet<_DashboardFabResult>(
Expand Down
1 change: 1 addition & 0 deletions app/lib/main/auth_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class _AuthAppState extends State<AuthApp> {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<Analytics>.value(value: widget.analytics),
ChangeNotifierProvider(
create:
(context) => SupportPageController(
Expand Down
6 changes: 3 additions & 3 deletions app/lib/main/run_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,14 @@ Future<AppDependencies> initializeDependencies({required Flavor flavor}) async {
final keyValueStore = FlutterKeyValueStore(
pluginInitializations.sharedPreferences,
);
final analytics = Analytics(getBackend());
final registrationGateway = RegistrationGateway(
references.users,
firebaseDependencies.auth!,
analytics: analytics,
);
final blocDependencies = BlocDependencies(
analytics: Analytics(getBackend()),
analytics: analytics,
firestore: firebaseDependencies.firestore!,
keyValueStore: keyValueStore,
sharedPreferences: pluginInitializations.sharedPreferences,
Expand Down Expand Up @@ -165,8 +167,6 @@ Future<AppDependencies> initializeDependencies({required Flavor flavor}) async {
// ignore:close_sinks
final beitrittsversuche = runBeitrittsVersuche();

final analytics = Analytics(getBackend());

UserGateway? userGateway;
SharezoneGateway? sharezoneGateway;

Expand Down
12 changes: 7 additions & 5 deletions app/lib/main/sharezone.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Sharezone extends StatefulWidget {
final Stream<Beitrittsversuch?> beitrittsversuche;
final Flavor flavor;
final bool isIntegrationTest;
final Analytics? analytics;

const Sharezone({
super.key,
Expand All @@ -65,10 +66,9 @@ class Sharezone extends StatefulWidget {
required this.beitrittsversuche,
required this.flavor,
this.isIntegrationTest = false,
this.analytics,
});

static Analytics analytics = Analytics(getBackend());

@override
State createState() => _SharezoneState();
}
Expand All @@ -78,12 +78,14 @@ class _SharezoneState extends State<Sharezone> with WidgetsBindingObserver {
late StreamSubscription<AuthUser?> authSubscription;
late StreamingKeyValueStore streamingKeyValueStore;
late FeatureFlagl10n featureFlagl10n;
late Analytics _analytics;

@override
void initState() {
super.initState();

isIntegrationTest = widget.isIntegrationTest;
_analytics = widget.analytics ?? widget.blocDependencies.analytics;
signUpBloc = SignUpBloc();

// You have to wait a little moment (1000 milliseconds), to
Expand All @@ -107,7 +109,7 @@ class _SharezoneState extends State<Sharezone> with WidgetsBindingObserver {
}

void logAppOpen() {
Sharezone.analytics.log(NamedAnalyticsEvent(name: 'app_open'));
_analytics.log(NamedAnalyticsEvent(name: 'app_open'));
}

@override
Expand Down Expand Up @@ -178,13 +180,13 @@ class _SharezoneState extends State<Sharezone> with WidgetsBindingObserver {
snapshot.data;
return SharezoneApp(
widget.blocDependencies,
Sharezone.analytics,
_analytics,
widget.beitrittsversuche,
);
}
return AuthApp(
blocDependencies: widget.blocDependencies,
analytics: Sharezone.analytics,
analytics: _analytics,
);
},
),
Expand Down
Loading