티스토리 뷰

앱바 레이아웃 구현

 

 

앱바 우측에 위치한 아이콘을 누르면

테마가 변경되도록 구현할 것이다.

 

 

 

모든 화면에서 공통적으로 사용할 앱바를 하나 만들어주자

 

 

extension ThemeExtension on BuildContext {
  bool get isLight => Theme.of(this).brightness == Brightness.light;
}

colors.dart에 이걸 추가하면 

'context.isLight'로 현재테마가 무슨 테마인지 감지할 수 있다.

 

context.isLight는 현재 테마가 라이트테마이면 true를 반환하고

그렇지 않다면 false를 반환한다.

 

 

import 'package:escape_anchovy/res/text/styles.dart';
import 'package:flutter/material.dart';

class CommonAppBar extends StatefulWidget implements PreferredSizeWidget {
  const CommonAppBar({super.key, required this.title});

  final String title;

  @override
  State<CommonAppBar> createState() => _CommonAppBarState();

  @override
  Size get preferredSize => const Size.fromHeight(44.0);
}

class _CommonAppBarState extends State<CommonAppBar> {
  @override
  Widget build(BuildContext context) {
    return AppBar(
      backgroundColor: Colors.transparent,
      title: Center(
        child: Text(widget.title, style: TextStyles.h3Bold),
      ),
    );
  }
}

기본적인 앱바의 틀이다.

 

테마변경 아이콘을 누르면 모양이 바뀌는 상태변화가 있으므로

StatefulWidget으로 생성해주고 

 

PreferredSizeWidget를 상속하여 

앱바의 높이를 44로 정의해준다.

 

문자열 형태의 title이라는 프로퍼티를 생성해주고

프로퍼티의 값을 앱바의 제목에 해당하는 위치에 할당해준다.

'widget.프로퍼티명' 으로 프로퍼티를 사용할 수 있다.

 

 

import 'package:escape_anchovy/res/text/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

class CommonSvg extends StatefulWidget {
  const CommonSvg({super.key, required this.src});

  final String src;

  @override
  State<CommonSvg> createState() => _CommonSvgState();
}

class _CommonSvgState extends State<CommonSvg> {
  @override
  Widget build(BuildContext context) {
    return SvgPicture.asset(
      widget.src,
      colorFilter: ColorFilter.mode(
          context.isLight
              ? DarkModeColors.background
              : LightModeColors.background,
          BlendMode.srcIn),
    );
  }
}

프로젝트에서 대부분의 svg파일은

라이트모드일 땐 검은색을, 다크모드일 때 하얀색의 색상을 가진다.

 

단색으로 이루어진 아이콘(svg파일)은 

colorFilter라는 프로퍼티를 이용해 아이콘의 색상을 지정해줄 수 있는데

하나하나 속성주기 귀찮으니까 그냥 CommonSvg란 클래스를 하나 만들었다.

 

 

// 프로퍼티 추가
const CommonAppBar({super.key, required this.title, this.isHome = false});

final String title;
final bool isHome;

//홈 화면 앱바
appBar: const CommonAppBar(
        title: '메인화면',
        isHome: true,
      ),

bool형의 isHome이라는 프로퍼티를 추가해줬다.

isHome은 현재 화면이 홈화면인지의 구분하는데 사용된다.

 

홈화면의 앱바에서 isHome을 true값으로 주고

기본값은 false로 설정함으로써 홈화면과 홈화면이 아닌 화면을 구분한다.

 

 

return AppBar(
      backgroundColor: Colors.transparent,
      title: Center(
        child: Text(widget.title, style: TextStyles.h3Bold),
      ),
      leadingWidth: 72,
      leading: widget.isHome
          ? Padding(
              padding: const EdgeInsets.only(right: 8),
              child: SvgPicture.asset(
                context.isLight
                    ? 'asset/svg/app_logo.svg'
                    : 'asset/svg/dark_app_logo.svg',
                fit: BoxFit.scaleDown,
              ),
            )
          : const Padding(
              padding: EdgeInsets.fromLTRB(14, 14, 24, 14),
              child: CommonSvg(
                src: 'asset/svg/back.svg',
              ),
            ),
      actions: widget.isHome
          ? [
              const CommonSvg(src: 'asset/svg/user_info.svg'),
              const SizedBox(
                width: 12,
              ),
              Padding(
                padding: const EdgeInsets.only(right: 16.0),
                child: context.isLight
                    ? SvgPicture.asset('asset/svg/light_mode.svg')
                    : SvgPicture.asset('asset/svg/dark_mode.svg'),
              )
            ]
          : [
              const SizedBox(
                width: 72,
              )
            ],
    );

좌측에는 로고를 우측에는 아이콘들을 배치시켜줬다.

 

leading의 width값과 actions의 width값을 똑같이 맞춰주면

제목을 화면 정중앙에 위치시킬 수 있다.

 

leadingWidth라는 프로퍼티를 이용해 leading의 width값을 72로 설정해줬고

actions의 width값은 Svg파일의 width값이 24인걸 이용하여 똑같이 72로 맞춰졌다.

 

 

 

이런식으로 나온다.

 

 

 

테마변경기능 구현

 

themes.dart

