Skip to content

[webview] [platform_view] error ( Unhandled Exception: setState() called after dispose(): _PlatformViewLinkState#a7b8e(lifecycle state: defunct, not mounted) ) #84628

@filipenanclarez

Description

@filipenanclarez

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),
        ));
      },
    ));
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: error messageError messages from the Flutter frameworka: platform-viewsEmbedding Android/iOS views in Flutter appsfound in release: 2.2Found to occur in 2.2has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: webviewThe WebView pluginpackageflutter/packages repository. See also p: labels.platform-androidAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions