Chapter 21

  検索ボックスの実装

heyhey1028
heyhey1028
2023.02.28に更新

UI の開発の初めは、検索ボックスの実装です。

現時点で検索画面である search_screen.dart は以下のような実装になっています。ここに検索ボックスを追加していきましょう

search_screen.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';

import 'package:flutter/material.dart';
import 'package:qiita_search/models/article.dart';

class SearchScreen extends StatefulWidget {
  const SearchScreen({super.key});

  
  State<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Qiita Search'),
      ),
      body: Container(),
    );
  }

  Future<List<Article>> searchQiita(String keyword) async {
    final uri = Uri.https('qiita.com', '/api/v2/items', {
      'query': 'title:$keyword',
      'per_page': '10',
    });

    final String token = dotenv.env['QIITA_ACCESS_TOKEN'] ?? '';
    final http.Response res = await http.get(uri, headers: {
      'Authorization': 'Bearer $token',
    });

    if (res.statusCode == 200) {
      // レスポンスをモデルクラスへ変換
      final List<dynamic> body = jsonDecode(res.body);
      return body.map((dynamic json) => Article.fromJson(json)).toList();
    } else {
      return [];
    }
  }
}

実装

さてそれでは1つ1つ実装していきましょう。

TextFieldを配置する

今回は検索ボックスを上、その下に検索結果を一覧で表示します。画面は検索ボックスと検索結果の2つのパーツが上から縦に並んでいるようなレイアウトになるので、Column widget を使います。

body: Column(
  children: [
    // 検索ボックス
    // 検索結果一覧
  ],
),

検索結果一覧の部分は次章で実装するとして、まず検索ボックスとなるTextFieldwidget を Column の中に配置しておきましょう。

body: Column(
  children: [
    // 検索ボックス
    TextField(), // ← TextFieldをchildren内に追加
    // 検索結果一覧
  ],
),

検索ボックスに余白を作る

TextFieldを配置すると下記のように横幅いっぱいに広がってしまい、見てくれが悪いので、見た目を少し調整していきましょう。

余白を作るにはPaddingwidget を使います。余白を追加したいTextFieldPaddingで囲み、paddingプロパティにEdgeInsetsを渡すことで余白を指定できます。

EdgeInsetsクラスでは上下左右を1つ1つ指定することもできますが、今回は水平方向、垂直方向に同じ分だけ余白を作りたいので、.symmetric()メソッドを使って、余白を指定します。

body: Column(
  children: [
    // 検索ボックス
      Padding( // ← Paddingで囲む
        padding: const EdgeInsets.symmetric(
          vertical: 12,
          horizontal: 36,
        ),
      child: TextField(),
    ),
    // 検索結果一覧
  ],
),

余白が出来ました。

検索ボックスのフォントを調整する

文字が少し小さいのでフォントサイズを大きくしてみましょう。

TextFieldwidget にはstyleプロパティがあり、TextStyleを渡すことでフォントのサイズや色などを指定することができます。

body: Column(
  children: [
    // 検索ボックス
    Padding(
      padding: const EdgeInsets.symmetric(
        vertical: 12,
        horizontal: 36,
      ),
      child: TextField(
        style: TextStyle( // ← TextStyleを渡す
          fontSize: 18,
          color: Colors.black,
        ),
      ),
    ),
    // 検索結果一覧
  ],
),

検索ボックスにプレースホルダーを設定する

また検索ボックスだと分かりやすいようにプレースホルダーを配置しておきましょう。

TextFieldwidget にはdecorationプロパティがあり、InputDecorationを渡すことでプレースホルダーを指定することができます。

body: Column(
  children: [
    // 検索ボックス
    Padding(
      padding: const EdgeInsets.symmetric(
        vertical: 12,
        horizontal: 36,
      ),
      child: TextField(
        style: TextStyle(
          fontSize: 18,
        ),
        decoration: InputDecoration( // ← InputDecorationを渡す
          hintText: '検索ワードを入力してください',
        ),
      ),
    ),
    // 検索結果一覧
  ],
),

Enter キーで検索を実行する

それでは UI が出来たところで、検索処理を繋ぎましょう。

TextFieldにはonSubmittedというプロパティがあり、TextFieldに記入された文字列を返すコールバック関数を渡すことができます。

child: TextField(
  onSubmitted: (String value) {
    print(value); // ← 入力された文字列を受け取り処理を実行する
  },

前章で作成した検索処理の関数searchQiitaをこの中で呼び出しましょう。非同期処理になるのでasync/awaitを忘れずに。

body: Column(
  children: [
    // 検索ボックス
    Padding(
      padding: const EdgeInsets.symmetric(
        vertical: 12,
        horizontal: 36,
      ),
      child: TextField(
        style: TextStyle(
          fontSize: 18,
        ),
        decoration: InputDecoration(
          hintText: '検索ワードを入力してください',
        ),
        onSubmitted: (String value) async {
          final results = await searchQiita(value); // ← 検索処理を実行する
        },
      ),
    ),
    // 検索結果一覧
  ],
),

これで検索ボックスから検索処理を行えるようになりました。

検索結果を保持する

最後に処理から返ってきた検索結果を変数に保持しましょう。

StatefulWidgetではStateクラスのsetState()メソッドを使うことで、Stateクラスに定義された変数を更新することができます。

まず State クラスに検索結果を保持するための変数articlesを定義します。

class _SearchScreenState extends State<SearchScreen> {
  List<Article> articles = [];
  ...

その上で、先ほどのonSubmittedの中で検索結果を受け取り、setState()articlesに代入します。

TextField(
  onSubmitted: (String value) async {
    final results = await searchQiita(value);
    setState(()=>articles = results);
  },

🙌 検索ボックスの完成

完成したコード
search_screen.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';

import 'package:flutter/material.dart';
import 'package:qiita_search/models/article.dart';

class SearchScreen extends StatefulWidget {
  const SearchScreen({super.key});

  
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  List<Article> articles = [];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Qiita Search'),
      ),
-      body: Container(),
+      body: Column(
+        children: [
+          // 検索ボックス
+          Padding(
+            padding: const EdgeInsets.symmetric(
+              vertical: 12,
+              horizontal: 36,
+            ),
+            child: TextField(
+              style: TextStyle(
+                fontSize: 18,
+              ),
+              decoration: InputDecoration(
+                hintText: '検索ワードを入力してください',
+              ),
+              onSubmitted: (String value) async {
+                final results = await searchQiita(value);
+                setState(() => articles = results);
+              },
+            ),
+          ),
          // 検索結果一覧
        ],
      ),
    );
  }

  Future<List<Article>> searchQiita(String keyword) async {
    final uri = Uri.https('qiita.com', '/api/v2/items', {
      'query': 'title:$keyword',
      'per_page': '10',
    });

    final String token = dotenv.env['QIITA_ACCESS_TOKEN'] ?? '';
    final http.Response res = await http.get(uri, headers: {
      'Authorization': 'Bearer $token',
    });

    if (res.statusCode == 200) {
      // レスポンスをモデルクラスへ変換
      final List<dynamic> body = jsonDecode(res.body);
      return body.map((dynamic json) => Article.fromJson(json)).toList();
    } else {
      return [];
    }
  }
}

まとめ

次章では取得し保存した検索結果を一覧表示する UI を作成していきます。