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
Herocom GoRouter. -
Gestão de estado reativa de altíssimo nível com Riverpod, garantindo atualizações de UI sem
setStatedesnecessá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).