노마드코더 Flutter로 웹툰 앱 만들기 강의를 들으면서 만들었던 웹툰 앱을 리팩토링해봤다.
리팩토링 항목은 다음과 같다. 링크 클릭 시 해당 커밋으로 이동한다.
3번에 대해서는 별도로 글을 작성했다. 링크
// main.dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
primary: Colors.indigo.shade400,
),
appBarTheme: AppBarTheme(
centerTitle: true,
foregroundColor: Colors.indigo.shade400,
elevation: 2,
surfaceTintColor: Colors.white, // elevation 추가 후 앱바 어두워지는 문제 해결
shadowColor: Colors.black,
),
),
home: HomeScreen(),
);
}
}
MaterialApp
에 colorScheme
를 추가했다.seedColor
에는 shade
가 적용되지 않는 거 같아서 나중에 context
로 접근할 때 사용하기 위해 primary color
까지 추가했다.이 방법 외에 클래스를 따로 생성해서 커스텀 컬러를 상수로 지정하는 방법도 있다. 블로그 참고
AppBar
를 사용하고 있었기 때문에 MaterialApp
에 appBarTheme
을 추가하고 각 스크린에 중복되었던 AppBar
코드는title
을 제외하고 지워주었다.primary
컬러에 접근하고 싶을 때는 color: Theme.of(context).colorScheme.primary
이렇게 작성하면 된다.MaterialApp
에서 primary
컬러를 바꾸기만 하면 앱의 모든 primary
컬러를 한 번에 바꿀 수 있는 장점이 있다.class WebtoonThumb extends StatelessWidget {
final String thumb;
const WebtoonThumb({
super.key,
required this.thumb,
});
Widget build(BuildContext context) {
return Container(
width: 200,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
blurRadius: 10,
offset: const Offset(10, 10),
color: Colors.black.withOpacity(0.3),
),
],
),
child: Image.network(
thumb,
),
);
}
}
// 실제 사용 시
Hero(
tag: id,
child: WebtoonThumb(thumb: thumb),
),
src
가 필요하기 때문에 클래스 호출 시 thumb
값을 받는다.수정 전 | 수정 후 |
![]() |
![]() |
수정 전 | 수정 후 |
![]() |
![]() |
// 텍스트가 넘치는 문제를 해결하기 위해 Row와 Text에 Flexible 위젯 추가
Flexible(
child: Row(
children: [
Container(
height: 50,
width: 85,
margin: const EdgeInsets.only(right: 10),
// 자식의 부모 영역 침범을 제어함 (BorderRadius 적용 위해 추가)
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
),
child: Image.network(
episode.thumb,
),
),
Flexible(
child: Text(
episode.title,
style: const TextStyle(
fontSize: 16,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Container
위젯에 width
값을 고정하고, Row
와 Text
위젯 상위에 Flexible
위젯들 추가하고, Text
위젯에 overflow: TextOverflow.ellipsis
속성을 추가했다.// StatelessWidget에서 불가능
Future<WebtoonDetailModel> webtoon = ApiService.getToonById(id);
id
를 인자로 전달해야 했는데 인스턴스 변수(webtoon
)를 초기화할 때 다른 프로퍼티(id
)에는 접근할 수 없기 때문에, 강의에서는 메소드 호출을 위해 기존의 StatelessWidget
을 StatefulWidget
으로 변환한 후 메소드를 호출하는 방법을 알려주었다.class DetailScreen extends StatefulWidget {
final String title, thumb, id;
const DetailScreen({
super.key,
required this.title,
required this.thumb,
required this.id,
});
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
// Constructor에서는 widget을 참조할 수 없기 때문에 변수를 선언만 함
late Future<WebtoonDetailModel> webtoon;
// initState에서는 widget으로 참조 가능
void initState() {
super.initState();
webtoon = ApiService.getToonById(widget.id);
}
Widget build(BuildContext context) {
return Scaffold(...)
}
}
StatefulWidget
의 initState()
메소드에서 ApiService.getToonById()
메소드를 호출하는 방식을 사용했는데, 메소드 호출을 위해 state
가 필요 없는데도 StatelessWidget
을 StatefulWidget
으로 변환하는 과정이 불필요하게 느껴졌다.class DetailScreen extends StatelessWidget {
final String title, thumb, id;
late final Future<WebtoonDetailModel> webtoon;
late final Future<List<WebtoonEpisodeModel>> episodes;
DetailScreen({
super.key,
required this.title,
required this.thumb,
required this.id,
}) : webtoon = ApiService.getToonById(id),
episodes = ApiService.getLatestEpisodesById(id);
Widget build(BuildContext context) {
return Scaffold(...)
}
}
StatelessWidget
을 이용하는 방법을 알려주셔서 이 방법을 사용해봤다.