Construindo um App de Dramas e Reels com Flutter 3.41: Animações, UX e Deploy (Final)

Neste artigo de encerramento, focaremos na Experiência do Usuário (UX) com animações de transição cinematográficas, tela de Splash e interações diretas no player. Por fim, prepararemos o código para a publicação oficial na Google Play e App Store.

💧 1. A Tela Inicial (Splash Screen Inteligente)

Uma Splash Screen não deve ser apenas uma imagem estática; ela deve mascarar o tempo de carregamento do banco de dados (Isar) e verificar se o usuário já está logado.

Arquivo: lib/features/splash/splash_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/providers/database_providers.dart';

class SplashScreen extends ConsumerStatefulWidget {
  const SplashScreen({super.key});

  @override
  ConsumerState<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends ConsumerState<SplashScreen> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _fadeAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(vsync: this, duration: const Duration(seconds: 2));
    _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
    
    _animationController.forward();
    _initializeApp();
  }

  Future<void> _initializeApp() async {
    // 1. Garante que a animação dure pelo menos 2 segundos para efeito visual
    await Future.delayed(const Duration(seconds: 2));
    
    // 2. Inicializa o Isar e verifica o usuário
    final isar = await ref.read(isarServiceProvider).db;
    final user = await isar.userProfiles.get(1); // Busca o usuário local
    
    if (mounted) {
      if (user != null) {
        context.go('/home'); // Usuário existe, vai para o feed
      } else {
        context.go('/login'); // Precisa se registrar/logar
      }
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.surface,
      body: Center(
        child: FadeTransition(
          opacity: _fadeAnimation,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.movie_filter, size: 100, color: Colors.redAccent),
              const SizedBox(height: 24),
              Text(
                'DramaReels',
                style: Theme.of(context).textTheme.headlineLarge?.copyWith(
                  fontWeight: FontWeight.bold,
                  letterSpacing: 2.0,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

🔀 2. Proteção de Rotas com GoRouter (Redirect)

Para garantir que ninguém acesse a tela /home sem estar logado, configuramos o parâmetro redirect no GoRouter. Isso centraliza a segurança do app.

Snippet do app_router.dart:

final appRouter = GoRouter(
  initialLocation: '/splash',
  routes: [
    GoRoute(path: '/splash', builder: (context, state) => const SplashScreen()),
    GoRoute(path: '/login', builder: (context, state) => LoginScreen()),
    // ... demais rotas
  ],
);

🦸 3. Animações Avançadas: Hero Transitions

Quando o usuário clica no pôster de um Drama no grid “Descubra” ou “Minha Lista”, o pôster deve “voar” suavemente e se transformar no topo da tela de Detalhes. Isso é feito com o widget Hero.

Na tela de Origem (Grid de Filmes):

GestureDetector(
  onTap: () => context.push('/drama/${video.id}'),
  child: Hero(
    tag: 'poster_${video.id}', // A tag deve ser única por vídeo
    child: Container(
      decoration: BoxDecoration(
        image: DecorationImage(image: NetworkImage(video.thumbnailUrl), fit: BoxFit.cover),
        borderRadius: BorderRadius.circular(8),
      ),
    ),
  ),
)

Na tela de Destino (DramaDetailScreen):

SliverAppBar(
  expandedHeight: 300,
  flexibleSpace: FlexibleSpaceBar(
    background: Hero(
      tag: 'poster_${video.id}', 0// Mesma tag da origem
      child: Image.network(video.thumbnailUrl, fit: BoxFit.cover),
    ),
  ),
)

🎬 4. Transições de Página Customizadas (GoRouter)

As transições padrão do Android (deslizar de baixo para cima) podem não combinar com a fluidez de um app de vídeos. Vamos aplicar um FadeTransition global nas rotas.

Atualizando a rota no GoRouter:

GoRoute(
  path: '/drama/:id',
  pageBuilder: (context, state) {
    final videoId = int.parse(state.pathParameters['id']!);
    return CustomTransitionPage(
      key: state.pageKey,
      child: DramaDetailScreen(videoId: videoId),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        // Transição de esmaecimento suave
        return FadeTransition(opacity: animation, child: child);
      },
    );
  },
),

❤️ 5. Opção de Favoritar no Player de Vídeo

O usuário deve poder favoritar o Drama diretamente enquanto assiste, sem precisar voltar para a tela anterior. Adicionaremos um botão flutuante sobre o nosso CustomVideoPlayer.

Adição no _buildControls() do CustomVideoPlayer:

// Adicione este botão no canto superior direito do Stack do player
Positioned(
  top: 16,
  right: 16,
  child: IconButton(
    icon: Icon(
      widget.video.inMyList ? Icons.bookmark : Icons.bookmark_border,
      color: Colors.white,
      size: 32,
    ),
    onPressed: () async {
      // 1. Atualiza o banco Isar localmente
      final isar = await ref.read(isarServiceProvider).db;
      await isar.writeTxn(() async {
        widget.video.inMyList = !widget.video.inMyList;
        await isar.videoContents.put(widget.video);
      });
      
      // 2. Invalida o provider para atualizar a UI atual e a tela "Minha Lista"
      ref.invalidate(minhaListaProvider);
      
      // 3. Feedback visual
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(widget.video.inMyList ? 'Adicionado à Minha Lista' : 'Removido da Lista')),
      );
    },
  ),
)

💖 6. Feedback Visual Imersivo: Like no Double-Tap (Reels)

No TikTok/Reels, dar dois toques na tela não apenas curte o vídeo, mas exibe um coração gigante que aparece e some (Fade In/Out + Scale).

Lógica na Tela de Reels (reels_screen.dart):

bool _showHeart = false;

GestureDetector(
  onDoubleTap: () {
    setState(() => _showHeart = true);
    // Favorita no banco Isar (mesma lógica do Passo 5)
    
    // Esconde o coração após 800ms
    Future.delayed(const Duration(milliseconds: 800), () {
      if (mounted) setState(() => _showHeart = false);
    });
  },
  child: Stack(
    alignment: Alignment.center,
    children: [
      CustomVideoPlayer(url: reelUrl), // O player em si
      
      // A Animação do Coração
      if (_showHeart)
        TweenAnimationBuilder<double>(
          tween: Tween<double>(begin: 0.5, end: 1.5),
          duration: const Duration(milliseconds: 400),
          curve: Curves.elasticOut,
          builder: (context, scale, child) {
            return Transform.scale(
              scale: scale,
              child: const Icon(Icons.favorite, color: Colors.redAccent, size: 100),
            );
          },
        ),
    ],
  ),
)

🚪 7. Opção de Logout no Drawer

Para garantir o controle de sessão, adicionaremos o botão de “Sair” no nosso StreamingDrawer. O Isar é mantido intacto para uso futuro, mas limpamos o estado do provedor e redirecionamos o usuário.

Adição no StreamingDrawer:

const Divider(),
ListTile(
  leading: const Icon(Icons.logout, color: Colors.redAccent),
  title: const Text('Sair da Conta', style: TextStyle(color: Colors.redAccent)),
  onTap: () {
    // Pode-se optar por deletar o usuário do Isar:
    // final isar = await ref.read(isarServiceProvider).db;
    // await isar.writeTxn(() async => await isar.userProfiles.clear());
    
    // Volta para o Login, limpando a pilha de telas
    context.go('/login');
  },
),

📦 8. Preparativos para as Lojas: Ícones e Nomenclatura

Um app profissional precisa de ícones adequados no celular do usuário (Launchers). Usaremos o pacote flutter_launcher_icons.

No pubspec.yaml:

dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/images/app_icon.png" # Crie um ícone 1024x1024
  min_sdk_android: 21 # Requisito de apps modernos

Rode o comando no terminal: flutter pub run flutter_launcher_icons

🚀 9. Build e Deploy (Google Play e App Store)

O Flutter facilita a criação de binários otimizados. Para um app de mídia, o tamanho (App Size) e a performance da renderização de vídeo são prioridades máximas.

Para Android (Google Play – Android App Bundle): O formato AAB garante que o Google Play entregue apenas o código necessário para a arquitetura do celular do usuário (ARM, x86).

flutter build appbundle --obfuscate --split-debug-info=./debug_info

Dica: O parâmetro --obfuscate protege o código contra engenharia reversa, ocultando as chaves do Isar e lógica interna.

Para iOS (App Store – Arquivo IPA): Gere o pacote validado para subir via Transporter ou Xcode.

flutter build ipa --obfuscate --split-debug-info=./debug_info

🏁 Fim do Projeto

Finalizamos nossa gigantesca arquitetura de Streaming de Dramas e Reels!

Vocês construíram:

  • Um banco de dados Isar NoSQL ultrarrápido para carregamento de catálogo e cache de “Minha Lista”.

  • Roteamento profundo e animações Hero com GoRouter.

  • Gestão de estado reativa de altíssimo nível com Riverpod, garantindo atualizações de UI sem setState desnecessários.

  • Um Player de Vídeo Customizado, com suporte a offline (arquivos locais), controles táteis e dupla-interação (Double Tap para like e seek).

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