Supabase云同步架構(gòu):Flutter應(yīng)用的數(shù)據(jù)同步策略

BeeCount(蜜蜂記賬)是一款開源、簡(jiǎn)潔、無(wú)廣告的個(gè)人記賬應(yīng)用。所有財(cái)務(wù)數(shù)據(jù)完全由用戶掌控,支持本地存儲(chǔ)和可選的云端同步,確保數(shù)據(jù)絕對(duì)安全。

引言

在現(xiàn)代移動(dòng)應(yīng)用開發(fā)中,多設(shè)備數(shù)據(jù)同步已成為用戶的基本需求。用戶希望在手機(jī)、平板、不同設(shè)備間無(wú)縫切換,同時(shí)保持?jǐn)?shù)據(jù)的一致性和安全性。BeeCount選擇Supabase作為云端后臺(tái)服務(wù)https://www.naquan.com/,不僅因?yàn)槠溟_源特性和強(qiáng)大功能,更重要的是它提供了完整的數(shù)據(jù)安全保障。

Supabase架構(gòu)優(yōu)勢(shì)

開源與自主可控

開源透明:完全開源的后臺(tái)即服務(wù)(BaaS)解決方案

數(shù)據(jù)主權(quán):支持自建部署,數(shù)據(jù)完全可控

標(biāo)準(zhǔn)技術(shù):基于PostgreSQL,無(wú)廠商鎖定風(fēng)險(xiǎn)

功能完整性

實(shí)時(shí)數(shù)據(jù)庫(kù):基于PostgreSQL的實(shí)時(shí)數(shù)據(jù)同步

用戶認(rèn)證:完整的身份驗(yàn)證和授權(quán)系統(tǒng)

文件存儲(chǔ):對(duì)象存儲(chǔ)服務(wù),支持大文件上傳

邊緣函數(shù):服務(wù)端邏輯處理能力

同步架構(gòu)設(shè)計(jì)

整體架構(gòu)圖

┌─────────────────┐? ? ┌──────────────────┐? ? ┌─────────────────┐

│? Flutter App? │? ? │? Supabase? ? ? │? ? │? Other Device? │

│? (Local SQLite) │?──?│? (PostgreSQL)? ? │?──?│? (Local SQLite) │

│? ? ? ? ? ? ? ? │? ? │? (Auth + Storage) │? ? │? ? ? ? ? ? ? ? │

└─────────────────┘? ? └──────────────────┘? ? └─────────────────┘

? ? ? ? │? ? ? ? ? ? ? ? ? ? ? │? ? ? ? ? ? ? ? ? ? ? │

? ? ? ? └───── 加密備份文件 ─────┴───── 加密備份文件 ─────┘

核心設(shè)計(jì)原則

本地優(yōu)先:所有操作優(yōu)先在本地完成,確保響應(yīng)速度

增量同步:只同步變更數(shù)據(jù),降低網(wǎng)絡(luò)開銷

端到端加密:敏感數(shù)據(jù)在客戶端加密后上傳

沖突處理:合理的沖突解決策略

離線可用:網(wǎng)絡(luò)異常時(shí)應(yīng)用仍可正常使用

認(rèn)證系統(tǒng)集成

Supabase認(rèn)證配置

class SupabaseAuthService implements AuthService {

? final s.SupabaseClient client;


? SupabaseAuthService({required this.client});

? @override

? Future<AuthResult> signInWithEmail({

? ? required String email,

? ? required String password,

? }) async {

? ? try {

? ? ? final response = await client.auth.signInWithPassword(

? ? ? ? email: email,

? ? ? ? password: password,

? ? ? );


? ? ? if (response.user != null) {

? ? ? ? return AuthResult.success(user: AppUser.fromSupabase(response.user!));

? ? ? } else {

? ? ? ? return AuthResult.failure(error: 'Login failed');

? ? ? }

? ? } catch (e) {

? ? ? return AuthResult.failure(error: e.toString());

? ? }

? }

? @override

? Future<AuthResult> signUpWithEmail({

? ? required String email,

? ? required String password,

? }) async {

? ? try {

? ? ? final response = await client.auth.signUp(

? ? ? ? email: email,

? ? ? ? password: password,

? ? ? );


? ? ? return AuthResult.success(user: AppUser.fromSupabase(response.user!));

? ? } catch (e) {

? ? ? return AuthResult.failure(error: e.toString());

? ? }

? }

? @override

? Stream<AuthState> get authStateChanges {

? ? return client.auth.onAuthStateChange.map((data) {

? ? ? if (data.session?.user != null) {

? ? ? ? return AuthState.authenticated(

? ? ? ? ? user: AppUser.fromSupabase(data.session!.user)

? ? ? ? );

? ? ? }

? ? ? return AuthState.unauthenticated();

? ? });

? }

}

