🧑🏻🎨 자동 완성 검색창이에요! 검색어 입력하면 실시간으로 밑에 키워드가 나오고, 그 아래 레이어에 결과가 보였으면 좋겠어요.
👩🏻💻 그러니까 구글이나 네이버 검색창 같은거 말씀이시죠?
🧑🏻🎨 네 맞아요!
=> 플러터에서 제공하는 위젯만 잘 써먹으면 스택을 사용하지 않고 구현할 수 있다.
Overlay는 Stack의 일종이다. Overlay에게 전달된 child 위젯은 각각 독립적인 상태를 가질 수 있으며, UI상에서 마치 떠있는 것처럼 보여지게 된다. Overlay의 child로 들어갈 수 있는 위젯 클래스는 OverlayEntry, Overlay의 상태에 대한 정보를 담는 클래스는 OverlayState이다.
그러니까 Overlay가 위젯을 담는 스택이 되고 그 안에 우리가 원하는 위젯을 OverlayEntry로 넣으면 된다. 단, Overlay에는 다른 위젯과 달리 children이라는 파라미터가 없어서 위젯을 바로 넘겨줄 수 없다. 대신 Overlay의 상태를 나타내는 OverlayState의 멤버 함수인 insert
를 사용해야 한다. 이렇게 해야 하는 이유는 개발자가 Overlay의 children을 원하는 시점에 보여줄 수 있어야 하기 때문이다.
아래는 OverlayState.insert
함수의 코드다. 먼저 entry._overlay = this;
를 통해 주어진 OverlayEntry가 현재 OverlayState의 주인인 this
Overlay에 속한다는 것을 등록한다. OverlayEntry 하나는 절대 2개 이상의 서로 다른 Overlay에 속할 수 없기 때문에 어떤 Overlay에 속하는지 알 수 있어야 한다. 이 함수의 마지막 assert
문에서 이 사실을 확인하고 있다.
void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) {
assert(_debugVerifyInsertPosition(above, below));
assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.');
assert(entry._overlay == null, 'The specified entry is already present in another Overlay.');
entry._overlay = this;
setState(() {
_entries.insert(_insertionIndex(below, above), entry);
});
}
그 다음으로 setState
를 호출하면서 주어진 OverlayEntry를 OverlayState._entries
에 삽입하고 있다. 따라서 우리가 insert
함수를 실행하면 setState
가 호출되고, Overlay는 한번 더 build 되어서 OverlayEntry가 화면에 보이게 된다. 즉, Overlay는 child 위젯이 보여지는 시점을 조절할 수 있는 Stack이라고 볼 수 있다.
insert
로 넣은 위젯은 OverlayEntry.remove
로 뺄 수 있다. 이 때에도 build가 한번 더 일어나서 OverlayEntry의 위젯이 사라진다. 기억할 점은 insert
는 OverlayState 클래스에, remove
는 OverlayEntry 클래스에 있다는 점이다.
// stateful widget
late final OverlayEntry overlayEntry =
OverlayEntry(builder: _overlayEntryBuilder);
void dispose() {
overlayEntry.dispose();
super.dispose();
}
void insertOverlay() { // 적절한 타이밍에 호출
if (!overlayEntry.mounted) {
OverlayState overlayState = Overlay.of(context)!;
overlayState.insert(overlayEntry);
}
}
void removeOverlay() { // 적절한 타이밍에 호출
if (overlayEntry.mounted) {
overlayEntry.remove();
}
}
Widget _overlayEntryBuilder(BuildContext context) {
Offset position = _getOverlayEntryPosition();
Size size = _getOverlayEntrySize();
return Positioned(
left: position.dx,
top: position.dy,
width: Get.size.width - MyConstants.SCREEN_HORIZONTAL_MARGIN.horizontal,
child: AutoCompleteKeywordList(),
);
}
네.. 그러네요.
Overlay의 역할은 "스택처럼 보여주기"에서 끝난다. 그래서 여기까지만 하면 검색바와 키워드 목록이 분리되어 따로 돌아다닐 수 있다. 하지만 우리가 원하는건 스크롤했을 때에도 검색바와 키워드 목록이 분리되지 않고 잘 따라다니는 UI니까 추가 작업이 필요하다.
CompositedTransformTarget과 CompositedTransformFollower를 사용하면 쉽게 이 둘을 연결할 수 있다. 이름에서 짐작할 수 있듯이, follower 위젯이 target 위젯을 따라다닌다. 이 둘을 연결하려면 LayerLink 를 사용한다. layerLink에 target과 follower를 등록해서 사용하는 방식이다.
기존 코드에서 검색바를 CompositedTransfromTarget, 키워드 목록을 CompositedTransfromFollower로 감싸주고 link
에 서로 같은 LayerLink 오브젝트를 넘겨주면 된다. 이 때 follower의 offset
을 통해 상대적인 위치를 정할 수 있다. 검색바의 높이만큼 dy가 필요하기 때문에 Offset(0.0, size.height)
를 넘겨주었다. 여기서 size
는 검색바의 사이즈를 의미한다.
// 기존 stateful widget에 추가
final LayerLink _searchBarLink = LayerLink();
// 기존 검색바 위젯의 build 함수
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _searchBarLink,
child: // (기존 검색바 위젯)
);
}
// 키워드 목록 overlayEntryBuilder
Widget _overlayEntryBuilder(BuildContext context) {
Offset position = _getOverlayEntryPosition();
Size size = _getOverlayEntrySize();
return Positioned(
left: position.dx,
top: position.dy,
width: Get.size.width - MyConstants.SCREEN_HORIZONTAL_MARGIN.horizontal,
child: CompositedTransformFollower(
link: _searchBarLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height),
child: AutoCompleteKeywordList(),
),
);
}
드디어 검색바와 키워드 목록은 뗄 수 없는 관계가 되었다.🤗 검색 결과와 키워드 목록을 보여주기 위해 google search api를 연동했다.