기존 코드

import 'package:flutter/material.dart';
import 'package:meals_clone/screens/categories.dart';
// import 'package:riverpod/riverpod.dart';

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

  @override
  State<TabScreen> createState() => _TabScreenState();
}

class _TabScreenState extends State<TabScreen> {
  int _selectedPageIndex = 0;
  Widget categoriesScreen = const CategoriesScreen();
  Widget placeholder = const Placeholder();
  Widget activePage = const CategoriesScreen();

  void _selectPage(int index){
    setState(() {
      _selectedPageIndex = index;
    });
    if(_selectedPageIndex == 0){
      activePage = categoriesScreen;
    }
    else if(_selectedPageIndex == 1){
      activePage = placeholder;
    }
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Categories'),
        centerTitle: true,
      ),
      // body: const CategoriesScreen(),
      body: activePage,
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(
              icon: Icon(
                Icons.set_meal,
                size: 30,
              ),
              label: '카테고리'),
          BottomNavigationBarItem(
              icon: Icon(
                Icons.star,
                size: 30,
              ),
              label: '즐겨찾기')
        ],
        showSelectedLabels: false, // 선택된 레이블 숨기기
        showUnselectedLabels: false, // 선택되지 않은 레이블 숨기기
        currentIndex: _selectedPageIndex,
        onTap: _selectPage,
      ),
    );
  }
}

수정된 코드

import 'package:flutter/material.dart';
import 'package:meals_clone/screens/categories.dart';
// import 'package:riverpod/riverpod.dart';

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

  @override
  State<TabScreen> createState() => _TabScreenState();
}

class _TabScreenState extends State<TabScreen> {
  int _selectedPageIndex = 0;

  void _selectPage(int index) {
    setState(() {
      _selectedPageIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 현재 선택된 페이지에 따라 적절한 위젯을 동적으로 선택합니다.
    Widget activePage = _selectedPageIndex == 0
        ? const CategoriesScreen()
        : const Placeholder();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Categories'),
        centerTitle: true,
      ),
      body: activePage,
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(
              icon: Icon(
                Icons.set_meal,
                size: 30,
              ),
              label: '카테고리'),
          BottomNavigationBarItem(
              icon: Icon(
                Icons.star,
                size: 30,
              ),
              label: '즐겨찾기')
        ],
        showSelectedLabels: false, // 선택된 레이블 숨기기
        showUnselectedLabels: false, // 선택되지 않은 레이블 숨기기
        currentIndex: _selectedPageIndex,
        onTap: _selectPage,
      ),
    );
  }
}

수정된 내용

  1. 불필요한 인스턴스화 제거: categoriesScreen과 placeholder 위젯을 미리 인스턴스화할 필요가 없다. 이들은 필요할 때 생성될 수 있으며, 위와 같이 수정하면 메모리 사용량을 줄이고 성능을 향상시킬 수 있다.

  2. 상태 관리 개선: 상태를 더 효율적으로 관리하기 위해, _selectPage 메서드 내에서 activePage를 직접 변경하는 대신, build 메서드 내에서 _selectedPageIndex에 기반하여 동적으로 페이지를 결정하도록 수정되었다.


+추가 의문사항

이렇게 수정하고나니 만약 탭이 여러 개일 때는 어떻게 구현해야 적절할지, 그리고 방식에 따라서 메모리는 어떻게 사용될지 궁금해서 찾아보았다.

1. List 방식

설명:

  • List<Widget> 방식에서는 탭에 대응하는 모든 위젯을 List에 미리 저장한다. 탭 전환 시, 이 List에서 해당 인덱스의 위젯을 가져와 렌더링한다.

  • 이 방식에서는 각 탭의 위젯이 List에 미리 인스턴스화되어 있으며, 탭 전환 시 해당 인스턴스를 재사용한다.

