Flutter IndexedStack 组件总结

72 阅读6分钟

Flutter IndexedStack 组件总结

1. 概述

IndexedStack 是 Flutter 中一个特殊的布局组件,继承自 Stack。与普通的 Stack 不同,IndexedStack 一次只显示其子组件列表中的一个组件,其他组件保持隐藏状态但仍然存在于组件树中。通过 index 属性可以控制当前显示的子组件,这使得 IndexedStack 成为实现标签页、轮播图、状态切换等功能的理想选择。

2. 原理说明

2.1 继承关系

Object > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Stack > IndexedStack

2.2 实现原理

  • IndexedStack 继承自 Stack,复用了 Stack 的布局逻辑
  • 重写了 paintStack 方法,只绘制 index 指定的子组件
  • 所有子组件都会参与布局计算,但只有指定索引的组件可见
  • 隐藏的组件保持其状态,不会被销毁和重建
  • 组件的尺寸由所有子组件中最大的尺寸决定

2.3 渲染机制

  • 布局阶段: 所有子组件都参与布局,计算各自的尺寸
  • 绘制阶段: 只绘制 index 指定的子组件
  • 状态保持: 未显示的组件虽然不可见,但状态得以保留
  • 尺寸计算: 取所有子组件中的最大宽度和高度作为自身尺寸

2.4 与 Stack 的区别

特性StackIndexedStack
显示方式同时显示所有子组件只显示指定索引的子组件
性能所有子组件都绘制只绘制一个子组件
状态保持自然保持主动保持隐藏组件状态
使用场景重叠布局切换显示

3. 构造函数

IndexedStack({
  Key? key,
  AlignmentGeometry alignment = AlignmentDirectional.topStart,
  TextDirection? textDirection,
  StackFit sizing = StackFit.loose,
  int? index = 0,
  List<Widget> children = const <Widget>[],
})

4. 主要参数详解

4.1 核心参数

参数类型默认值说明
indexint?0核心参数,指定当前显示的子组件索引
childrenList<Widget><Widget>[]子组件列表

4.2 布局参数

参数类型默认值说明
alignmentAlignmentGeometryAlignmentDirectional.topStart子组件的对齐方式
textDirectionTextDirection?null文本方向,影响 AlignmentDirectional
sizingStackFitStackFit.loose尺寸适配方式

4.3 StackFit 枚举值

说明
StackFit.loose使用子组件的自然尺寸
StackFit.expand强制子组件填满 Stack 的尺寸
StackFit.passthrough将约束传递给子组件

4.4 AlignmentGeometry 常用值

说明
Alignment.topLeft左上角对齐
Alignment.topCenter顶部居中
Alignment.topRight右上角对齐
Alignment.centerLeft左侧居中
Alignment.center居中对齐
Alignment.centerRight右侧居中
Alignment.bottomLeft左下角对齐
Alignment.bottomCenter底部居中
Alignment.bottomRight右下角对齐

5. 使用示例

5.1 基础用法

class BasicIndexedStackExample extends StatefulWidget {
  @override
  _BasicIndexedStackExampleState createState() => _BasicIndexedStackExampleState();
}

class _BasicIndexedStackExampleState extends State<BasicIndexedStackExample> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: [
          Container(
            color: Colors.red,
            child: Center(
              child: Text(
                '页面 1',
                style: TextStyle(fontSize: 24, color: Colors.white),
              ),
            ),
          ),
          Container(
            color: Colors.green,
            child: Center(
              child: Text(
                '页面 2',
                style: TextStyle(fontSize: 24, color: Colors.white),
              ),
            ),
          ),
          Container(
            color: Colors.blue,
            child: Center(
              child: Text(
                '页面 3',
                style: TextStyle(fontSize: 24, color: Colors.white),
              ),
            ),
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: '搜索'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
        ],
      ),
    );
  }
}

5.2 状态保持示例

class StatefulPage extends StatefulWidget {
  final String title;
  final Color color;

  const StatefulPage({Key? key, required this.title, required this.color}) : super(key: key);

  @override
  _StatefulPageState createState() => _StatefulPageState();
}

class _StatefulPageState extends State<StatefulPage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: widget.color,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              widget.title,
              style: TextStyle(fontSize: 24, color: Colors.white),
            ),
            SizedBox(height: 20),
            Text(
              '计数器: $_counter',
              style: TextStyle(fontSize: 18, color: Colors.white),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _counter++;
                });
              },
              child: Text('增加计数'),
            ),
          ],
        ),
      ),
    );
  }
}

