[flutter] 9주 완성 프로젝트 캠프 과제일지 (유데미x스나이퍼팩토리): 페이지네이션, 맨위로이동, 미러링, 토글

KoEunseo·2023년 9월 22일
0

flutter

목록 보기
14/45


과제폭탄이 떨어졌다 으악! 💣

과제 화면 모음

과제1: 페이지네이션

다음과 같이 버튼 [1번 과제], [2번 과제], [3번 과제]를 구성하고, 클릭 시 과제 페이지로 이동하도록 만드세요.
( 대충 버튼 세개가 세로로 나란히 있는 짤 )

  • 1번 과제를 클릭하면, 1번 과제의 내용 페이지로 이동됩니다.
  • 2번 과제를 클릭하면, 2번 과제의 내용 페이지로 이동됩니다.
  • 3번 과제를 클릭하면, 3번 과제의 내용 페이지로 이동됩니다.

구현화면

과제2: 맨 위로 이동하기 버튼

ScrollController를 활용하여 가장 상단으로 이동하는 기능을 구현합니다.

  • ListView.builder 위젯을 활용하여 높이가 300인 동물 위젯을 생성합니다.
    • 이 때, 사용되는 데이터는 다음과 같습니다.
      List animalList = ['강아지', '고양이', '앵무새', '토끼', '오리', '거위', '원숭이'];
  • 하단의 FAB(FloatingActionButton)을 누르면, 스크롤 위치가 최상단으로 이동되게합니다.
    • 이 때, 사용되는 아이콘 명은 다음과 같습니다.

구현

ListView.builder를 쓰려고 보니 itemBuilder가 필수다.
그놈의 context를 첫번째 매개변수로 보내야한다.
두번째 매개변수로는 int를 보낸다.
습관적으로 map을 쓰려다가 플러터 문서를 보니 굳이 map을 쓸 필요는 없어 보인다. 인덱스가 있으니까...

final List<String> entries = <String>['A', 'B', 'C'];
final List<int> colorCodes = <int>[600, 500, 100];

Widget build(BuildContext context) {
  return ListView.builder(
    padding: const EdgeInsets.all(8),
    itemCount: entries.length,
    itemBuilder: (BuildContext context, int index) {
      return Container(
        height: 50,
        color: Colors.amber[colorCodes[index]],
        child: Center(child: Text('Entry ${entries[index]}')),
      );
    }
  );
}

rangeError 발생!


itemCount도 함께 전달해야한다고 한다.
이쯤 써보니까 플러터에서는 사용할 공간을 지정하고 확보하는 게 중요한 것 같다는 느낌이 든다. 아무래도 모바일 기기에서 쓸 어플을 주로 개발하니까 최적화를 위해서 그런걸까?

ListView.builder

ListView.builder(
  itemCount: animalList.length,
  itemBuilder: (BuildContext context, int i) {
  return SizedBox(
    height: 300,
      child: Center(
      child: Text(animalList[i]),
      ),
    );
  },
),

top button

ScrollController를 사용한다.
스크롤 컨트롤러에서 animateTo라는 속성이 있다.
현재 값에서 지정된 값까지 위치를 애니메이션한다고 함.(flutter doc)
이걸로 컨트롤하면 됨!

근데 매개변수가 좀 많다. 셋다 필수임. 애니메이션하는 요소에 duration, curve는 거의 필수로 들어가는 것 같고, 스크롤 좌표를 지정하는거라 offset도 여기선 포함된 듯.

  • offset: 어디로
  • duration: 얼마동안
  • curve: 어떤 식으로
  final scrollController = ScrollController();
  void scrollToTop() {
    scrollController.animateTo(
      0,
      duration: const Duration(
        milliseconds: 300,
      ),
      curve: Curves.easeIn,
    );
  }

근데 scrollToTop에서 컨트롤러를 쓰려다보니 scrollController를 최상단에 썼는데, 이게 맞는걸까?

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

  final List animalList = ['강아지', '고양이', '앵무새', '토끼', '오리', '거위', '원숭이'];
  var scrollController = ScrollController();
  void scrollToTop() {
    scrollController.animateTo(
      0,
      duration: const Duration(
        milliseconds: 300,
      ),
      curve: Curves.easeIn,
    );
  }

이런 상황인데, 일단 리스트를 선언하면서 super.key 부분에 const를 삭제하게 됐다. List를 final로 지정했는데도 불구하고 const를 지워야하는 이유가 뭘까?

그리고 지금 노랗게 에러는 아니지만 경고사인이 나고 있는데,
This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: FirstAssignment.scrollController
라고 한다...

