-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
I got this errros on use webview on tabview child using List.Generator.
Works very well, using any widget on child page.
I got following errors when add the first tab. Second and another pages not fire this errors.
E/flutter (20419): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)]
Unhandled Exception: setState() called after dispose():
_PlatformViewLinkState#a7b8e(lifecycle state: defunct, not mounted)
E/flutter (20419): This error happens if you call setState() on a
State object for a widget that no longer appears in the widget tree
(e.g., whose parent widget no longer includes the widget in its
build). This error can occur when code calls setState() from a timer
or an animation callback.
E/flutter (20419): The preferred solution is
to cancel the timer or stop listening to the animation in the
dispose() callback. Another solution is to check the "mounted"
property of this object before calling setState() to ensure the object
is still in the tree.
E/flutter (20419): This error might indicate a
memory leak if setState() is being called because another object is
retaining a reference to this State object after it has been removed
from the tree. To avoid memory leaks, consider breaking the reference
to this object during dispose().
To reproduce, run this code:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'dart:async';
import 'dart:io';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Home(),
debugShowCheckedModeBanner: false,
);
}
}
class Home extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<Home> {
List<Widget> data = [Container()];
int initPosition = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) {
if (index == 0) {
return Tab(icon: Icon(Icons.home));
} else {
return Tab(text: index.toString());
}
},
pageBuilder: (context, index) {
if (index == 0) {
return Center(
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
data.add(Page(
url: 'https://www.google.com',
key: ValueKey('$index'),
));
});
}));
} else {
return Center(child: (data[index]));
}
},
onPositionChange: (index) {
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
),
);
}
}
/// Implementation
class CustomTabView extends StatefulWidget {
final int? itemCount;
final IndexedWidgetBuilder? tabBuilder;
final IndexedWidgetBuilder? pageBuilder;
final Widget? stub;
final ValueChanged<int>? onPositionChange;
final ValueChanged<double>? onScroll;
final int? initPosition;
CustomTabView({
@required this.itemCount,
@required this.tabBuilder,
@required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
@override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView>
with TickerProviderStateMixin {
TabController? controller;
int? _currentCount;
int? _currentPosition;
@override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition!,
);
controller!.addListener(onPositionChange);
controller!.animation!.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
@override
void didUpdateWidget(CustomTabView oldWidget) {
print('current position: $_currentPosition');
if (_currentCount != widget.itemCount) {
print('_currentCount != widget.itemCount');
controller!.animation!.removeListener(onScroll);
controller!.removeListener(onPositionChange);
controller!.dispose();
if (widget.initPosition != null) {
print('widget.initPosition != null');
_currentPosition = widget.initPosition;
}
if (_currentPosition! > widget.itemCount! - 1) {
print('_currentPosition > widget.itemCount - 1');
_currentPosition = widget.itemCount! - 1;
_currentPosition = _currentPosition! < 0 ? 0 : _currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (mounted) {
widget.onPositionChange!(_currentPosition!);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition!,
);
controller!.addListener(onPositionChange);
controller!.animation!.addListener(onScroll);
});
controller!.animateTo(widget.itemCount! - 1);
} else if (widget.initPosition != null) {
controller!.animateTo(widget.initPosition!);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
controller!.animation!.removeListener(onScroll);
controller!.removeListener(onPositionChange);
controller!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.itemCount! < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: TabBarView(
physics: NeverScrollableScrollPhysics(),
controller: controller,
children: List.generate(
widget.itemCount!,
(index) => widget.pageBuilder!(context, index),
),
),
),
Container(
alignment: Alignment.centerLeft,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount!,
(index) => widget.tabBuilder!(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller!.indexIsChanging) {
_currentPosition = controller!.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange!(_currentPosition!);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll!(controller!.animation!.value);
}
}
}
class Page extends StatefulWidget {
const Page({key, this.url}) : super(key: key);
final String? url;
@override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin<Page> {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
late final url = widget.url!;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: WebView(
key: widget.key,
initialUrl: url,
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
debugPrint("WebView is created");
_controller.complete(webViewController);
},
onProgress: (int progress) {
debugPrint("WebView is loading (progress : $progress%)");
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Page finished loading: $url'),
duration: Duration(seconds: 5),
));
},
));
}
}