class Themes {
  static final light = ThemeData.light().copyWith(
    scaffoldBackgroundColor: LightModeColors.background,
    textSelectionTheme:
        const TextSelectionThemeData(cursorColor: LightModeColors.blue),
    textTheme: const TextTheme(
      bodyLarge: TextStyle(color: DarkModeColors.background),
      bodyMedium: TextStyle(color: DarkModeColors.background),
      bodySmall: TextStyle(color: DarkModeColors.background),
    ),
  );
  static final dark = ThemeData.dark().copyWith(
    scaffoldBackgroundColor: DarkModeColors.background,
    textSelectionTheme:
        const TextSelectionThemeData(cursorColor: DarkModeColors.blue),
    textTheme: const TextTheme(
      bodyLarge: TextStyle(color: LightModeColors.background),
      bodyMedium: TextStyle(color: LightModeColors.background),
      bodySmall: TextStyle(color: LightModeColors.background),
    ),
  );
}

라이트모드와 다크모드에서 사용할 테마를 각각 정의한다.

 

테마별로 배경색(Scaffold), 글자색, 텍스트필드 커서 색을 일괄적으로 설정해줬다.

 

 

main.dart

class SettingController with ChangeNotifier {
  ThemeMode themeMode = ThemeMode.system;
  String theme = '';

  Future<void> updateThemeMode(ThemeMode? newThemeMode, String newTheme) async {
    themeMode = newThemeMode!;
    theme = newTheme;
    notifyListeners();
  }
}

void main() {
  final settingController = SettingController();

  runApp(MyApp(settingController: settingController));
}

세팅화면이 따로 존재하지 않아 앱 설정 관련 컨트롤러는

main.dart에다 만들어 MyApp으로 넘겨주었다.

 

theme는 아이콘 출력하는 변수로써 사용되고

themeMode는 앱의 테마를 변경하는 변수로써 사용된다.

 

테마를 변경하는 함수에서 마지막에

notifyListners()를 활용해 상태변경을 앱에 알린다.

 

 

app.dart

class MyApp extends StatefulWidget {
  const MyApp({super.key, required this.settingController});

  final SettingController settingController;

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: widget.settingController,
        builder: (snapshot, context) {
          return MaterialApp(
            home: const SplashScreen(),
            theme: Themes.light,
            darkTheme: Themes.dark,
            themeMode: widget.settingController.themeMode,
            initialRoute: SplashScreen.routeName,
            onGenerateRoute: (RouteSettings routeSettings) {
              return route(routeSettings);
            },
          );
        });
  }

  MaterialPageRoute<void> route(RouteSettings routeSettings) {
    return MaterialPageRoute<void>(
        settings: routeSettings,
        builder: (BuildContext context) {
          switch (routeSettings.name) {
            case SplashScreen.routeName:
              return const SplashScreen();
            case HomeScreen.routeName:
              return HomeScreen(settingController: widget.settingController);
            default:
              return const Text('Error');
          }
        });
  }
}

MaterialApp을 AnimatedBuilder로 감싸준다.

상태관리가 필요한 모든 화면은 AnimatedBuilder로 감싸준다고 보면 된다.

 

라이트테마(theme)와 다크테마에 프로퍼티에 해당하는 테마를 할당해주고

세팅 컨트롤러의 themeMode를 themeMode프로퍼티에 할당해줌으로써

themeMode의 값에 따라 앱의 테마가 변경되도록 설정해준다.

 

컨트롤러를 넘겨받기 위해 라우팅 설정을 해주었다.

앱을 처음 실행 할 때 스플래쉬 화면으로 가도록 설정해주었고

 route함수에 루트네임 별로 이동할 화면을 설정해주었다.

 

 

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key, required this.settingController});
  
  // 루트네임 추가 
  static const routeName = '/home';

  final SettingController settingController;

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

이런 식으로 모든 화면에 겹치지 않도록 루트네임을 추가해주면 된다.

 

 

 

// 홈 화면에서 컨트롤러를 받아서 넘겨준다.
CommonAppBar(
        title: '메인화면',
        isHome: true,
        settingController: widget.settingController,
      ),

// 앱바에서 넘겨준 컨트롤러를 받는다.
class CommonAppBar extends StatefulWidget implements PreferredSizeWidget {
  const CommonAppBar(
      {super.key,
      required this.title,
      this.isHome = false,
      this.settingController});

  final String title;
  final bool isHome;
  final SettingController? settingController;

아까 루트에서 홈화면으로 컨트롤러를 넘겨주도록 설정했다.

거기서 넘겨받은 컨트롤러를 다시 앱바로 넘겨준다.

 

 

 

정리하자면 이렇게 된다.

 

 

Widget build(BuildContext context) {
    if (widget.isHome) {
      widget.settingController!.theme =
          context.isLight ? 'light_mode' : 'dark_mode';
    }

앱바에서 이 위치에 theme(String)를 초기화해준다.

initState에서 초기화하려 했는데 현재테마를 감지하는 변수가 문제를 일으켜

여기다가 변수를 초기화하게 되었다.

 

 

GestureDetector(
                onTap: () {
                  if (widget.settingController!.theme == 'light_mode') {
                    widget.settingController!
                        .updateThemeMode(ThemeMode.dark, 'dark_mode');
                  } else {
                    widget.settingController!
                        .updateThemeMode(ThemeMode.light, 'light_mode');
                  }
                },
                child: SvgPicture.asset(
                  'asset/svg/${widget.settingController!.theme}.svg',
                ),
              ),

아이콘을 눌렀을 때 현재 테마에 따라 초기화한 문자열을 기반으로

테마와 아이콘을 변경하도록 해준다.

 

 

테스트

 

 

정상적으로 앱의 테마가 잘 변하는 모습이다.

두 아이콘의 크기를 같게 해주는 것도 잊지말도록 하자.

 

 

 

깃허브

 

GitHub - hamond12/EscapeAnchovy: 운동일지 앱

운동일지 앱. Contribute to hamond12/EscapeAnchovy development by creating an account on GitHub.

github.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함