Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ plugins {
}

android {
namespace = "com.example.myapp"
namespace = "com.google.firebase.example.friendlymeals"
compileSdk = flutter.compileSdkVersion
ndkVersion = "28.2.13676358"

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
Expand All @@ -22,11 +23,10 @@ android {
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.myapp"
applicationId = "com.google.firebase.example.friendlymeals"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:label="myapp"
android:label="FriendlyMeals"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
Expand Down Expand Up @@ -41,5 +47,17 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="geo"/>
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.myapp
package com.google.firebase.example.friendlymeals

import io.flutter.embedding.android.FlutterActivity

Expand Down
5 changes: 4 additions & 1 deletion firebase-ai-friendly-meals/flutter/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
gradle.user.home=/home/user/.gradle
org.gradle.caching=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ pluginManagement {

plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("com.android.application") version "8.11.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}

include(":app")
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions firebase-ai-friendly-meals/flutter/lib/data/app_repositories.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'datasource/ai_remote_datasource.dart';
import 'datasource/auth_remote_datasource.dart';
import 'datasource/database_remote_datasource.dart';
import 'datasource/live_ai_remote_datasource.dart';
import 'datasource/remote_config_service.dart';
import 'datasource/storage_remote_datasource.dart';
import 'repository/ai_repository.dart';
import 'repository/auth_repository.dart';
import 'repository/database_repository.dart';
import 'repository/live_ai_repository.dart';
import 'repository/storage_repository.dart';

class AppRepositories {
AppRepositories({
required this.remoteConfigService,
required this.authRepository,
required this.databaseRepository,
required this.storageRepository,
required this.aiRepository,
required this.liveAiRepository,
});

factory AppRepositories.create(RemoteConfigService remoteConfigService) {
final authDataSource = AuthRemoteDataSource();
final databaseDataSource = DatabaseRemoteDataSource();
final storageDataSource = StorageRemoteDataSource();
final aiDataSource = AIRemoteDataSource(remoteConfig: remoteConfigService);
final liveAiDataSource = LiveAIRemoteDataSource(
remoteConfig: remoteConfigService,
);

return AppRepositories(
remoteConfigService: remoteConfigService,
authRepository: AuthRepository(authDataSource),
databaseRepository: DatabaseRepository(databaseDataSource),
storageRepository: StorageRepository(storageDataSource),
aiRepository: AIRepository(aiDataSource),
liveAiRepository: LiveAIRepository(liveAiDataSource),
);
}

final RemoteConfigService remoteConfigService;
final AuthRepository authRepository;
final DatabaseRepository databaseRepository;
final StorageRepository storageRepository;
final AIRepository aiRepository;
final LiveAIRepository liveAiRepository;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import 'dart:convert';

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:flutter/foundation.dart';

import '../schema/meal_schema.dart';
import '../schema/recipe_schema.dart';
import '../schema/store_schema.dart';
import 'remote_config_service.dart';

class AIRemoteDataSource {
AIRemoteDataSource({
FirebaseAI? aiModel,
required RemoteConfigService remoteConfig,
}) : _aiModel = aiModel ?? FirebaseAI.googleAI(),
_remoteConfig = remoteConfig;

final FirebaseAI _aiModel;
final RemoteConfigService _remoteConfig;

// Remote Config Keys
static const String generateRecipeKey = 'generate_recipe';
static const String generateRecipePhotoGeminiKey =
'generate_recipe_photo_gemini';
static const String scanMealKey = 'scan_meal';
static const String findStoresKey = 'find_stores';
static const String hybridCloudModelKey = 'hybrid_cloud_model';
static const String hybridIngredientsPromptKey = 'hybrid_ingredients_prompt';
static const String recipePairingKey = 'recipe_pairing';
static const String voiceProfileKey = 'recipe_pairing_voice_profile';
static const String ttsModelKey = 'tts_model';

// Template input fields
static const String imageDataField = 'imageData';
static const String mimeTypeField = 'mimeType';
static const String ingredientsField = 'ingredients';
static const String notesField = 'notes';
static const String recipeTitleField = 'recipeTitle';
static const String dayField = 'dayOfWeek';
static const String timeField = 'currentTime';

// Template input values
static const String mimeTypeValue = 'image/jpeg';

// Grounding with Maps config
static const String language = 'en_US';

// TTS config
static const String ttsVoice = 'Charon';
static const String ttsLanguage = 'en-US';

GenerativeModel get _hybridGenerativeModel => _aiModel.generativeModel(
model: _remoteConfig.getString(hybridCloudModelKey),
);

// ignore: experimental_member_use
TemplateGenerativeModel get _templateGenerativeModel =>
// ignore: experimental_member_use
_aiModel.templateGenerativeModel();

Future<List<StoreSchema>> findStores({
required List<String> ingredients,
required double latitude,
required double longitude,
required String currentTime,
required String dayOfWeek,
}) async {
try {
// ignore: experimental_member_use
final response = await _templateGenerativeModel.generateContent(
_remoteConfig.getString(findStoresKey),
inputs: {
ingredientsField: ingredients.join(', '),
dayField: dayOfWeek,
timeField: currentTime,
},
toolConfig: TemplateToolConfig(
retrievalConfig: RetrievalConfig(
latLng: LatLng(latitude: latitude, longitude: longitude),
languageCode: language,
),
),
);

final decoded = _extractJsonObject(response.text);
if (decoded == null) {
return const [];
}
return StoreFinderResult.fromJson(decoded).stores;
} catch (e) {
debugPrint('Error finding stores with these ingredients: $e');
return const [];
}
}

Future<String> generateIngredients(Uint8List imageBytes) async {
final trace = FirebasePerformance.instance.newTrace('hybrid-inference');
await trace.start();
try {
final prompt = Content.multi([
InlineDataPart(mimeTypeValue, imageBytes),
TextPart(_remoteConfig.getString(hybridIngredientsPromptKey)),
]);

final response = await _hybridGenerativeModel.generateContent([prompt]);
trace.putAttribute('inferenceSource', 'In cloud');
return response.text ?? '';
} finally {
await trace.stop();
}
}

Future<RecipeSchema?> generateRecipe(
String ingredients,
String notes,
) async {
final inputs = <String, Object?>{
ingredientsField: ingredients,
if (notes.trim().isNotEmpty) notesField: notes,
};

// ignore: experimental_member_use
final response = await _templateGenerativeModel.generateContent(
_remoteConfig.getString(generateRecipeKey),
inputs: inputs,
);

final decoded = _extractJsonObject(response.text);
if (decoded == null) {
return null;
}
return RecipeSchema.fromJson(decoded);
}

Future<Uint8List?> generateRecipePhoto(String recipeTitle) async {
// ignore: experimental_member_use
final response = await _templateGenerativeModel.generateContent(
_remoteConfig.getString(generateRecipePhotoGeminiKey),
inputs: {recipeTitleField: recipeTitle},
);

final parts = response.candidates.firstOrNull?.content.parts ?? const [];
for (final part in parts) {
if (part is InlineDataPart) {
return part.bytes;
}
}
return null;
}

Future<MealSchema?> scanMeal(String imageData) async {
// ignore: experimental_member_use
final response = await _templateGenerativeModel.generateContent(
_remoteConfig.getString(scanMealKey),
inputs: {
mimeTypeField: mimeTypeValue,
imageDataField: imageData,
},
);

final decoded = _extractJsonObject(response.text);
if (decoded == null) {
return null;
}
return MealSchema.fromJson(decoded);
}

Map<String, dynamic>? _extractJsonObject(String? rawText) {
if (rawText == null || rawText.trim().isEmpty) {
return null;
}
final start = rawText.indexOf('{');
final end = rawText.lastIndexOf('}');
if (start == -1 || end == -1 || end < start) {
return null;
}
final cleanJson = rawText.substring(start, end + 1);
return jsonDecode(cleanJson) as Map<String, dynamic>;
}

Future<String> craftRecipePairing(
String dishTitle,
List<String> ingredients,
) async {
// ignore: experimental_member_use
final response = await _templateGenerativeModel.generateContent(
_remoteConfig.getString(recipePairingKey),
inputs: {
recipeTitleField: dishTitle,
ingredientsField: ingredients.join(', '),
},
);

return (response.text ?? '').trim();
}

Future<Uint8List?> generateSpeech(String? text) async {
if (text == null || text.trim().isEmpty) return null;

final model = _aiModel.generativeModel(
model: _remoteConfig.getString(ttsModelKey),
generationConfig: GenerationConfig(
responseModalities: [ResponseModalities.audio],
speechConfig: SpeechConfig(
voiceName: ttsVoice,
languageCode: ttsLanguage,
),
),
);

final voiceProfile = _remoteConfig.getString(voiceProfileKey);
final response = await model.generateContent([
Content.text('$voiceProfile $text'),
]);

final part =
response.candidates.firstOrNull?.content.parts.firstOrNull;
if (part is InlineDataPart) {
return part.bytes;
}
return null;
}

Future<void> loadOnDeviceModel() async {
// On-device model preloading via Gemini Nano is handled by cloud fallback in Flutter.
debugPrint('AIRemoteDataSource: Cloud inference configured for hybrid model');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:firebase_auth/firebase_auth.dart' as fb_auth;
import '../model/user.dart';

class AuthRemoteDataSource {
AuthRemoteDataSource({fb_auth.FirebaseAuth? auth})
: _auth = auth ?? fb_auth.FirebaseAuth.instance;

final fb_auth.FirebaseAuth _auth;

fb_auth.User? get currentUser => _auth.currentUser;

Stream<fb_auth.User?> get authStateChanges => _auth.authStateChanges();

Future<AppUser> createAnonymousAccount() async {
final authResult = await _auth.signInAnonymously();
return AppUser(authId: authResult.user?.uid ?? '');
}
}
Loading
Loading