[Flutter] 스나이퍼팩토리 18일차

KWANWOO·2023년 2월 17일
0
post-thumbnail

스나이퍼팩토리 플러터 18일차

18일차에서는 스나이퍼 팩토리의 비밀듣는 고양이라는 백엔드 서버를 사용하여 비밀듣는 고양이 앱을 제작했다.

학습한 내용

  • 비밀듣는 고양이 앱 제작

추가 내용 정리

Flutter에 폰트 적용하는 법

먼저 사용할 폰트를 다운로드하고, 프로젝트의 최상위 폴더에 assets > fonts 폴더를 만들어 파일을 저장한다.

다음으로 pubspec.yaml 파일에서 font:라고 작성된 부분에 아래와 같이 작성한다.

  fonts:
    - family: 폰트명
      fonts:
        - asset: assets/fonts/폰트명-이탈릭.ttf
          weight: 100
          style: italic

해당 파일에서 weightstyle도 작성할 수 있다.

폰트는 family 속성에 작성한 이름으로 적용시킬 수 있다.

전체 앱에 적용시키고 싶다면 아래와 같이 MaterialApp의 속성으로 작성하면 된다.

class MyApp extends StatelessWidget{
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {

    return MaterialApp(
      home: MyHome(),
      theme: ThemeData(fontFamily: '적용할 폰트 이름'),
      themeMode: ThemeMode.system,
    );
  }
}

별도로 지정하고 싶다면 위젯의 TextStyle에서 fontFamily 속성에 설정할 수 있다.

스낵바(Snack bar)

스낵바는 화면 하단에 간단하게 메세지를 띄우는 것이다.

스낵바를 구현하기 위새서는 ScaffoldMessenger.of(context)함수와 그 뒤에 ShowSnackBar()를 사용하면 된다. 이 메소드는 반드시 Scaffold의 위치를 참조한 뒤에 사용해야 한다.

  • ScaffoldMessenger.of(context)의 의미
    주어진 context에서 위로 올라가면서 가장 가까운 scaffold를 찾아서 반환

아래는 스낵바의 예시 코드이다.

TextButton(
	child: Text(
    	'show me'
    ),
    onPressed () {
    	ScaffoldMessenger.of(context).showSnackBar(
        	SnackBar(
            	context: Text('This is a SnackBar!')
            )
        );
    },
),


18일차 과제

  1. 비밀듣는 고양이 앱 제작
  2. 추가 사항

1. 비밀듣는 고양이 앱 제작

  • Assignment
    • 제공되는 패키지 secrets_cat_sdk를 활용하여 다음의 기대 결과물을 따라 만드세요.
    • 이번 과제는 최대한 다음의 결과물과 다른 디자인으로 제작하는데 목표를 두세요.

  • Requirements
    • 앱 이름은 [비밀듣는 고양이]가 아닌 다른 이름으로 진행하세요.
    • 앱 내에서 사용될 폰트는 다음과 같습니다. neo.ttf
    • 매인 캐릭터 또한 다음의 링크에서 마음에 드는 이미지를 골라서 진행하세요.
    • 페이지들의 배경이미지는 다음의 링크에서 마음에 드는 이미지를 골라서 진행하세요.
    • 각 위젯별 애니메이션은 최소 3개 이상이 적용되어야 합니다. 이 때 적용되는 애니메이션은 자유입니다.
    • 페이지는 3개 이상입니다. 필수 페이지는 다음과 같습니다.
      • SecretPage : 비밀을 볼 수 있는 페이지며, 모든 비밀을 데이터로 불러오며 각 비밀은 페이지로 이루어짐.
      • AuthorPage : 모든 작성자(회원)을 볼 수 있는 페이지
      • UploadPage: 비밀을 업로드할 수 있는 페이지
    • 페키지를 설치하면 Author와 Secret 데이터타입을 사용할 수 있습니다.
      데이터와 데이터타입을 활용하여 최대한 위 결과물의 비슷하게 앱을 만들어보세요.

코드 작성

  • pubspec.yaml
dependencies:
  secret_cat_sdk: ^0.0.5+2
  animate_do: ^3.0.2
  
  assets:
    - assets/images/piggy-bank.png
    - assets/images/background_image.jpg
    
  fonts:
    - family: Neo
      fonts:
        - asset: assets/fonts/neo.ttf

pubspec.yaml에 사용할 패키지 secret_cat_sdkanimate_do를 설치하고 이미지 파일을 등록했다. 그리고 Neo 폰트를 사용하기 위해 설정해 주었다.

  • main.dart