class StatePreservationExample extends StatefulWidget {
  @override
  _StatePreservationExampleState createState() => _StatePreservationExampleState();
}

class _StatePreservationExampleState extends State<StatePreservationExample> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('状态保持示例')),
      body: IndexedStack(
        index: _currentIndex,
        children: [
          StatefulPage(title: '页面 1', color: Colors.red),
          StatefulPage(title: '页面 2', color: Colors.green),
          StatefulPage(title: '页面 3', color: Colors.blue),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.looks_one), label: '页面1'),
          BottomNavigationBarItem(icon: Icon(Icons.looks_two), label: '页面2'),
          BottomNavigationBarItem(icon: Icon(Icons.looks_3), label: '页面3'),
        ],
      ),
    );
  }
}

5.3 自定义对齐方式

class AlignmentExample extends StatefulWidget {
  @override
  _AlignmentExampleState createState() => _AlignmentExampleState();
}

class _AlignmentExampleState extends State<AlignmentExample> {
  int _currentIndex = 0;
  AlignmentGeometry _alignment = Alignment.center;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('对齐方式示例')),
      body: Column(
        children: [
          Expanded(
            child: IndexedStack(
              index: _currentIndex,
              alignment: _alignment,
              children: [
                Container(
                  width: 200,
                  height: 200,
                  color: Colors.red,
                  child: Center(child: Text('红色', style: TextStyle(color: Colors.white))),
                ),
                Container(
                  width: 150,
                  height: 150,
                  color: Colors.green,
                  child: Center(child: Text('绿色', style: TextStyle(color: Colors.white))),
                ),
                Container(
                  width: 100,
                  height: 100,
                  color: Colors.blue,
                  child: Center(child: Text('蓝色', style: TextStyle(color: Colors.white))),
                ),
              ],
            ),
          ),
          Wrap(
            spacing: 8.0,
            children: [
              ElevatedButton(
                onPressed: () => setState(() => _alignment = Alignment.topLeft),
                child: Text('左上'),
              ),
              ElevatedButton(
                onPressed: () => setState(() => _alignment = Alignment.topCenter),
                child: Text('顶部'),
              ),
              ElevatedButton(
                onPressed: () => setState(() => _alignment = Alignment.topRight),
                child: Text('右上'),
              ),
              ElevatedButton(
                onPressed: () => setState(() => _alignment = Alignment.center),
                child: Text('居中'),
              ),
              ElevatedButton(
                onPressed: () => setState(() => _alignment = Alignment.bottomLeft),
                child: Text('左下'),
              ),
              ElevatedButton(
                onPressed: () => setState(() => _alignment = Alignment.bottomRight),
                child: Text('右下'),
              ),
            ],
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) => setState(() => _currentIndex = index),
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.crop_square), label: '大'),
          BottomNavigationBarItem(icon: Icon(Icons.crop_din), label: '中'),
          BottomNavigationBarItem(icon: Icon(Icons.crop_free), label: '小'),
        ],
      ),
    );
  }
}

5.4 轮播图实现

class CarouselExample extends StatefulWidget {
  @override
  _CarouselExampleState createState() => _CarouselExampleState();
}

class _CarouselExampleState extends State<CarouselExample> {
  int _currentIndex = 0;
  late Timer _timer;
  
  final List<String> _images = [
    'https://via.placeholder.com/400x200/FF0000/FFFFFF?text=Image+1',
    'https://via.placeholder.com/400x200/00FF00/FFFFFF?text=Image+2',
    'https://via.placeholder.com/400x200/0000FF/FFFFFF?text=Image+3',
  ];

  @override
  void initState() {
    super.initState();
    _startAutoPlay();
  }

