appxiomcoreplugin 1.0.1
appxiomcoreplugin: ^1.0.1 copied to clipboard
Detect and report bugs in Flutter based mobile apps. Reports issues like memory leaks, crashes, ANR and exceptions. Plugin has low memory and size footprint.
example/lib/main.dart
import 'dart:convert';
import 'package:appxiomcoreplugin/observe.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:appxiomcoreplugin/appxiomcoreplugin.dart';
import 'package:appxiomcoreplugin/state_custom.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized(); // Must be called first
Ax.init("<android_app_key>",
"<android_platform_key>"); //Get appropriate keys from Appxiom dashboard.
Ax.initIOS(); //Add app_id and platform_id to info.plist. Get appropriate keys from Appxiom dashboard.
Observe().setUrlPatterns([ //To Make sure URLs with dynamic segments are grouped together as a single ticket.
'/users/{id}',
'/posts/{id}'
]);
Observe().setMaskedHeaders([ //To mask sensitive headers in both requests and responses from being reported to Appxiom dashboard.
'X-API-KEY'
]);
// Run the app last
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Memory Leak Detection Test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomeScreen(),
);
}
}
/// Home screen with navigation to test screens
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends AxInitialState<HomeScreen> {
String _apiResult = 'No API call made yet';
bool _isLoading = false;
// Goal tracking variables
int? _currentGoalId;
String _goalStatus = 'No goal started';
final List<Map<String, dynamic>> _completedGoals = [];
// Activity markers variables
int _markerCount = 5;
String _markerStatus = 'No markers set yet';
Future<void> _makeApiCall() async {
setState(() {
_isLoading = true;
_apiResult = 'Loading...';
});
try {
// This will be automatically tracked by Appxiom using HttpInterceptor
final response = await AxHttpInterceptor.get(
Uri.parse('<url>'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
json.decode(response.body);
setState(() {
_apiResult = 'Success! (Tracked by HttpInterceptor.get)';
});
} else {
setState(() {
_apiResult =
'Error: ${response.statusCode} - ${response.reasonPhrase}';
});
}
} catch (e) {
setState(() {
_apiResult = 'Exception occurred: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _makeTrackedApiCall() async {
setState(() {
_isLoading = true;
_apiResult = 'Loading with tracked client...';
});
try {
// Use the tracked client to ensure API tracking
final client = AxHttpInterceptor.createAxClient();
final response = await client.get(
Uri.parse('<url>'),
headers: {'Content-Type': 'application/json'},
);
client.close();
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_apiResult = 'Success with TrackedClient!\n\n'
'ID: ${data['id']}\n'
'Title: ${data['title']}\n'
'Completed: ${data['completed']}\n'
'User ID: ${data['userId']}';
});
} else {
setState(() {
_apiResult =
'Error: ${response.statusCode} - ${response.reasonPhrase}';
});
}
} catch (e) {
setState(() {
_apiResult = 'Exception occurred: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
void _reportTestIssue() {
//Give a descirption and detailed description for the issue being reported
Ax.reportIssue("main", 'Payment attempt failed',
'User attempted to make a payment but it failed due to incorrect production credentials.');
}
// POST API call with random JSON
Future<void> _makePostApiCall() async {
setState(() {
_isLoading = true;
_apiResult = 'Making POST request...';
});
try {
// Generate random JSON data
final randomData = {'title': 'title', 'body': 'body'};
final response = await AxHttpInterceptor.post(
Uri.parse('<url>'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
},
body: json.encode(randomData),
);
if (response.statusCode == 201 || response.statusCode == 200) {
json.decode(response.body);
setState(() {
_apiResult = 'POST Success! (Tracked by HttpInterceptor.post)\n\n'
'Status: ${response.statusCode}';
});
} else {
setState(() {
_apiResult =
'POST Error: ${response.statusCode} - ${response.reasonPhrase}';
});
}
} catch (e) {
setState(() {
_apiResult = 'POST Exception occurred: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
// URL Pattern Testing Methods
Future<void> _testTodosPattern1() async {
try {
await AxHttpInterceptor.get(
Uri.parse('<url>'),
);
} catch (e) {
debugPrint('Error: $e');
}
}
Future<void> _testTodosPattern2() async {
try {
await AxHttpInterceptor.get(
Uri.parse('<url>'),
);
} catch (e) {
debugPrint('Error: $e');
}
}
Future<void> _testUsersPattern() async {
try {
await AxHttpInterceptor.get(
Uri.parse('<url>'),
);
} catch (e) {
debugPrint('Error: $e');
}
}
Future<void> _testCommentsPattern() async {
try {
await AxHttpInterceptor.get(
Uri.parse('<url>'),
);
} catch (e) {
debugPrint('Error: $e');
}
}
// Goal Management Methods
Future<void> _beginGoal1() async {
try {
final goalId = await Ax.beginGoal("main", 'Sign_Up');
setState(() {
_currentGoalId = goalId;
_goalStatus =
'Goal 1 Active: Complete API Integration Test (ID: $goalId)';
});
} catch (e) {
setState(() {
_goalStatus = 'Error starting Goal 1: $e';
});
}
}
Future<void> _beginGoal2() async {
try {
final goalId = await Ax.beginGoal("main", 'Payment_Process');
setState(() {
_currentGoalId = goalId;
_goalStatus = 'Goal 2 Active: Test URL Pattern Matching (ID: $goalId)';
});
} catch (e) {
setState(() {
_goalStatus = 'Error starting Goal 2: $e';
});
}
}
void _completeCurrentGoal() {
if (_currentGoalId == null) {
setState(() {
_goalStatus = 'No active goal to complete';
});
return;
}
try {
Ax.completeGoal("main", _currentGoalId!);
setState(() {
_completedGoals.add({
'id': _currentGoalId,
'completedAt': DateTime.now().toIso8601String(),
});
_goalStatus =
'Goal completed! ID: $_currentGoalId (Total completed: ${_completedGoals.length})';
_currentGoalId = null;
});
} catch (e) {
setState(() {
_goalStatus = 'Error completing goal: $e';
});
}
}
// Activity Markers Method
void _setMultipleActivityMarkers() {
try {
for (int i = 1; i <= _markerCount; i++) {
Ax.setActivityMarkerAt("main", "Marker $i");
}
setState(() {
_markerStatus =
'Set $_markerCount activity markers (1 to $_markerCount)';
});
} catch (e) {
setState(() {
_markerStatus = 'Error setting markers: $e';
});
}
}
void _incrementMarkerCount() {
setState(() {
_markerCount++;
});
}
void _decrementMarkerCount() {
if (_markerCount > 1) {
setState(() {
_markerCount--;
});
}
}
// Crash Testing Functions
void _testCrashReporting() {
// This will trigger a Flutter crash that should be caught by FlutterError.onError
throw FlutterError('Null check operator on a null value');
}
void _testCustomException() {
try {
// Simulate a custom exception
throw Exception(
'Unhandled Exception: LateInitializationError: Field _userData@123451234 has not been initialized.');
} catch (error, stackTrace) {
// Report the error using Ax.reportError API
Ax.reportError(error, stackTrace);
// Show a message to user that the error was reported
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Custom exception reported via Ax.reportError API'),
backgroundColor: Colors.green,
),
);
}
}
}
@override
Widget build(BuildContext context) {
// Call ActivityTrail marker manually since super.build() throws UnimplementedError
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: const Text('Memory Leak Test App'),
backgroundColor: Colors.blue,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Test Memory Leak Detection',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
const Text(
'Navigate to different screens to test State lifecycle tracking and memory leak detection.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
_buildNavigationButton(
context,
'Simple Screen Test',
'Test basic AxState lifecycle',
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const SimpleTestScreen())),
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Timer Screen (Potential Leak)',
'Test screen with Timer that may leak',
() => Navigator.push(context,
MaterialPageRoute(builder: (_) => const TimerTestScreen())),
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Stream Screen (Potential Leak)',
'Test screen with Stream subscription',
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const StreamTestScreen())),
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Animation Screen',
'Test screen with AnimationController',
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const AnimationTestScreen())),
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Multiple Screens Rapid',
'Rapidly create/dispose multiple screens',
() => _navigateRapidScreens(context),
),
const SizedBox(height: 16),
_buildNavigationButton(
context,
'Static Reference Screen',
'Screen that adds itself to static list',
() => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const StaticReferenceScreen())),
),
const SizedBox(height: 30),
const Divider(),
const Text(
'API Testing',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _makeApiCall,
icon: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.api),
label: const Text('Interceptor API'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _makeTrackedApiCall,
icon: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.track_changes),
label: const Text('Tracked API'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
),
),
],
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _isLoading ? null : _makePostApiCall,
icon: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.send),
label: const Text('POST API with Random JSON'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _reportTestIssue,
icon: const Icon(Icons.bug_report),
label: const Text('Report Test Issue'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
const SizedBox(height: 24),
const Divider(),
const Text(
'Crash & Exception Testing',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const Text(
'Test crash reporting via FlutterError.onError and custom exceptions via Ax.reportError',
style: TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _testCrashReporting,
icon: const Icon(Icons.error_outline),
label: const Text('Test Crash'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _testCustomException,
icon: const Icon(Icons.warning),
label: const Text('Custom Error'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
),
),
],
),
const SizedBox(height: 8),
const SizedBox(height: 24),
const Text(
'URL Pattern Testing',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Text(
'Test API calls that match configured patterns',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: _testTodosPattern1,
icon: const Icon(Icons.filter_1),
label: const Text('Test /todos/1 (Pattern)'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _testTodosPattern2,
icon: const Icon(Icons.filter_2),
label: const Text('Test /todos/42 (Pattern)'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _testUsersPattern,
icon: const Icon(Icons.person),
label: const Text('Test /users/123 (Pattern)'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _testCommentsPattern,
icon: const Icon(Icons.comment),
label: const Text('Test /posts/1/comments/5 (Pattern)'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 24),
const Text(
'Goal Management',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Text(
'Test goal tracking functionality',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _beginGoal1,
icon: const Icon(Icons.flag),
label: const Text('Begin Goal 1'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _beginGoal2,
icon: const Icon(Icons.flag_outlined),
label: const Text('Begin Goal 2'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
),
],
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _currentGoalId != null ? _completeCurrentGoal : null,
icon: const Icon(Icons.check_circle),
label: Text(_currentGoalId != null
? 'Complete Current Goal'
: 'No Active Goal'),
style: ElevatedButton.styleFrom(
backgroundColor:
_currentGoalId != null ? Colors.orange : Colors.grey,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(color: Colors.blue.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Goal Status:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(_goalStatus),
if (_completedGoals.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'Completed Goals: ${_completedGoals.length}',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
],
],
),
),
const SizedBox(height: 24),
const Text(
'Activity Markers',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Text(
'Set multiple activity markers at once',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 12),
Row(
children: [
const Text('Count:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
IconButton(
onPressed: _decrementMarkerCount,
icon: const Icon(Icons.remove),
style: IconButton.styleFrom(
backgroundColor: Colors.red.shade100,
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$_markerCount',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
IconButton(
onPressed: _incrementMarkerCount,
icon: const Icon(Icons.add),
style: IconButton.styleFrom(
backgroundColor: Colors.green.shade100,
),
),
],
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _setMultipleActivityMarkers,
icon: const Icon(Icons.timeline),
label: Text('Set $_markerCount Activity Markers'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.purple.shade50,
border: Border.all(color: Colors.purple.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Marker Status:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(_markerStatus),
],
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'API Response:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
_apiResult,
style: const TextStyle(fontSize: 14),
),
],
),
),
],
),
),
),
);
}
Widget _buildNavigationButton(BuildContext context, String title,
String subtitle, VoidCallback onPressed) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
onPressed: onPressed,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(fontSize: 12),
),
],
),
);
}
void _navigateRapidScreens(BuildContext context) async {
for (int i = 0; i < 5; i++) {
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RapidTestScreen(screenNumber: i + 1)));
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
Navigator.pop(context);
await Future.delayed(const Duration(milliseconds: 200));
}
}
}
/// Simple test screen using AxState
class SimpleTestScreen extends StatefulWidget {
const SimpleTestScreen({super.key});
@override
State<SimpleTestScreen> createState() => _SimpleTestScreenState();
}
class _SimpleTestScreenState extends AxState<SimpleTestScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: const Text('Simple Test Screen'),
backgroundColor: Colors.green,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'This screen uses AxState and should be properly disposed.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
Text(
'Counter: $_counter',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: const Text('Increment'),
),
],
),
),
);
}
}
/// Screen with Timer that might leak if not properly disposed
class TimerTestScreen extends StatefulWidget {
const TimerTestScreen({super.key});
@override
State<TimerTestScreen> createState() => _TimerTestScreenState();
}
class _TimerTestScreenState extends AxState<TimerTestScreen> {
Timer? _timer;
int _seconds = 0;
@override
void initState() {
super.initState();
// Start a timer - potential memory leak if not cancelled
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
_seconds++;
});
});
}
@override
void dispose() {
_timer?.cancel(); // Properly cancel timer
super.dispose();
}
@override
Widget build(BuildContext context) {
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: const Text('Timer Test Screen'),
backgroundColor: Colors.orange,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'This screen has a Timer.',
style: TextStyle(fontSize: 18),
),
const Text(
'Timer is properly cancelled in dispose().',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
Text(
'Seconds: $_seconds',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 20),
const Text(
'Navigate back to test disposal',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
);
}
}
/// Screen with Stream subscription
class StreamTestScreen extends StatefulWidget {
const StreamTestScreen({super.key});
@override
State<StreamTestScreen> createState() => _StreamTestScreenState();
}
class _StreamTestScreenState extends AxState<StreamTestScreen> {
StreamSubscription<int>? _subscription;
int _value = 0;
@override
void initState() {
super.initState();
// Create a stream subscription
_subscription = Stream.periodic(const Duration(milliseconds: 500), (i) => i)
.listen((value) {
setState(() {
_value = value;
});
});
}
@override
void dispose() {
_subscription?.cancel(); // Properly cancel subscription
super.dispose();
}
@override
Widget build(BuildContext context) {
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: const Text('Stream Test Screen'),
backgroundColor: Colors.purple,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'This screen has a Stream subscription.',
style: TextStyle(fontSize: 18),
),
const Text(
'Subscription is properly cancelled in dispose().',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
Text(
'Stream Value: $_value',
style: const TextStyle(fontSize: 24),
),
],
),
),
);
}
}
/// Screen with AnimationController
class AnimationTestScreen extends StatefulWidget {
const AnimationTestScreen({super.key});
@override
State<AnimationTestScreen> createState() => _AnimationTestScreenState();
}
class _AnimationTestScreenState extends AxState<AnimationTestScreen>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose(); // Properly dispose animation controller
super.dispose();
}
@override
Widget build(BuildContext context) {
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: const Text('Animation Test Screen'),
backgroundColor: Colors.teal,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'This screen has an AnimationController.',
style: TextStyle(fontSize: 18),
),
const Text(
'Controller is properly disposed.',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
),
),
);
},
),
],
),
),
);
}
}
/// Screen for rapid navigation testing
class RapidTestScreen extends StatefulWidget {
final int screenNumber;
const RapidTestScreen({super.key, required this.screenNumber});
@override
State<RapidTestScreen> createState() => _RapidTestScreenState();
}
class _RapidTestScreenState extends AxState<RapidTestScreen> {
@override
Widget build(BuildContext context) {
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: Text('Rapid Test ${widget.screenNumber}'),
backgroundColor: Colors.red,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Rapid Screen #${widget.screenNumber}',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 20),
const Text(
'This screen will be disposed quickly',
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}
/// Screen that intentionally creates a potential memory leak by adding itself to a static list
class StaticReferenceScreen extends StatefulWidget {
const StaticReferenceScreen({super.key});
@override
State<StaticReferenceScreen> createState() => _StaticReferenceScreenState();
}
// Static list that holds references (potential memory leak)
class _GlobalStateHolder {
static final List<State> _states = [];
static void addState(State state) {
_states.add(state);
}
static void removeState(State state) {
_states.remove(state);
}
static int get stateCount => _states.length;
}
class _StaticReferenceScreenState extends AxState<StaticReferenceScreen> {
bool _isAddedToGlobalList = false;
void _toggleGlobalReference() {
setState(() {
if (_isAddedToGlobalList) {
_GlobalStateHolder.removeState(this);
_isAddedToGlobalList = false;
} else {
_GlobalStateHolder.addState(this);
_isAddedToGlobalList = true;
}
});
}
@override
void dispose() {
// Uncomment the line below to properly clean up and avoid memory leaks
// if (_isAddedToGlobalList) _GlobalStateHolder.removeState(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
Ax.setActivityMarkerAt(
runtimeType.toString(), "build ${context.toString()}");
return Scaffold(
appBar: AppBar(
title: const Text('Static Reference Test'),
backgroundColor: Colors.red,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Memory Leak Simulation',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
Text(
'Global state count: ${_GlobalStateHolder.stateCount}',
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
Text(
_isAddedToGlobalList
? 'This State IS in global list (potential leak if not removed in dispose)'
: 'This State is NOT in global list',
style: TextStyle(
fontSize: 16,
color: _isAddedToGlobalList ? Colors.red : Colors.green,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _toggleGlobalReference,
style: ElevatedButton.styleFrom(
backgroundColor:
_isAddedToGlobalList ? Colors.red : Colors.green,
),
child: Text(
_isAddedToGlobalList
? 'Remove from Global List'
: 'Add to Global List (Create Leak)',
),
),
const SizedBox(height: 20),
const Text(
'Navigate back after adding to global list to test memory leak detection.',
style: TextStyle(fontSize: 14, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
const Text(
'Check logcat for memory leak reports!',
style: TextStyle(
fontSize: 14,
color: Colors.orange,
fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
],
),
),
);
}
}