import 'package:flutter/material.dart';
import 'package:secret_app/page/home_page.dart';

void main() {
  runApp(const SecretApp());
}

class SecretApp extends StatelessWidget {
  const SecretApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(fontFamily: 'Neo'),
      home: HomePage(),
    );
  }
}

main.dart에서는 HomePage 위젯을 호출한다.

  • home_page.dart
import 'package:flutter/material.dart';
import 'package:secret_app/page/author_page.dart';
import 'package:secret_app/page/secret_page.dart';
import 'package:secret_app/page/upload_page.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  //페이지 이동 라우트 생성
  Route _createRoute(Widget page) {
    return PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) => page,
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        var begin = Offset(1.0, 0.0); //오른쪽 위 시작 지점
        var end = Offset.zero; //왼쪽 위 끝 지점
        var curve = Curves.easeIn;

        var tween =
            Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

        return SlideTransition(
          position: animation.drive(tween),
          child: child,
        );
      },
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                color: Colors.greenAccent,
                shape: BoxShape.circle,
              ),
              clipBehavior: Clip.antiAlias,
              child: Image.asset('assets/images/piggy-bank.png'),
            ),
            SizedBox(height: 8),
            Text(
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 40,
                color: Colors.white,
              ),
              '비밀 저금통',
            ),
            SizedBox(height: 48),
            ListTile(
              tileColor: Colors.white70,
              title: Text('비밀 보기'),
              subtitle: Text('놀러가기'),
              trailing: Image.asset('assets/images/piggy-bank.png'),
              onTap: () =>
                  Navigator.of(context).push(_createRoute(SecretPage())),
            ),
            SizedBox(height: 24),
            ListTile(
              tileColor: Colors.white70,
              title: Text('작성자들 보기'),
              subtitle: Text('놀러가기'),
              trailing: Image.asset('assets/images/piggy-bank.png'),
              onTap: () =>
                  Navigator.of(context).push(_createRoute(AuthorPage())),
            ),
            SizedBox(height: 24),
            ListTile(
              tileColor: Colors.white70,
              title: Text('비밀 공유'),
              subtitle: Text('놀러가기'),
              trailing: Image.asset('assets/images/piggy-bank.png'),
              onTap: () =>
                  Navigator.of(context).push(_createRoute(UploadPage())),
            ),
          ],
        ),
      ),
    );
  }
}

우선 HomePage에서는 페이지 이동의 라우트를 생성할 _createRoute() 함수를 작성했는데 페이지 이동 시 페이지가 오른쪽에서 왼쪽으로 슬라이딩 되도록 애니메이션을 적용했다.

본문에서는 Column으로 둥근 이미지와 제목 텍스트, 세개의 리스트 타일을 넣어 각 리스트 타일을 눌렀을때 맞는 페이지로 이동하도록 onTap 이벤트를 작성했다.

  • secret_page.dart
import 'package:flutter/material.dart';
import 'package:secret_cat_sdk/api/api.dart';
import 'package:animate_do/animate_do.dart';

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

  
  State<SecretPage> createState() => _SecretPageState();
}

class _SecretPageState extends State<SecretPage> {
  late Future result; // 데이터 가져오기 결과
  var pageController = PageController();

  
  void initState() {
    super.initState();
    result = SecretCatApi.fetchSecrets();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        title: Text('뒤로가기'),
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Stack(
        children: [
          Image.asset(
            height: double.infinity,
            fit: BoxFit.cover,
            'assets/images/background_image.jpg',
          ),
          FutureBuilder(
            future: result,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                return PageView.builder(
                  controller: pageController,
                  itemCount: snapshot.data.length,
                  itemBuilder: ((context, index) {
                    return Padding(
                      padding: const EdgeInsets.all(32.0),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          FadeInRight(
                            child: Image.asset(
                              width: 80,
                              height: 80,
                              'assets/images/piggy-bank.png',
                            ),
                          ),
                          SizedBox(height: 16),
                          SlideInRight(
                            child: Text(
                              style: TextStyle(
                                color: Colors.white,
                                fontWeight: FontWeight.bold,
                              ),
                              textAlign: TextAlign.center,
                              snapshot.data[index].secret.toString(),
                            ),
                          ),
                          SizedBox(height: 16),
                          if (snapshot.data[index].author != null)
                            ElasticInRight(
                              child: Text(
                                  style: TextStyle(
                                    color: Colors.white,
                                    fontWeight: FontWeight.bold,
                                  ),
                                  snapshot.data[index].author.username
                                      .toString()),
                            ),
                          if (snapshot.data[index].author == null)
                            ElasticInRight(
                              child: Text(
                                style: TextStyle(
                                  color: Colors.white,
                                  fontWeight: FontWeight.bold,
                                ),
                                '익명',
                              ),
                            ),
                        ],
                      ),
                    );
                  }),
                );
              }
              return Center(child: CircularProgressIndicator());
            },
          ),
        ],
      ),
    );
  }
}

