# 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