과제폭탄이 떨어졌다 으악! 💣
다음과 같이 버튼 [1번 과제], [2번 과제], [3번 과제]를 구성하고, 클릭 시 과제 페이지로 이동하도록 만드세요.
( 대충 버튼 세개가 세로로 나란히 있는 짤 )
ScrollController를 활용하여 가장 상단으로 이동하는 기능을 구현합니다.
List animalList = ['강아지', '고양이', '앵무새', '토끼', '오리', '거위', '원숭이'];
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]}')),
);
}
);
}
itemCount도 함께 전달해야한다고 한다.
이쯤 써보니까 플러터에서는 사용할 공간을 지정하고 확보하는 게 중요한 것 같다는 느낌이 든다. 아무래도 모바일 기기에서 쓸 어플을 주로 개발하니까 최적화를 위해서 그런걸까?
ListView.builder(
itemCount: animalList.length,
itemBuilder: (BuildContext context, int i) {
return SizedBox(
height: 300,
child: Center(
child: Text(animalList[i]),
),
);
},
),
ScrollController를 사용한다.
스크롤 컨트롤러에서 animateTo라는 속성이 있다.
현재 값에서 지정된 값까지 위치를 애니메이션한다고 함.(flutter doc)
이걸로 컨트롤하면 됨!
근데 매개변수가 좀 많다. 셋다 필수임. 애니메이션하는 요소에 duration, curve는 거의 필수로 들어가는 것 같고, 스크롤 좌표를 지정하는거라 offset도 여기선 포함된 듯.
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();
로 바꾸니 해결됐다.
입력된 텍스트 미러링하는 화면을 제작합니다.
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,
),
),
);
}
}
다음의 UI를 구성하고 각각의 조건에 맞추어 코딩하시오.
탭 아이템을 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),
),
);
}
}
이렇게 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로 값을 바꾸도록 했다.
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),
),
);
}
}
4번의 과제를 활용하여 다음의 추가적인 UI를 구성하여 해결하시오.
걍 한글자판이 안쳐짐. 찾아보니 버전이 낮아서 그런것같다... ㅠ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주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.