amplitude_experiment 0.1.0-alpha.1 copy "amplitude_experiment: ^0.1.0-alpha.1" to clipboard
amplitude_experiment: ^0.1.0-alpha.1 copied to clipboard

The official Amplitude Experiment Flutter SDK

example/lib/main.dart

import 'package:amplitude_flutter/amplitude.dart';
import 'package:amplitude_flutter/configuration.dart';
import 'package:flutter/material.dart';

import 'package:amplitude_experiment/amplitude_experiment.dart';

/// API keys are provided at build time via --dart-define-from-file:
///
///   flutter run --dart-define-from-file=.env.json
///
/// Copy .env.example.json to .env.json and fill in your keys.
/// See .env.example.json for the required variables.
const _amplitudeApiKey = String.fromEnvironment('AMPLITUDE_API_KEY');
const _experimentDeploymentKey = String.fromEnvironment('EXPERIMENT_DEPLOYMENT_KEY');

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Experiment Flutter Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const LoginExamplePage(),
    );
  }
}

class LoginExamplePage extends StatefulWidget {
  const LoginExamplePage({super.key});

  @override
  State<LoginExamplePage> createState() => _LoginExamplePageState();
}

class _LoginExamplePageState extends State<LoginExamplePage> {
  // State variables
  bool _isInitializing = true;
  bool _isLoggedIn = false;
  String? _username;
  String _welcomeMessage = 'Welcome!';
  Amplitude? _amplitude;
  ExperimentClient? _experiment;
  final TextEditingController _usernameController = TextEditingController();
  String? _errorMessage;

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

  @override
  void dispose() {
    _usernameController.dispose();
    super.dispose();
  }

  /// Initialize Amplitude and Experiment instances
  Future<void> _initializeApp() async {
    if (_amplitudeApiKey.isEmpty || _experimentDeploymentKey.isEmpty) {
      setState(() {
        _isInitializing = false;
        _errorMessage =
            'API keys not configured.\n\n'
            'Copy .env.example.json to .env.json, fill in your keys, '
            'then run:\n\n'
            'flutter run --dart-define-from-file=.env.json';
      });
      return;
    }

    try {
      // Initialize Amplitude
      _amplitude = Amplitude(
        Configuration(apiKey: _amplitudeApiKey),
      );
      await _amplitude!.isBuilt;

      // Initialize Experiment with Amplitude integration.
      // The Experiment instanceName must match the Amplitude Analytics instance
      // name (case-sensitive) for automatic identity sharing to work. Both SDKs
      // default to '$default_instance', so no action is needed for single-instance usage.
      _experiment = await Experiment.initializeWithAmplitude(
        _experimentDeploymentKey,
        ExperimentConfig(
          // trackingProvider: CustomTrackingProvider(),
          // userProvider: CustomUserProvider(),
        ),
      );

      if (!mounted) return;

      setState(() {
        _isInitializing = false;
        _errorMessage = null;
      });
    } catch (e) {
      if (!mounted) return;

      setState(() {
        _isInitializing = false;
        _errorMessage = 'Failed to initialize: ${e.toString()}';
      });
    }
  }

  /// Handle login flow
  Future<void> _handleLogin() async {
    final username = _usernameController.text.trim();

    // Validate username length
    if (username.length < 8) {
      setState(() {
        _errorMessage = 'Username must be at least 8 characters long';
      });
      return;
    }

    if (_amplitude == null || _experiment == null) {
      setState(() {
        _errorMessage = 'App not initialized. Please restart the app.';
      });
      return;
    }

    setState(() {
      _isInitializing = true;
      _errorMessage = null;
    });

    try {
      // Update Amplitude user
      await _amplitude!.setUserId(username);

      // Fetch latest variants from Experiment
      await _experiment!.fetch(ExperimentUser(userId: username));

      // Get welcome message variant
      final variant = await _experiment!.variant('flutter-welcome-message');
      await _experiment!.exposure('flutter-welcome-message');

      // Extract welcome message from variant, with fallback
      String welcomeMessage;
      if (variant.value != null &&
          variant.value!.isNotEmpty &&
          variant.value == 'treatment') {
        welcomeMessage =
            "Hello, $username! You have been selected for the treatment!";
      } else {
        welcomeMessage = 'Welcome, $username!';
      }

      if (!mounted) return;

      setState(() {
        _isLoggedIn = true;
        _username = username;
        _welcomeMessage = welcomeMessage;
        _isInitializing = false;
        _errorMessage = null;
      });

      // Clear username input
      _usernameController.clear();
    } catch (e) {
      if (!mounted) return;

      setState(() {
        _isInitializing = false;
        _errorMessage = 'Login failed: ${e.toString()}';
      });
    }
  }