用戶模型設(shè)計(jì)

class AppUser {

? final String id;

? final String email;

? final DateTime? lastSignInAt;

? const AppUser({

? ? required this.id,

? ? required this.email,

? ? this.lastSignInAt,

? });

? factory AppUser.fromSupabase(s.User user) {

? ? return AppUser(

? ? ? id: user.id,

? ? ? email: user.email ?? '',

? ? ? lastSignInAt: user.lastSignInAt,

? ? );

? }

? bool get isAnonymous => email.isEmpty;

}

數(shù)據(jù)同步策略

備份文件格式

BeeCount采用加密備份文件的方式進(jìn)行數(shù)據(jù)同步:

class BackupData {

? final String version;

? final DateTime createdAt;

? final String deviceId;

? final Map<String, dynamic> ledgers;

? final Map<String, dynamic> accounts;

? final Map<String, dynamic> categories;

? final Map<String, dynamic> transactions;


? BackupData({

? ? required this.version,

? ? required this.createdAt,

? ? required this.deviceId,

? ? required this.ledgers,

? ? required this.accounts,

? ? required this.categories,

? ? required this.transactions,

? });

? Map<String, dynamic> toJson() => {

? ? 'version': version,

? ? 'createdAt': createdAt.toIso8601String(),

? ? 'deviceId': deviceId,

? ? 'ledgers': ledgers,

? ? 'accounts': accounts,

? ? 'categories': categories,

? ? 'transactions': transactions,

? };

? factory BackupData.fromJson(Map<String, dynamic> json) {

? ? return BackupData(

? ? ? version: json['version'],

? ? ? createdAt: DateTime.parse(json['createdAt']),

? ? ? deviceId: json['deviceId'],

? ? ? ledgers: json['ledgers'],

? ? ? accounts: json['accounts'],

? ? ? categories: json['categories'],

? ? ? transactions: json['transactions'],

? ? );

? }

}

同步服務(wù)實(shí)現(xiàn)

