[Flutter] : TDD - 12. User Interface
드디어 마지막 단계다. 잘 진행해보자.
1. 페이지 생성
number_trivia_page를 만들어보자
…/presentation/pages/number_trivia_page.dart
import 'package:flutter/material.dart';
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
);
}
}
자 stateless widget으로 생성하였다. 내용들을 하나씩 채워보자.
기본 생성되면 있던 main.dart 내용물을 다 지우고 아래 처럼 채워보자.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Number Trivia',
theme: ThemeData(
primaryColor: Colors.green.shade800,
accentColor: Colors.green.shade600,
),
home: NumberTriviaPage(),
);
}
}
2. Presentation Logic Holder (Bloc)
Clean Architecture에 따르면 UI와 통신 하는 것은 오직 Presentation Logic Holder 뿐이다.
이 부분이 Mobx나 Provider로 수정되어도 문제 없다.
이제 우리는 UI에 bloc을 전달해주기 위해 BlocProvider
를 사용할 것이다.
우리는 NumberTriviaBloc을 Get_it을 통해 ServiceLocator에 생성해 놓았다. 이제 불러와서 연결해보자.
number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: BlocProvider(
create: (_) => sl<NumberTriviaBloc>(),
child: Container(),
),
);
}
}
데이터 연결은 끝이 났다.
이제 화면을 그려보자.
3. 화면 그리기
위 화면을 만들어야 한다.
- 상단에는 결과물인 숫자와 텍스트가 나올 것이고, 에러나 “Start Searching!”같은 문구가 나올 것이다. 그리고 검색 중에는 CircularLoadingIndicator를 보여주자.
- 하단에는 숫자를 입력받는 텍스트 필드와 버튼들이 있다.
코드를 작성해보자.
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: buildBody(context),
);
}
BlocProvider<NumberTriviaBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => sl<NumberTriviaBloc>(),
child: Container(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
SizedBox(
height: 10,
),
// Top half
Container(
height: MediaQuery.of(context).size.height / 3,
// message Text Widget / CircularLoadingIndicator
child: Placeholder(),
),
SizedBox(
height: 20,
),
// Bottom half
Column(
children: [
//TextField
Placeholder(
fallbackHeight: 40,
),
SizedBox(
height: 10,
),
Row(
children: [
Expanded(
child: Placeholder(
fallbackHeight: 30,
),
),
SizedBox(
width: 10,
),
Expanded(
child: Placeholder(
fallbackHeight: 30,
),
),
],
)
],
)
],
),
),
),
);
}
}
PlaceHolder로 화면을 잡아 봤다.
4. 상단 데이터 연결
이제 bloc 에서 데이터를 불러와 보여줘 보자.
먼저 상단 데이터 부분을 봐보자
Top half
// Top half
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text('Start searching!'),
),
);
}
},
),
현재 State가 비어있음, 즉 초기 상태이면 Start searching 메시지를 띄우게 했다.
이 메시지 띄우는 부분은 글씨를 키워야 하고 또 앞으로 숫자가 뜰 공간이니 Widget으로 따로 만들어주자.
// Top half
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: "Start searching!",
);
}
},
),
....
class MessageDisplay extends StatelessWidget {
final String message;
const MessageDisplay({Key key, this.message})
: assert(message != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: SingleChildScrollView(
child: Text(
message,
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
),
),
);
}
}
에러 메시지도 띄우도록 분기 처리를 해주자.
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: "Start searching!",
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
Loading
이제 로딩 하는 부분을 처리해주자. API를 Call 할 때 Loading이 필요하다.
state에 따라 분기 처리 해주자.
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: "Start searching!",
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
} else if (state is Loading) {
return LoadingWidget();
}
},
),
....
class LoadingWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: CircularProgressIndicator(),
),
);
}
}
Loaded
이제 드디어 숫자 데이터를 표시 해주는 부분이다.
Trivia Display를 처리해주자.
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: "Start searching!",
);
} else if (state is Loading) {
return LoadingWidget();
} else if (state is Loaded) {
return TriviaDisplay(
numberTrivia: state.trivia,
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
...
class TriviaDisplay extends StatelessWidget {
final NumberTrivia numberTrivia;
const TriviaDisplay({Key key, this.numberTrivia})
: assert(numberTrivia != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Column(
children: [
Text(
numberTrivia.number.toString(),
style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
),
Expanded(
child: Center(
child: SingleChildScrollView(
child: Text(
numberTrivia.text,
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
),
),
)
],
),
);
}
}
자 이제 데이터를 화면에 처리하는 부분이 끝이 났다.
- 초기값
- 로딩
- 에러
- 데이터 로드
이제 Widget별로 파일을 만들어 넣어주자.
widgets.dart
export 'loading_widget.dart';
export 'message_display.dart';
export 'trivia_display.dart';
이렇게 설정 해주고
page파일에서 widget.dart 만 import 해주자.
5. 하단 데이터 연결
이제 아랫 부분 버튼과 데이터를 연결 해주자.
여기서는 bloc 에서 dispatching event가 발생한다.
control 할 수 있도록 stateful widget을 만들어보자.
입력 받는 텍스트 필드가 있기 때문에 state가 필요하다.
tirivia_controls.dart
class TriviaControls extends StatefulWidget {
@override
_TriviaControlsState createState() => _TriviaControlsState();
}
class _TriviaControlsState extends State<TriviaControls> {
final controller = TextEditingController();
String inputStr;
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(), hintText: 'Input a number'),
onChanged: (value) {
inputStr = value;
},
onSubmitted: (_) {
dispatchConcrete();
},
),
SizedBox(
height: 10,
),
Row(
children: [
Expanded(
child: RaisedButton(
child: Text('Search'),
color: Theme.of(context).accentColor,
textTheme: ButtonTextTheme.primary,
onPressed: dispatchConcrete,
),
),
SizedBox(
width: 10,
),
Expanded(
child: RaisedButton(
child: Text('Get random trivia'),
onPressed: dispatchRandom,
),
)
],
)
],
);
}
void dispatchConcrete() {
//텍스트 필드 지우기, 다음 입력을 위해
controller.clear();
BlocProvider.of<NumberTriviaBloc>(context)
.add(GetTriviaForConcreteNumber(inputStr));
}
void dispatchRandom() {
controller.clear();
BlocProvider.of<NumberTriviaBloc>(context).add(GetTriviaForRandomNumber());
}
}
이 파일도 widgets.dart에 export 해주자.
아주 정상적으로 잘 작동한다!!
6. 마지막 수정
시뮬레이터에서 아래 키보드가 올라오면 화면이 깨지는 경우가 있다.
이걸 수정하도록 하자.
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: SingleChildScrollView(child: buildBody(context)),
);
}
...
}
SingleChildScrollView로 Scroll로 처리하게 하였다.
마무리
드디어 모든 강의가 끝이 났다. 총 14개의 강의를 정리하였는데
bloc 패턴에 대해서 조금 알게 되었다. (별로 쓰고 싶지 않다….)
이제 provider를 mvvm패턴으로 사용하면서 TDD처리하도록 한번 코드를 수정해보자!!