  void _startAutoPlay() {
    _timer = Timer.periodic(Duration(seconds: 3), (timer) {
      setState(() {
        _currentIndex = (_currentIndex + 1) % _images.length;
      });
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('轮播图示例')),
      body: Column(
        children: [
          Container(
            height: 200,
            child: IndexedStack(
              index: _currentIndex,
              children: _images.map((imageUrl) => 
                Container(
                  width: double.infinity,
                  decoration: BoxDecoration(
                    image: DecorationImage(
                      image: NetworkImage(imageUrl),
                      fit: BoxFit.cover,
                    ),
                  ),
                )
              ).toList(),
            ),
          ),
          SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: _images.asMap().entries.map((entry) {
              return Container(
                width: 8.0,
                height: 8.0,
                margin: EdgeInsets.symmetric(horizontal: 4.0),
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: _currentIndex == entry.key 
                    ? Colors.blue 
                    : Colors.grey,
                ),
              );
            }).toList(),
          ),
          SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _currentIndex = (_currentIndex - 1 + _images.length) % _images.length;
                  });
                },
                child: Text('上一张'),
              ),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _currentIndex = (_currentIndex + 1) % _images.length;
                  });
                },
                child: Text('下一张'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

5.5 表单向导实现

class FormWizardExample extends StatefulWidget {
  @override
  _FormWizardExampleState createState() => _FormWizardExampleState();
}

class _FormWizardExampleState extends State<FormWizardExample> {
  int _currentStep = 0;
  final _formKey = GlobalKey<FormState>();
  
  String _name = '';
  String _email = '';
  String _phone = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('表单向导'),
        backgroundColor: Colors.blue,
      ),
      body: Column(
        children: [
          // 进度指示器
          Container(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                for (int i = 0; i < 3; i++)
                  Expanded(
                    child: Container(
                      height: 4,
                      margin: EdgeInsets.horizontal(2),
                      decoration: BoxDecoration(
                        color: i <= _currentStep ? Colors.blue : Colors.grey.shade300,
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                  ),
              ],
            ),
          ),
          Expanded(
            child: Form(
              key: _formKey,
              child: IndexedStack(
                index: _currentStep,
                children: [
                  // 第一步:姓名
                  _buildStepOne(),
                  // 第二步:邮箱
                  _buildStepTwo(),
                  // 第三步:电话
                  _buildStepThree(),
                ],
              ),
            ),
          ),
          // 导航按钮
          Container(
            padding: EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                _currentStep > 0
                    ? ElevatedButton(
                        onPressed: () {
                          setState(() {
                            _currentStep--;
                          });
                        },
                        child: Text('上一步'),
                      )
                    : SizedBox.shrink(),
                ElevatedButton(
                  onPressed: _currentStep < 2 ? _nextStep : _submitForm,
                  child: Text(_currentStep < 2 ? '下一步' : '提交'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildStepOne() {
    return Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('步骤 1: 输入姓名', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 20),
          TextFormField(
            initialValue: _name,
            decoration: InputDecoration(
              labelText: '姓名',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入姓名';
              }
              return null;
            },
            onSaved: (value) => _name = value ?? '',
          ),
        ],
      ),
    );
  }

  Widget _buildStepTwo() {
    return Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('步骤 2: 输入邮箱', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 20),
          TextFormField(
            initialValue: _email,
            decoration: InputDecoration(
              labelText: '邮箱',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入邮箱';
              }
              if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
                return '请输入有效邮箱';
              }
              return null;
            },
            onSaved: (value) => _email = value ?? '',
          ),
        ],
      ),
    );
  }

  Widget _buildStepThree() {
    return Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('步骤 3: 输入电话', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 20),
          TextFormField(
            initialValue: _phone,
            decoration: InputDecoration(
              labelText: '电话',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入电话';
              }
              return null;
            },
            onSaved: (value) => _phone = value ?? '',
          ),
        ],
      ),
    );
  }

  void _nextStep() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      setState(() {
        _currentStep++;
      });
    }
  }

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      // 处理表单提交
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('提交成功'),
          content: Text('姓名: $_name\n邮箱: $_email\n电话: $_phone'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text('确定'),
            ),
          ],
        ),
      );
    }
  }
}

6. 最佳实践

6.1 性能优化

  • 合理使用: 仅在需要保持状态时使用 IndexedStack
  • 避免过多子组件: 子组件数量过多会影响布局性能
  • 延迟加载: 对于复杂子组件,考虑延迟初始化
// 延迟加载示例
class LazyIndexedStack extends StatefulWidget {
  @override
  _LazyIndexedStackState createState() => _LazyIndexedStackState();
}

class _LazyIndexedStackState extends State<LazyIndexedStack> {
  int _currentIndex = 0;
  Map<int, Widget> _pages = {};

  Widget _getPage(int index) {
    if (!_pages.containsKey(index)) {
      _pages[index] = _buildPage(index);
    }
    return _pages[index]!;
  }