class SupabaseSyncService implements SyncService {

? final s.SupabaseClient client;

? final BeeDatabase db;

? final BeeRepository repo;

? final AuthService auth;

? final String bucket;


? // 狀態(tài)緩存和上傳窗口管理

? final Map<int, SyncStatus> _statusCache = {};

? final Map<int, _RecentUpload> _recentUpload = {};

? final Map<int, DateTime> _recentLocalChangeAt = {};

? SupabaseSyncService({

? ? required this.client,

? ? required this.db,

? ? required this.repo,

? ? required this.auth,

? ? this.bucket = 'beecount-backups',

? });

? @override

? Future<SyncStatus> getSyncStatus(int ledgerId) async {

? ? // 檢查緩存

? ? if (_statusCache.containsKey(ledgerId)) {

? ? ? final cached = _statusCache[ledgerId]!;

? ? ? if (DateTime.now().difference(cached.lastCheck).inMinutes < 5) {

? ? ? ? return cached;

? ? ? }

? ? }

? ? try {

? ? ? final user = await auth.getCurrentUser();

? ? ? if (user == null) {

? ? ? ? return SyncStatus.notLoggedIn();

? ? ? }

? ? ? // 獲取云端文件信息

? ? ? final fileName = 'ledger_${ledgerId}_backup.json';

? ? ? final cloudFile = await _getCloudFileInfo(fileName);


? ? ? // 計(jì)算本地?cái)?shù)據(jù)指紋

? ? ? final localFingerprint = await _calculateLocalFingerprint(ledgerId);


? ? ? if (cloudFile == null) {

? ? ? ? final status = SyncStatus.localOnly(

? ? ? ? ? localFingerprint: localFingerprint,

? ? ? ? ? hasLocalChanges: true,

? ? ? ? );

? ? ? ? _statusCache[ledgerId] = status;

? ? ? ? return status;

? ? ? }

? ? ? // 比較指紋判斷同步狀態(tài)

? ? ? final isUpToDate = localFingerprint == cloudFile.fingerprint;

? ? ? final status = SyncStatus.synced(

? ? ? ? localFingerprint: localFingerprint,

? ? ? ? cloudFingerprint: cloudFile.fingerprint,

? ? ? ? isUpToDate: isUpToDate,

? ? ? ? lastSyncAt: cloudFile.lastModified,

? ? ? );


? ? ? _statusCache[ledgerId] = status;

? ? ? return status;

? ? } catch (e) {

? ? ? logger.error('Failed to get sync status', e);

? ? ? return SyncStatus.error(error: e.toString());

? ? }

? }

? @override

? Future<SyncResult> uploadBackup(int ledgerId) async {

? ? try {

? ? ? final user = await auth.getCurrentUser();

? ? ? if (user == null) {

? ? ? ? return SyncResult.failure(error: 'Not logged in');

? ? ? }

? ? ? // 生成備份數(shù)據(jù)

? ? ? final backupData = await _generateBackup(ledgerId);

? ? ? final jsonString = json.encode(backupData.toJson());


? ? ? // 加密備份數(shù)據(jù)

? ? ? final encryptedData = await _encryptBackupData(jsonString);


? ? ? // 上傳到Supabase Storage

? ? ? final fileName = 'ledger_${ledgerId}_backup.json';

? ? ? final uploadResult = await client.storage

? ? ? ? ? .from(bucket)

? ? ? ? ? .uploadBinary(fileName, encryptedData);

? ? ? if (uploadResult.isNotEmpty) {

? ? ? ? // 記錄上傳成功

? ? ? ? final fingerprint = await _calculateLocalFingerprint(ledgerId);

? ? ? ? _recentUpload[ledgerId] = _RecentUpload(

? ? ? ? ? fingerprint: fingerprint,

? ? ? ? ? uploadedAt: DateTime.now(),

? ? ? ? );


? ? ? ? // 更新緩存

? ? ? ? _statusCache[ledgerId] = SyncStatus.synced(

? ? ? ? ? localFingerprint: fingerprint,

? ? ? ? ? cloudFingerprint: fingerprint,

? ? ? ? ? isUpToDate: true,

? ? ? ? ? lastSyncAt: DateTime.now(),

? ? ? ? );

? ? ? ? return SyncResult.success(

? ? ? ? ? syncedAt: DateTime.now(),

? ? ? ? ? message: 'Backup uploaded successfully',

? ? ? ? );

? ? ? }

? ? ? return SyncResult.failure(error: 'Upload failed');

? ? } catch (e) {

? ? ? logger.error('Failed to upload backup', e);

? ? ? return SyncResult.failure(error: e.toString());

? ? }

? }

? @override

? Future<SyncResult> downloadRestore(int ledgerId) async {

? ? try {

? ? ? final user = await auth.getCurrentUser();

? ? ? if (user == null) {

? ? ? ? return SyncResult.failure(error: 'Not logged in');

? ? ? }

? ? ? // 下載備份文件

? ? ? final fileName = 'ledger_${ledgerId}_backup.json';

? ? ? final downloadData = await client.storage

? ? ? ? ? .from(bucket)

? ? ? ? ? .download(fileName);

? ? ? if (downloadData.isEmpty) {

? ? ? ? return SyncResult.failure(error: 'No backup found');

? ? ? }

? ? ? // 解密備份數(shù)據(jù)

? ? ? final decryptedData = await _decryptBackupData(downloadData);

? ? ? final backupData = BackupData.fromJson(json.decode(decryptedData));

? ? ? // 執(zhí)行數(shù)據(jù)恢復(fù)

? ? ? await _restoreFromBackup(backupData, ledgerId);

? ? ? // 更新狀態(tài)

? ? ? final fingerprint = await _calculateLocalFingerprint(ledgerId);

? ? ? _statusCache[ledgerId] = SyncStatus.synced(

? ? ? ? localFingerprint: fingerprint,

? ? ? ? cloudFingerprint: fingerprint,

? ? ? ? isUpToDate: true,

? ? ? ? lastSyncAt: DateTime.now(),

? ? ? );

? ? ? return SyncResult.success(

? ? ? ? syncedAt: DateTime.now(),

? ? ? ? message: 'Data restored successfully',

? ? ? );

? ? } catch (e) {

? ? ? logger.error('Failed to download restore', e);

? ? ? return SyncResult.failure(error: e.toString());

? ? }

? }

? // 數(shù)據(jù)加密/解密

? Future<Uint8List> _encryptBackupData(String jsonData) async {

? ? final key = await _getDerivedKey();

? ? final cipher = AESCipher(key);

? ? return cipher.encrypt(utf8.encode(jsonData));

? }

? Future<String> _decryptBackupData(Uint8List encryptedData) async {

? ? final key = await _getDerivedKey();

? ? final cipher = AESCipher(key);

? ? final decrypted = cipher.decrypt(encryptedData);

? ? return utf8.decode(decrypted);

? }

}

