이번 시간에는 검색 화면을 구현해보았다!
일치하는 데이터를 보여주는 것 까쥐~
이제 곧 클론 코딩의 끝이 보인다 +_+
# 검색 문자 추적
데이터의 변화를 지켜보기 위해 Screen을 StatefulWidget으로 만든다.
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
_SearchScreenState createState() => _SearchScreenState();
}
State를 제어하는 class 생성 후 TextEditingController 의 listener를 이용해 텍스트의 변화를 추적한다.
class _SearchScreenState extends State<SearchScreen> {
final TextEditingController _filter = TextEditingController(); // the widget to control search widget
FocusNode focusNode = FocusNode(); // for state wheather cursor is on the search widget
String _searchText = ""; // current search text
_SearchScreenState() {
_filter.addListener(() {
// if filter has a change, set searchText's state.
setState(() {
_searchText = _filter.text;
});
});
}
...
# 파이어베이스에서 데이터 검색 후 일치하는 데이터 추출
Widget _buildBody(BuildContext context)
데이터를 읽었을 때와 마찬가지로 StreamBuilder를 이용해서 파이어베이스에서 데이터를 가져온다.
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('movie').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const LinearProgressIndicator();
return _buildList(context, snapshot.data!.docs);
});
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot)
그 이후 현재 입력한 텍스트 값과 위에서 받은 데이터 값의 리스트를 비교하여 일치하는 값은 따로 저장해준다.
List<DocumentSnapshot> searchResults = [];
for (DocumentSnapshot d in snapshot) {
log('searchText = $_searchText // d.data = ${d.data().toString()}');
if (d.data().toString().contains(_searchText)) {
searchResults.add(d);
}
}
이 값들을 GridView 를 이용해 보여준다.
GridView.count(
crossAxisCount: 3,
childAspectRatio: 1 / 1.5,
padding: const EdgeInsets.all(3),
children: searchResults
.map((data) => _buildListItem(context, data))
.toList()));
전체 함수를 보면
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
List<DocumentSnapshot> searchResults = [];
for (DocumentSnapshot d in snapshot) {
log('searchText = $_searchText // d.data = ${d.data().toString()}');
if (d.data().toString().contains(_searchText)) {
searchResults.add(d);
}
}
return Expanded(
child: GridView.count(
crossAxisCount: 3,
childAspectRatio: 1 / 1.5,
padding: const EdgeInsets.all(3),
children: searchResults
.map((data) => _buildListItem(context, data))
.toList()));
}
이렇게 나타낼 수 있다.
Widget _buildListItem(BuildContext context, DocumentSnapshot data)
각 데이터는 영화 포스터를 보여주고, 클릭 시 디테일 화면으로 넘어가게 해준다.
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final movie = Movie.fromSnapshot(data);
return InkWell(
child: Image.network(movie.poster),
onTap: () {
Navigator.of(context).push(MaterialPageRoute<Null>(
fullscreenDialog: true,
builder: (BuildContext context) {
return DetailScreen(movie);
}));
},
);
}
# 검색 화면 UI
취소 버튼 / 검색 입력 초기화 버튼
취소 버튼을 누르거나 텍스트 뷰에서 X 버튼을 눌렀을 때, 입력했던 데이터가 모두 지워져야한다.
텍스트뷰에 있는 데이터 뿐만 아니라 위에서 전역변수로 사용하고 있는 _searchText도 초기화가 되어야한다.
onPressed: () {
setState(() {
_filter.clear();
_searchText = "";
});
},
취소 버튼을 눌렀을 때는 포커스를 지우는 기능까지 추가.
...
onPressed: () {
setState(() {
_filter.clear();
_searchText = "";
focusNode.unfocus();
});
},
...
Expanded
Expanded 위젯은 자식 위젯이 부모 위젯의 공간을 모두 사용할 수 있도록 하는 데 사용하는 위젯.
부모 위젯의 공간을 비율로 나누어 모두 사용 할 수 있게 함.
안드로이드의 릴레이티브 레이아웃과 유사한 역할을 함.
flex : Expanded의 속성으로 비율을 지정해줌.
검색창 + 취소 버튼을 5:1 비율로 가로를 꽉 차게 해서 구현하기 위해 사용.
Expanded(
flex: 5,
child: TextField(
focusNode: focusNode,
style: const TextStyle(
fontSize: 15,
),
autofocus: true,
controller: _filter,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white12,
prefixIcon: const Icon(
Icons.search,
color: Colors.white60,
size: 20,
),
suffixIcon: focusNode.hasFocus
? IconButton(
icon: const Icon(
Icons.cancel,
size: 20,
),
onPressed: () {
setState(() {
_filter.clear();
_searchText = "";
});
},
)
: Container(),
hintText: 'Search',
labelStyle: const TextStyle(color: Colors.white),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
border: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
전체 UI는
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
const Padding(padding: EdgeInsets.all(30)),
Container(
color: Colors.black,
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
child: Row(
children: <Widget>[
Expanded(
flex: 5,
child: TextField(
focusNode: focusNode,
style: const TextStyle(
fontSize: 15,
),
autofocus: true,
controller: _filter,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white12,
prefixIcon: const Icon(
Icons.search,
color: Colors.white60,
size: 20,
),
suffixIcon: focusNode.hasFocus
? IconButton(
icon: const Icon(
Icons.cancel,
size: 20,
),
onPressed: () {
setState(() {
_filter.clear();
_searchText = "";
});
},
)
: Container(),
hintText: 'Search',
labelStyle: const TextStyle(color: Colors.white),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
border: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
focusNode.hasFocus
? Expanded(
child: TextButton(
onPressed: () {
setState(() {
_filter.clear();
_searchText = "";
focusNode.unfocus();
});
},
child: const Text('Cancle',
style: TextStyle(fontSize: 13)),
),
)
: Expanded(
flex: 0,
child: Container(),
)
],
),
),
_buildBody(context)
],
),
);
}
}
이렇습니다.
동작하는 모습 ::