@immutable이라는 키워드를 쓴 곳이 없는데? final로 선언하라는 얘기 같아서 final scrollController = ScrollController();로 바꾸니 해결됐다.

과제3: Text 미러링

입력된 텍스트 미러링하는 화면을 제작합니다.

  • TextField에 입력시, 바로 밑에 위치한 하단의 Text위젯에 똑같이 적용되도록 합니다.

  • FAB(FloatingActionButton)을 클릭하면, 작성중이던 모든 내용이 사라집니다.

    • 이 때, 사용되는 아이콘 명은 다음과 같습니다.
      Icons.close

    구현

    statefull widget으로 생성.

    textController의 input 값을 먼저 var userTxt = ''; 이렇게 변수로 따로 초기화해 선언했다.

    일단 onChanged로 textController의 변화를 감지하지 않으면 Text에서 아무리 textController.text를 찍어도 미러링이 되지 않는다.
    필자는 getText라는 함수를 따로 만들어서 onChanged에 연결시켰는데, 변화가 생길때마다 setState를 해준다.

class _SecondAssignmentState extends State<SecondAssignment> {
  var textController = TextEditingController();

  var userTxt = '';
  void getText(val) {
    userTxt = textController.text;
    setState(() {});
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TextField(
            controller: textController,
            onChanged: getText,
          ),
          Text(userTxt),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          textController.clear();
          userTxt = '';
          setState(() {});
        },
        child: const Icon(
          Icons.close,
        ),
      ),
    );
  }
}

과제4: toggle

다음의 UI를 구성하고 각각의 조건에 맞추어 코딩하시오.

  • Sun, Moon, Star라는 값이 있으며,
    오른쪽의 버튼을 눌렀을 때, 스위칭이 각각 될 수 있도록 함.
  • 이 때 스위칭이란, 활성화 여부를 뜻하며
    불이 들어와 있을 땐 끄고, 꺼져있을 땐 켜는 것을 뜻함.
  • FAB를 클릭하면 모든 활성화되어있는 아이콘이 비활성화됨.

구현 : menuTile

탭 아이템을 menuTile이라고 이름지어 만들었다.
하나하나 치고 복붙하고 귀찮다구~~

import 'package:flutter/material.dart';

class MenuTile extends StatefulWidget {
  const MenuTile(
      {super.key,
      required this.icon,
      required this.title,
      required this.color});
  final IconData icon;
  final String title;
  final Color color;

  
  State<MenuTile> createState() => _MenuTileState();
}

class _MenuTileState extends State<MenuTile> {
  bool isActive = false;

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        isActive = !isActive;
        setState(() {});
      },
      child: ListTile(
        leading: Icon(
          widget.icon,
          color: isActive ? widget.color : Colors.grey,
        ),
        title: Text(widget.title),
        trailing: const Icon(Icons.navigate_next),
      ),
    );
  }
}

구현: 과제4

이렇게 list를 일단 만들고 map으로 돌렸다.

  final List<Map<String, String>> menus = [
    {
      "icon": sunny,
      "title": "Sun",
      "activeColor": red,
    },
    {
      "icon": nightlight,
      "title": "Moon",
      "activeColor": yellow,
    },
    {
      "icon": star,
      "title": "Star",
      "activeColor": yellow,
    },
  ];

이때 문제가 하나 발생했으니...
String을 메뉴타일에서 제대로 받지 못했다. String이라 그런가ㅠㅠ
리스트를 만들때부터 아예 IconData, Color 타입으로 바꾸었더니 제대로 동작했다. 리스트 타입은 List<Map<String, dynamic>> 으로 됨.

import 'package:assignment1/MenuTile.dart';
import 'package:flutter/material.dart';

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

  
  State<LastAssignment> createState() => _LastAssignmentState();
}

class _LastAssignmentState extends State<LastAssignment> {
  final List<Map<String, dynamic>> menus = [
    {
      "icon": Icons.sunny,
      "title": "Sun",
      "activeColor": Colors.red,
    },
    {
      "icon": Icons.nightlight,
      "title": "Moon",
      "activeColor": Colors.yellow,
    },
    {
      "icon": Icons.star,
      "title": "Star",
      "activeColor": Colors.yellow,
    },
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("4번 과제"),
      ),
      body: Column(
        children: menus
            .map(
              (menu) => MenuTile(
                icon: menu["icon"]!,
                title: menu["title"]!,
                color: menu["activeColor"]!,
              ),
            )
            .toList(),
      ),
    );
  }
}

구현화면

아니 버튼이 하나 더 있었다;ㅠㅠ