數(shù)據(jù)安全保障

端到端加密

class AESCipher {

? final Uint8List key;

? AESCipher(this.key);

? Uint8List encrypt(List<int> plaintext) {

? ? final cipher = AESEngine()

? ? ? ..init(true, KeyParameter(key));


? ? // 生成隨機(jī)IV

? ? final iv = _generateRandomIV();

? ? final cbcCipher = CBCBlockCipher(cipher)

? ? ? ..init(true, ParametersWithIV(KeyParameter(key), iv));

? ? // PKCS7填充

? ? final paddedPlaintext = _padPKCS7(Uint8List.fromList(plaintext));

? ? final ciphertext = Uint8List(paddedPlaintext.length);


? ? for (int i = 0; i < paddedPlaintext.length; i += 16) {

? ? ? cbcCipher.processBlock(paddedPlaintext, i, ciphertext, i);

? ? }

? ? // IV + 密文

? ? return Uint8List.fromList([...iv, ...ciphertext]);

? }

? Uint8List decrypt(Uint8List encrypted) {

? ? // 分離IV和密文

? ? final iv = encrypted.sublist(0, 16);

? ? final ciphertext = encrypted.sublist(16);

? ? final cipher = AESEngine()

? ? ? ..init(false, KeyParameter(key));

? ? final cbcCipher = CBCBlockCipher(cipher)

? ? ? ..init(false, ParametersWithIV(KeyParameter(key), iv));

? ? final decrypted = Uint8List(ciphertext.length);

? ? for (int i = 0; i < ciphertext.length; i += 16) {

? ? ? cbcCipher.processBlock(ciphertext, i, decrypted, i);

? ? }

? ? // 移除PKCS7填充

? ? return _removePKCS7Padding(decrypted);

? }

? Uint8List _generateRandomIV() {

? ? final random = Random.secure();

? ? return Uint8List.fromList(

? ? ? List.generate(16, (_) => random.nextInt(256))

? ? );

? }

}

密鑰派生

Future<Uint8List> _getDerivedKey() async {

? final user = await auth.getCurrentUser();

? if (user == null) throw Exception('User not authenticated');

? // 使用用戶ID和設(shè)備特征生成密鑰

? final salt = utf8.encode('${user.id}_${await _getDeviceId()}');

? final password = utf8.encode(user.id);

? // PBKDF2密鑰派生

? final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))

? ? ..init(Pbkdf2Parameters(salt, 10000, 32));

? return pbkdf2.process(password);

}

Future<String> _getDeviceId() async {

? final prefs = await SharedPreferences.getInstance();

? String? deviceId = prefs.getString('device_id');


? if (deviceId == null) {

? ? deviceId = const Uuid().v4();

? ? await prefs.setString('device_id', deviceId);

? }


? return deviceId;

}

沖突處理策略

沖突檢測(cè)

class ConflictDetector {

? static ConflictResolution detectConflict({

? ? required BackupData localData,

? ? required BackupData cloudData,

? ? required DateTime lastSyncAt,

? }) {

? ? final localChanges = <String, dynamic>{};

? ? final cloudChanges = <String, dynamic>{};


? ? // 檢測(cè)交易記錄沖突

? ? _detectTransactionConflicts(

? ? ? localData.transactions,

? ? ? cloudData.transactions,

? ? ? lastSyncAt,

? ? ? localChanges,

? ? ? cloudChanges,

? ? );

? ? if (localChanges.isEmpty && cloudChanges.isEmpty) {

? ? ? return ConflictResolution.noConflict();

? ? }

? ? if (localChanges.isNotEmpty && cloudChanges.isEmpty) {

? ? ? return ConflictResolution.localWins(changes: localChanges);

? ? }

? ? if (localChanges.isEmpty && cloudChanges.isNotEmpty) {

? ? ? return ConflictResolution.cloudWins(changes: cloudChanges);

? ? }

? ? // 存在雙向沖突,需要用戶選擇

? ? return ConflictResolution.needsResolution(

? ? ? localChanges: localChanges,

? ? ? cloudChanges: cloudChanges,

? ? );

? }

? static void _detectTransactionConflicts(

? ? Map<String, dynamic> localTxs,

? ? Map<String, dynamic> cloudTxs,

? ? DateTime lastSyncAt,

? ? Map<String, dynamic> localChanges,

? ? Map<String, dynamic> cloudChanges,

? ) {

? ? // 檢測(cè)本地新增/修改的交易

? ? localTxs.forEach((id, localTx) {

? ? ? final txUpdatedAt = DateTime.parse(localTx['updatedAt'] ?? localTx['createdAt']);

? ? ? if (txUpdatedAt.isAfter(lastSyncAt)) {

? ? ? ? localChanges[id] = localTx;

? ? ? }

? ? });

? ? // 檢測(cè)云端新增/修改的交易

? ? cloudTxs.forEach((id, cloudTx) {

? ? ? final txUpdatedAt = DateTime.parse(cloudTx['updatedAt'] ?? cloudTx['createdAt']);

? ? ? if (txUpdatedAt.isAfter(lastSyncAt)) {

? ? ? ? cloudChanges[id] = cloudTx;

? ? ? }

? ? });

? }

}