SecretPage는 비밀을 볼 수 있는 페이지로 initState()에서 데이터를 가져와 result에 저장했다.

Stack 위젯으로 본문에 배경 이미지를 넣고, 그 위에 비밀들을 출력할 수 있도록 FutureBuilder를 사용했다. FutureBuiderfuture에는 앞서 불러왔던 데이터인 result를 연결 시키고, 각 비밀을 PageView로 출력했다.

내부의 이미지와 글씨들은 animate_do 패키지를 사용하여 각각 다른 애니메이션을 적용해 보았다.

  • author_page.dart
import 'package:animate_do/animate_do.dart';
import 'package:flutter/material.dart';
import 'package:secret_cat_sdk/api/api.dart';

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

  
  State<AuthorPage> createState() => _AuthorPageState();
}

class _AuthorPageState extends State<AuthorPage> {
  late Future result;

  
  void initState() {
    super.initState();
    result = SecretCatApi.fetchAuthors();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        title: Text('뒤로가기'),
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Stack(
        children: [
          Image.asset(
            height: double.infinity,
            fit: BoxFit.cover,
            'assets/images/background_image.jpg',
          ),
          FutureBuilder(
            future: result,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                return GridView.builder(
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 3,
                  ),
                  itemCount: snapshot.data.length,
                  itemBuilder: ((context, index) {
                    return Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Column(
                        children: [
                          Expanded(
                            child: ZoomIn(
                              child: Image.network(snapshot.data[index].avatar),
                            ),
                          ),
                          SizedBox(height: 8),
                          Pulse(
                            child: Text(
                              style: TextStyle(color: Colors.white),
                              snapshot.data[index].name.toString(),
                            ),
                          ),
                        ],
                      ),
                    );
                  }),
                );
              }
              return Center(child: CircularProgressIndicator());
            },
          ),
        ],
      ),
    );
  }
}

해당 페이지는 글을 쓴 사람들을 볼 수 있는 페이지로 initState()에서 데이터를 가져와 result에 넣었다.

이번에도 Stack 위젯을 사용하여 배경 이미지를 넣고 그 위에 FutureBuilder를 사용해 데이터를 출력해주었다.

글을 쓴 사람들은 GridView.builder를 사용해 만들었다. 각 요소에는 animate_do 패키지의 다른 애니메이션을 적용해 보았다.

  • upload_page.dart
import 'package:flutter/material.dart';
import 'package:secret_cat_sdk/api/api.dart';

class UploadPage extends StatelessWidget {
  UploadPage({super.key});

