flutter TabBarView 动态添加删除页面

在TabBarView 动态添加页面后删除其中一个页面会导致后面的页面状态错误或删除的页面不正确。出现这种问题是由于创建子页面时没有为子页面设置唯一的key导致的。

复制代码
 1   void addNewPage() {
 2     _pageCount++;
 3     setState(() {
 4       String title = "页面$_pageCount";
 5       PageContent page = PageContent(data: title, pageId: _pageCount,);
 6       PageData data = PageData(data: title, pageId: _pageCount, content: page);
 7       listPages.add(data);
 8       nowIndex = listPages.length -1;
 9       resetTabController();
10     });
11   }

如上面的代码所示, 在创建PageContent 组件时如果没有指定全局唯一的key, 关闭页面时就会导致后面的页面被再次build或删除错误的页面,正确的代码如下

复制代码
 1 void addNewPage() {
 2     _pageCount++;
 3     setState(() {
 4       String title = "页面$_pageCount";
 5       PageContent page = PageContent(data: title, pageId: _pageCount, key: ValueKey(title),);
 6       PageData data = PageData(data: title, pageId: _pageCount, content: page);
 7       listPages.add(data);
 8       nowIndex = listPages.length -1;
 9       resetTabController();
10     });
11   }

指定了全局唯一key后在删除子页面,后续页面就可以正常显示。

所有代码如下

复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.white),
        primaryColor: Colors.white,
        scaffoldBackgroundColor: Colors.white,
        dialogBackgroundColor: Colors.white,
        useMaterial3: true,
      ),
      home: const PageMain(),
      /*
      home: ChangeNotifierProvider(
          create: (context) => HomeProvider(),
          builder: (context, child) => const HomePage(),
      ),
      */
    );
  }
}

class PageData {
  final String data;
  final int pageId;
  final Widget content;

  PageData({
    required this.data,
    required this.pageId,
    required this.content,
  });
}

class _StatePageMain extends State<PageMain> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
  final List<PageData> listPages = <PageData>[];
  int nowIndex = 0;
  int _pageCount = 0;
  TabController? tabController;

  @override
  void initState() {
    super.initState();
    setState(() {
      tabController = TabController(length: listPages.length, vsync: this);
    });
  }

  @override
  bool get wantKeepAlive => true;

  void addNewPage() {
    _pageCount++;
    setState(() {
      String title = "页面$_pageCount";
      PageContent page = PageContent(data: title, pageId: _pageCount, key: ValueKey(title),);
      PageData data = PageData(data: title, pageId: _pageCount, content: page);
      listPages.add(data);
      nowIndex = listPages.length -1;
      resetTabController();
    });
  }

  //选中某个页面
  void onSelectPage(PageData page) {
    //页面已经选中
    int selIndex = 0;
    for (int index = 0; index < listPages.length; index++) {
      PageData item = listPages[index];
      if (item.pageId == page.pageId) {
        selIndex = index;
        break;
      }
    }
    //选中页面没有更改
    if (selIndex == nowIndex) {
      return;
    }
    setState(() {
      nowIndex = selIndex;
      tabController?.animateTo(nowIndex);
    });
  }

  //关闭页面
  void onClosePage(PageData data) {
    int closedIndex = 0;
    for (int index = 0; index < listPages.length; index++) {
      PageData item = listPages[index];
      if (item.pageId == data.pageId) {
        closedIndex = index;
        break;
      }
    }
    setState(() {
      listPages.removeAt(closedIndex);
      if (closedIndex <= nowIndex) {
        nowIndex--;
      }
      if (nowIndex < 0) {
        nowIndex = 0;
      } else if (nowIndex >= listPages.length) {
        nowIndex = listPages.length -1;
      }
      resetTabController();
    });
  }

  void resetTabController() {
    if (tabController?.length != listPages.length) {
      tabController?.dispose();
      tabController = TabController(
        length: listPages.length,
        vsync: this,
        initialIndex: nowIndex,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
      appBar: AppBar(
        bottom: PreferredSize(
            preferredSize: const Size.fromHeight(40),
            child: TabBar(
                controller: tabController,
                tabs: listPages.map((item) => Tab(child: TitleBarItem(data: item, closeCallback: (data) => onClosePage(data)),)).toList(),
            ),
        ),
      ),
      body: TabBarView(
        controller: tabController,
        children: listPages.map((item) => item.content).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => addNewPage(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class PageMain extends StatefulWidget {
  const PageMain({super.key});

  @override
  State<PageMain> createState() => _StatePageMain();
}

class _StatePageContent extends State<PageContent> with AutomaticKeepAliveClientMixin {
  List<String> listItems = <String>[];

  @override
  void initState() {
    print("初始化页面内容控制器:${widget.data}");
    setState(() {
      for (int index = 0; index <= 30; index++) {
        listItems.add("${widget.data} - $index");
      }
    });
    super.initState();
  }

  @override
  void dispose() {
    print("释放页面内容控制器:${widget.data}");
    super.dispose();
  }

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    print("Build页面 ${widget.data}");
    return Container(
      alignment: Alignment.center,
      child: Column(
        children: [
          Text(widget.data),
          Expanded(
            child: ListView.builder(
                itemExtent: 30,
                itemCount: listItems.length,
                itemBuilder: (context, index) {
                  return Text(listItems[index]);
                }
            ),
          ),
        ],
      ),
    );
  }
}

class PageContent extends StatefulWidget {
  final int pageId;
  final String data;

  const PageContent({super.key, required this.data, required this.pageId});

  @override
  State<PageContent> createState() {
    return _StatePageContent();
  }
}

typedef ClickCallback = void Function(PageData data);

class TitleBarItem extends StatelessWidget {
  final PageData data;
  final ClickCallback closeCallback;

  const TitleBarItem({
    super.key,
    required this.data,
    required this.closeCallback,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 200,
      child: Row(
        children: [
          Expanded(child: Text(data.data)),
          IconButton(
              onPressed: () => closeCallback(data),
              icon: const Icon(Icons.close))
        ],
      ),
    );
  }
}
相关推荐
程序员Ctrl喵7 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难8 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡9 小时前
flutter列表中实现置顶动画
flutter
始持10 小时前
第十二讲 风格与主题统一
前端·flutter
始持10 小时前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持10 小时前
第十三讲 异步操作与异步构建
前端·flutter
新镜10 小时前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴11 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区11 小时前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎12 小时前
树形选择器组件封装
前端·flutter