沖突解決

class ConflictResolver {

? static Future<BackupData> resolveConflict({

? ? required BackupData localData,

? ? required BackupData cloudData,

? ? required ConflictResolution resolution,

? }) async {

? ? switch (resolution.type) {

? ? ? case ConflictType.noConflict:

? ? ? ? return localData;


? ? ? case ConflictType.localWins:

? ? ? ? return localData;


? ? ? case ConflictType.cloudWins:

? ? ? ? return cloudData;


? ? ? case ConflictType.needsResolution:

? ? ? ? return await _mergeData(localData, cloudData, resolution);

? ? }

? }

? static Future<BackupData> _mergeData(

? ? BackupData localData,

? ? BackupData cloudData,

? ? ConflictResolution resolution,

? ) async {

? ? // 實(shí)現(xiàn)智能合并策略

? ? final mergedTransactions = <String, dynamic>{};


? ? // 優(yōu)先保留較新的數(shù)據(jù)

? ? mergedTransactions.addAll(cloudData.transactions);


? ? resolution.localChanges.forEach((id, localTx) {

? ? ? final localUpdatedAt = DateTime.parse(localTx['updatedAt'] ?? localTx['createdAt']);


? ? ? if (resolution.cloudChanges.containsKey(id)) {

? ? ? ? final cloudTx = resolution.cloudChanges[id];

? ? ? ? final cloudUpdatedAt = DateTime.parse(cloudTx['updatedAt'] ?? cloudTx['createdAt']);


? ? ? ? // 保留時(shí)間戳較新的版本

? ? ? ? if (localUpdatedAt.isAfter(cloudUpdatedAt)) {

? ? ? ? ? mergedTransactions[id] = localTx;

? ? ? ? } else {

? ? ? ? ? mergedTransactions[id] = cloudTx;

? ? ? ? }

? ? ? } else {

? ? ? ? mergedTransactions[id] = localTx;

? ? ? }

? ? });

? ? return BackupData(

? ? ? version: localData.version,

? ? ? createdAt: DateTime.now(),

? ? ? deviceId: localData.deviceId,

? ? ? ledgers: localData.ledgers,

? ? ? accounts: localData.accounts,

? ? ? categories: localData.categories,

? ? ? transactions: mergedTransactions,

? ? );

? }

}

性能優(yōu)化

增量同步優(yōu)化