이렇게되면 isActive를 타일이 아니라 상위 위젯에서 다뤄야할거같다... 대공사 시작이다... 휴

계~속 해보다가 결국 isActive도 list로 관리하도록 했다.
처음엔 reset 변수를 하나 선언해서 reset이 true일때 타일에서 isActive가 false가 되도록 하려고 하다 뭐가 잘못됐는지 잘 안돼서...
리셋 변수대신 함수를 만들어서 버튼을 누르면 list 내 isActive를 순회하면서 false로 값을 바꾸도록 했다.

과제4 페이지

import 'package:assignment1/MenuTile.dart';
import 'package:flutter/material.dart';

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

  
  State<LastAssignment> createState() => _LastAssignmentState();
}

class _LastAssignmentState extends State<LastAssignment> {
  final List<Map<String, dynamic>> menus = [
    {
      "icon": Icons.sunny,
      "title": "Sun",
      "activeColor": Colors.red,
      "isActive": false,
    },
    {
      "icon": Icons.nightlight,
      "title": "Moon",
      "activeColor": Colors.yellow,
      "isActive": false,
    },
    {
      "icon": Icons.star,
      "title": "Star",
      "activeColor": Colors.yellow,
      "isActive": false,
    },
  ];
  void resetTile() {
    for (var menu in menus) {
      menu["isActive"] = false;
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("4번 과제"),
      ),
      body: Column(
        children: menus
            .map(
              (menu) => MenuTile(
                icon: menu["icon"]!,
                title: menu["title"]!,
                color: menu["activeColor"]!,
                isActive: menu["isActive"]!,
              ),
            )
            .toList(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            resetTile();
          });
        },
        child: const Icon(
          Icons.refresh,
        ),
      ),
    );
  }
}
import 'package:flutter/material.dart';

class MenuTile extends StatefulWidget {
  MenuTile({
    super.key,
    required this.icon,
    required this.title,
    required this.color,
    required this.isActive,
  });
  final IconData icon;
  final String title;
  final Color color;
  bool isActive;

  
  State<MenuTile> createState() => _MenuTileState();
}

class _MenuTileState extends State<MenuTile> {
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          widget.isActive = !widget.isActive;
        });
      },
      child: ListTile(
        leading: Icon(
          widget.icon,
          color: widget.isActive ? widget.color : Colors.grey,
        ),
        title: Text(widget.title),
        trailing: const Icon(Icons.navigate_next),
      ),
    );
  }
}

과제5:

4번의 과제를 활용하여 다음의 추가적인 UI를 구성하여 해결하시오.

  • 아이콘의 색이 켜졌을 땐 끄고, 꺼져있을 때는 켜는 시스템을 제작함.
  • “태양” 입력 후 “엔터(혹은 제출)”하였을 때, 달 아이콘의 색상이 스위칭이 되도록 함.
  • “달” 입력 후 “엔터(혹은 제출)”하였을 때, 달 아이콘의 색상이 스위칭이 되도록 함.
  • “별” 입력 후 “엔터(혹은 제출)”하였을 때, 달 아이콘의 색상이 스위칭이 되도록 함.
  • FAB를 클릭하면 모든 활성화되어있는 아이콘이 비활성화됨.

과제5는 시작부터 난관에 부딪혔다.

걍 한글자판이 안쳐짐. 찾아보니 버전이 낮아서 그런것같다... ㅠ0ㅠ
일단은 영어로 치도록 해서 구현하고 월요일에 문의를 하던 해야겠다.

구현코드

일단 대문자던 소문자던 글자가 일치하면 찾을 수 있도록 toLowerCase를 써서 비교했다.
for문을 쓰다가 find같은 게 dart에도 있을 것 같아서 찾아보니 firstWhere이라는 게 있더라.
일치하는 값 자체를 리턴한다. 여기서는 해당 List 요소인 객체 자체를 리턴한다.
찾은 targetTile을 변수에 담고, isActive를 토글하도록 했다.

var textController = TextEditingController();

...생략
TextField(
            keyboardType: TextInputType.text,
            controller: textController,
            decoration: const InputDecoration(
              hintText: "키고 끄고싶은 아이콘을 입력하세요.",
              border: OutlineInputBorder(),
            ),
            onSubmitted: (val) {
              final targetTile = menus.firstWhere(
                  (menu) => val.toLowerCase() == menu["title"].toLowerCase());

              setState(() {
                targetTile["isActive"] = !targetTile["isActive"];
              });
            },
          ),
...생략

구현화면


본 후기는 유데미-스나이퍼팩토리 9주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.

profile
주니어 플러터 개발자의 고군분투기

0개의 댓글