# Implementing [TOC] ## 1 Prerequisites * Environment requirements in [README.md](../README.md). * Clone the branch `3.1-BasicViews` of this repository to get the full project with all the view code, and install the dependencies. See more information in [Step3.1](./3.1AddViews.md). ```bash git clone --branch 3.1-BasicViews cd flutter pub get ``` ## 2 Configure Amplify Category ### 2.1 Configure Auth Category To start provisioning auth resources in the backend, go to your project directory and **execute the command**: ```bash amplify add auth ``` Enter the following when prompted: ```console ? Do you want to use the default authentication and security configuration? `Default configuration` ? How do you want users to be able to sign in? `Username` ? Do you want to configure advanced settings? `No, I am done.` ``` To push your changes to the cloud, **execute the command**: ```bash amplify push ``` Upon completion, `amplifyconfiguration.dart` should be updated to reference provisioned backend auth resources. Note that these files should already be a part of your project if you followed the [2.InitializeAmplify](./2.InitializeAmplify.md). ### 2.2 Configure Storage Category Run the following command from the root of your project: ``` amplify add storage ``` Enter the following when prompted: ``` ? Select from one of the below mentioned services: Content (Images, audio, video, etc.) ✔ Provide a friendly name for your resource that will be used to label this category in the project: · powerToolkitStorage ✔ Provide bucket name: · ✔ Who should have access: · Auth and guest users ✔ What kind of access do you want for Authenticated users? · create/update, read, delete ✔ What kind of access do you want for Guest users? · create/update, read ✔ Do you want to add a Lambda Trigger for your S3 Bucket? (y/N) · no ✅ Successfully added resource powerToolkitStorage locally ``` To push your changes to the cloud, **execute the command**: ``` amplify push ``` ### 2.3 Configure Analytics Category Run the following command from the root of your project: ``` amplify add analytics ``` Enter the following when prompted: ``` ? Select an Analytics provider Amazon Pinpoint ? Provide your pinpoint resource name: powertoolkit Successfully added resource powertoolkit locally ``` To deploy your backend, run: ```bash amplify push ``` ### 2.4 Configure API Category #### 2.4.1Amplify Configuration Run the following command from the root of your project: ``` amplify add api ``` Enter the following when prompted: ```dart ? Select from one of the below mentioned services: GraphQL ? Here is the GraphQL API that we will create. Select a setting to edit or continue Authorization modes: API key (default, expiration time: 7 days from now) ? Choose the default authorization type for the API (Use arrow keys) Amazon Cognito User Pool Do you want to use the default authentication and security configuration? (Use arrow keys) Default configuration How do you want users to be able to sign in? (Use arrow keys) Username Do you want to configure advanced settings? (Use arrow keys) No, I am done. ? Configure additional auth types? (y/N) N ? Here is the GraphQL API that we will create. Select a setting to edit or continue (Use arrow keys) Continue ? Choose a schema template: Blank Schema ✔ Do you want to edit the schema now? (Y/n) N ``` > Since the application needs to interact with other AWS services besides AWS AppSync, such as Amazon S3, DynamoDB, etc. Here we set the authentication mode as AWS IAM credentials with Amazon Cognito Identity Pools. Update the GraphQL schema file (`amplify/backend/api/{api_name}/schema.graphql`) with the following code: ``` type User @model @auth(rules: [{allow: private}]) { id: ID! username: String! email: String! avatarKey: String description: String deviceId: String } ``` To push your changes to the cloud, **execute the command**: ```bash amplify push ``` #### 2.4.2 Generate Model class To generate the model, change directories to your project folder and **execute the command**: ```bash amplify codegen models ``` The generated files will be under the `lib/models` directory by default. Replace `User.dart` file with the following code: ```dart /* * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ // NOTE: This file is generated and may not follow lint rules defined in your app // Generated files can be excluded from analysis in analysis_options.yaml // For more info, see: https://dart.dev/guides/language/analysis-options#excluding-code-from-analysis // ignore_for_file: public_member_api_docs, annotate_overrides, dead_code, dead_codepublic_member_api_docs, depend_on_referenced_packages, file_names, library_private_types_in_public_api, no_leading_underscores_for_library_prefixes, no_leading_underscores_for_local_identifiers, non_constant_identifier_names, null_check_on_nullable_type_parameter, prefer_adjacent_string_concatenation, prefer_const_constructors, prefer_if_null_operators, prefer_interpolation_to_compose_strings, slash_for_doc_comments, sort_child_properties_last, unnecessary_const, unnecessary_constructor_name, unnecessary_late, unnecessary_new, unnecessary_null_aware_assignments, unnecessary_nullable_for_final_variable_declarations, unnecessary_string_interpolations, use_build_context_synchronously import 'package:amplify_core/amplify_core.dart'; import 'package:flutter/foundation.dart'; /** This is an auto generated class representing the User type in your schema. */ @immutable class User extends Model { static const classType = const _UserModelType(); final String id; final String? username; final String? email; final String? avatarKey; final String? description; final String? deviceId; final TemporalDateTime? createdAt; final TemporalDateTime? updatedAt; @override getInstanceType() => classType; @override String getId() { return id; } const User._internal( {required this.id, this.username, this.email, this.avatarKey, this.description, this.deviceId, this.createdAt, this.updatedAt}); factory User( {String? id, String? username, String? email, String? avatarKey, String? description, String? deviceId}) { return User._internal( id: id == null ? UUID.getUUID() : id, username: username, email: email, avatarKey: avatarKey, description: description, deviceId: deviceId); } bool equals(Object other) { return this == other; } @override bool operator ==(Object other) { if (identical(other, this)) return true; return other is User && id == other.id && username == other.username && email == other.email && avatarKey == other.avatarKey && description == other.description && deviceId == other.deviceId; } @override int get hashCode => toString().hashCode; @override String toString() { var buffer = new StringBuffer(); buffer.write("User {"); buffer.write("id=" + "$id" + ", "); buffer.write("username=" + "$username" + ", "); buffer.write("email=" + "$email" + ", "); buffer.write("avatarKey=" + "$avatarKey" + ", "); buffer.write("description=" + "$description" + ", "); buffer.write("deviceId=" + "$deviceId" + ", "); buffer.write("createdAt=" + (createdAt != null ? createdAt!.format() : "null") + ", "); buffer.write("updatedAt=" + (updatedAt != null ? updatedAt!.format() : "null")); buffer.write("}"); return buffer.toString(); } User copyWith( {String? id, String? username, String? email, String? avatarKey, String? description, String? deviceId}) { return User._internal( id: id ?? this.id, username: username ?? this.username, email: email ?? this.email, avatarKey: avatarKey ?? this.avatarKey, description: description ?? this.description, deviceId: deviceId ?? this.deviceId); } User.fromJson(Map json) : id = json['id'], username = json['username'], email = json['email'], avatarKey = json['avatarKey'], description = json['description'], deviceId = json['deviceId'], createdAt = json['createdAt'] != null ? TemporalDateTime.fromString(json['createdAt']) : null, updatedAt = json['updatedAt'] != null ? TemporalDateTime.fromString(json['updatedAt']) : null; Map toJson() => { 'id': id, 'username': username, 'email': email, 'avatarKey': avatarKey, 'description': description, 'deviceId': deviceId, 'createdAt': createdAt?.format(), 'updatedAt': updatedAt?.format() }; static final QueryField ID = QueryField(fieldName: "user.id"); static final QueryField USERNAME = QueryField(fieldName: "username"); static final QueryField EMAIL = QueryField(fieldName: "email"); static final QueryField AVATARKEY = QueryField(fieldName: "avatarKey"); static final QueryField DESCRIPTION = QueryField(fieldName: "description"); static final QueryField DEVICEID = QueryField(fieldName: "deviceId"); static var schema = Model.defineSchema(define: (ModelSchemaDefinition modelSchemaDefinition) { modelSchemaDefinition.name = "User"; modelSchemaDefinition.pluralName = "Users"; modelSchemaDefinition.authRules = [ AuthRule( authStrategy: AuthStrategy.PRIVATE, operations: [ModelOperation.CREATE, ModelOperation.UPDATE, ModelOperation.DELETE, ModelOperation.READ]) ]; modelSchemaDefinition.addField(ModelFieldDefinition.id()); modelSchemaDefinition.addField(ModelFieldDefinition.field( key: User.USERNAME, isRequired: false, ofType: ModelFieldType(ModelFieldTypeEnum.string))); modelSchemaDefinition.addField(ModelFieldDefinition.field( key: User.EMAIL, isRequired: false, ofType: ModelFieldType(ModelFieldTypeEnum.string))); modelSchemaDefinition.addField(ModelFieldDefinition.field( key: User.AVATARKEY, isRequired: false, ofType: ModelFieldType(ModelFieldTypeEnum.string))); modelSchemaDefinition.addField(ModelFieldDefinition.field( key: User.DESCRIPTION, isRequired: false, ofType: ModelFieldType(ModelFieldTypeEnum.string))); modelSchemaDefinition.addField(ModelFieldDefinition.field( key: User.DEVICEID, isRequired: false, ofType: ModelFieldType(ModelFieldTypeEnum.string))); modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField( fieldName: 'createdAt', isRequired: false, isReadOnly: true, ofType: ModelFieldType(ModelFieldTypeEnum.dateTime))); modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField( fieldName: 'updatedAt', isRequired: false, isReadOnly: true, ofType: ModelFieldType(ModelFieldTypeEnum.dateTime))); }); } class _UserModelType extends ModelType { const _UserModelType(); @override User fromJson(Map jsonData) { return User.fromJson(jsonData); } } ``` ## 3 Install Amplify Libraries Add the following dependency to your **app**'s `pubspec.yaml` along with others you added above in **Prerequisites**: ```yaml dependencies: flutter: sdk: flutter # amplify dependencies. amplify_core: ^0.4.5 amplify_flutter: ^0.4.5 amplify_auth_cognito: ^0.4.5 amplify_storage_s3: ^0.4.5 amplify_analytics_pinpoint: ^0.4.5 amplify_api: ^0.4.5 ``` ## 4 Initialize Amplify Configure in Flutter Call `Amplify.configure()` to complete initialization. The `main.dart` code should look like this: ```dart import 'package:amplify_analytics_pinpoint/amplify_analytics_pinpoint.dart'; import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:amplify_storage_s3/amplify_storage_s3.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:/amplifyconfiguration.dart'; import 'package:/app_navigator.dart'; import 'package:/auth/auth_repository.dart'; import 'package:/loading_view.dart'; import 'package:/models/ModelProvider.dart'; import 'package:/repository/data_repository.dart'; import 'package:/session/session_cubit.dart'; void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _myAppState(); } class _myAppState extends State { bool _isAmplifyConfigured = false; @override void initState() { super.initState(); _configureAmplify(); } @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), home: _isAmplifyConfigured ? MultiRepositoryProvider( providers: [ RepositoryProvider(create: (context) => AuthRepository()), RepositoryProvider(create: (context) => DataRepository()), ], child: BlocProvider( create: (context) => SessionCubit( authRepo: context.read(), dataRepo: context.read(), ), child: AppNavigator(), ), ) : const LoadingView()); } Future _configureAmplify() async { try { Amplify.addPlugins([ AmplifyAPI(modelProvider: ModelProvider.instance), AmplifyAuthCognito(), AmplifyStorageS3(), AmplifyAnalyticsPinpoint(), ]); await Amplify.configure(amplifyconfig); setState(() => _isAmplifyConfigured = true); print('Successfully configured Amplify.'); } catch (e) { print('Could not configure Amplify.'); } } } ``` ## 5 Authentication with Amplify Cognito Implementing in the auth_repository.dart file. (lib/auth/auth_repository.dart) ### 5.1 Login with username & password ```dart Future login({ required String username, required String password, }) async { try { final result = await Amplify.Auth.signIn( username: username.trim(), password: password.trim(), ); return result.isSignedIn ? (await _getUserIdFromAttributes()) : throw Exception("login failed."); } catch (e) { rethrow; } } ``` For the user who has loged in, you can get user attribtues including userId from the attributes like below: ```dart Future _getUserIdFromAttributes() async { try { final attributes = await Amplify.Auth.fetchUserAttributes(); final userId = attributes .firstWhere((element) => element.userAttributeKey == 'sub') .value; return userId; } catch (e) { throw e; } } ``` ### 5.2 SignUp ```dart Future signUp({ required String username, required String email, required String password, }) async { final options = CognitoSignUpOptions(userAttributes: {CognitoUserAttributeKey.email: email.trim()}); try { final result = await Amplify.Auth.signUp( username: username.trim(), password: password.trim(), options: options, ); return result.isSignUpComplete; } catch (e) { rethrow; } } ``` ### 5.3 Confirmation Code ```dart Future confirmSignUp({ required String username, required String confirmationCode, }) async { try { final result = await Amplify.Auth.confirmSignUp( username: username.trim(), confirmationCode: confirmationCode.trim(), ); return result.isSignUpComplete; } catch (e) { rethrow; } } ``` ### 5.4 Login as A Guest The AWS Cognito Auth Plugin can be configured to automatically obtain guest credentials once the device is online so that you are able to use other categories "anonymously" without the need to sign in. You will not be able to perform user specific methods while in this state such as updating attributes, changing your password, or getting the current user. However, you can obtain the unique Identity ID which is assigned to the device through the `fetchAuthSession` method [described here](https://docs.amplify.aws/lib/auth/access_credentials/q/platform/flutter/). ### 5.5 SingOut ```dart Future signOut() async { await Amplify.Auth.signOut(); } ``` ### 5.6 Testing Full Code of auth_repository.dart should looks like this: ```dart import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; class AuthRepository { Future attemptAutoLogin() async { try { final session = await Amplify.Auth.fetchAuthSession(); return session.isSignedIn ? (await _getUserIdFromAttributes()) : throw Exception("auto login failed."); } catch (e) { rethrow; } } Future login({ required String username, required String password, }) async { try { final result = await Amplify.Auth.signIn( username: username.trim(), password: password.trim(), ); return result.isSignedIn ? (await _getUserIdFromAttributes()) : throw Exception("login failed."); } catch (e) { rethrow; } } Future signUp({ required String username, required String email, required String password, }) async { final options = CognitoSignUpOptions(userAttributes: {CognitoUserAttributeKey.email: email.trim()}); try { final result = await Amplify.Auth.signUp( username: username.trim(), password: password.trim(), options: options, ); return result.isSignUpComplete; } catch (e) { rethrow; } } Future confirmSignUp({ required String username, required String confirmationCode, }) async { try { final result = await Amplify.Auth.confirmSignUp( username: username.trim(), confirmationCode: confirmationCode.trim(), ); return result.isSignUpComplete; } catch (e) { rethrow; } } Future signOut() async { await Amplify.Auth.signOut(); } Future getUserEmailFromAttributes() async { try { // sub, email, email_verified final attributes = await Amplify.Auth.fetchUserAttributes(); final email = attributes.firstWhere((element) => element.userAttributeKey.key == 'email').value; return email; } catch (e) { rethrow; } } Future _getUserIdFromAttributes() async { try { // sub, email, email_verified final attributes = await Amplify.Auth.fetchUserAttributes(); final userId = attributes.firstWhere((element) => element.userAttributeKey.key == 'sub').value; return userId; } catch (e) { rethrow; } } } ``` Then you can run the project in iOS simulator or Android emulator. When the app is launched, you can sign up with an email and input the confirmation code you received by email, then you will see the home view. Switch to the profile view, and logout by the button at the upper-right corner. ## 6 Create Update and Query User by Amplify QraphQL `DataRepository.dart` contains CRUD operations for user model, some code examples are shown below. ### 6.1 Create User ```dart final newUser = User(id: userId, username: username, email: email, deviceId: deviceId); try { final request = ModelMutations.create(newUser); final response = await Amplify.API.mutate(request: request).response; final user = response.data; if (user == null) { throw Exception('failed to create user, ${response.errors.toString()}'); } return user; } catch (e) { rethrow; } ``` ### 6.2 Update User ```dart try { final request = ModelMutations.update(user); final response = await Amplify.API.mutate(request: request).response; final updatedUser = response.data; if (updatedUser == null) { throw Exception('failed to update user, ${response.errors.toString()}'); } return updatedUser; } catch (e) { rethrow; } ``` ### 6.3 Query User by ID ```dart try { final request = ModelQueries.get(User.classType, userId); final response = await Amplify.API.query(request: request).response; return response.data; } catch (e) { rethrow; } ``` ### 6.4 data_repository.dart Full Code of data_repository.dart should looks like this: ```dart import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:/models/User.dart'; class DataRepository { Future getUserById(String userId) async { try { final request = ModelQueries.get(User.classType, userId); final response = await Amplify.API.query(request: request).response; return response.data; } catch (e) { rethrow; } } Future createUser({ required String userId, required String username, required String email, String? deviceId, }) async { final newUser = User(id: userId, username: username, email: email, deviceId: deviceId); try { final request = ModelMutations.create(newUser); final response = await Amplify.API.mutate(request: request).response; final user = response.data; if (user == null) { throw Exception('failed to create user, ${response.errors.toString()}'); } return user; } catch (e) { rethrow; } } Future updateUser(User user) async { try { final request = ModelMutations.update(user); final response = await Amplify.API.mutate(request: request).response; final updatedUser = response.data; if (updatedUser == null) { throw Exception('failed to update user, ${response.errors.toString()}'); } return updatedUser; } catch (e) { rethrow; } } } ``` ## 7 Save User in Auth Flow In step 5, we implemented the authentication using Amplify Cognito. We can now save the newly registered users to DynamoDB via Amplify GraphQL in the `session_cubit.dart`. ### 7.1 Save User when login When the user has just registered or logged, we should get the full user information from DynamoDB by id. If the user does not exist, create a new user record. ```dart void createSession(String userId, String username) async { User? user = await dataRepo.getUserById(userId); if (user == null) { final email = await authRepo.getUserEmailFromAttributes(); final deviceId = await _getDeviceId(); user = await dataRepo.createUser(userId: userId, username: username, email: email, deviceId: deviceId); } emit(Authenticated(userId: userId, user: user)); print('session created.'); } ``` ### 7.2 Get User Info at Auto-Login This application support auto-login by using `Amplify.Auth.fetchAuthSession()`. We also need to complete the user info in session by querying the user by id in DynamoDB. If the user does not exist in DB, log out and go to the login page. ```dart void attemptAutoLogin() async { try { print("attemptAutoLogin, start."); final userId = await authRepo.attemptAutoLogin(); User? user = await dataRepo.getUserById(userId); if (user == null) { signOut(); // Sign out and go to login page. throw Exception("user not found in database, please login."); } emit(Authenticated(userId: userId, user: user)); print("attemptAutoLogin, success."); } on Exception { emit(Unauthenticated()); print("attemptAutoLogin, failed."); } } ``` ### 7.3 Save User when Login As a Guest Nothing todo, just for demonstration. ## 8 Upload File by Amplify Storage After logging in, you can see the home page and profile page. In the profile page you will see your username and email. You can also change your avatar by selecting an image from the gallery or taking a photo with camera. ### 8.1 Upload and Get Image We use `StorageRepository.dart` to manage file uploads and downloads. ```dart import 'dart:io'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:amplify_storage_s3/amplify_storage_s3.dart'; class StorageRepository { Future uploadFile({required File file, void Function(TransferProgress)? onProgress}) async { try { final fileName = "avatar/" + DateTime.now().toIso8601String(); final result = await Amplify.Storage.uploadFile( local: file, key: fileName + '.jpg', onProgress: onProgress, ); return result.key; } catch (e) { rethrow; } } Future getUrlForFile(String fileKey) async { try { final result = await Amplify.Storage.getUrl(key: fileKey); return result.url; } catch (e) { rethrow; } } } ``` ### 8.2 Pick an Image or Taking a photo In the `profile_bloc.dart` file, we upload the file through StorageRepository after selecting the image. ```dart void _openImagePicker(OpenImagePicker event, Emitter emit) async { print('_openImagePicker, imageSource: ${event.imageSource}'); emit(state.copyWith(imageSourceActionSheetIsVisible: false)); final pickedImage = await _picker.pickImage(source: event.imageSource); if (pickedImage == null) return; emit(state.copyWith(uploading: true)); // upload image then update user. final imageKey = await storageRepo.uploadFile(file: File(pickedImage.path)); final updatedUser = state.user.copyWith(avatarKey: imageKey); final results = await Future.wait([ dataRepo.updateUser(updatedUser), storageRepo.getUrlForFile(imageKey), ]); emit(state.copyWith(uploading: false)); emit(state.copyWith(avatarPath: results.last as String)); } ``` ### 8.3 Initialize Profile View The avatar url needs to be updated the first time the profile view is rended. This can be archieved in the constructor of ProfileBloc class. ```dart ProfileBloc({required this.dataRepo, required this.storageRepo, required User user}) : super(ProfileState(user: user)) { if (user.avatarKey != null) { storageRepo.getUrlForFile(user.avatarKey!).then((url) => add(ProvideImagePath(avatarPath: url))); } on((event, emit) => emit(state.copyWith(imageSourceActionSheetIsVisible: true))); on(_openImagePicker); on((event, emit) => emit(state.copyWith(avatarPath: event.avatarPath))); } ``` ## 9 Monitoring Metrics with Amplify Analytics The Analytics category enables you to collect analytics data for your App. ### 9.1 Define Analytics Events Create a new file `analytics_events.dart` in `lib > service > analytics` directory. Define an abstract class and add some events you want. ```dart import 'package:amplify_analytics_pinpoint/amplify_analytics_pinpoint.dart'; import 'package:image_picker/image_picker.dart'; abstract class AbstractAnalyticsEvent { final AnalyticsEvent value; AbstractAnalyticsEvent.withName({required String eventName}) : value = AnalyticsEvent(eventName); AbstractAnalyticsEvent.withEvent({required AnalyticsEvent event}) : value = event; } class LoginEvent extends AbstractAnalyticsEvent { final bool success; LoginEvent(this.success) : super.withName(eventName: "login") { value.properties.addBoolProperty("success", success); } } class LoginAsGuestEvent extends AbstractAnalyticsEvent { LoginAsGuestEvent() : super.withName(eventName: "login_as_guest"); } class AutoLoginEvent extends AbstractAnalyticsEvent { final bool success; AutoLoginEvent(this.success) : super.withName(eventName: "auto_login") { value.properties.addBoolProperty("success", success); } } class SignUpEvent extends AbstractAnalyticsEvent { final bool success; SignUpEvent(this.success) : super.withName(eventName: "sign_up") { value.properties.addBoolProperty("success", success); } } class ConfirmationCodeEvent extends AbstractAnalyticsEvent { final bool success; ConfirmationCodeEvent(this.success) : super.withName(eventName: "confirmation_code") { value.properties.addBoolProperty("success", success); } } class SignOutEvent extends AbstractAnalyticsEvent { SignOutEvent() : super.withName(eventName: "sign_out"); } class ChangeAvatarEvent extends AbstractAnalyticsEvent { final ImageSource imageSource; ChangeAvatarEvent(this.imageSource) : super.withName(eventName: "change_avatar") { value.properties.addStringProperty("image_source", imageSource.toString().split('.').last); } } ``` ### 9.2 Create AnalyticsService Create a new file `analytics_service.dart` in the same directory. ```dart import 'package:amplify_flutter/amplify_flutter.dart'; import 'analytics_events.dart'; class AnalyticsService { static void log(AbstractAnalyticsEvent event) { Amplify.Analytics.recordEvent(event: event.value); } } ``` ### 9.3 Recording Events Now you can record events anywhere. Take LoginEvent as an example: ```dart // Import the package in the login_bloc.dart. import 'package:/service/analytics/analytics_service.dart'; import 'package:/service/analytics/analytics_events.dart' as analytics_events; // Log event. AnalyticsService.log(analytics_events.LoginEvent(true)); ```