class IncrementalSync {

? static Future<BackupData> generateIncrementalBackup({

? ? required int ledgerId,

? ? required DateTime lastSyncAt,

? ? required BeeRepository repo,

? }) async {

? ? // 只獲取自上次同步后的變更數(shù)據(jù)

? ? final changedTransactions = await repo.getTransactionsSince(

? ? ? ledgerId: ledgerId,

? ? ? since: lastSyncAt,

? ? );

? ? final changedAccounts = await repo.getAccountsSince(

? ? ? ledgerId: ledgerId,

? ? ? since: lastSyncAt,

? ? );

? ? // 構(gòu)建增量備份數(shù)據(jù)

? ? return BackupData(

? ? ? version: '1.0',

? ? ? createdAt: DateTime.now(),

? ? ? deviceId: await _getDeviceId(),

? ? ? ledgers: {}, // 賬本信息變化較少,可按需包含

? ? ? accounts: _mapAccountsToJson(changedAccounts),

? ? ? categories: {}, // 分類變化較少,可按需包含

? ? ? transactions: _mapTransactionsToJson(changedTransactions),

? ? );

? }

? static Map<String, dynamic> _mapTransactionsToJson(List<Transaction> transactions) {

? ? return Map.fromEntries(

? ? ? transactions.map((tx) => MapEntry(

? ? ? ? tx.id.toString(),

? ? ? ? {

? ? ? ? ? 'id': tx.id,

? ? ? ? ? 'ledgerId': tx.ledgerId,

? ? ? ? ? 'type': tx.type,

? ? ? ? ? 'amount': tx.amount,

? ? ? ? ? 'categoryId': tx.categoryId,

? ? ? ? ? 'accountId': tx.accountId,

? ? ? ? ? 'toAccountId': tx.toAccountId,

? ? ? ? ? 'happenedAt': tx.happenedAt.toIso8601String(),

? ? ? ? ? 'note': tx.note,

? ? ? ? ? 'updatedAt': DateTime.now().toIso8601String(),

? ? ? ? }

? ? ? ))

? ? );

? }

}

網(wǎng)絡(luò)優(yōu)化

class NetworkOptimizer {

? static const int maxRetries = 3;

? static const Duration retryDelay = Duration(seconds: 2);

? static Future<T> withRetry<T>(Future<T> Function() operation) async {

? ? int attempts = 0;


? ? while (attempts < maxRetries) {

? ? ? try {

? ? ? ? return await operation();

? ? ? } catch (e) {

? ? ? ? attempts++;


? ? ? ? if (attempts >= maxRetries) {

? ? ? ? ? rethrow;

? ? ? ? }


? ? ? ? // 指數(shù)退避

? ? ? ? await Future.delayed(retryDelay * (1 << attempts));

? ? ? }

? ? }


? ? throw Exception('Max retries exceeded');

? }

? static Future<bool> isNetworkAvailable() async {

? ? try {

? ? ? final result = await InternetAddress.lookup('supabase.co');

? ? ? return result.isNotEmpty && result[0].rawAddress.isNotEmpty;

? ? } catch (_) {

? ? ? return false;

? ? }

? }

}

用戶體驗(yàn)設(shè)計(jì)

同步狀態(tài)展示

class SyncStatusWidget extends ConsumerWidget {

? final int ledgerId;

? const SyncStatusWidget({Key? key, required this.ledgerId}) : super(key: key);

? @override

? Widget build(BuildContext context, WidgetRef ref) {

? ? final syncStatus = ref.watch(syncStatusProvider(ledgerId));

? ? return syncStatus.when(

? ? ? data: (status) => _buildStatusIndicator(status),

? ? ? loading: () => const SyncLoadingIndicator(),

? ? ? error: (error, _) => SyncErrorIndicator(error: error.toString()),

? ? );

? }

? Widget _buildStatusIndicator(SyncStatus status) {

? ? switch (status.type) {

? ? ? case SyncStatusType.synced:

? ? ? ? return Row(

? ? ? ? ? mainAxisSize: MainAxisSize.min,

? ? ? ? ? children: [

? ? ? ? ? ? Icon(Icons.cloud_done, color: Colors.green, size: 16),

? ? ? ? ? ? SizedBox(width: 4),

? ? ? ? ? ? Text('已同步', style: TextStyle(fontSize: 12, color: Colors.green)),

? ? ? ? ? ],

? ? ? ? );


? ? ? case SyncStatusType.localOnly:

? ? ? ? return Row(

? ? ? ? ? mainAxisSize: MainAxisSize.min,

? ? ? ? ? children: [

? ? ? ? ? ? Icon(Icons.cloud_off, color: Colors.orange, size: 16),

? ? ? ? ? ? SizedBox(width: 4),

? ? ? ? ? ? Text('僅本地', style: TextStyle(fontSize: 12, color: Colors.orange)),

? ? ? ? ? ],

? ? ? ? );


? ? ? case SyncStatusType.needsUpload:

? ? ? ? return Row(

? ? ? ? ? mainAxisSize: MainAxisSize.min,

? ? ? ? ? children: [

? ? ? ? ? ? Icon(Icons.cloud_upload, color: Colors.blue, size: 16),

? ? ? ? ? ? SizedBox(width: 4),

? ? ? ? ? ? Text('待上傳', style: TextStyle(fontSize: 12, color: Colors.blue)),

? ? ? ? ? ],

? ? ? ? );


? ? ? case SyncStatusType.needsDownload:

? ? ? ? return Row(

? ? ? ? ? mainAxisSize: MainAxisSize.min,

? ? ? ? ? children: [

? ? ? ? ? ? Icon(Icons.cloud_download, color: Colors.purple, size: 16),

? ? ? ? ? ? SizedBox(width: 4),

? ? ? ? ? ? Text('有更新', style: TextStyle(fontSize: 12, color: Colors.purple)),

? ? ? ? ? ],

? ? ? ? );


? ? ? default:

? ? ? ? return SizedBox.shrink();

? ? }

? }

}

