Componentes Principais
Visão Geral
O sistema é composto por 5 componentes principais que trabalham em conjunto para fornecer a funcionalidade completa do editor visual de JSON.
MyApp
Arquivo: lib/main.dart
Responsabilidade: Ponto de entrada e configuração global da aplicação.
Implementação
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'JSON Canvas Viewer',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: EditorTheme.backgroundColor,
colorScheme: const ColorScheme.dark(
surface: EditorTheme.surfaceColor,
primary: EditorTheme.textColor,
onSurface: EditorTheme.textColor,
),
),
home: const HomePage(),
);
}
}
Características
- Tema dark por padrão
- ColorScheme customizado para consistência
- Remove banner de debug
- Configuração global de cores
HomePage
Arquivo: lib/src/modules/home/ui/home_page.dart
Responsabilidade: Coordenador principal, gerencia estado compartilhado e comunicação entre editor e canvas.
Estado Gerenciado
class _HomePageState extends State<HomePage> {
String _jsonData = ''; // JSON atual
bool _isValid = false; // Validação
bool _isUpdatingFromCanvas = false; // Flag anti-loop
}
Métodos Principais
onJsonChanged
Recebe atualizações do editor:
void _onJsonChanged(String jsonData) {
if (_isUpdatingFromCanvas) return; // Previne loop
Future.microtask(() {
if (mounted) {
setState(() {
_jsonData = jsonData;
});
}
});
}
onValidationChanged
Recebe status de validação:
void _onValidationChanged(bool isValid) {
Future.microtask(() {
if (mounted) {
setState(() {
_isValid = isValid;
});
}
});
}
Layout
Row(
children: [
Expanded(
flex: 1,
child: JsonEditorWidget(
onJsonChanged: _onJsonChanged,
onValidationChanged: _onValidationChanged,
externalJsonData: _jsonData.isNotEmpty ? _jsonData : null,
),
),
Expanded(
flex: 1,
child: CanvasViewerWidget(
jsonData: _jsonData,
isValid: _isValid,
onJsonUpdated: (updatedJson) {
// Atualização do canvas
},
),
),
],
)
JsonEditorWidget
Arquivo: lib/src/modules/home/ui/widgets/json_editor_widget.dart
Responsabilidade: Editor de código JSON com validação em tempo real.
Estado Interno
class _JsonEditorWidgetState extends State<JsonEditorWidget> {
late CodeController _codeController;
String _errorMessage = '';
bool _isValid = false;
bool _isUpdatingFromExternal = false;
String _lastExternalData = '';
Timer? _debounceTimer;
}
Debouncing
Implementa timer de 300ms para otimizar performance:
void _onCodeChanged() {
if (_isUpdatingFromExternal) return;
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
final text = _codeController.text;
widget.onJsonChanged(text);
_validateJson(text);
});
}
Validação JSON
void _validateJson(String jsonString) {
try {
if (jsonString.trim().isEmpty) {
setState(() {
_errorMessage = '';
_isValid = false;
});
widget.onValidationChanged(false);
return;
}
const JsonDecoder().convert(jsonString);
setState(() {
_errorMessage = '';
_isValid = true;
});
widget.onValidationChanged(true);
} catch (e) {
setState(() {
_errorMessage = 'Invalid JSON: ${e.toString()}';
_isValid = false;
});
widget.onValidationChanged(false);
}
}
JSON Inicial
Fornece exemplo funcional:
String _getInitialJson() {
return '''{
"canvas": {
"width": 360,
"height": 640,
"backgroundColor": "#F8F9FA"
},
"elements": [
{
"type": "rect",
"x": "center",
"y": 100,
"width": 280,
"height": 200,
"color": "#FFFFFF",
"borderRadius": 20,
"zIndex": 1
}
]
}''';
}
Interface
- Header com título e ícone
- Indicador de validação (check/error)
- Mensagem de erro quando inválido
- Editor com TextField estilizado
CanvasViewerWidget
Arquivo: lib/src/modules/home/ui/widgets/canvas_viewer_widget.dart
Responsabilidade: Visualização do canvas e detecção de estrutura JSON.
Detecção de Canvas
try {
final decoded = const JsonDecoder().convert(widget.jsonData);
if (decoded.containsKey('canvas') || decoded.containsKey('elements')) {
// Usa JsonCanvasViewer
return JsonCanvasViewer(
jsonString: widget.jsonData,
onElementMoved: _updateJsonOnElementMove,
);
}
} catch (e) {
// Fall back para code viewer
}
Atualização de Elementos
Callback chamado quando elemento é movido/redimensionado:
void _updateJsonOnElementMove(
int elementIndex,
double newX,
double newY,
{double? newWidth, double? newHeight}
) {
try {
final decoded = const JsonDecoder().convert(widget.jsonData);
final elements = decoded['elements'] as List<dynamic>;
if (elementIndex < elements.length) {
final element = elements[elementIndex] as Map<String, dynamic>;
element['x'] = newX;
element['y'] = newY;
if (newWidth != null) element['width'] = newWidth;
if (newHeight != null) element['height'] = newHeight;
const encoder = JsonEncoder.withIndent(' ');
final updatedJson = encoder.convert(decoded);
Future.microtask(() {
widget.onJsonUpdated?.call(updatedJson);
});
}
} catch (e) {
debugPrint('Error updating JSON: $e');
}
}
Interface
- Header com indicador de status
- Container principal com canvas ou placeholder
- Placeholder para JSON inválido
- Code viewer para JSON não-canvas
JsonCanvasViewer
Arquivo: lib/src/shared/jsonWidget/json_canvas_widget.dart
Responsabilidade: Motor de renderização do canvas. Parse JSON, calcula dimensões, renderiza elementos e gerencia interações.
Estado Principal
class _JsonCanvasViewerState extends State<JsonCanvasViewer> {
final _canvasKey = GlobalKey();
double canvasWidth = 360;
double canvasHeight = 640;
double designWidth = 1080;
double designHeight = 1920;
Map<String, dynamic>? parsed;
List<Map<String, dynamic>> elements = [];
// Drag state
int? _draggedElementIndex;
Offset? _dragStartPosition;
bool _isDragging = false;
// Resize state
int? _resizingElementIndex;
Offset? _resizeStartPosition;
bool _isResizing = false;
String? _resizeHandle;
}
Parse e Setup
void _parseAndSetupJson() {
try {
parsed = parseJson(widget.jsonString);
// Extrai dimensões
final canvas = parsed!['canvas'] as Map<String, dynamic>?;
designWidth = (canvas?['exportWidth'] as num?)?.toDouble() ?? 1080;
designHeight = (canvas?['exportHeight'] as num?)?.toDouble() ?? 1920;
// Calcula tamanho do canvas
final jsonWidth = (canvas?['width'] as num?)?.toDouble();
final jsonHeight = (canvas?['height'] as num?)?.toDouble();
if (widget.width != null && widget.height != null) {
canvasWidth = widget.width!;
canvasHeight = widget.height!;
} else if (jsonWidth != null && jsonHeight != null) {
final scale = min(min(800.0 / jsonWidth, 600.0 / jsonHeight), 1.0);
canvasWidth = jsonWidth * scale;
canvasHeight = jsonHeight * scale;
}
// Ordena por zIndex
elements = List<Map<String, dynamic>>.from(parsed!['elements']);
elements.sort((a, b) => (a['zIndex'] ?? 0).compareTo(b['zIndex'] ?? 0));
} catch (e) {
debugPrint('Error parsing JSON: $e');
}
}
Sistema de Escala
double _getElementScaleFactor() {
return math.min(canvasWidth / designWidth, canvasHeight / designHeight);
}
Parsing de Cores
Color _parseColor(String hexColor) {
hexColor = hexColor.replaceAll('#', '');
if (hexColor.length == 6) hexColor = 'FF$hexColor';
return Color(int.parse('0x$hexColor'));
}
Peso de Fonte
FontWeight _getFontWeight(String? weightName) {
if (weightName == null) return FontWeight.normal;
const weightMap = {
'thin': FontWeight.w100,
'light': FontWeight.w300,
'normal': FontWeight.w400,
'medium': FontWeight.w500,
'semi-bold': FontWeight.w600,
'bold': FontWeight.w700,
'extra-bold': FontWeight.w800,
'black': FontWeight.w900,
};
return weightMap[weightName.toLowerCase()] ?? FontWeight.normal;
}
Renderização
Widget build(BuildContext context) {
return Container(
width: canvasWidth,
height: canvasHeight,
decoration: BoxDecoration(
color: _parseColor(canvasBackground),
border: Border.all(color: Colors.grey.shade300),
),
child: Stack(
fit: StackFit.expand,
children: elements.asMap().entries.map((entry) {
final index = entry.key;
final el = entry.value;
// Renderiza baseado no tipo
Widget elementWidget = _renderElement(el);
// Envolve com gestos
return _wrapWithDragGesture(elementWidget, index);
}).toList(),
),
);
}
Lifecycle
initState
: Chama_parseAndSetupJson()
didUpdateWidget
: Re-parse se JSON mudoubuild
: Renderiza canvas e elementos
ApplyRotation
Arquivo: lib/src/shared/jsonWidget/json_canvas_widget.dart
Responsabilidade: Aplica transformação de rotação a widgets.
Implementação
class ApplyRotation extends StatelessWidget {
const ApplyRotation({
super.key,
required this.child,
this.rotation
});
final Widget child;
final double? rotation;
@override
Widget build(BuildContext context) {
if (rotation == null || rotation == 0) return child;
return Transform.rotate(
angle: rotation! * (math.pi / 180), // Converte graus para radianos
child: child
);
}
}
Uso
ApplyRotation(
rotation: element['rotation'],
child: Container(...),
)
Comunicação entre Componentes
graph LR
HP[HomePage]
JE[JsonEditor]
CV[CanvasViewer]
JCV[JsonCanvasViewer]
JE -->|onJsonChanged| HP
JE -->|onValidationChanged| HP
HP -->|jsonData| CV
HP -->|isValid| CV
HP -->|externalJsonData| JE
CV -->|onJsonUpdated| HP
CV -->|jsonString| JCV
JCV -->|onElementMoved| CV
Próximos Passos
- Veja Renderização para detalhes de renderização
- Explore Interatividade para gestos
- Confira JSON Spec para formato de dados