Flutter: Armazenamento Local

Dados sem persistência são orvalho da manhã — desaparecem quando o app fecha. O armazenamento local dá aos dados um lar no dispositivo.

📋 Pré-requisitos: Você precisa dominar o seguinte primeiro

1. O Que Você Vai Aprender


2. Uma História de Perda de Dados Offline

(1) O Problema: Dados Resetam a Cada Inicialização

Os usuários do ShopApp do Bob relatam que toda vez que abrem o app, o carrinho está vazio, os produtos favoritos desapareceram, e as configurações de tema voltam ao padrão. Isso acontece porque todos os dados existem apenas na memória — fechar o app significa perder tudo. Pior, o JWT Token também fica na memória e se perde ao trocar de página, forçando os usuários a fazer login repetidamente.

(2) A Solução com Armazenamento em Camadas

Dados diferentes têm necessidades de armazenamento diferentes: tokens precisam de criptografia, caches de produtos precisam de armazenamento estruturado, e configurações do usuário precisam apenas de pares chave-valor simples.

DART
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ Install dependency: flutter pub add flutter_secure_storage hive hive_flutter shared_preferences

// Layered storage strategy
final token = await SecureStorage.getToken();      // Encrypted
final cached = await HiveBox.getProducts();         // NoSQL
final theme = await SharedPreferences.getTheme();   // KV
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

(3) Benefício: Uso Offline + Login Sem Interrupção

Após implementar armazenamento local, Bob obtém persistência criptografada do token para login sem interrupção, cache de produtos para navegação offline, e preferências que sobrevivem a reinicializações do app.


3. Seleção de Solução de Armazenamento

100%
graph LR
    subgraph Storage Selection
        SP[SharedPreferences] --> |KV lightweight| Settings[User Settings]
        HV[Hive] --> |NoSQL| Cache[Product Cache]
        DR[Drift/SQLite] --> |SQL| Orders[Order History]
        FSS[SecureStorage] --> |Encrypted| Token[JWT Token]
    end
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
Solução Tipo de Dado Criptografado Capacidade Caso de Uso
SharedPreferences KV valores simples Pequena Configurações, flags
Hive NoSQL documentos Opcional Média Caches, objetos
Drift/SQLite SQL relacional Grande Pedidos, histórico
SecureStorage KV criptografado Pequena Tokens, chaves

4. SharedPreferences

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Preferências do usuário

DART
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ Install dependency: flutter pub add shared_preferences

class UserPreferences {
  static const _keyTheme = 'theme_mode';
  static const _keyLocale = 'locale';
  static const _keyFirstLaunch = 'first_launch';
  static const _keyCurrency = 'currency';

  static Future<ThemeMode> getTheme() async {
    final prefs = await SharedPreferences.getInstance();
    final value = prefs.getString(_keyTheme);
    return ThemeMode.values.firstWhere((m) => m.name == value, orElse: () => ThemeMode.system);
  }

  static Future<void> setTheme(ThemeMode mode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyTheme, mode.name);
  }

  static Future<bool> isFirstLaunch() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_keyFirstLaunch) ?? true;
  }

  static Future<void> setFirstLaunchDone() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_keyFirstLaunch, false);
  }

  static Future<String> getCurrency() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyCurrency) ?? 'USD';
  }

  static Future<void> setCurrency(String currency) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyCurrency, currency);
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Página de onboarding no primeiro acesso

DART
import 'package:flutter/material.dart';

// Custom class definition source:
// - UserPreferences: see Lesson 4 SharedPreferences example in this lesson

class SplashPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    _checkFirstLaunch(context);
    return const Scaffold(body: Center(child: CircularProgressIndicator()));
  }

  Future<void> _checkFirstLaunch(BuildContext context) async {
    final isFirst = await UserPreferences.isFirstLaunch();
    if (isFirst) {
      Navigator.pushReplacementNamed(context, '/onboarding');
      await UserPreferences.setFirstLaunchDone();
    } else {
      Navigator.pushReplacementNamed(context, '/home');
    }
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

5. Banco de Dados NoSQL Hive

(1) Conceitos Centrais do Hive

Conceito Descrição
Hive Instância do banco de dados
Box Similar a uma tabela, armazena pares chave-valor
TypeAdapter Serialização/desserialização de objetos personalizados
HiveObject Objeto persistente com chave gerenciada automaticamente

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Cache de produtos com Hive

DART
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';

// ⚙️ Install dependency: flutter pub add hive hive_flutter
// ⚙️ Dev dependency: flutter pub add --dev hive_generator build_runner

// Custom class definition source:
// - Product: see Lesson 11 json_serializable model (simplified version below)
/*
class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  final String category;
  const Product({required this.id, required this.name, required this.price,
    required this.imageUrl, required this.category});
}
*/

// Model with Hive adapter
@HiveType(typeId: 0)
class ProductHive extends HiveObject {
  @HiveField(0) late int id;
  @HiveField(1) late String name;
  @HiveField(2) late double price;
  @HiveField(3) late String imageUrl;
  @HiveField(4) late String category;
}

// Initialize Hive
Future<void> initHive() async {
  await Hive.initFlutter();
  Hive.registerAdapter(ProductHiveAdapter());
  await Hive.openBox<ProductHive>('products');
  await Hive.openBox('cart');
}

// Product cache repository
class ProductCache {
  static const _boxName = 'products';

  static Future<void> saveProducts(List<Product> products) async {
    final box = Hive.box<ProductHive>(_boxName);
    await box.clear();
    for (final p in products) {
      await box.put(p.id, ProductHive()
        ..id = p.id
        ..name = p.name
        ..price = p.price
        ..imageUrl = p.imageUrl
        ..category = p.category);
    }
  }

  static Future<List<Product>> getProducts() async {
    final box = Hive.box<ProductHive>(_boxName);
    if (box.isEmpty) return [];
    return box.values.map((h) => Product(
      id: h.id, name: h.name, price: h.price,
      imageUrl: h.imageUrl, category: h.category,
    )).toList();
  }

  static Future<void> clearCache() async {
    final box = Hive.box<ProductHive>(_boxName);
    await box.clear();
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Persistência do carrinho com Hive

DART
import 'package:hive/hive.dart';

// ⚙️ Install dependency: flutter pub add hive hive_flutter

// Custom class definition source:
// - CartItem: see Lesson 12 CartNotifier example

class CartStorage {
  static const _boxName = 'cart';

  static Future<void> saveCart(List<CartItem> items) async {
    final box = Hive.box(_boxName);
    await box.clear();
    await box.put('items', items.map((i) => {
      'product_id': i.product.id,
      'quantity': i.quantity,
    }).toList());
  }

  static Future<List<Map<String, dynamic>>> loadCart() async {
    final box = Hive.box(_boxName);
    final data = box.get('items');
    return data != null ? List<Map<String, dynamic>>.from(data) : [];
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

6. Drift (SQLite) Banco de Dados Relacional

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Banco de dados de pedidos com Drift

DART
import 'package:drift/drift.dart';
import 'package:drift/native.dart';

// ⚙️ Install dependency: flutter pub add drift drift_flutter sqlite3_flutter_libs
// ⚙️ Dev dependency: flutter pub add --dev drift_dev build_runner

// Table definition
class Orders extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get orderNumber => text().withLength(min: 8, max: 20)();
  RealColumn get total => real()();
  TextColumn get status => text().withDefault(const Constant('pending'))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
  TextColumn get currency => text().withDefault(const Constant('USD'))();
}

class OrderItems extends Table {
  IntColumn get id => integer().autoIncrement()();
  IntColumn get orderId => integer().references(Orders, #id)();
  TextColumn get productName => text()();
  RealColumn get price => real()();
  IntColumn get quantity => integer()();
}

// Database class
@DriftDatabase(tables: [Orders, OrderItems])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(NativeDatabase.memory());

  @override
  int get schemaVersion => 1;

  // Create order
  Future<int> createOrder(OrdersCompanion order) =>
      into(orders).insert(order);

  // Get order with items
  Future<List<OrderWithItems>> getOrderWithItems(int orderId) {
    final query = select(orders).join([
      leftOuterJoin(orderItems, orderItems.orderId.equalsExp(orders.id)),
    ])..where(orders.id.equals(orderId));
    return query.map((row) {
      final order = row.readTable(orders);
      final item = row.readTableOrNull(orderItems);
      return OrderWithItems(order: order, item: item);
    }).toList();
  }

  // Get recent orders
  Future<List<Order>> getRecentOrders() =>
      (select(orders)..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
        ..limit(20)).get();
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

7. Flutter Secure Storage

▶ Exemplo

TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

: Armazenamento criptografado de JWT Token

DART
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

// ⚙️ Install dependency: flutter pub add flutter_secure_storage

class SecureStorage {
  static const _storage = FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
  );

  static const _keyAccessToken = 'access_token';
  static const _keyRefreshToken = 'refresh_token';
  static const _keyUserId = 'user_id';

  static Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
    required String userId,
  }) async {
    await _storage.write(key: _keyAccessToken, value: accessToken);
    await _storage.write(key: _keyRefreshToken, value: refreshToken);
    await _storage.write(key: _keyUserId, value: userId);
  }

  static Future<String?> getAccessToken() =>
      _storage.read(key: _keyAccessToken);

  static Future<String?> getRefreshToken() =>
      _storage.read(key: _keyRefreshToken);

  static Future<void> clearAll() => _storage.deleteAll();

  static Future<bool> isLoggedIn() async {
    final token = await getAccessToken();
    return token != null && token.isNotEmpty;
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

8. Exemplo Completo: Camada de Armazenamento do ShopApp

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ Install dependency: flutter pub add flutter_riverpod riverpod_annotation shared_preferences
// ⚙️ Dev dependency: flutter pub add --dev riverpod_generator build_runner

// Custom class definition sources:
// - SecureStorage: see Lesson 7 Flutter Secure Storage in this lesson
// - Product: see Lesson 11 json_serializable model
// - User/AuthService: see Lesson 14 Auth Notifier
// - productRepositoryProvider: see Lesson 14 ProductRepository

// storage_provider.dart - Riverpod integration
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  throw UnimplementedError('Override in main');
});

final secureStorageProvider = Provider<SecureStorage>((ref) => SecureStorage());

final productCacheProvider = Provider<ProductCache>((ref) => ProductCache());

// Auth state with persistent token
@riverpod
class Auth extends _$Auth {
  @override
  Future<User?> build() async {
    final storage = ref.read(secureStorageProvider);
    final token = await storage.getAccessToken();
    if (token == null) return null;
    // Validate token with API
    try {
      final user = await AuthService.validateToken(token);
      return user;
    } catch (_) {
      await storage.clearAll();
      return null;
    }
  }

  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final result = await AuthService.login(email, password);
      await ref.read(secureStorageProvider).saveTokens(
        accessToken: result.accessToken,
        refreshToken: result.refreshToken,
        userId: result.user.id,
      );
      return result.user;
    });
  }

  Future<void> logout() async {
    await ref.read(secureStorageProvider).clearAll();
    state = const AsyncData(null);
  }
}

