13일차에서는 Map 데이터타입을 학습하고 이와 관련된 코드를 작성해 보았다.
학습한 내용
- Map
- 단어 앱 제작
12일차 마지막에 나왔던 내용인데 지금 정리해보고자 한다.
앱을 사용할 때 리스트 스크롤에서 마지막 부분에 가면 자동으로 이전 과거 내용이 이어지는 스크롤이 있다. 이를 Infinity Scroll 이라고 한다.
우선 아래와 같이 scrollController
를 initState
메소드에서 addListener
를 작성한다.
final ScrollController scrollController = ScrollController();
void initState() {
scrollController.addListener(() {
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent
) {
_getData();
}
});
super.initState();
}
scrollController.position.pixels
는 현재 스크롤의 위치를 픽셀값으로 가져오고, scrollController.position.maxScrollExtent
는 스크롤의 마지막 값을 가져온다. 따라서 조건문이 충족할 때 새로운 데이터를 불러오면 무한 스크롤을 보여줄 수 있다.
addListener
는 컨트롤러 안의 속성값이 변경되는 경우에 실행되는 함수이다. addListener
는 거의 모든 컨트롤러에 사용할 수 있고 매개 변수를 void Function()
으로 받는다.
void addListener(VoidCallback listener) {
Map은 Key와 Value 쌍으로 이루어진 객체이다. 키와 값은 어던 자료형이던지 상관 없다.
Map은 중괄호나 new Map()
등을 사용해 선언할 수 있다. 다만 new Map()
을 사용하는 경우 값을 따로 초기화 해줘야 한다.
데이터 삽입은 map[key] = value
로 할 수 있다.
Map my_map = {};
Map my_map2 = new Map();
my_map2['name'] = 'my map 2';
이 방식 이외에도 Map으로 된 데이터를 addAll()
메소드를 사용하여 추가할 수도 있다.
Map my_map3 = {'type': 'tutorial'};
my_map.addAll(my_map3);
첫 번째 데이터 삽입 방식과 같은 방식으로 데이터 변경이 가능한데 키 값을 기존에 존재하는 값으로 입력하면 된다.
my_map['name'] = 'your map';
update()
메소드를 사용해도 데이터를 변경할 수 있는데 Map.update(key, function)
형태로 전달한다. 만약 키 값이 존재하지 않을 때 값을 업데이트하지 않고 초기 값으로 추가를 하고 싶다면 ifAbset
를 같이 사용하면 된다.
Map<String, int> map = {};
map.update('a', (value) => value + 10, ifAbsent: () => 0);
remove()
메소드를 사용하면 데이터를 삭제할 수 있다. 매개 변수로는 키 값을 전달해 준다.
my_map.remove('name');
clear()
를 사용하면 Map 안의 모든 엔트리가 제거된다.
my_map.clear();
containsKey
와 containsValue
를 사용할 수 있으며 boolean 값을 반환한다.print(my_map.containsKey('name'));
print(my_map.containsValue('Kim'));
다른 Map의 유용한 속성들
.keys
: Map에 담긴 key들을 Iterable 객체로 반환해 준다..values
: Map에 담긴 value들을 Iterable 객체로 반환해 준다..entries
: Map에 담긴 key-value쌍들을 Iterable 객체로 반환해 준다..isEmpty
: Map이 비었다면 true, 그렇지 않다면 false를 반환한다..isNotEmpty
: Map에 뭔가 들어 있다면 true, 비었다면 false를 반환한다..length
: Map에 든 key-value 쌍이 몇 개인지 반환한다.
Dart에는 입력된 정보를 통해 타입을 추론해서 데이터 형식을 정의하는 dynamic
타입이 있다.
var
와 유사한 데이터타입인데 var
의 경우 추론된 타입이 한번 입력되고 나면 다른 타입을 저장할 수 없지만 dynamic
의 경우 어떤 형식이라도 항상 입력이 가능하다.
var varName = 'var test';
print(varName); // 출력 var test;
name = 123; // Error 발생
dynamic dynamicName = 'var test';
print(dynamicName); // 출력 var test;
name = 123; // name에 123 입력
print(dynamicName); // 123
따라서 하나의 리스트에 여러 값을 담거나 Map을 사용할 때 제네릭으로 dynamic
을 자주 사용한다.
아래에서 정리할 단어 앱 만들기를 진행하면서 두 개의 ScrollPhysics
를 사용하게 되어 내용을 정리하고자 한다.
두 개의 ScrollPhysics
를 함께 사용하려면 parent
속성을 사용하거나 applyTo()
메소드를 사용하면 된다.
우선 ScrollPhysics
를 반환하는 applyTo()
메소드는 아래와 같은 구조로 작성되어 있다.
ScrollPhysics applyTo(ScrollPhysics? ancestor) {
return ScrollPhysics(parent: buildParent(ancestor));
}
즉, 아래의 두 코드는 두 개의 ScrollPhysics
를 연결하는 같은 기능을 수행한다.
final FooScrollPhysics x = const FooScrollPhysics().applyTo(const BarScrollPhysics());
const FooScrollPhysics y = FooScrollPhysics(parent: BarScrollPhysics());
자세한 내용은 아래의 공식 문서를 참고
applyTo method - ScrollPhysics class - widgets library - Dart API
- 단어 앱 제작
제공되는 단어 데이터를 활용하여 단어 앱을 만든다.
기본 5개이며 추가가능, 본인의 단어 데이터 활용가능
List<Map<String, String>> words = [
{
"word": "apple",
"meaning": "사과",
"example": "I want to eat an apple"
},
{
"word": "paper",
"meaning": "종이",
"example": "Could you give me a paper"
},
{
"word": "resilient",
"meaning": "탄력있는, 회복력있는",
"example": "She's a resilient girl"
},
{
"word": "revoke",
"meaning": "취소하다",
"example": "The authorities have revoked their original decision to allow development of this rural area."
},
{
"word": "withdraw",
"meaning": "철회하다",
"example": "After lunch, we withdrew into her office to finish our discussion in private."
},
];
FloatingActionButton이 두 개가 떠있는 형태로, 양쪽에 위치합니다.
ThemeData.dark() 를 활용합니다.
단어와 뜻의 자간을 -1만큼 좁혀봅니다. 예문은 1만큼의 자간을 갖도록 합니다.
단어는 최소 5개 이상으로 준비합니다.
결과물 예시
import 'package:first_app/page/home_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// root Widget
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: HomePage(), // 홈 페이지 호출
);
}
}
main.dart
에서는 다른 파일에 작성된 HomePage()
를 호출한다. 테마는 ThemeData.dark()
로 다크 모드를 설정했다.
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
//단어 리스트
List<Map<String, String>> words = [
{"word": "apple", "meaning": "사과", "example": "I want to eat an apple"},
{
"word": "paper",
"meaning": "종이",
"example": "Could you give me a paper"
},
{
"word": "resilient",
"meaning": "탄력있는, 회복력있는",
"example": "She's a resilient girl"
},
{
"word": "revoke",
"meaning": "취소하다",
"example":
"The authorities have revoked their original decision to allow development of this rural area."
},
{
"word": "withdraw",
"meaning": "철회하다",
"example":
"After lunch, we withdrew into her office to finish our discussion in private."
},
{
"word": "factory",
"meaning": "공장",
"example": "The factory has been earmarked for closure."
},
];
var pageViewController = PageController(); //PageView 컨트롤러
return Scaffold(
body: PageView.builder(
physics: NeverScrollableScrollPhysics(
parent: BouncingScrollPhysics()), //스크롤 동작
controller: pageViewController, //컨트롤러 연결
itemCount: words.length,
itemBuilder: (context, index) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: -1, //자간
),
words[index]['word']!,
),
Text(
style: TextStyle(
color: Colors.grey,
letterSpacing: -1, //자간
),
words[index]['meaning']!,
),
SizedBox(height: 16),
Text(
style: TextStyle(
color: Colors.grey,
letterSpacing: 1, //자간
),
textAlign: TextAlign.center, //텍스트 가운데 정렬
"\"${words[index]['example']!}\"",
),
],
),
),
);
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//이전으로 이동 버튼
FloatingActionButton(
heroTag: 'back',
onPressed: () => pageViewController.previousPage(
duration: Duration(seconds: 1),
curve: Curves.ease,
),
child: Icon(
size: 16,
Icons.arrow_back_ios,
),
),
// 다음으로 이동 버튼
FloatingActionButton(
heroTag: 'next',
onPressed: () => pageViewController.nextPage(
duration: Duration(milliseconds: 600),
curve: Curves.ease,
),
child: Icon(
size: 16,
Icons.arrow_forward_ios,
),
),
],
),
),
);
}
}
HomePage
위젯에 주어진 단어 데이터를 words
리스트로 생성했다. 기존의 단어에 factory를 추가해 총 6개의 단어를 포함한다.
그리고 페이지의 동작을 제어할 pageViewController
를 PageController()
로 생성했다.
화면은 Scaffold
에서 PageView.builder
로 만들었다. 스크롤 화면은 손으로 스와이핑이 불가능 하고 Bounce 효과를 넣어야 하므로 NeverScrollableScrollPhysics
의 parent
속성을 BouncingScrollPhysics
로 설정했다. 또한 앞에서 만든 컨트롤러를 연결했다.
페이지 뷰의 본문 내용은 Column
으로 구성하여 3개의 텍스트 뷰를 넣었다. 텍스트 뷰에서 letterSpacing
속성을 사용해 단어와 뜻의 자간은 -1로, 예문의 자간은 1로 설정했다. 예문에서는 텍스트를 가운데 정렬하기 위해 textAlign
속성을 TextAlign.center
로 설정했다.
FAB은 먼저 위치를 centerFloat
로 설정했고, Row
를 사용해 두 개의 버튼을 위치시켰다. 각 버튼은 onPressed
이벤트로 페이지 뷰의 컨트롤러에서 previousPage()
와 nextPage()
메소드를 사용해 이전과 다음으로 이동하는 기능을 추가했다.
이번주도 끝났다... 뭔가 금방 지나간거 같기도 하고ㅎㅎ 오늘은 과제도 그렇고 포스팅도 그렇고 좀 빨리 끝났다. 어제 포스팅도 2개나 하고 과제가 좀 많아서 오래걸렸는데 상대적으로 짧게 느껴져서 그런가 어렵진 않았던거 같다. 주말에 주간평가랑 도전과제도 열심히 해보자!!!! 🫠🫠