# Kassalapp API med Flutter - Komplett Guide ## Innledning Kassalapp API gir deg tilgang til Norges mest omfattende database over dagligvarepriser. Denne guiden viser deg hvordan du integrerer API-et i Flutter-applikasjoner for både iOS og Android, med støtte for strekkodeskanning, prissammenligning og butikkfinner. ## Innhold 1. [Kom i gang](#kom-i-gang) 2. [Autentisering](#autentisering) 3. [Oppsett med Dio](#oppsett-med-dio) 4. [Modeller og Type Safety](#modeller-og-type-safety) 5. [Produktsøk](#produktsøk) 6. [Strekkodeskanning](#strekkodeskanning) 7. [Butikklokasjoner](#butikklokasjoner) 8. [Handleliste-funksjonalitet](#handleliste-funksjonalitet) 9. [Feilhåndtering](#feilhåndtering) 10. [State Management](#state-management) 11. [Praktiske eksempler](#praktiske-eksempler) ## Kom i gang ### 1. Registrer deg for API-nøkkel Først må du registrere deg for å få en API-nøkkel: 1. Gå til [https://kassal.app/api](https://kassal.app/api) 2. Opprett en konto eller logg inn 3. Generer en ny API-nøkkel 4. Lagre nøkkelen trygt ### 2. Legg til nødvendige pakker Oppdater `pubspec.yaml`: ```yaml name: kassalapp_flutter description: Flutter app for Kassalapp API integration version: 1.0.0+1 environment: sdk: '>=3.0.0 <4.0.0' flutter: '>=3.10.0' dependencies: flutter: sdk: flutter dio: ^5.7.0 dio_cache_interceptor: ^3.5.0 mobile_scanner: ^6.0.2 # Note: v7+ has breaking changes, using stable v6 geolocator: ^13.0.1 flutter_secure_storage: ^9.2.2 provider: ^6.1.2 json_annotation: ^4.9.0 freezed_annotation: ^2.4.4 # Keeping v2 for stability cached_network_image: ^3.4.1 shimmer: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.4.13 json_serializable: ^6.8.0 freezed: ^2.5.7 # Keeping v2 for stability flutter_lints: ^5.0.0 ``` ### 3. Konfigurer platform-spesifikke tillatelser #### iOS (ios/Runner/Info.plist) ```xml NSCameraUsageDescription Appen bruker kamera for å skanne strekkoder NSLocationWhenInUseUsageDescription Appen bruker din posisjon for å finne butikker i nærheten ``` #### Android (android/app/src/main/AndroidManifest.xml) ```xml ``` ## Autentisering ### Sikker lagring av API-nøkkel ```dart // lib/services/secure_storage.dart import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorageService { static const _storage = FlutterSecureStorage(); static const _apiKeyKey = 'kassalapp_api_key'; static Future saveApiKey(String apiKey) async { await _storage.write(key: _apiKeyKey, value: apiKey); } static Future getApiKey() async { return await _storage.read(key: _apiKeyKey); } static Future deleteApiKey() async { await _storage.delete(key: _apiKeyKey); } } ``` ## Oppsett med Dio ### API Client konfigurasjon ```dart // lib/services/api_client.dart import 'package:dio/dio.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'secure_storage.dart'; class ApiClient { static const String _baseUrl = 'https://kassal.app/api/v1'; late final Dio _dio; static final ApiClient _instance = ApiClient._internal(); factory ApiClient() => _instance; ApiClient._internal() { _initializeDio(); } void _initializeDio() { // Cache konfigurasjon final cacheOptions = CacheOptions( store: MemCacheStore(), policy: CachePolicy.request, hitCacheOnErrorExcept: [401, 403], maxStale: const Duration(hours: 1), priority: CachePriority.normal, keyBuilder: CacheOptions.defaultCacheKeyBuilder, ); _dio = Dio(BaseOptions( baseUrl: _baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, )); // Legg til interceptors _dio.interceptors.addAll([ DioCacheInterceptor(options: cacheOptions), AuthInterceptor(), LoggingInterceptor(), ErrorInterceptor(), ]); } Dio get dio => _dio; } // Auth Interceptor class AuthInterceptor extends Interceptor { @override void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { final apiKey = await SecureStorageService.getApiKey(); if (apiKey != null) { options.headers['Authorization'] = 'Bearer $apiKey'; } handler.next(options); } } // Logging Interceptor class LoggingInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { print('🚀 REQUEST[${options.method}] => PATH: ${options.path}'); handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { print('✅ RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}'); handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) { print('❌ ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}'); handler.next(err); } } // Error Interceptor class ErrorInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) { switch (err.response?.statusCode) { case 401: // Handle unauthorized _handleUnauthorized(); break; case 429: // Handle rate limiting _handleRateLimiting(err); break; default: break; } handler.next(err); } void _handleUnauthorized() { // Navigate to login or refresh token SecureStorageService.deleteApiKey(); } void _handleRateLimiting(DioException err) { // Implement retry logic or show user message print('Rate limit reached. Please wait before retrying.'); } } ``` ## Modeller og Type Safety ### Freezed modeller for API responses ```dart // lib/models/product.dart import 'package:freezed_annotation/freezed_annotation.dart'; part 'product.freezed.dart'; part 'product.g.dart'; @freezed class Product with _$Product { const factory Product({ required int id, required String name, String? brand, String? vendor, String? description, String? ingredients, String? url, String? image, String? ean, required Store store, @JsonKey(name: 'current_price') double? currentPrice, @JsonKey(name: 'current_unit_price') double? currentUnitPrice, double? weight, @JsonKey(name: 'weight_unit') String? weightUnit, // Additional fields from actual API @JsonKey(name: 'view_count') int? viewCount, @JsonKey(name: 'price_diff') double? priceDiff, @JsonKey(name: 'prev_price') double? prevPrice, @JsonKey(name: 'last_price_checked_at') DateTime? lastPriceCheckedAt, @JsonKey(name: 'minimum_durability') int? minimumDurability, // Timestamps @JsonKey(name: 'created_at') DateTime? createdAt, @JsonKey(name: 'updated_at') DateTime? updatedAt, // Related data - category is actually an array List? category, List? priceHistory, List? allergens, List? nutritionalContents, List