今日的程式碼 => GTIHBU
今天來講講搜尋的介紹。那當然,肯定要以 github 搜尋為範例啦。哈哈哈~~
今天會用到
程式碼參考來自 => RxDart 範例
可以看到,這邊有一個變數叫做 cache,用來暫時儲存快取的資料,是一個 Map 的型態 Map<搜尋的字, 搜尋結果>,當我們要 fetch api 時,可以先去判斷 cache 裡面有沒有這筆資料的值,這樣子。
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
class GithubApi {
/// url
final String baseUrl;
/// 快取,<搜尋的字, 搜尋結果>
final Map<String, SearchResult> cache;
/// http client
final http.Client _client;
GithubApi({
http.Client? client,
Map<String, SearchResult>? cache,
this.baseUrl = 'https://api.github.com/search/repositories?q=',
}) : _client = client ?? http.Client(),
cache = cache ?? <String, SearchResult>{};
/// Search Github for repositories using the given term
/// 處理快取
Future<SearchResult> search(String term) async {
final cached = cache[term];
if (cached != null) {
return cached;
} else {
final result = await _fetchResults(term);
cache[term] = result;
return result;
}
}
/// 請求 api
Future<SearchResult> _fetchResults(String term) async {
final response = await _client.get(Uri.parse('$baseUrl$term'));
final results = json.decode(response.body);
return SearchResult.fromJson(results['items']);
}
}
class SearchResult {
final List<SearchResultItem> items;
SearchResult(this.items);
factory SearchResult.fromJson(dynamic json) {
final items = (json as List)
.map((item) => SearchResultItem.fromJson(item))
.toList(growable: false);
return SearchResult(items);
}
bool get isPopulated => items.isNotEmpty;
/// 定義 isEmpty,這樣就不用使用 SearchResult.items.isEmpty
bool get isEmpty => items.isEmpty;
}
class SearchResultItem {
final String fullName;
final String url;
final String avatarUrl;
SearchResultItem(this.fullName, this.url, this.avatarUrl);
factory SearchResultItem.fromJson(Map<String, dynamic> json) {
return SearchResultItem(
json['full_name'] as String,
json['html_url'] as String,
(json['owner'] as Map<String, dynamic>)['avatar_url'] as String,
);
}
}
這邊先去 New 一個 GitHubApi 出來,之後用來在 SearchScreen 實作 bloc。
void main() => runApp(SearchApp(api: GithubApi()));
class SearchApp extends StatelessWidget {
final GithubApi api;
const SearchApp({Key? key, required this.api}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RxDart Github Search',
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.grey,
),
home: SearchScreen(api: api),
);
}
}
這邊是一個放邏輯的地方。
可以看到我們使用了 RxDart
Rxdart
相信這邊看官網會比較清楚的(ㄅ~~
可以看到我們使用了一個 PublishSubject,他是一個類似 stream 的 broadcast 效果。
下面用到的一些函示都是 RxDart 幫我們整合的函式。
更多的資料可以看 Rx Function 文件
class SearchBloc {
final Sink<String> onTextChanged;
final Stream<SearchState> state;
/// 這邊用 factory 的目的,是為了讓這個 SearchBloc 參數一樣時,物件判定會是一樣的。
/// 雖然傳遞的參數只有一個,但是實際上我們要創建這個 SearchBloc 需要兩個參數。
///
/// 建構子前以關鍵字 factory 宣告一個工廠建構子,工廠建構子不一定會產生一個新物件,可能回傳一個既存物件。
/// 要注意工廠建構子在return之前還未有實體,故不能使用this引用成員變數的值或呼叫函數。
factory SearchBloc(GithubApi api) {
final onTextChanged = PublishSubject<String>();
final state = onTextChanged
// If the text has not changed, do not perform a new search
// 如果與前一比資料一樣,將不會觸發。
.distinct()
// Wait for the user to stop typing for 250ms before running a search
// 等 0.25 秒後,才開始搜尋,執行 api
.debounceTime(const Duration(milliseconds: 250))
// Call the Github api with the given search term and convert it to a
// State. If another search term is entered, switchMap will ensure
// the previous search is discarded so we don't deliver stale results
// to the View.
// 如果輸入 a 的時候,開始搜尋,再輸入 b 後,變成 ab,但是 a 的搜尋結果還沒出來,那麼變成 ab 後,他會把這個還沒搜尋完成的 a 流程給停止掉。避免浪費搜尋時間。
.switchMap<SearchState>((String term) => _search(term, api))
// The initial state to deliver to the screen.
// 在這個 stream 的最前面加上一個初始值
.startWith(SearchNoTerm());
// 這邊已經初始化完成了,在第 15 行、17 行
return SearchBloc._(onTextChanged, state);
}
SearchBloc._(this.onTextChanged, this.state);
// 給畫面 call 的,好讓這個 dispose 掉。
void dispose() {
onTextChanged.close();
}
static Stream<SearchState> _search(String term, GithubApi api) => term.isEmpty
? Stream.value(SearchNoTerm())
// when the future completes, this stream will fire one event, either data or error, and then close with a done-event.
// Rx.fromCallable,它在偵聽時調用您指定的函數,然後發出從該函數返回的值。這整個 Rx.fromCallable(()=>future) 會是一個 Stream
: Rx.fromCallable(() => api.search(term))
.map((result) =>
result.isEmpty? SearchEmpty() : SearchPopulated(result))
.startWith(SearchLoading())
.onErrorReturn(SearchError());
}
class SearchState {}
class SearchLoading extends SearchState {}
class SearchError extends SearchState {}
class SearchNoTerm extends SearchState {}
class SearchPopulated extends SearchState {
final SearchResult result;
SearchPopulated(this.result);
}
class SearchEmpty extends SearchState {}
class SearchScreen extends StatefulWidget {
final GithubApi api;
const SearchScreen({Key? key, required this.api}) : super(key: key);
@override
SearchScreenState createState() {
return SearchScreenState();
}
}
class SearchScreenState extends State<SearchScreen> {
late final SearchBloc bloc;
@override
void initState() {
super.initState();
bloc = SearchBloc(widget.api);
}
@override
void dispose() {
bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<SearchState>(
stream: bloc.state,
initialData: SearchNoTerm(),
builder: (BuildContext context, AsyncSnapshot<SearchState> snapshot) {
final state = snapshot.requireData;
return Scaffold(
body: Stack(
children: <Widget>[
Flex(direction: Axis.vertical, children: <Widget>[
Container(
padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 4.0),
child: TextField(
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Search Github...',
),
style: const TextStyle(
fontSize: 36.0,
fontFamily: 'Hind',
decoration: TextDecoration.none,
),
onChanged: bloc.onTextChanged.add,
),
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _buildChild(state),
),
)
])
],
),
);
},
);
}
Widget _buildChild(SearchState state) {
print(state);
if (state is SearchNoTerm) {
return const SearchIntro();
} else if (state is SearchEmpty) {
return const EmptyWidget();
} else if (state is SearchLoading) {
return const LoadingWidget();
} else if (state is SearchError) {
return const SearchErrorWidget();
} else if (state is SearchPopulated) {
return SearchResultWidget(items: state.result.items);
}
throw Exception('${state.runtimeType} is not supported');
}
}