同步操作界面

class SyncActionsSheet extends ConsumerWidget {

? final int ledgerId;

? const SyncActionsSheet({Key? key, required this.ledgerId}) : super(key: key);

? @override

? Widget build(BuildContext context, WidgetRef ref) {

? ? final syncStatus = ref.watch(syncStatusProvider(ledgerId));

? ? return DraggableScrollableSheet(

? ? ? initialChildSize: 0.4,

? ? ? minChildSize: 0.2,

? ? ? maxChildSize: 0.8,

? ? ? builder: (context, scrollController) {

? ? ? ? return Container(

? ? ? ? ? decoration: BoxDecoration(

? ? ? ? ? ? color: Theme.of(context).scaffoldBackgroundColor,

? ? ? ? ? ? borderRadius: BorderRadius.vertical(top: Radius.circular(20)),

? ? ? ? ? ),

? ? ? ? ? child: Column(

? ? ? ? ? ? children: [

? ? ? ? ? ? ? // 拖拽指示器

? ? ? ? ? ? ? Container(

? ? ? ? ? ? ? ? width: 40,

? ? ? ? ? ? ? ? height: 4,

? ? ? ? ? ? ? ? margin: EdgeInsets.symmetric(vertical: 12),

? ? ? ? ? ? ? ? decoration: BoxDecoration(

? ? ? ? ? ? ? ? ? color: Colors.grey[300],

? ? ? ? ? ? ? ? ? borderRadius: BorderRadius.circular(2),

? ? ? ? ? ? ? ? ),

? ? ? ? ? ? ? ),


? ? ? ? ? ? ? // 標(biāo)題

? ? ? ? ? ? ? Text(

? ? ? ? ? ? ? ? '云端同步',

? ? ? ? ? ? ? ? style: Theme.of(context).textTheme.headlineSmall,

? ? ? ? ? ? ? ),


? ? ? ? ? ? ? Expanded(

? ? ? ? ? ? ? ? child: ListView(

? ? ? ? ? ? ? ? ? controller: scrollController,

? ? ? ? ? ? ? ? ? padding: EdgeInsets.all(16),

? ? ? ? ? ? ? ? ? children: [

? ? ? ? ? ? ? ? ? ? _buildSyncActions(context, ref, syncStatus),

? ? ? ? ? ? ? ? ? ],

? ? ? ? ? ? ? ? ),

? ? ? ? ? ? ? ),

? ? ? ? ? ? ],

? ? ? ? ? ),

? ? ? ? );

? ? ? },

? ? );

? }

? Widget _buildSyncActions(BuildContext context, WidgetRef ref, AsyncValue<SyncStatus> syncStatus) {

? ? return syncStatus.when(

? ? ? data: (status) {

? ? ? ? switch (status.type) {

? ? ? ? ? case SyncStatusType.localOnly:

? ? ? ? ? ? return Column(

? ? ? ? ? ? ? children: [

? ? ? ? ? ? ? ? _buildActionCard(

? ? ? ? ? ? ? ? ? title: '上傳備份',

? ? ? ? ? ? ? ? ? subtitle: '將本地?cái)?shù)據(jù)上傳到云端',

? ? ? ? ? ? ? ? ? icon: Icons.cloud_upload,

? ? ? ? ? ? ? ? ? color: Colors.blue,

? ? ? ? ? ? ? ? ? onTap: () => _uploadBackup(ref),

? ? ? ? ? ? ? ? ),

? ? ? ? ? ? ? ],

? ? ? ? ? ? );


? ? ? ? ? case SyncStatusType.needsDownload:

? ? ? ? ? ? return Column(

? ? ? ? ? ? ? children: [

? ? ? ? ? ? ? ? _buildActionCard(

? ? ? ? ? ? ? ? ? title: '下載恢復(fù)',

? ? ? ? ? ? ? ? ? subtitle: '從云端恢復(fù)數(shù)據(jù)(會(huì)覆蓋本地?cái)?shù)據(jù))',

? ? ? ? ? ? ? ? ? icon: Icons.cloud_download,

? ? ? ? ? ? ? ? ? color: Colors.purple,

? ? ? ? ? ? ? ? ? onTap: () => _downloadRestore(context, ref),

? ? ? ? ? ? ? ? ),

? ? ? ? ? ? ? ? SizedBox(height: 16),

? ? ? ? ? ? ? ? _buildActionCard(

? ? ? ? ? ? ? ? ? title: '強(qiáng)制上傳',

? ? ? ? ? ? ? ? ? subtitle: '用本地?cái)?shù)據(jù)覆蓋云端備份',

? ? ? ? ? ? ? ? ? icon: Icons.upload,

? ? ? ? ? ? ? ? ? color: Colors.orange,

? ? ? ? ? ? ? ? ? onTap: () => _forceUpload(context, ref),

? ? ? ? ? ? ? ? ),

? ? ? ? ? ? ? ],

? ? ? ? ? ? );


? ? ? ? ? default:

? ? ? ? ? ? return _buildSyncInfo(status);

? ? ? ? }

? ? ? },

? ? ? loading: () => Center(child: CircularProgressIndicator()),

? ? ? error: (error, _) => Text('錯(cuò)誤: $error'),

? ? );

? }

? Widget _buildActionCard({

? ? required String title,

? ? required String subtitle,

? ? required IconData icon,

? ? required Color color,

? ? required VoidCallback onTap,

? }) {

? ? return Card(

? ? ? child: ListTile(

? ? ? ? leading: CircleAvatar(

? ? ? ? ? backgroundColor: color.withOpacity(0.1),

? ? ? ? ? child: Icon(icon, color: color),

? ? ? ? ),

? ? ? ? title: Text(title),

? ? ? ? subtitle: Text(subtitle),

? ? ? ? trailing: Icon(Icons.arrow_forward_ios, size: 16),

? ? ? ? onTap: onTap,

? ? ? ),

? ? );

? }

}