// Product with offline cache
@riverpod
class Products extends _$Products {
  @override
  Future<List<Product>> build() async {
    final cache = ref.read(productCacheProvider);
    // Try cache first
    final cached = await cache.getProducts();
    if (cached.isNotEmpty) return cached;
    // Fallback to API
    return _loadFromApi();
  }

  Future<List<Product>> _loadFromApi() async {
    final repo = ref.read(productRepositoryProvider);
    final products = await repo.getProducts();
    await ref.read(productCacheProvider).saveProducts(products);
    return products;
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _loadFromApi());
  }
}
TEXT
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.

❓ Perguntas Frequentes

P: O que acontece se SharedPreferences armazenar grandes quantidades de dados? R: SharedPreferences carrega tudo na memória de uma vez, então armazenar dados grandes desperdiça memória. Use Hive ou Drift para grandes volumes de dados.

P: Como escolher entre Hive e Drift? R: Hive é para caching e dados não estruturados (NoSQL, simples e rápido); Drift é para dados estruturados que exigem consultas complexas e relacionamentos (SQL, seguro quanto a tipos).

P: SecureStorage é seguro na Web? R: Na Web usa localStorage, que não é verdadeiramente criptografado. Dados sensíveis na Web devem ser gerenciados via sessões no backend.

P: E se o typeId do Hive for duplicado? R: O typeId deve ser globalmente único — duplicatas causam erros de desserialização. Mantenha uma tabela de alocação de typeIds.

P: Como lidar com migrações de banco de dados (upgrade de versão de schema)? R: Drift usa o callback onUpgrade para migrações. Hive usa box.deleteAndSaveFromStorage() ou conversão manual de dados.

P: Qual a relação entre Hive e Isar? R: Isar é o produto de próxima geração do autor do Hive, com melhor desempenho mas uma API diferente. Hive ainda é amplamente usado e estável.


📖 Resumo


📝 Exercícios

  1. Básico (⭐): Use SharedPreferences para persistir a seleção de modo escuro, mantendo a escolha após reiniciar o app.
  2. Intermediário (⭐⭐): Use Hive para persistir o carrinho de compras localmente — os dados do carrinho sobrevivem ao fechar e reabrir o app.
  3. Desafio (⭐⭐⭐): Implemente uma estratégia completa de cache offline: leia o cache Hive primeiro para exibição, busque da API em segundo plano, depois atualize a UI e o cache após a resposta.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%