장점:

  • 초기 세팅의 단순함: 모든 탭의 위젯을 미리 정의하고 관리하기 때문에, 초기 코드 구성이 단순.

  • 렌더링 성능: 이미 생성된 위젯 인스턴스를 재사용하기 때문에, 탭 전환 자체는 빠르게 이루어진다.

단점:

  • 메모리 사용량: 모든 탭의 위젯이 메모리에 미리 로드되므로, 앱 시작 시 메모리 사용량이 높아질 수 있다.

  • 상태 유지의 부재: 탭 전환 시 위젯의 상태가 유지되지 않는다. 새로운 탭으로 전환할 때마다 위젯이 재생성될 수 있으며, 이는 이전 탭의 상태가 초기화될 수 있음을 의미한다.

예시 코드:

class MyTabScreen extends StatefulWidget {
  @override
  _MyTabScreenState createState() => _MyTabScreenState();
}

class _MyTabScreenState extends State<MyTabScreen> {
  int _selectedIndex = 0;
  final List<Widget> _tabs = [
    WidgetA(), // 탭 A에 대응하는 위젯
    WidgetB(), // 탭 B에 대응하는 위젯
    WidgetC(), // 탭 C에 대응하는 위젯
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _tabs[_selectedIndex], // 현재 선택된 탭의 위젯을 렌더링
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) => setState(() => _selectedIndex = index),
        // BottomNavigationBarItems 정의
      ),
    );
  }
}

2. IndexedStack 방식

설명:

  • IndexedStack은 모든 자식 위젯을 미리 인스턴스화하지만, 한 번에 하나의 자식만 화면에 표시한다.

  • 이 방식은 탭 간 상태를 유지하며 탭 전환 시 빠른 반응성을 제공한다.

장점:

  • 탭의 상태가 유지된다. 예를 들어, 사용자가 스크롤한 위치는 그대로 유지된다.

  • 이미 로드된 위젯을 화면에 표시하기만 하면 되기 때문에 탭 전환 시 빠르게 반응한다.

단점:

  • 초기 앱 로드 시 모든 탭의 위젯이 메모리에 로드된다. 이는 초기 메모리 사용량이 크다는 것을 의미한다.

예시 코드:

class MyTabScreen extends StatefulWidget {
  @override
  _MyTabScreenState createState() => _MyTabScreenState();
}

class _MyTabScreenState extends State<MyTabScreen> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: [
          WidgetA(),
          WidgetB(),
          WidgetC(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) => setState(() => _selectedIndex = index),
        // ... BottomNavigationBarItems ...
      ),
    );
  }
}

3. 지연 인스턴스화 (Lazy Instantiation) 방식

설명:

  • 지연 인스턴스화 방식은 탭의 위젯을 실제로 필요할 때 생성한다. 즉, 탭이 활성화될 때 해당 탭의 위젯이 인스턴스화된다.

  • 이 방식은 메모리 사용을 최적화할 수 있으나, 탭의 상태가 유지되지 않는다.

장점:

  • 초기 앱 로드 시에는 필요한 탭의 위젯만 메모리에 로드되어 메모리 사용량이 적다.

  • 사용하지 않는 탭의 위젯은 메모리에 로드되지 않는다.

단점:

  • 탭 전환 시마다 새로운 위젯 인스턴스를 생성해야 한다.

  • 탭의 상태가 유지되지 않는다.

예시 코드:

class MyTabScreen extends StatefulWidget {
  @override
  _MyTabScreenState createState() => _MyTabScreenState();
}

class _MyTabScreenState extends State<MyTabScreen> {
  int _selectedIndex = 0;

  Widget _buildTab(int index) {
    switch (index) {
      case 0:
        return WidgetA();
      case 1:
        return WidgetB();
      case 2:
        return WidgetC();
      default:
        return Placeholder();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _buildTab(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) => setState(() => _selectedIndex = index),
        // ... BottomNavigationBarItems ...
      ),
    );
  }
}