最佳實(shí)踐總結(jié)

1. 架構(gòu)設(shè)計(jì)原則

本地優(yōu)先:確保應(yīng)用離線可用

漸進(jìn)式同步:支持部分同步,不影響核心功能

狀態(tài)透明:讓用戶清楚了解同步狀態(tài)

2. 安全性考慮

端到端加密:敏感數(shù)據(jù)客戶端加密

密鑰管理:使用安全的密鑰派生和存儲(chǔ)

權(quán)限控制:確保用戶只能訪問自己的數(shù)據(jù)

3. 性能優(yōu)化

增量同步:只傳輸變更數(shù)據(jù)

壓縮上傳:減少網(wǎng)絡(luò)傳輸量

后臺(tái)同步:不影響用戶操作

4. 用戶體驗(yàn)

狀態(tài)可見:清晰的同步狀態(tài)指示

操作簡(jiǎn)單:一鍵同步,自動(dòng)處理

錯(cuò)誤友好:明確的錯(cuò)誤提示和恢復(fù)建議

實(shí)際應(yīng)用效果

在BeeCount項(xiàng)目中,Supabase云同步系統(tǒng)帶來了顯著的價(jià)值:

用戶滿意度提升:多設(shè)備無(wú)縫切換,用戶數(shù)據(jù)永不丟失

技術(shù)債務(wù)減少:基于成熟的BaaS服務(wù),減少自建后臺(tái)成本

安全性保障:端到端加密確保財(cái)務(wù)數(shù)據(jù)安全

開發(fā)效率:快速集成,專注業(yè)務(wù)邏輯開發(fā)

結(jié)語(yǔ)

Supabase作為開源的BaaS解決方案,為Flutter應(yīng)用提供了完整的后端服務(wù)能力。通過合理的架構(gòu)設(shè)計(jì)、安全的加密策略和良好的用戶體驗(yàn)設(shè)計(jì),我們可以構(gòu)建出既安全又好用的云同步功能。

BeeCount的實(shí)踐證明,選擇合適的技術(shù)棧和設(shè)計(jì)模式,能夠在保證數(shù)據(jù)安全的前提下,為用戶提供便捷的多設(shè)備同步體驗(yàn)。這對(duì)于任何需要數(shù)據(jù)同步的應(yīng)用都具有重要的參考價(jià)值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容