최소한의 설정으로 빠르게 통합할 수 있도록 설계된 가볍고 고성능의 검색 페이지 위젯입니다. 다양한 플랫폼에서 원활한 검색 경험을 제공하도록 제작되었으며, 최적화된 검색 입력 처리 및 반응형 UI 업데이트를 포함합니다. 복잡한 맞춤 설정 없이 깔끔하고 기능적인 검색 인터페이스가 필요한 앱에 이상적입니다. 검색 기능의 속도와 단순성을 중시하는 개발자에게 적합합니다.
사용 사례
모바일 및 웹 앱에서의 빠른 검색 구현
최소한의 코드로 기존 페이지에 검색 통합하기
콘텐츠가 많은 앱을 위한 반응형 검색 인터페이스 구축
대시보드 또는 제품 목록에 검색 기능 추가
낮은 지연 시간으로 실시간 필터링 지원
주요 기능
빠른 검색 응답을 위한 최적화된 성능
크로스 플랫폼 호환성 (Android, iOS, Web, macOS, Windows, Linux)
설정에 필요한 최소한의 보일러플레이트
디바운스 지원이 있는 내장 검색 입력
정돈되고 사용자 정의 가능한 UI 컴포넌트
적합한 대상
빠른 검색 페이지 통합을 원하는 개발자
경량 검색 솔루션을 필요로 하는 프로젝트
일관된 UX를 갖춘 다중 플랫폼 대상 앱
성능과 유지보수에 중점을 둔 팀
고려 사항
제한된 내장 고급 필터링 옵션
복잡한 검색 요구 사항의 경우 사용자 정의 로직이 필요할 수 있습니다
기본에서 중급 검색 사용 사례에 가장 적합
높은 맞춤형 또는 기업용 검색 시스템에는 적합하지 않습니다
원본 README
연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.
search_app_bar_page
A Flutter package to give you a simple search page.
🤖 AI / LLM Usage Instructions
For AI Agents & Copilots:
Standard List: Use SearchAppBarPage (Memory list, no GetX).
GetX Reactive: Use SearchAppBarPageObx (Memory list, GetX reactivity).
API/Async: Use SearchAppBarPageVariableList (Async API calls via listVariableFunction).
Filtering: Always use whereFilter parameter for local lists.
Config: Map title to searchAppBarTitle and actions to searchAppBarActions.
Full decision tree and code snippets are available in the public library documentation (/// comments) of lib/search_app_bar_page.dart.
Introduction
This package was built to generate a complete and reactive search screen with the best possible
facility. It is based on another package. Open here
= > search_app_bar (by - <rodolfoggp@gmail.com>). There you
have the details of the search_app_bar functions. With the changes here, you do not need to extend
the controller class, but just pass the base list to be filtered or insert the stream that returns
your list to be filtered. In this case, there is already a StreamBuilder to do the background
treatment. Unfortunately, I was unable to update the basic package, as Rodolfo has not handled the
package for more than 01 year and has not responded to my request for changes by email.
✷ The page is divided between
SEARCH_APP_BAR
CONTROLLER
BODY de um Scaffold.
Required parameters
We have four pages:
SearchAppBarPageVariableList, SearchAppBarPageObx, SearchAppBarPage, SearchAppBarPageStream, SearchAppBarPagination
and SimpleAppBarPage
🔎 SearchAppBarPage, SearchAppBarPageObx and SearchAppBarPageVariableList work similarly regarding filtering parameters. They need a function to build the Widget (obxListBuilder) depending on the filtered list.
If you use typed objects (Generic <T>), you need [stringFilter]. This is a function that receives the parameter T and returns the String to filter by.
Example: For a Person object, return person.name. This will be used to filter by the search query.
Tips
The function [obxListBuilder] is inside an Obx. Place reactive verables into it.
✳️ There are two ways to add reactive variables
Boot your controller into a StatefulWidget.
Pass the reactive variable inside this function [obxListBuilder] in SearchAppBarPage and
SearchAppBarPageStream.
Add reactive authentication parameters. Insert your RxBool that changes with the authentication
status to reactivity. The body will be rebuilt when authentication is false.
Set [rxBoolAuth] to SearchAppBarPage, SearchAppBarPageStream and SearchAppBarPagination.
Example full for more details. Both examples
in SearchAppBarPage.
We just added SearchAppBarPageObx. If you have an RxBool isLoading, you can pass it inside the obxListBuilder function and it will control the loading while the list is being loaded. Unlike SearchAppBarPage where it was necessary to insert an Obx previously because the complete list is static.
Use now SearchAppBarPageObx!
classSearchAppBarPageObx<T> extendsStatefulWidget{
//...SearchAppBarPageObx( //.../// Parameters for the SearcherGetController/// final RxList<T> listRx;@requiredthis.listRx,
/// [obxListBuilder] Function applied when it is filtering in search.@requiredthis.obxListBuilder,
/// [stringFilter] Required if you type.'///You should at least type with String.this.stringFilter
// or → (not both together)/// [whereFilter] Required if you type./// Create your own function that returns a bool.this.whereFilter,
//...
)
///...// Example:
obxListBuilder:
(
BuildContext context,
List<Model> list,
bool isModSearch,
int highLightIndex,
/// Use highLightIndex to manipulate from the keyboard and select the item using [onEnter] or [onSubmit]./// There is a need to modify the list widget to move the scroll to the selected item, changing the background of the selected item.///👉 highLightIndex goes from item 0 to item list.length - 1.
) {
if (controller.isLoading.value) {
returnconstCenter(child: CircularProgressIndicator());
}
if (list.isEmpty) {
returnconstCenter(child: Text('Nothing was found'));
}
/// ...
}
Use highLightIndex, onEnter, and onSubmit to create a keyboard-friendly experience.
highLightIndex: Provided in obxListBuilder. Use it to visually highlight the currently selected item in your list (e.g., change background color). The index updates automatically with Arrow Up/Down keys.
onEnter: Triggered when the user presses Enter while navigating the full list (search is NOT active).
Signature: (List<T> listFull, int highLightIndex)
Usage: Open the details of the item at highLightIndex in listFull.
onSubmit: Triggered when the user presses Enter inside the search field.
Signature: (String query, List<T> listFiltered, int highLightIndex)
Usage: Open the details of the item at highLightIndex in listFiltered, or perform a search action with query.
Example Implementation:
SearchAppBarPageObx<PacienteModel>(
// ... other parameters
listRx: controller.pacientes,
// 1. Handle selection on full list
onEnter: (listFull, highLightIndex) {
if (highLightIndex >= 0 && highLightIndex < listFull.length) {
controller.abrirProntuario(listFull[highLightIndex]);
}
},
// 2. Handle selection on filtered list (Search)
onSubmit: (query, listFiltered, highLightIndex) {
if (highLightIndex >= 0 && highLightIndex < listFiltered.length) {
controller.abrirProntuario(listFiltered[highLightIndex]);
} elseif (listFiltered.isNotEmpty) {
// Fallback: Open first item if no specific highlight
controller.abrirProntuario(listFiltered[0]);
}
},
obxListBuilder: (context, list, isModSearch, highLightIndex) {
// ... handling loading and empty statesreturnListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
// 3. Visual Highlightfinal isSelected = index == highLightIndex;
returnContainer(
// Change color to indicate selection
color: isSelected ? Colors.blue.withOpacity(0.1) : null,
child: ListTile(
title: Text(item.nome),
// ...
),
);
},
);
},
);
We added SearchAppBarPageVariableList. Use it when you need to search for a value in a list that is not static. It receives a listVariableFunction function that returns a FutureOr<List<T>>. This function is called every time the search query changes. It is useful for searching in an API or a database. Return a list of items from a query. Filter however you want.
classSearchAppBarPageVariableList <T> extendsStatefulWidget{
//...SearchAppBarPageVariableList ( //.../// Parameters para o SearcherGetController/// final ListVariableFunction<T>? listVariableFunction;/// [listVariableFunction] Function to fetch list items asynchronously or no.@requiredthis.listVariableFunction,
/// [obxListBuilder] Function applied when it is filtering in search.@requiredthis.obxListBuilder,
)
}
We added SearchAppBarPage. Use it when you need to search for a value in a list that is static. It receives a listFull function that returns a List<T>. This function is called every time the search query changes. It is useful for searching in an API or a database. Return a list of items from a query. Filter however you want.
classSearchAppBarPage<T> extendsStatefulWidget{
//...SearchAppBarPage( //.../// Parameters para o SearcherGetController/// final List<T> listFull;@requiredthis.listFull,
/// [obxListBuilder] Function applied when it is filtering in search.@requiredthis.obxListBuilder,
/// [stringFilter] Required if you type.///You should at least type with String.this.stringFilter
//...
)
}
🔎 SearchAppBarPage
import'package:flutter/material.dart';
import'package:search_app_bar_page/search_app_bar_page.dart';
classSearchPageextendsStatefulWidget{
@override_SearchPageStatecreateState() => _SearchPageState();
}
class_SearchPageStateextendsState<SearchPage> {
TestController controll_1;
/// ## ✳️ Learning both ways to add reactive variables.@overridevoidinitState() {
///-------------------------------------------------------------------/// Add other reactive parameters inside the body./// ✅ Boot your controller into a StatefulWidget.///-------------------------------------------------------------------
controll_1 = Get.find<TestController>();
super.initState();
Future.delayed(constDuration(seconds: 4), () {
///------------------------------------------/// Test to check the reactivity of the screen./// Reactive variable as parameter - [rxBoolAuth]///------------------------------------------/// 👇🏼
controll_1.changeAuth = true;
});
Future.delayed(constDuration(seconds: 6), () {
///------------------------------------------/// Test to check the reactivity of the screen.////// Reactive variable as parameter within the function [obxListBuilder]///------------------------------------------/// 👇🏼
controll_1.rxList.update((value) {});
});
}
boolstartsWithListCalendar(Person test, String? query) {
if (query == null) {
returnfalse;
}
final realTestName = removeDiacritics(test.name).toLowerCase();
final realTestAge = removeDiacritics(test.age.toString()).toLowerCase();
final realQuery = removeDiacritics(query.toLowerCase());
// return realTest.contains(realQuery);return realTestName.startsWith(realQuery) || realTestAge.startsWith(realQuery);
}
@overrideWidgetbuild(BuildContext context) {
//return SearchAppBarPage<String>(returnSearchAppBarPage<Person>(
magnifyinGlassColor: Colors.white,
searchAppBarcenterTitle: true,
searchAppBarhintText: 'Search for a name',
searchAppBartitle: Text(
'Search Page',
style: TextStyle(fontSize: 20),
),
//listFull: dataList, // Lista String
listFull: dataListPerson2,
stringFilter: (Person person) => person.name,
//compare: false,///--------------------------------------------/// ✅ Add the auth reactive parameters./// The body will be rebuilt when the auth is false.///---------------------------------------------/// Do your own research manually.//whereFilter: startsWithListCalendar,
onSubmit: (query, listFiltered) {
debugPrint('🚀 main.dart - query - $query');
debugPrint('🚀 main.dart - listFiltered - ${listFiltered.toString()}');
},
rxBoolAuth: RxBoolAuth.input(
rxBoolAuthm: Get
.find<TestController>()
.rxAuth,
authFalseWidget: () =>
Center(
child: Text(
'Please login.',
style: TextStyle(fontSize: 22),
),
)),
filtersType: FiltersTypes.contains,
obxListBuilder: (context, list, isModSearch) {
// ☑️ This function is inside an Obx.// Place other reactive verables into it.///----------------------------------------------------/// Changes to the rxList will also rebuild the widget.///----------------------------------------------------print(' TEST -- ${controll_1.rxList.length.toString()} ');
///-------------------------------------------------------------/// Changes to the filtered list will also reconstruct the body.///-------------------------------------------------------------if (list.isEmpty) {
returnCenter(
child: Text(
'NOTHING FOUND',
style: TextStyle(fontSize: 14),
));
}
returnListView.builder(
itemCount: list.length,
itemBuilder: (_, index) {
returnCard(
margin: constEdgeInsets.symmetric(horizontal: 16, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4)),
// color: Theme.of(context).primaryColorDark,
child: Padding(
padding: constEdgeInsets.all(14.0),
child: Row(
children: [
Expanded(
child: Text(
'Name: ${list[index].name}',
style: TextStyle(fontSize: 16),
),
),
Expanded(
child: Text(
'Age: ${list[index].age.toStringAsFixed(2)}',
style: TextStyle(fontSize: 12),
),
)
],
),
));
},
);
},
);
}
}
final dataListPerson2 = <Person>[
Person(name: 'Rafaela Pinho', age: 30),
Person(name: 'Paulo Emilio Silva', age: 45),
Person(name: 'Pedro Gomes', age: 18),
Person(name: 'Orlando Guerra', age: 23),
Person(name: 'Zacarias Triste', age: 15),
Person(name: 'Antonio Rabelo', age: 33),
Person(name: 'Leticia Maciel', age: 47),
Person(name: 'Patricia Oliveira', age: 19),
Person(name: 'Pedro Lima', age: 15),
Person(name: 'Junior Rabelo', age: 33),
Person(name: 'Lucia Maciel', age: 47),
Person(name: 'Ana Oliveira', age: 19),
Person(name: 'Thiago Silva', age: 33),
Person(name: 'Charles Ristow', age: 47),
Person(name: 'Raquel Montenegro', age: 19),
Person(name: 'Rafael Peireira', age: 15),
Person(name: 'Nome Comum', age: 33),
];
classPerson{
finalString name;
finalint age;
Person({this.name, this.age});
@overrideStringtoString() {
return'Person{name: $name, age: $age}';
}
}
🔎 SearchAppBarPageStream needs a stream that is already worked on, that is, there is already
a Widget by default for error and waiting. You can modify them at will. You also need a function
that is passed on to assemble the Widget that will be presented on the Body, depending on the
filtered list. This is renewed by the natural flow of the stream and also by the search filtering.
classSearchAppBarPageStream<T> extendsStatefulWidget{
//...SearchAppBarPageStream( //.../// final Stream<List<T>> listStream;@requiredthis.listStream,
///final FunctionList<T> obxListBuilder;/// Function applied when receiving data through Stream or filtering in search.@requiredthis.obxListBuilder,
/// [stringFilter] Required if you type.///You should at least type with String.this.stringFilter
// ...
)
//...
}
🔎 SearchAppBarPageRefresh need a Future that will be remade the Refresh. It is important not
to put parentheses in the parameter. See below.
classSearchAppBarPageRefresh<T> extendsStatefulWidget{
//...SearchAppBarPageRefresh( //.../// final FuncionRefresh<T> functionRefreshrequiredthis.functionRefresh,
///final FunctionList<T> obxListBuilder;/// Function applied when receiving data through Stream or filtering in search.@requiredthis.obxListBuilder,
/// [stringFilter] Required if you type.///You should at least type with String.this.stringFilter
// ...
)
//...
}
🔎 SearchAppBarPagination is built for fragmented requests for your API. If you
have hundreds of data and would like to send them in parts, in addition to being able to filter them
efficiently, this Widget is the chosen one. There is a cache of requests to avoid getting (REST)
unnecessarily. The cache reset when a screen is disposed. What differs from StreamPage is that a
function that forwards the page and the search string query will always be called when necessary.
Example: reaches the bottom of the page. Remembering that this function must return a Future (get
for your API). You will see an example with the server side
below in Dart.
classSearchAppBarPagination<T> extendsStatefulWidget{
//...SearchAppBarPagination( //...///Returns Widget from the object (<T>). This comes from the List <T> index.///typedef WidgetsPaginationItemBuilder<T> = Widget Function(/// BuildContext context, int index, T objectIndex);///final WidgetsPaginationItemBuilder<T> paginationItemBuilder;@requiredthis.paginationItemBuilder,
///Return the list in parts or parts by query String///filtered. We make the necessary changes on the device side to update the///page to be requested. Eg: If numItemsPage = 6 and you receive 05 or 11///or send empty, = >>> it means that the data is over.///typedef FutureFetchPageItems<T> = Future<List<T>> Function(int page, String query);/// final FutureFetchPageItems<T> futureFetchPageItems;@requiredthis.futureFetchPageItems,
/// [stringFilter] Required if you type.///You should at least type with String.this.stringFilter
// ...
)
//...
}
😱 You can make a setState on your stream page and your pagination or a rot Reload without any problems. Even changing the value of initialData
Attencion: The number of elements must be more than 14 (numItemsPage > 14)
Ex: If numItemsPage = 20 (total items on a page) and you send a list with a length less than 20 or
send an empty list, = >>> means that the data is over. If you send null: if there is no data yet,
return an Exception; if a data already exists, nothing happens.
Return types for future FetchPageItems
List equal to numItemsPage (list.length == numItemsPage) = continues to request new pages.
Empty list or list smaller than numItemsPage (list.length < numItemsPage) = ends the request
for pages. Be it a complete list, be it the list filtered by the search. The API request for a
list filtered by the search is only fulfilled if the complete list is not finalized or the list
request filtered by the search has not been finalized by the same principles above.
List longer than numItemsPage (list.length> numItemsPage) = return an Exception. The current
page is calculated depending on the numItemsPage being constant.
Null return. If you send null: if there is still no data, return an Exception; if a data already
exists, it returns the same data in the cache. For now the cahe is restarted when the screen
receives dispose.
❕Tip: Build a cache of your requests for the API and setState your page without problems
I had to spend a few hours testing it so there were no mistakes. Do tests and if you find an error, I would be happy to resolve them as soon as possible
🔎 SimpleAppBarPage you need to do the work of assembling your page manually.
Start your controller, close it and implement the didUpdateWidget method for setState or Hot Reload
on your page. There is already a widget to fit the body of your page => [RxListWidget].
[iconConnectyOffAppBar] Appears when the connection status is off. There is already a default
icon. If you don't want to present a choice [hideDefaultConnectyIconOffAppBar] = true; If you want
to have a custom icon, do [hideDefaultConnectyIconOffAppBar] = true; and set
the [iconConnectyOffAppBar].
[widgetOffConnectyWaiting] Only shows something when it is disconnected and does not yet have the
first value of the stream. If the connection goes back to show the [widgetWaiting] until you
receive the first data. Everyone already comes with They all come with default widgets.
Now you can assemble a Widget from RxString, RxInt, RxDouble, RxNumber and RxList. With the
[getStreamWidget] extension. It turns your Rx variable into a [GetStreamWidget]. If
there is nothing in the stream, it starts the wait. When you add something, it launches
obxBuilder. If there is an error, it starts the ErrorWidget. Ready to use.