  /// Handle logout flow
  Future<void> _handleLogout() async {
    if (_amplitude == null) {
      return;
    }

    try {
      // Clear Amplitude user identity
      await _amplitude!.setUserId(null);
      // Clear Experiment data
      await _experiment!.clear();
    } catch (e) {
      // Log error but continue with logout
      debugPrint('Error clearing Amplitude user: $e');
    }

    if (!mounted) return;

    setState(() {
      _isLoggedIn = false;
      _username = null;
      _welcomeMessage = 'Welcome!';
      _errorMessage = null;
    });

    // Clear username input
    _usernameController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Experiment Flutter Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    // Show loading state during initialization
    if (_isInitializing && !_isLoggedIn) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('Initializing...'),
          ],
        ),
      );
    }

    // Show error state if initialization failed
    if (_errorMessage != null && !_isLoggedIn && !_isInitializing) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.error_outline,
                size: 64,
                color: Theme.of(context).colorScheme.error,
              ),
              const SizedBox(height: 16),
              Text(
                _errorMessage!,
                style: TextStyle(color: Theme.of(context).colorScheme.error),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _initializeApp,
                child: const Text('Retry'),
              ),
            ],
          ),
        ),
      );
    }

    // Show logged-in view
    if (_isLoggedIn) {
      return _buildLoggedInView();
    }

    // Show logged-out view
    return _buildLoggedOutView();
  }

  Widget _buildLoggedOutView() {
    final usernameLength = _usernameController.text.length;
    final isValidUsername = usernameLength >= 8;

    return Center(
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.login, size: 64, color: Colors.blue),
            const SizedBox(height: 24),
            Text(
              'Welcome! Please log in to continue.',
              style: Theme.of(context).textTheme.headlineSmall,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 32),
            TextField(
              controller: _usernameController,
              decoration: InputDecoration(
                labelText: 'Username',
                hintText: 'Enter username (8+ characters)',
                border: const OutlineInputBorder(),
                prefixIcon: const Icon(Icons.person),
                errorText: _errorMessage,
                helperText: 'Username must be at least 8 characters',
              ),
              onChanged: (value) {
                setState(() {
                  _errorMessage = null;
                });
              },
              textInputAction: TextInputAction.done,
              onSubmitted: (_) => _handleLogin(),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _isInitializing ? null : _handleLogin,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: _isInitializing
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Text('Log In', style: TextStyle(fontSize: 16)),
              ),
            ),
            if (!isValidUsername && usernameLength > 0)
              Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: Text(
                  '${8 - usernameLength} more characters needed',
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                    color: Theme.of(context).colorScheme.error,
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildLoggedInView() {
    return Center(
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.check_circle, size: 64, color: Colors.green),
            const SizedBox(height: 24),
            Text(
              _welcomeMessage,
              style: Theme.of(context).textTheme.headlineMedium,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 16),
            Text(
              'Logged in as: $_username',
              style: Theme.of(context).textTheme.bodyLarge,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 32),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton.icon(
                onPressed: _handleLogout,
                icon: const Icon(Icons.logout),
                label: const Text('Log Out', style: TextStyle(fontSize: 16)),
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  backgroundColor: Theme.of(context).colorScheme.error,
                  foregroundColor: Theme.of(context).colorScheme.onError,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
140
points
8
downloads

Publisher

unverified uploader

Weekly Downloads

The official Amplitude Experiment Flutter SDK

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface

More

Packages that depend on amplitude_experiment

Packages that implement amplitude_experiment