Construindo um App de Dramas e Reels com Flutter 3.41: Isar Community, Slivers e UI Imersiva (Parte 1)

Construir um aplicativo de Dramas (Séries/Filmes) e Reels (Vídeos Curtos) exige uma arquitetura de alta performance. Precisamos renderizar capas de vídeos rapidamente, gerenciar listas horizontais infinitas e armazenar o histórico do usuário. Para isso, o Isar Community (um banco NoSQL local ultrarrápido) é a escolha perfeita.

🏗️ 1. Arquitetura Orientada a Recursos (Feature-First)

Para um app de streaming que cresce rapidamente, separar por tipos técnicos (models, views, controllers) gera confusão. Vamos adotar a arquitetura Feature-First.

lib/
├── core/
│   ├── database/    # Instância do Isar Community e Exportação
│   └── theme/       # Material 3 Dark Theme
├── features/
│   ├── home/        # UI da Home e Slivers
│   ├── dramas/      # Lógica de vídeos longos e Histórico
│   ├── reels/       # Lógica de vídeos curtos verticais
│   └── profile/     # Drawer e Gestão do Usuário
└── main.dart

📦 2. Gerenciamento de Dependências

O Isar Community é uma versão mantida pela comunidade baseada no poderoso banco NoSQL Isar, garantindo estabilidade e atualizações frequentes.

Adicione ao seu pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  isar: ^3.1.0 # Versão estável (Community fork)
  isar_flutter_libs: ^3.1.0
  path_provider: ^2.1.2
  share_plus: ^7.2.0 # Para exportar o banco
  google_fonts: ^6.1.0

dev_dependencies:
  build_runner: ^2.4.8
  isar_generator: ^3.1.0

🧬 3. Modelagem de Dados NoSQL (O Schema do Isar)

No Isar, pensamos em “Documentos” em vez de tabelas. Vamos criar um modelo unificado para os vídeos, diferenciando Dramas de Reels por uma flag, e indexando propriedades cruciais para buscas rápidas nas listas horizontais.

Crie lib/features/dramas/models/video_content.dart:

import 'package:isar/isar.dart';

part 'video_content.g.dart'; // Arquivo gerado pelo build_runner

@collection
class VideoContent {
  Id id = Isar.autoIncrement;

  @Index(type: IndexType.value)
  late String title;
  
  late String thumbnailUrl;
  late String videoUrl;
  
  @Index()
  late String category; // Ex: K-Drama, Ação, Romance

  @Index()
  bool isReel = false; // Define se é um vídeo curto (9:16) ou Drama (16:9)

  @Index()
  bool inMyList = false; // Para a "Minha Lista"

  DateTime? lastWatched; // Para a lista de "Histórico"
}

(Não esqueça de rodar: dart run build_runner build no terminal para gerar o código do Isar).

👤 4. Modelagem do Perfil do Usuário

O usuário atual também será salvo no Isar para carregamento offline imediato.

Crie lib/features/profile/models/user_profile.dart:

import 'package:isar/isar.dart';

part 'user_profile.g.dart';

@collection
class UserProfile {
  Id id = 1; // Usaremos ID fixo pois é single-user local
  late String name;
  late String email;
  late String avatarUrl;
}

🗄️ 5. Inicialização do Isar e Injeção de Dependência

Precisamos abrir o banco de dados antes da UI ser renderizada. O Isar é síncrono para leituras, o que o torna incrivelmente rápido para carregar listas de filmes.

Crie lib/core/database/isar_service.dart:

import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import '../../features/dramas/models/video_content.dart';
import '../../features/profile/models/user_profile.dart';

class IsarService {
  late Future<Isar> db;

  IsarService() {
    db = _initDB();
  }

  Future<Isar> _initDB() async {
    final dir = await getApplicationDocumentsDirectory();
    if (Isar.instanceNames.isEmpty) {
      return await Isar.open(
        [VideoContentSchema, UserProfileSchema], // Schemas gerados
        directory: dir.path,
        inspector: true, // Permite ver o banco no navegador durante o debug
      );
    }
    return Future.value(Isar.getInstance());
  }
}

📤 6. Engenharia de Dados: Exportação do Banco Isar

Para que o usuário possa fazer backup dos seus favoritos (“Minha Lista”) e histórico, criamos uma função para exportar o arquivo físico .isar.

Adicione no IsarService:

import 'dart:io';
import 'package:share_plus/share_plus.dart';

Future<void> exportDatabase() async {
  final dir = await getApplicationDocumentsDirectory();
  // O Isar salva o arquivo como default.isar por padrão
  final dbPath = '${dir.path}/default.isar'; 
  final file = File(dbPath);

  if (await file.exists()) {
    await Share.shareXFiles(
      [XFile(dbPath)], 
      text: 'Backup do meu catálogo de Dramas e Reels'
    );
  } else {
    throw Exception('Arquivo de banco de dados não encontrado.');
  }
}

🎨 7. Design System: Tema Escuro para Streaming (M3)

Apps de vídeo (como Netflix ou TikTok) exigem temas escuros profundos para destacar as capas dos vídeos (Thumbnails).

Atualize o lib/main.dart:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'features/home/home_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Inicialização do BLoC ou Isar viria aqui
  runApp(const StreamingApp());
}

class StreamingApp extends StatelessWidget {
  const StreamingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DramaReels',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFE50914), // Vermelho Streaming
          brightness: Brightness.dark,
          surface: const Color(0xFF0A0A0A), // Fundo quase super preto
        ),
        textTheme: GoogleFonts.poppinsTextTheme(ThemeData.dark().textTheme),
      ),
      home: const HomeScreen(),
      debugShowCheckedModeBanner: false,
    );
  }
}