  var textController = TextEditingController();

  
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        title: Text('뒤로가기'),
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Stack(
        children: [
          Image.asset(
            height: double.infinity,
            fit: BoxFit.cover,
            'assets/images/background_image.jpg',
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                TextField(
                  controller: textController,
                  maxLines: 6,
                  decoration: InputDecoration(
                    hintText: '비밀을 입력하세요',
                    fillColor: Colors.white12,
                    filled: true,
                    border: InputBorder.none,
                    focusedBorder: OutlineInputBorder(
                      borderSide: BorderSide(
                        color: Colors.green,
                        width: 2,
                      ),
                    ),
                  ),
                ),
                SizedBox(height: 8),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.green,
                    ),
                    onPressed: () async {
                      await SecretCatApi.addSecret(textController.text);
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text('비밀 공유 완료!'),
                        ),
                      );
                    },
                    child: Text('비밀공유'),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

업로드 페이지에서도 같은 방식으로 이미지 배경을 넣었고, 그 위에 TextFieldElevatedButton을 만들었다.

TextField에는 TextEditingController()를 생성하여 연결해 주었다.

ElevatedButton에는 onPressed 이벤트에 서textController.text에 적힌 내용을 업데이트 시키는 작업을 수행했고, 작업이 끝난 뒤 비밀 공유 완료를 표시하는 스낵바를 띄워주었다.

결과

2. 추가 사항

과제로 개발을 진행하고, 강의를 수강한 뒤 아쉬웠던 점을 정리하고자 한다.

Stateless Stateful 에서 FutureBuilder를 사용할 때 데이터 불러오기

[Flutter] 스나이퍼팩토리 16일차에서 네트워크 데이터를 initState에서 불러오는 이유를 작성한 적이 있는데 그 이유는 build가 자주 불리게 되기 때문에 필요없는 API 요청을 막기 위해서 였다.

하지만 화면을 다시 그릴 필요 없는 Stateless 위젯은 build를 다시 호출하지 않기 때문에 굳이 Stateful 위젯의 initState를 사용하지 않고 FutureBuilderfuture에 바로 바인딩 해도 된다.

즉, 앞의 코드에서 SecretPageAuthorPage는 화면을 다시 그릴 필요가 없기 때문에 성능이 더 좋은 Stateless로 만드는 것이 더 좋았을 것이다.

Container의 colorFilter

직접 작성한 코드에서는 Statk 위젯을 사용해 Image로 배경 이미지를 넣었다. 하지만 결과물 예시를 자세히 보면 배경 이미지가 약간 어둡게 처리된 것을 알 수 있다.

ContainercolorFilter 속성을 사용하면 배경을 어둡게 처리할 수 있다. 아래는 예시 코드이다.

Container(
    alignment: Alignment.center,
    decoration: BoxDecoration(
      image: DecorationImage(
        image: NetworkImage(backgroundImg),
        fit: BoxFit.cover,
        colorFilter: ColorFilter.mode(
          Colors.black54,
          BlendMode.darken,
        ),
      ),
    ),
    ...

animate_do의 delay 속성

animate_dodelay 속성을 사용하면 애니메이션이 적용되는 딜레이를 설정할 수 있다. 결과물 예시에서 AuthorPage의 글쓴이 정보들은 하나씩 딜레이를 가지고 출력된다. 하지만 코드를 작성할 때 해결 방법을 찾지 못해 동시에 애니메이션을 적용했다. 여기에 delay 속성을 사용하면 되는데 각 정보들이 index를 가지고 있으므로 이를 사용하여 딜레이를 다르게 적용할 수 있다. 아래의 코드는 예시 코드이다.

...
return GridView.builder(
            itemCount: snapshot.data?.length,
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
            ),
            itemBuilder: (context, index) {
              return ZoomIn(
                delay: Duration(milliseconds: 200 * index),
                child: Column(
                  children: [
                    CircleAvatar(
                      radius: 48,
                      backgroundImage:
                          NetworkImage(snapshot.data![index].avatar!),
                    ),
                    SizedBox(
                      height: 8,
                    ),
                    Text(
                      snapshot.data![index].username,
                      style: TextStyle(color: Colors.white),
                    ),
                  ],
                ),
              );
            },
          );
          ...

해당 코드는 FutureBuilder 내에서 GridView를 사용한 것인데 ZoomIn 애니메이션을 사용할 때 delayDuration(millisecons: 200 *index)로 설정해 각 요소에 딜레이를 다르게 설정한 것을 알 수 있다.

null 확인

UploadPage에서 데이터를 ElevatedButton으로 업로드 할 때 TextEditingControllernull을 체크하지 않고 업로드를 하도록 작성했었다. 아래의 코드처럼 if문을 사용해 null이 아닐 경우에만 업로드를 하도록 작성하는 것이 더 좋다.

...
ElevatedButton(
            onPressed: () async {
              if (controller.text != '') {
                var secret = await SecretCatApi.addSecret(controller.text);
                if (secret != null) {
                  Navigator.pop(context);
                  ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text("비밀공유성공! ${secret.secret}")));
                }
              }
            },
            child: Text("업로드하기"),
          )
          ...

또한 위의 코드에서 업로드를 한 뒤에 정상적으로 업로드가 되었으면 Navigator.pop(context)를 사용해 뒤로 자동으로 이동하도록 기능을 추가하는 것도 좋은 방법이다.


시간이 좀 모자라다...ㅠㅠ

이번 과제는 2시까지 제출을 해야해서 시간이 조금 모자라다...ㅠ 최대한 만들 수 있는 부분까지 해 봤는데 시간이 없어서 완벽하게 만들지는 못한 것 같다. 다음에 따로 좀 더 수정을 해야겠다. 그리고 일단은 2시까지 제출을 해야해서 추가 내용 정리는 없습니다. (나중에 추가 내용 정리를 작성한다면 플러터에 폰트 적용하는 법을 적을까 생각중...)

  • (강의 듣고 작성했던 코드의 아쉬웠던 점이랑, 추가 내용 정리로 폰트 적용법과 스낵바에 대한 내용을 추가했습니다!)

📄 Reference

profile
관우로그

0개의 댓글

관련 채용 정보