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: [
// 検索ボックス
// 検索結果一覧
],
),
検索結果一覧の部分は次章で実装するとして、まず検索ボックスとなるTextField
widget を Column
の中に配置しておきましょう。
body: Column(
children: [
// 検索ボックス
TextField(), // ← TextFieldをchildren内に追加
// 検索結果一覧
],
),
検索ボックスに余白を作る
TextField
を配置すると下記のように横幅いっぱいに広がってしまい、見てくれが悪いので、見た目を少し調整していきましょう。
余白を作るにはPadding
widget を使います。余白を追加したいTextField
をPadding
で囲み、padding
プロパティにEdgeInsets
を渡すことで余白を指定できます。
EdgeInsets
クラスでは上下左右を1つ1つ指定することもできますが、今回は水平方向、垂直方向に同じ分だけ余白を作りたいので、.symmetric()
メソッドを使って、余白を指定します。
body: Column(
children: [
// 検索ボックス
Padding( // ← Paddingで囲む
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 36,
),
child: TextField(),
),
// 検索結果一覧
],
),
余白が出来ました。
検索ボックスのフォントを調整する
文字が少し小さいのでフォントサイズを大きくしてみましょう。
TextField
widget にはstyle
プロパティがあり、TextStyle
を渡すことでフォントのサイズや色などを指定することができます。
body: Column(
children: [
// 検索ボックス
Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 36,
),
child: TextField(
style: TextStyle( // ← TextStyleを渡す
fontSize: 18,
color: Colors.black,
),
),
),
// 検索結果一覧
],
),
検索ボックスにプレースホルダーを設定する
また検索ボックスだと分かりやすいようにプレースホルダーを配置しておきましょう。
TextField
widget には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);
},
🙌 検索ボックスの完成
完成したコード
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 を作成していきます。