flutter TabBarView 動態新增刪除頁面

爱学习的绿叶發表於2024-11-14

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

相關文章