  Widget _buildPage(int index) {
    // 根据索引构建页面
    return Container(
      child: Center(child: Text('页面 $index')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return IndexedStack(
      index: _currentIndex,
      children: List.generate(5, (index) => _getPage(index)),
    );
  }
}

6.2 内存管理

  • 及时清理: 对于不再需要的页面,考虑移除以释放内存
  • 状态管理: 使用适当的状态管理方案避免不必要的重建

6.3 用户体验

  • 加载状态: 为需要网络请求的页面提供加载指示器
  • 过渡动画: 考虑添加页面切换动画提升体验
class AnimatedIndexedStack extends StatefulWidget {
  final int index;
  final List<Widget> children;
  final Duration duration;

  const AnimatedIndexedStack({
    Key? key,
    required this.index,
    required this.children,
    this.duration = const Duration(milliseconds: 300),
  }) : super(key: key);

  @override
  _AnimatedIndexedStackState createState() => _AnimatedIndexedStackState();
}

class _AnimatedIndexedStackState extends State<AnimatedIndexedStack>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: widget.duration, vsync: this);
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
    _controller.forward();
  }

  @override
  void didUpdateWidget(AnimatedIndexedStack oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.index != widget.index) {
      _controller.reset();
      _controller.forward();
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Opacity(
          opacity: _animation.value,
          child: IndexedStack(
            index: widget.index,
            children: widget.children,
          ),
        );
      },
    );
  }
}

7. 常见问题

7.1 索引越界

问题: 设置的 index 超出子组件列表范围 解决: 始终检查索引有效性

int _safeIndex = 0;

void _setIndex(int newIndex) {
  if (newIndex >= 0 && newIndex < _children.length) {
    setState(() {
      _safeIndex = newIndex;
    });
  }
}

7.2 状态丢失

问题: 子组件状态意外丢失 解决: 确保子组件使用正确的 key,避免不必要的重建

IndexedStack(
  index: _currentIndex,
  children: [
    PageOne(key: ValueKey('page_one')),
    PageTwo(key: ValueKey('page_two')),
    PageThree(key: ValueKey('page_three')),
  ],
)

7.3 尺寸问题

问题: IndexedStack 尺寸不符合预期 解决: 理解 IndexedStack 尺寸计算规则,必要时使用 Container 包装

Container(
  height: 300, // 固定高度
  child: IndexedStack(
    index: _currentIndex,
    children: _pages,
  ),
)

7.4 性能问题

问题: 页面切换卡顿 解决:

  • 减少复杂子组件数量
  • 使用 RepaintBoundary 隔离重绘区域
  • 考虑使用 PageView 替代
IndexedStack(
  index: _currentIndex,
  children: _pages.map((page) => 
    RepaintBoundary(child: page)
  ).toList(),
)

8. 适用场景

8.1 适合使用 IndexedStack

  • 底部导航: 需要保持各页面状态的标签导航
  • 表单向导: 多步骤表单,需要保持之前步骤的输入状态
  • 轮播图: 简单的图片轮播,状态切换较少
  • 设置面板: 多个设置页面间的切换

8.2 不适合使用 IndexedStack

  • 大量页面: 子组件数量很多时,考虑使用 PageView
  • 动态内容: 频繁添加删除子组件的场景
  • 复杂动画: 需要复杂页面过渡动画时
  • 内存敏感: 对内存使用要求严格的场景

9. 替代方案

9.1 PageView

适用于滑动切换场景

PageView(
  controller: _pageController,
  children: _pages,
)

9.2 AnimatedSwitcher

适用于需要切换动画的场景

AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: _pages[_currentIndex],
)

9.3 Navigator

适用于复杂页面导航

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => NextPage()),
)

10. 相关组件

  • Stack: 基础重叠布局组件
  • PageView: 可滑动的页面视图
  • TabBarView: 标签页视图
  • AnimatedSwitcher: 带切换动画的组件
  • Navigator: 页面导航管理

11. 总结

IndexedStack 是 Flutter 中一个非常实用的组件,具有以下特点:

优点:

  • 保持子组件状态
  • 实现简单,性能相对较好
  • 适合标签页等固定页面切换场景
  • 继承 Stack 的所有布局特性

缺点:

  • 所有子组件都参与布局计算
  • 不适合大量子组件的场景
  • 缺少内置的切换动画

最佳实践:

  • 合理控制子组件数量
  • 为复杂子组件提供合适的 key
  • 考虑延迟加载策略
  • 处理好索引边界检查
  • 在适当场景选择替代方案

通过深入理解 IndexedStack 的工作原理和使用场景,可以在构建 Flutter 应用时做出更好的技术选择,创造出流畅、高效的用户体验。