🏠 8. Estruturando a Home Screen com Slivers

Uma interface de streaming é complexa. Temos um cabeçalho que esconde ao rolar, listas horizontais (Filmes) e grids (Reels). Usar ListView aninhada causa erros de performance. A solução profissional no Flutter é o CustomScrollView com Slivers.

Crie lib/features/home/home_screen.dart:

import 'package:flutter/material.dart';
import 'widgets/horizontal_video_list.dart'; // Criaremos no próximo passo
import 'widgets/streaming_drawer.dart'; // Criaremos no próximo passo

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const StreamingDrawer(),
      body: CustomScrollView(
        slivers: [
          // AppBar Translúcida e Flutuante
          SliverAppBar(
            floating: true,
            pinned: false,
            backgroundColor: Colors.black.withOpacity(0.7),
            title: const Text('DramaReels', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.redAccent)),
            actions: [
              IconButton(icon: const Icon(Icons.search), onPressed: () {}),
            ],
          ),

          // 1. Destaque Principal (Hero Banner)
          SliverToBoxAdapter(
            child: Container(
              height: 300,
              decoration: const BoxDecoration(
                image: DecorationImage(
                  image: NetworkImage('https://placeholder.com/hero_drama.jpg'),
                  fit: BoxFit.cover,
                ),
              ),
              child: const Align(
                alignment: Alignment.bottomCenter,
                child: Padding(
                  padding: EdgeInsets.all(16.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      Text('Série', style: TextStyle(fontWeight: FontWeight.bold)),
                      Text('•'),
                      Text('Romance'),
                    ],
                  ),
                ),
              ),
            ),
          ),

          // 2. Minha Lista
          const SliverToBoxAdapter(child: HorizontalVideoList(title: 'Minha Lista', isReels: false)),
          
          // 3. Reels (Vídeos Curtos em Formato Retrato)
          const SliverToBoxAdapter(child: HorizontalVideoList(title: 'Reels e Shorts', isReels: true)),

          // 4. Histórico (Continue Assistindo)
          const SliverToBoxAdapter(child: HorizontalVideoList(title: 'Continue Assistindo', isReels: false)),

          // 5. Categorias Populares
          const SliverToBoxAdapter(child: HorizontalVideoList(title: 'K-Dramas em Alta', isReels: false)),

          const SliverToBoxAdapter(child: SizedBox(height: 40)), // Espaçamento final
        ],
      ),
    );
  }
}

📱 9. Componentização e Menu Drawer

Para fechar nossa UI inicial, criaremos o componente genérico de lista horizontal e o menu lateral do usuário.

Lista Horizontal (lib/features/home/widgets/horizontal_video_list.dart):

class HorizontalVideoList extends StatelessWidget {
  final String title;
  final bool isReels;

  const HorizontalVideoList({super.key, required this.title, this.isReels = false});

  @override
  Widget build(BuildContext context) {
    // Proporção: Reels são mais esticados verticalmente (9:16), Dramas são 16:9 ou pôsteres 2:3.
    final double cardHeight = isReels ? 250 : 160;
    final double cardWidth = isReels ? 140 : 110;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
          child: Text(title, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
        ),
        SizedBox(
          height: cardHeight,
          child: ListView.builder(
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.symmetric(horizontal: 12),
            itemCount: 10, // Mock: viria do Isar
            itemBuilder: (context, index) {
              return Container(
                width: cardWidth,
                margin: const EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.surfaceContainerHighest,
                  borderRadius: BorderRadius.circular(8),
                  image: const DecorationImage(
                    image: NetworkImage('https://via.placeholder.com/300x450'), // Placeholder
                    fit: BoxFit.cover,
                  ),
                ),
                child: isReels 
                  ? const Align(alignment: Alignment.bottomLeft, child: Padding(padding: EdgeInsets.all(8), child: Icon(Icons.play_arrow_outlined)))
                  : null,
              );
            },
          ),
        ),
      ],
    );
  }
}

Navigation Drawer M3 (lib/features/home/widgets/streaming_drawer.dart):

class StreamingDrawer extends StatelessWidget {
  const StreamingDrawer({super.key});

  @override
  Widget build(BuildContext context) {
    return NavigationDrawer(
      children: [
        Padding(
          padding: const EdgeInsets.fromLTRB(28, 40, 16, 20),
          child: Row(
            children: [
              const CircleAvatar(radius: 30, backgroundImage: NetworkImage('https://via.placeholder.com/150')),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text('Nome do Usuário', style: Theme.of(context).textTheme.titleMedium),
                    Text('Editar Perfil', style: TextStyle(color: Theme.of(context).colorScheme.primary, fontSize: 12)),
                  ],
                ),
              ),
            ],
          ),
        ),
        const Divider(),
        const NavigationDrawerDestination(icon: Icon(Icons.download_done), label: Text('Meus Downloads')),
        const NavigationDrawerDestination(icon: Icon(Icons.settings), label: Text('Configurações do App')),
        const Divider(),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: FilledButton.tonalIcon(
            onPressed: () {
              // Chama a função de exportação do IsarService
              // IsarService().exportDatabase();
            },
            icon: const Icon(Icons.backup),
            label: const Text('Exportar Banco de Dados'),
          ),
        ),
      ],
    );
  }
}

✅ Conclusão

Nesta primeira etapa, erguemos uma infraestrutura colossal.

  1. Escolhemos o Isar Community para garantir consultas em milissegundos nas nossas futuras milhares de mídias.

  2. Desenhamos a Home com Slivers, essencial para a fluidez (60/120fps) em listas horizontais dinâmicas.

  3. Consolidamos o Material 3 com um tema imersivo.

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20