Arquitetura software
Ecossistema Flutter e Dart
Flutter e um framework de UI da Google para criar aplicacoes nativas para mobile, web e desktop a partir de uma unica base de codigo. Dart e a linguagem de programacao usada pelo Flutter.
Dart - A Linguagem
Caracteristicas
| Caracteristica | Descricao |
|---|---|
| Tipagem | Estatica e forte com inferencia |
| Null Safety | Seguranca contra null por padrao |
| AOT/JIT | Compilacao ahead-of-time e just-in-time |
| Async | Suporte nativo a Future e Stream |
| Isolates | Concorrencia sem compartilhamento de memoria |
Sintaxe Basica
// Variaveis e tipos
String nome = 'Kaique';
int idade = 30;
double altura = 1.75;
bool ativo = true;
var automatico = 'inferido'; // String
// Null safety
String? nullable; // Pode ser null
String nonNull = 'sempre tem valor';
// Late initialization
late String inicializadoDepois;
// Funcoes
int soma(int a, int b) => a + b;
// Named parameters
void criarUsuario({required String nome, int? idade}) {
print('Nome: $nome, Idade: ${idade ?? "N/A"}');
}
// Classes
class Usuario {
final String nome;
final String email;
int? idade;
Usuario({required this.nome, required this.email, this.idade});
// Factory constructor
factory Usuario.fromJson(Map<String, dynamic> json) {
return Usuario(
nome: json['nome'],
email: json['email'],
idade: json['idade'],
);
}
}
// Async/Await
Future<String> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com'));
return response.body;
}
// Streams
Stream<int> contagem() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}Flutter - Fundamentos
Estrutura de Projeto
meu_app/
├── lib/
│ ├── main.dart # Entry point
│ ├── app.dart # MaterialApp/CupertinoApp
│ ├── core/
│ │ ├── constants/
│ │ ├── theme/
│ │ └── utils/
│ ├── features/
│ │ ├── auth/
│ │ │ ├── data/
│ │ │ ├── domain/
│ │ │ └── presentation/
│ │ └── home/
│ └── shared/
│ ├── widgets/
│ └── services/
├── test/
├── android/
├── ios/
├── web/
├── pubspec.yaml
└── analysis_options.yamlWidgets Fundamentais
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Meu App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Bem-vindo!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DetalhesPage()),
);
},
child: const Text('Ver Detalhes'),
),
],
),
),
);
}
}StatefulWidget
class ContadorPage extends StatefulWidget {
const ContadorPage({super.key});
@override
State<ContadorPage> createState() => _ContadorPageState();
}
class _ContadorPageState extends State<ContadorPage> {
int _contador = 0;
void _incrementar() {
setState(() {
_contador++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Contador')),
body: Center(
child: Text(
'$_contador',
style: Theme.of(context).textTheme.headlineLarge,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementar,
child: const Icon(Icons.add),
),
);
}
}Gerenciamento de Estado
Provider
// pubspec.yaml: provider: ^6.0.0
// Model
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
int get totalItems => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void add(Item item) {
_items.add(item);
notifyListeners();
}
void remove(Item item) {
_items.remove(item);
notifyListeners();
}
}
// Setup
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
// Consumir
class CartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total: ${cart.totalItems} items');
},
);
}
}
// Ou usando context.watch/read
class CartButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Badge(
label: Text('${cart.totalItems}'),
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () => context.read<CartModel>().add(item),
),
);
}
}Riverpod
// pubspec.yaml: flutter_riverpod: ^2.0.0
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Providers
final contadorProvider = StateProvider<int>((ref) => 0);
final usuarioProvider = FutureProvider<Usuario>((ref) async {
final response = await http.get(Uri.parse('/api/usuario'));
return Usuario.fromJson(jsonDecode(response.body));
});
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);
void add(Todo todo) {
state = [...state, todo];
}
void remove(String id) {
state = state.where((todo) => todo.id != id).toList();
}
void toggle(String id) {
state = state.map((todo) {
if (todo.id == id) {
return todo.copyWith(completed: !todo.completed);
}
return todo;
}).toList();
}
}
// Setup
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
// Consumir
class ContadorPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final contador = ref.watch(contadorProvider);
return Scaffold(
body: Center(child: Text('$contador')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(contadorProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
// Async data
class UsuarioPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usuarioAsync = ref.watch(usuarioProvider);
return usuarioAsync.when(
data: (usuario) => Text(usuario.nome),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Erro: $err'),
);
}
}BLoC
// pubspec.yaml: flutter_bloc: ^8.0.0
import 'package:flutter_bloc/flutter_bloc.dart';
// Events
abstract class ContadorEvent {}
class Incrementar extends ContadorEvent {}
class Decrementar extends ContadorEvent {}
// State
class ContadorState {
final int valor;
ContadorState(this.valor);
}
// Bloc
class ContadorBloc extends Bloc<ContadorEvent, ContadorState> {
ContadorBloc() : super(ContadorState(0)) {
on<Incrementar>((event, emit) {
emit(ContadorState(state.valor + 1));
});
on<Decrementar>((event, emit) {
emit(ContadorState(state.valor - 1));
});
}
}
// Cubit (simplificado)
class ContadorCubit extends Cubit<int> {
ContadorCubit() : super(0);
void incrementar() => emit(state + 1);
void decrementar() => emit(state - 1);
}
// Setup
void main() {
runApp(
BlocProvider(
create: (_) => ContadorCubit(),
child: const MyApp(),
),
);
}
// Consumir
class ContadorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<ContadorCubit, int>(
builder: (context, count) => Text('$count'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<ContadorCubit>().incrementar(),
child: const Icon(Icons.add),
),
);
}
}Navegacao
Go Router
// pubspec.yaml: go_router: ^12.0.0
import 'package:go_router/go_router.dart';
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'detalhes/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetalhesPage(id: id);
},
),
],
),
GoRoute(
path: '/perfil',
builder: (context, state) => const PerfilPage(),
),
ShellRoute(
builder: (context, state, child) => MainLayout(child: child),
routes: [
GoRoute(path: '/home', builder: (_, __) => const HomePage()),
GoRoute(path: '/busca', builder: (_, __) => const BuscaPage()),
GoRoute(path: '/config', builder: (_, __) => const ConfigPage()),
],
),
],
redirect: (context, state) {
final isLoggedIn = authService.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/';
return null;
},
);
// Uso
void main() {
runApp(MaterialApp.router(routerConfig: router));
}
// Navegar
context.go('/detalhes/123');
context.push('/perfil');
context.pop();HTTP e APIs
Dio
// pubspec.yaml: dio: ^5.0.0
import 'package:dio/dio.dart';
class ApiClient {
late final Dio _dio;
ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
));
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = authService.token;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
authService.logout();
}
return handler.next(error);
},
));
}
Future<List<Usuario>> getUsuarios() async {
final response = await _dio.get('/usuarios');
return (response.data as List)
.map((json) => Usuario.fromJson(json))
.toList();
}
Future<Usuario> createUsuario(Usuario usuario) async {
final response = await _dio.post('/usuarios', data: usuario.toJson());
return Usuario.fromJson(response.data);
}
}Retrofit
// pubspec.yaml:
// retrofit: ^4.0.0
// retrofit_generator: ^8.0.0 (dev)
// build_runner: ^2.0.0 (dev)
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
part 'api_service.g.dart';
@RestApi(baseUrl: 'https://api.example.com')
abstract class ApiService {
factory ApiService(Dio dio, {String baseUrl}) = _ApiService;
@GET('/usuarios')
Future<List<Usuario>> getUsuarios();
@GET('/usuarios/{id}')
Future<Usuario> getUsuario(@Path('id') String id);
@POST('/usuarios')
Future<Usuario> createUsuario(@Body() Usuario usuario);
@PUT('/usuarios/{id}')
Future<Usuario> updateUsuario(@Path('id') String id, @Body() Usuario usuario);
@DELETE('/usuarios/{id}')
Future<void> deleteUsuario(@Path('id') String id);
}Armazenamento Local
Hive
// pubspec.yaml:
// hive: ^2.0.0
// hive_flutter: ^1.0.0
// hive_generator: ^2.0.0 (dev)
import 'package:hive_flutter/hive_flutter.dart';
part 'usuario.g.dart';
@HiveType(typeId: 0)
class Usuario extends HiveObject {
@HiveField(0)
String nome;
@HiveField(1)
String email;
Usuario({required this.nome, required this.email});
}
// Inicializar
Future<void> main() async {
await Hive.initFlutter();
Hive.registerAdapter(UsuarioAdapter());
await Hive.openBox<Usuario>('usuarios');
runApp(const MyApp());
}
// Usar
class UsuarioRepository {
final Box<Usuario> _box = Hive.box<Usuario>('usuarios');
List<Usuario> getAll() => _box.values.toList();
Future<void> add(Usuario usuario) => _box.add(usuario);
Future<void> update(int index, Usuario usuario) => _box.putAt(index, usuario);
Future<void> delete(int index) => _box.deleteAt(index);
}SharedPreferences
// pubspec.yaml: shared_preferences: ^2.0.0
import 'package:shared_preferences/shared_preferences.dart';
class PreferencesService {
late SharedPreferences _prefs;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
String? get token => _prefs.getString('token');
set token(String? value) {
if (value != null) {
_prefs.setString('token', value);
} else {
_prefs.remove('token');
}
}
bool get isDarkMode => _prefs.getBool('darkMode') ?? false;
set isDarkMode(bool value) => _prefs.setBool('darkMode', value);
String get locale => _prefs.getString('locale') ?? 'pt_BR';
set locale(String value) => _prefs.setString('locale', value);
}Testes
Unit Tests
// test/usuario_test.dart
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Usuario', () {
test('deve criar usuario a partir de JSON', () {
final json = {'nome': 'Kaique', 'email': 'kaique@email.com'};
final usuario = Usuario.fromJson(json);
expect(usuario.nome, 'Kaique');
expect(usuario.email, 'kaique@email.com');
});
test('deve converter usuario para JSON', () {
final usuario = Usuario(nome: 'Kaique', email: 'kaique@email.com');
final json = usuario.toJson();
expect(json['nome'], 'Kaique');
expect(json['email'], 'kaique@email.com');
});
});
}Widget Tests
// test/contador_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Contador deve incrementar', (tester) async {
await tester.pumpWidget(const MaterialApp(home: ContadorPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
testWidgets('Deve exibir lista de usuarios', (tester) async {
final usuarios = [
Usuario(nome: 'User 1', email: 'user1@email.com'),
Usuario(nome: 'User 2', email: 'user2@email.com'),
];
await tester.pumpWidget(
MaterialApp(home: UsuariosPage(usuarios: usuarios)),
);
expect(find.text('User 1'), findsOneWidget);
expect(find.text('User 2'), findsOneWidget);
});
}Integration Tests
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Fluxo completo de login', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.pumpAndSettle();
// Digitar credenciais
await tester.enterText(find.byKey(const Key('email')), 'test@email.com');
await tester.enterText(find.byKey(const Key('senha')), '123456');
// Fazer login
await tester.tap(find.byKey(const Key('btnLogin')));
await tester.pumpAndSettle();
// Verificar navegacao para home
expect(find.text('Bem-vindo'), findsOneWidget);
});
}Pacotes Essenciais
| Categoria | Pacote | Descricao |
|---|---|---|
| Estado | riverpod, bloc, provider | Gerenciamento de estado |
| Navegacao | go_router, auto_route | Roteamento declarativo |
| HTTP | dio, retrofit | Cliente HTTP |
| Storage | hive, shared_preferences | Armazenamento local |
| Injecao | get_it, injectable | Dependency injection |
| Freezed | freezed, json_serializable | Code generation |
| UI | flutter_hooks, animations | Utilitarios de UI |
| Forms | reactive_forms, flutter_form_builder | Formularios |
| Imagens | cached_network_image | Cache de imagens |
| Notificacoes | firebase_messaging, flutter_local_notifications | Push notifications |
Recursos
- Flutter Documentation
- Dart Documentation
- Pub.dev - Repositorio de pacotes
- Flutter Gems - Pacotes curados
- Riverpod
- BLoC Library