SELF STUDY/Flutter

[Flutter] 넷플릭스 클론 코딩 #10 | 검색 화면 | 파이어베이스에서 데이터 검색 후 결과 추출 | Expanded | TextEditingController |

호이호이호잇 2024. 5. 11. 12:30
728x90
반응형

이번 시간에는 검색 화면을 구현해보았다!

일치하는 데이터를 보여주는 것 까쥐~

 

이제 곧 클론 코딩의 끝이 보인다 +_+

 

# 검색 문자 추적

데이터의 변화를 지켜보기 위해 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)
        ],
      ),
    );
  }
}

 

이렇습니다.

 

동작하는 모습 ::

 

728x90
반응형