ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter | 간단 메모 앱 만들어보고 광고까지 달아서 배포하기 - 2. 메모 목록 메인 화면 만들기
    개발/Flutter 2023. 2. 11. 00:40
    반응형

    이제 진짜로 메모앱을 만들어 보겠습니다. 우선 디자인적으로는 직접 마음대로 하셔도 되지만 저는 윈도우의 스티커 메모를 따라서 해보겠습니다.

    제일 먼저 새로운 프로젝트를 만들어 보겠습니다.

    프로젝트 이름은 sticky_memo로 해주고 스마트폰으로만 출시 할것이기때문에 안드로이드와 ios를 선택해줍니다. 안드로이드 언어와 ios 언어는 기본적으로 kotlin과 swift로 선택되어 있는데 여기서는 건드리지 않을것이기때문 각 플랫폼의 기본 언어인 java와 objective-c로 변경해 주겠습니다.

    프로젝트를 생성한 후, android - app - build.gradle를 열어준 뒤 GradleException를 FileNotFoundException로 변경해줍니다. 전에 했던 기억을 되짚어보면 해당부분에 에러가 있어서 바꿔줬던것으로 기억합니다. 또 applicationId 에서 _ 언더바를 지워줍니다. 그리고 ios 출시를 위해서 ios - Runner.xcodeproj - project.pbxproj 폴더에서 PRODUCT_BUNDLE_IDENTIFIER 에서 뒷부분에 대문자로 되어있는 부분을 소문자로 변경해줍니다. 

    이제 화면을 만들어 보겠습니다. lib 폴더 아래에 page 폴더를 만들어 준 후, note_list_page.dart를 만들어 줍니다. 우선 화면이 노트에 변경이 있는 페이지이므로  stateful 위젯으로 만들어 줍니다. stf만 쓰면 자동으로 완성해주는 창이 뜹니다. 그리고 클래스 명을 NoteListPage로 해주시고 StatefulWidget에서 ctrl + 스페이스를 누르신뒤 material.dart인 부분을 선택해 주시면 상단에 import 되신것을 확인할 수 있습니다.

     

    import 'package:flutter/material.dart';
    
    class NoteListPage extends StatefulWidget {
      const NoteListPage({Key? key}) : super(key: key);
    
      @override
      State<NoteListPage> createState() => _NoteListPageState();
    }
    
    class _NoteListPageState extends State<NoteListPage> {
      @override
      Widget build(BuildContext context) {
        return const Placeholder();
      }
    }
    

    그리고 Placeholder 을 Scaffold로 변경해준뒤 appBar에 AppBar, 그 안에 title 속성에 text로 Sticky Memo를 넣어주겠습니다.

    class _NoteListPageState extends State<NoteListPage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Sticky Memo'),
          ),
        );
      }
    }

    이제 메인화면에 띄우기 위해서 main.dart로 돌아가서 하단의 MyHomePage를 구성하는 부분을 전부 삭제 해준 뒤 home 속성에 NoteListPage를 넣어주시면 됩니다.

    import 'package:flutter/material.dart';
    import 'package:sticky_memo/page/note_list_page.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const NoteListPage(),
        );
      }
    }

    이제 앱을 실행해 보면 정상적으로 화면이 나와있는것을 확인 할 수 있습니다.

    여기서 색상을 꾸며주기 위해서 ThemeData 안에 primarySwatch 속성에 Colors를 추가해 주시면 됩니다. 저는 blue로 변경해 보겠습니다. * 이전에 Material3이 적용되기 전에는 ThemeData의 primarySwatch만 변경해주면 앱의 전체 컬러가 따로 지정하지 않는이상 통일되었는데 Material3를 사용하면서 현재까지 코드로는 색이 변경되지 않는다.

    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const NoteListPage(),
        );
      }

    하지만 전 Material3를 사용할 것이기때문에 다시 원래대로 돌려놓겠습니다. 그리고 note_list_page에 가서 AppBar안에 backgroundColor에 값에 Colors.blue로 값을 주겠습니다.

    이제 화면에 나타내는 정보를 담을 dart파일을 새로 만들어 주겠습니다. 우선 새로운 data 폴더를 lib 폴더 아래에 만들어 준 뒤, note.dart를 만들어 줍니다. 그리고 Note라는 클래스를 만들어 준 뒤 String title과 String body를 작성해줍니다. 그리고 두개의 앞에 final을 넣어줘서 값이 입력된 후에 변경되지 않게 해줍니다.

    class Note{
      final String title;
      final String body;
    }

    이러면 title과 body에 빨간줄이 나올텐데 생성자가 없기때문에 생긴것으로 생성자를 만들어 주겠습니다. class명과 똑같이 만들수 있습니다. Note();의 안에 값을 넣어주시면 됩니다. 제목없이 내용만으로 저장할 수 있도록 title은 선택으로 값을 받게 해주겠습니다. title은 값을 넣지 않아도 기본으로 '' 값이 넣어지도록 하겠습니다.

    Note(this.body,{this.title = '',});

    위와 같이 {}안에 넣어주면 그 안에 값은 네임드파라미터로 사용하는 것인데 보통 생성자는 순서대로 값을 넣어야 하는데 이때 직접 title을 불러서 값을 집어 넣으면 순서에 상관없이 쓸 수 있습니다. null-safety를 유지하기 위해 앞에 @required 를 넣어주거나 위와같이 기본값을 넣어주면 됩니다.

    class Note{
      final String title;
      final String body;
      Note(this.body,{this.title = '',});
    }

    클래스의 생성은 끝났습니다. 이제 이 데이터를 보여줘야 되니 note_list_page.dart로 이동에서 Scaffold 안에 body를 만들어 주겠습니다. 하단의 마지막 } 전에 보여줄 위젯을 만들어 보겠습니다. 메모 하나를 보여주는 위젯으로 위젯명은 _buildCard로 만들고 Note값을 받아오겠습니다. 그리고 return으로 Card 위젯으로 보여주겠습니다. 각 카드모서리와 떨어져 보여야 하기 때문에 Card 안에 Padding으로 12의 값을 주고 child에 Column으로 아래로 늘어난 형태로 보여주겠습니다. children에 우선 제목부터 보여주기 위해서 Text위젯을 사용하고 note.title.isEmpty를 사용해서 제목이 없으면 (제목없음) 있으면 note.title이 보여주게 설정하고 TextStyle를 fontsize는 16, fontWeight는 bold를 사용하겠습니다. 그리고 SizedBox를 height를 16을 줘서 제목과 본문이 떨어져보이게 해줍니다. 그 밑에 본문을 보여주기 위해서 Text를 사용해서 note.body로 내용을 보여줍니다. 너무 길지 않게 overflow를 fade를 줘서 위젯의 크기가 너무 커지지 않고 크기만큼만 글 내용이 보이게 해줍니다. 또한 각 Card가 너무 많은 범위를 가져가지 않고 위젯의 크기만큼만 차지하게 하기 위해 mainAxisSize를 MainAxisSize.min으로 해줍니다. 그리고 좌측 정렬을 위해서 crossAxisAlignment도 CrossAxisAlignment.start로 지정해줍니다.

    Widget _buildCard(Note note) {
      return Card(
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                note.title.isEmpty ? '(제목없음)' : note.title,
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(
                height: 16,
              ),
              Expanded(
                child: Text(
                  note.body,
                  overflow: TextOverflow.fade,
                ),
              ),
            ],
          ),
        ),
      );
    }

    이제 이 위젯을 보여줄 곳을 Scaffold에 body에 만들어 주겠습니다. 저는 GridView 위젯을 사용해서 GridView형태로 보여주겠습니다. gridDelegate 옵션은 gridview가 어떤형태로 보여주는지 몇개의 가로갯수로 보여줄지 정하는 속성입니다. 저는 가로 2개만 보여줄 것이기 때문에 SliverGridDelegateWithFixedCrossAxisCount를 사용하겠습니다. 다른 SliverGridDelegate 도 있는데 직접 사용해보시거나 공식문서를 보시면서 해보시면 쉽게 이해하실수 있습니다. 저는 crossAxisCount를 2를 해주고 비율은 정사각형으로 보여주기 위해서 childAspectRaio를 1로 하겠습니다. 가로 / 세로 의 비율로 사용하시면 됩니다. 그리고 각 카드마다 떨어져 있어야 하기 때문에 padding을 EdgeInsets.symmetric 사용해서 가로 horizontal을 12, 세로 vertical을 16을 주겠습니다.  이제 children 안에 값을 넣어주면 됩니다. 하지만 하나하나 넣어도 되지만 List로 값을 받아와서 만드는게 코드도 깔끔하고 편합니다. 그러므로 _buildCard 위젯 위에 List<Widget> _buildCards를 만들어 줍니다. notes는 Note의 List 값을 넣어주기 위해서 <Note>[]를 넣어줍니다. 그리고 이 list를 다른 항목으로 바꿀때는 map을 사용하는데 notes.map이라 작성하면 자동완성이 되서 notes.map((e) => null);나오는것을 볼 수 있습니다. note값으로 불러와서 _buildCard에 넣어준 값을 List형태로 바꿔주고 return해주면 되겠습니다.

    List<Widget> _buildCards(){
      final notes = <Note>[];
      return notes.map((note) => _buildCard(note)).toList();
    }

    children에 _buildCards()를 넣어주면 되겠습니다. 여기까지한 뒤 실행해봐도 아무것도 나오지 않는데 그 이유는 note 데이터가 없기 때문입니다. 우선 샘플 코드를 넣어서 확인해 보겠습니다. _buildCards()에 notes를 변경해 주시면 됩니다.

    List<Note> notes = [
    	  Note('Flutter 공부하기'),
          Note('금주'),
          Note(
            '''
    라면
    맥주
    육포
    휴지
          ''',
            title: '장보기 목록',
          ),
          ];

    실행하면 화면이 잘 나오는것 확인할 수 있습니다.

    이제 각 메모에 색상을 넣어보겠습니다. 색상은 아래의 색을 사용하겠습니다.

    static const colorDefault = Colors.white;
    
    static const colorRed = Color(0xFFFFCDD2);
    
    static const colorOrange = Color(0xFFFFE0B2);
    
    static const colorYellow = Color(0xFFFFF9C4);
    
    static const colorLime = Color(0xFFF0F4C3);
    
    static const colorBlue = Color(0xFFBBDEFB);

    class Note 안에 넣어주겠습니다. 그러면 빨간줄이 나오는데  Alt + Enter을 눌러서 import 시켜주시면 됩니다. 그리고 final Color color;을 만들어 준 뒤, 생성자의 중괄호 안에 넣어주고 기본값이 color.Default로 지정해 줍니다.

    import 'package:flutter/material.dart';
    
    class Note {
      static const colorDefault = Colors.white;
    
      static const colorRed = Color(0xFFFFCDD2);
    
      static const colorOrange = Color(0xFFFFE0B2);
    
      static const colorYellow = Color(0xFFFFF9C4);
    
      static const colorLime = Color(0xFFF0F4C3);
    
      static const colorBlue = Color(0xFFBBDEFB);
    
      final String title;
      final String body;
      final Color color;
    
      Note(
        this.body, {
        this.title = '',
        this.color = colorDefault,
      });
    }
    

    이제  Card에 color를 넣어주겠습니다. note_list_page에 Card에서 color 속성에 note.color를 넣어주시면 됩니다. 그리고 샘플 notes 데이터에 색들을 넣어주시면 됩니다.

        List<Note> notes = [
          Note('Flutter 공부하기',color:Note.colorLime,),
          Note('금주',color:Note.colorBlue,),
          Note(
            '''
    라면
    맥주
    육포
    휴지
          ''',
            title: '장보기 목록',
            color: Note.colorRed,
          ),
        ];

    이제 화면을 확인해 보면 색이 바뀐것을 확인할 수 있습니다.

    지금까지 메모 리스트를 보여주는 메인화면 구성을 해봤습니다. 이제 다음에는 메모를 작성하는 페이지를 만들어 보도록 하겠습니다.

    지금까지의 전체 코드 입니다.

    main.dart

    import 'package:flutter/material.dart';
    import 'package:sticky_memo/page/note_list_page.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const NoteListPage(),
        );
      }
    }

    note_list_page.dart

    import 'package:flutter/material.dart';
    import 'package:sticky_memo/data/note.dart';
    
    class NoteListPage extends StatefulWidget {
      const NoteListPage({Key? key}) : super(key: key);
    
      @override
      State<NoteListPage> createState() => _NoteListPageState();
    }
    
    class _NoteListPageState extends State<NoteListPage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Sticky Memo'),
            backgroundColor: Colors.blue,
          ),
          body: GridView(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 1,
            ),
            padding: const EdgeInsets.symmetric(
              horizontal: 12,
              vertical: 16,
            ),
            children: _buildCards(),
          ),
        );
      }
    
      List<Widget> _buildCards() {
        List<Note> notes = [
          Note('Flutter 공부하기',color:Note.colorLime,),
          Note('금주',color:Note.colorBlue,),
          Note(
            '''
    라면
    맥주
    육포
    휴지
          ''',
            title: '장보기 목록',
            color: Note.colorRed,
          ),
        ];
        return notes.map((note) => _buildCard(note)).toList();
      }
    
      Widget _buildCard(Note note) {
        return Card(
          color: note.color,
          child: Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  note.title.isEmpty ? '(제목없음)' : note.title,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(
                  height: 16,
                ),
                Expanded(
                  child: Text(
                    note.body,
                    overflow: TextOverflow.fade,
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    note.dart

    import 'package:flutter/material.dart';
    
    class Note {
      static const colorDefault = Colors.white;
    
      static const colorRed = Color(0xFFFFCDD2);
    
      static const colorOrange = Color(0xFFFFE0B2);
    
      static const colorYellow = Color(0xFFFFF9C4);
    
      static const colorLime = Color(0xFFF0F4C3);
    
      static const colorBlue = Color(0xFFBBDEFB);
    
      final String title;
      final String body;
      final Color color;
    
      Note(
        this.body, {
        this.title = '',
        this.color = colorDefault,
      });
    }
    
    반응형

    댓글

Designed by Tistory.