【第二二天 - Flutter GitHub Search 范例+RxDart+搜寻快取】

前言

今日的程序码 => GTIHBU

今天来讲讲搜寻的介绍。那当然,肯定要以 github 搜寻为范例啦。哈哈哈~~
今天会用到

程序码参考来自 => RxDart 范例

GithubApi

可以看到,这边有一个变数叫做 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,
    );
  }
}

Home

这边先去 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),
    );
  }
}

Search_Bloc

这边是一个放逻辑的地方。
可以看到我们使用了 RxDart
Rxdart

相信这边看官网会比较清楚的(ㄅ~~

可以看到我们使用了一个 PublishSubject,他是一个类似 stream 的 broadcast 效果。
下面用到的一些函示都是 RxDart 帮我们整合的函式。

  • distinct()
    • 如果与前一比资料一样,将不会触发。
  • debounceTime(const Duration(milliseconds: 250))
    • 等 0.25 秒後,才开始搜寻,执行 api
  • switchMap((String term) => _search(term, api))
    • 这个超重要,搜寻的时候常常会用到。如果输入 a 的时候,开始搜寻,再输入 b 後,变成 ab,但是 a 的搜寻结果还没出来,那麽变成 ab 後,他会把这个还没搜寻完成的 a 流程给停止掉。避免浪费搜寻时间。
  • startWith(SearchNoTerm())
    • 在这个 stream 的最前面加上一个初始值。

更多的资料可以看 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());
}

事件的状态 SearchState

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 {}

SearchScreen

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');
  }
}

<<:  中阶魔法 - 陈述式与表达式

>>:  Day 22 盘点资料敏感度实作

# Day17--那天....我学Wendy跪着读完的OOP

什麽是物件导向? 为什麽需要物件导向? 物件导向重要在什麽地方? 要回答第一个问题前,必须先回答一...

D-8. Rails 用Postman测试自己的WEB API && Valid Parentheses

请先安装Postman 今天完成整个CRUD,简单介绍操作Postman。 接续昨天文章 9.修改r...

模型架构--2

SphereFace 在2017年发表在CVPR的文章,改进原先使用softmax作为loss fu...

Day4 跟着官方文件学习Laravel-CSRF保护

举例: 想像你的产品有个/user/email route允许post request去修改已经认证...

Ascii - 产生 3D 旋转甜甜圈的甜甜圈形 C 程序码参考笔记

Ascii - 产生 3D 旋转甜甜圈的甜甜圈形 C 程序码参考笔记 参考资料 参考资料: Donu...