import 'dart:convert'; // JSON 디코딩을 위해 필요
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // HTTP 패키지
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('HTTP API Example with FutureBuilder'),
),
body: PostScreen(),
),
);
}
}
class PostScreen extends StatelessWidget {
// 데이터를 가져오는 비동기 함수
Future<Map<String, dynamic>> fetchPost() async {
final response =
await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
if (response.statusCode == 200) {
// API 응답이 성공적이면 데이터를 파싱합니다.
return json.decode(response.body);
} else {
// 응답이 실패하면 에러 처리
throw Exception('Failed to load post');
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, dynamic>>(
future: fetchPost(), // 비동기 데이터를 가져오는 Future
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// 데이터가 로딩 중일 때
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// 에러가 발생한 경우
return Center(child: Text('Failed to load data'));
} else if (snapshot.hasData) {
// 데이터를 성공적으로 가져온 경우
final postData = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Title: ${postData['title']}',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Text(
'Body: ${postData['body']}',
style: TextStyle(fontSize: 16),
),
],
),
);
} else {
// 데이터를 못 가져온 경우 대비
return Center(child: Text('No data found'));
}
},
);
}
}
FutureBuilder를 통해 데이터를 받아서 화면에 출력할 수 있습니다. snapshot을 이용해 데이터 로딩 등을 처리할 수 있습니다.
📝Flutter 컨트롤러
final TextEditingController _usernameController = TextEditingController();
// 메모리 누수 잡기 [필수]
@override
void dispose() {
_usernameController.dispose(); // 컨트롤러 dispose를 제때 사용 안하면 메모리 누수 발생
super.dispose(); // 컨트롤러 관한 걸 제거한 후에 상위 것들도 dispose 한다 [ 일반적으로 이렇게 많이 사용함]
}
@override
void initState() {
super.initState();
// 텍스트 쓸때마다 감지한다 [ 상태관리 비슷한 개념 ]
_usernameController.addListener(() {
setState(() {
_username = _usernameController.text; // 텍스트 필드의 값을 가져온다
});
});
}
TextField(
controller: _usernameController, // 컨트롤러
...
)
컨트롤러의 경우 위젯이 로직을 처리할 수 있게 도와주는 역할을 한다.
반드시 사용을 한 이후에는 dispose로 메모리 누수를 잡아줘야한다
addListener로 연동시킨 Widget의 변화가 있을 때마다 감지해서 이벤트 처리를 줄 수 있다
📝상태관리 (Provider)
// main.dart
final preferences = await SharedPreferences.getInstance();
final repository = PlaybackConfigRepository(preferences);
runApp(MultiProvider(
providers: [ // 프로바이더에 쓰일 것들...
ChangeNotifierProvider( // 프로바이더 지원 함수 [데이터 변경 감지 및 데이터 관리 해줌 + UI업데이트 등]
create: (context) => PlaybackConfigViewModel(repository), // 해당 뷰모델을 프로바이더로 쓰겠다
)
],
child: const TikTokApp(),
));
// ViewModel 선언
...
class PlaybackConfigViewModel extends ChangeNotifier
// View에서 ViewModel을 통해 로직 실행및 가져오기
SwitchListTile.adaptive(
value: context.watch<PlaybackConfigViewModel>().muted,
onChanged: (value) =>
context.read<PlaybackConfigViewModel>().setMuted(value),
title: const Text("Mute video"),
subtitle: const Text("Video will be muted by default."),
),
MVVM 기반으로 개발하며 Proivder를 이용해 데이터를 관리할 수 있다. main에서 provider를 사용하고 viewmodel을 연결해서 감지시키고 모든 곳에서 접근해서 사용이 가능하다. Riverpod이 더 많은 기능을 제공하고 더 잘 분리되어있다.
context.watch<T>()
T의 데이터 값이 변경되었을 때 위젯을 재빌드한다.
context.read<T>()
T의 데이터 값이 변경되었을 때 위젯을 재빌드하지 않는다.
📝Flutter 애니메이션
Animation 효과 줄 수 있는 위젯 (setState 필수) [비추] → 전체 랜더링 비효율적
데이터가 동적으로 변해야 할 때 쓴다. 예를 들자면 "클릭시 카운트가 올라간다."가 이에 해당한다. 동적페이지를 만들기 위해서는 StatefulWidget을 상속받아 사용해야한다.
StatefulWidget을 사용할 때 주의점들은 아래와 같다.
전체가 리렌더링되면 부하가 크기 때문에 부분적으로 따로 위젯을 뽑아 사용한다. (Custom Widget으로 분리)
상태가 변한 걸로 화면이 바뀌었다는 걸 인식하기 때문에 같은 위젯들의 상태가 변화하는 경우 key를 할당해줘야한다. → Custom으로 여러개 만든 경우 그들끼리 구분하기 위해서 필요하다. React이서 Key를 정해주는 것과 같은 개념
데이터 변화가 있는 후setState를 작동시 state의 변화를 감지해 statefulWidget을 다시 랜더링하게 된다.
동작 방식
const App({super.key}); 에서 상위에 위젯 키를 넘긴다
State<App> createState() => _AppState(); → _AppState에서 Build할 내용을 받아와서 상태를 만든다
📝Widget Lifecycle (위젯 라이프 사이클)
initState()
build를 하기 전에 항상 먼저 실행된다. (대표적으로 API를 불러올 때 사용된다)
dispose()
화면에서 사라질 때 실행한다.
build()
화면을 그려주는 역할을 한다.setState와 같이 리랜더링시 이 부분만 작동하게 된다.
📝Key란?
대부분의 경우는 key를 사용하지 않는다. 하지만 상태를 유지하고 있는 같은 종류의 위젯을 컬렉션에 더하거나, 제거하거나, 정렬할 때 key가 필요하다. 예를 들면 ToDoList를 보면 List안에 내용들은 똑같은 내용 즉, 똑같은 위젯을 재활용한 것이기 때문에 이것들을 관리하기 위해서는 key가 필요합니다. 이건 동적인 데이터이기 때문에 key가 쓰이는 경우는 StatefulWidget에 해당합니다.
하위 위젯인 MyLargeTitle에서 상위에 있는 위젯을 사용하려면 상위 위젯에 대한 문맥(context)를 알아야하는데BuildContext가 그러한 역할을 해준다. 그래서 Theme.of(context)를 이용해 context에 있는 Theme을 가져오고 그 안에 textTheme 그 안에 titleLarge를 가져오면 된다.
어떤 위젯이 부모의 크기를 비율로 가져가는 경우 부모의 크기가 이론상 무한대로 확장 가능할 때 제한해야한다 예를 들면 Row가 부모 위젯인 경우 Width가 이론상 무한이고 Column의 경우 Height가 이론상 무한이기 때문에 이 두개가 있을 경우 Expanded따위로 제한해야한다 참고로 Row 안에 Column 안에 있는 위젯의 경우 둘다 Expanded로 제한해줘야한다
📝Flutter 개발 팁
// Size 예시 [Gaps, FontSize, Color 등…]
class Gaps {
static const v1 = SizedBox(height: 1);
...
}
Flex박스를 만든다 → 각각의 박스를 비율로 줄 수 있어서 어떤 핸드폰이든 %이기 때문에 깨지지 않는다
flex : 비율 → int child : FlexBox에 넣을 위젯 → Widget
Flexible( flex: 1,…
Expanded
사용 가능한 크기만큼 가져갑니다 (최대 화면의 크기)
child : 확장된 Row안에 들어갈에 넣을 위젯 → Widget flex : 나눌 비율 크기 → int
Expanded( child: Container(
ListView
리스트의 형태로 데이터를 보여준다 → 많은 데이터 보여줄 때 사용 하지만… 모든 데이터를 가져오기 때문에 메모리 누수 현상 발생 가능성 존재
ListView.builder
리스트 형태로 데이터를 보여주고 화면에 보이는 부분만 렌더링 하기 때문에 메모리 누수 걱정이 없다
scrollDirection : 리스트뷰 스크롤 방향 → ScrollWidget itemCount : 전체 아이템 수 → int padding : 패딩 옵션 → EdgeInsets itemBuilder : 새로운 인덱스 감지시 itemBuilder함수를 호출해 렌더링할 위젯을 설정한다 추후 스크롤 시 안 보이는 부분은 사라지고 나중에 다시 보일 때 재활용하게 된다 → 일반적으로 itemBuilder : (context, index) 이런형식으로 이용된다
ListView.seperator
ListView.builder와 동일하며 리스트 사이에 위젯을 넣을 수 있다
위와 동일seperatorBuilder : 리스트 사이에 들어갈 위젯 → 일반적으로 speratorBuilder : (BuildContext context, int index) ⇒ 위젯..
Image.network
이미지를 가져온다
CircularProgressIndicator
로딩바
X
CircularProgressIndicator()
SafeArea
와이파이, 배터리 등 위에 있는 부분을 제외한 위젯 박스
child : 모든 위젯 [일반적으로 Column, Container 등..] → Widget
SafeArea(child: Column...)
FractionallySizedBox
부모 위젯의 전체 크기를 비율로 가져가는 위젯 박스
widthFactor : 0.5 → double heightFactor : 0.5 → double child : 비율로 가져가는 하위 위젯 → Widget
import 'package:angular_components/angular_components.dart' as angular_components;
CamelCase
class SliderMenu { ... }
function
function args
variable
PascalCase
typedef Predicate<T> = bool Function(T value);
@Foo(anArg)
extension MyFancyList<T> on List<T> { ... }
Tip
타입캐스팅
var map = table.asMap(); 또는 toMap이긴한데 얕은복사냐 깊은복사냐에 따라 다르다고하다 그냥 to로 쓰자 귀찮다
bool 변수는 긍정이름 선호
hasData (O)
isEmpty (X)
메소드명
행위가 강조되는 경우 downloadData처럼 데이터를 반환하는 경우 get 제거
breakfastOrder (O)
getBreakfastOrder (X)
메소드 매개변수
add(element) [O]
addElement(element) [X]
함수 New 타입
클래스를 하나 만들지말고 typedef Predicate<E> = bool Function(E element); 이렇게 써라. 너무 남발하면 복잡해지니 자제하면서 알아서 사용
// 좋은 예시
typedef Comparison<T> = int Function(T a, T b);
// 나쁜 예시
typedef int Comparison<T>(T a, T b); // 구식 문법
메서드 체이닝
체이닝을 위해 return에 this를 반환하는것보다는 매서드 케서케이드(..)를 사용하는게 좋다.
// wrong code
class Person {
String name = '';
int age = 0;
Person setName(String name) {
this.name = name;
return this; // `this`를 반환
}
Person setAge(int age) {
this.age = age;
return this; // `this`를 반환
}
}
void main() {
var person = Person()
.setName('Alice') // 메서드 체이닝
.setAge(30); // 메서드 체이닝
print('Name: ${person.name}, Age: ${person.age}');
}
// right code
class Person {
String name = '';
int age = 0;
void setName(String name) {
this.name = name;
}
void setAge(int age) {
this.age = age;
}
}
void main() {
var person = Person()
..setName('Alice') // 메서드 캐스케이드
..setAge(30); // 메서드 캐스케이드
print('Name: ${person.name}, Age: ${person.age}');
}
타입 명시화
로컬 변수 타입이 명확하면 생략하는게 좋습니다.
Dart 대부분 제네릭 타입인자 추론이 가능하지만 정보가 없으면 타입인자 명시 해야합니다.
// 좋은 예시
var desserts = <List<Ingredient>>[];
// 나쁜 예시
List<List<Ingredient>> desserts = <List<Ingredient>>[];
// ─────────────────────────────────────────────────────────────
// 좋은 예시
var playerScores = <String, int>{}; // 명시적으로 타입 인자 지정
final events = StreamController<Event>();
// 나쁜 예시
var playerScores = {}; // 타입 인자 없음
final events = StreamController(); // 타입 인자 없음
생성자 생성
// 간단한 방식
Point(this.x, this.y);
Setter 반환타입 지정하지 않기
// 좋은 예시
set foo(Foo value) { ... }
// 나쁜 예시
void set foo(Foo value) { ... } // 불필요한 반환 타입
dynamic 타입 금지
컴파일 타입체크를 비활성하기 때문에 사용하지 않는 게 좋다
함수 매개변수 명시화
매개변수 위치에 따라 받는 것보다는 이렇게 명시해서 데이터를 직접 받는게 안전하고 이해가 쉽다.
class ListBox {
final bool scroll;
final bool showScrollbars;
ListBox({this.scroll = false, this.showScrollbars = false});
}
void main() {
// 명명된 매개변수를 사용하여 `ListBox` 인스턴스를 생성
var listBox = ListBox(scroll: true, showScrollbars: true);
}
// 명명된 매개변수를 가진 함수
void createUser({required String name, int age = 18, String? email}) {
print('Name: $name, Age: $age, Email: $email');
}