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))
        ],
      ),
    );
  }
}
相关推荐
火柴就是我16 小时前
flutter 之真手势冲突处理
android·flutter
Speed12316 小时前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间16 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭17 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
玲珑Felone17 小时前
从flutter源码看其渲染机制
android·flutter
ALLIN2 天前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei2 天前
Flutter 国际化
flutter
Dabei2 天前
Flutter MQTT 通信文档
flutter
Dabei2 天前
Flutter 中实现 TCP 通信
flutter
孤鸿玉2 天前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter