diff --git a/lib/api_client/api_client.dart b/lib/api_client/api_client.dart index 0fdaf18..5c4912f 100644 --- a/lib/api_client/api_client.dart +++ b/lib/api_client/api_client.dart @@ -15,4 +15,10 @@ abstract class ApiClient { @Query("category") String category, @Query("apiKey") String apiKey, ); + + @GET("/everything") + Future fetchSearchNews( + @Query("q") String query, + @Query("apiKey") String apiKey, + ); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 975a257..95870e3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,42 +32,4 @@ class NewsApiApp extends StatelessWidget { home: const HomePage(title: 'News API'), ); } -} - -class NewsApiPage extends StatefulWidget { - const NewsApiPage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _NewsApiPageState(); -} - -class _NewsApiPageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - return const NewsListPage(); - } -} +} \ No newline at end of file diff --git a/lib/repository/articles_repository.dart b/lib/repository/articles_repository.dart index 95e85ac..2400bb8 100644 --- a/lib/repository/articles_repository.dart +++ b/lib/repository/articles_repository.dart @@ -5,19 +5,33 @@ import '../../model/news.dart'; import '../ui/response/result.dart'; abstract class ArticlesRepository { - Future> fetchHeadlines({required String country, required String category}); + Future> fetchHeadlines( + {required String country, required String category}); + Future> fetchSearchNews( + {required String query}); } class ArticlesRepositoryImpl extends ArticlesRepository { final ApiClient _client; - ArticlesRepositoryImpl([ApiClient? client]): _client = client ?? ApiClient(Dio()); + ArticlesRepositoryImpl([ApiClient? client]) + : _client = client ?? ApiClient(Dio()); @override - Future> fetchHeadlines({required String country, required String category}) { + Future> fetchHeadlines( + {required String country, required String category}) { return _client .fetchHeadlines(country, category, EnvironemntVariables.newsApiKey) .then((news) => Result.success(news)) - .catchError((error)=>Result.failure(error)); + .catchError((error) => Result.failure(error)); } -} \ No newline at end of file + + @override + Future> fetchSearchNews( + {required String query}) { + return _client + .fetchSearchNews(query, EnvironemntVariables.newsApiKey) + .then((news) => Result.success(news)) + .catchError((error) => Result.failure(error)); + } +} diff --git a/lib/state/articles_notifier_provider.dart b/lib/state/articles.dart similarity index 83% rename from lib/state/articles_notifier_provider.dart rename to lib/state/articles.dart index ece7138..c89c266 100644 --- a/lib/state/articles_notifier_provider.dart +++ b/lib/state/articles.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_news_api/repository/articles_repository.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../model/news.dart'; @@ -9,13 +10,15 @@ final articlesNotifierProvider = StateNotifierProvider> { ArticlesNotifier(): super([]); - void fetchHeadlines() async { + void fetch() async { final ArticlesRepository repository = ArticlesRepositoryImpl(); await repository.fetchHeadlines(country: "us", category: "business").then((result) { result.when(success: (news) { state = news.articles; }, failure: (error) { - print(error.message); + if (kDebugMode) { + print(error.message); + } }); }); } diff --git a/lib/state/news_search_text_controller.dart b/lib/state/news_search_text_controller.dart new file mode 100644 index 0000000..eb98140 --- /dev/null +++ b/lib/state/news_search_text_controller.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final newsSearchTextProvider = StateProvider((ref) { + return TextEditingController(text: ''); +}); diff --git a/lib/state/search_articles.dart b/lib/state/search_articles.dart new file mode 100644 index 0000000..503802e --- /dev/null +++ b/lib/state/search_articles.dart @@ -0,0 +1,28 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_news_api/repository/articles_repository.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../model/news.dart'; + +final searchArticlesNotifierProvider = StateNotifierProvider>((ref) { + return SearchArticlesNotifier(); +}); + +class SearchArticlesNotifier extends StateNotifier> { + SearchArticlesNotifier(): super([]); + + void search({ + required String query + }) async { + final ArticlesRepository repository = ArticlesRepositoryImpl(); + await repository.fetchSearchNews(query: query).then((result) { + result.when(success: (news) { + state = news.articles; + }, failure: (error) { + if (kDebugMode) { + print(error.message); + } + }); + }); + } +} + diff --git a/lib/ui/components/articles_list.dart b/lib/ui/components/articles_list.dart new file mode 100644 index 0000000..e318866 --- /dev/null +++ b/lib/ui/components/articles_list.dart @@ -0,0 +1,37 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_api/model/news.dart'; + +import 'articles_list_item.dart'; + +class ArticlesList extends StatelessWidget { + const ArticlesList({ + Key? key, + required this.articles, + required this.onRefresh, + }) : super(key: key); + + final List
articles; + final Function onRefresh; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + await Future.delayed(const Duration(seconds: 0), () { + onRefresh(); + }); + }, + child: Center( + child: ListView.builder( + shrinkWrap: true, + // physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 5), + itemCount: articles.length, + itemBuilder: (context, index) { + return ArticleListItem(article: articles[index]); + }), + ), + ); + } +} diff --git a/lib/ui/components/articles_list_item.dart b/lib/ui/components/articles_list_item.dart new file mode 100644 index 0000000..66ffb85 --- /dev/null +++ b/lib/ui/components/articles_list_item.dart @@ -0,0 +1,71 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_api/model/news.dart'; +import 'package:flutter_news_api/ui/webview/custom_webview.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class ArticleListItem extends StatelessWidget { + const ArticleListItem({ + Key? key, + required this.article, + }) : super(key: key); + + final Article article; + + @override + Widget build(BuildContext context) { + timeago.setLocaleMessages('ja', timeago.JaMessages()); + final now = DateTime.now(); + final ago = now.difference(article.publishedAt); + return GestureDetector( + onTap: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) { + return CustomWebview(url: article.url); + })); + }, + child: Container( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(width: 0.3, color: Colors.grey))), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + height: 100, + child: Row( + children: [ + Image.network( + article.urlToImage ?? + "https://www.shoshinsha-design.com/wp-content/uploads/2020/05/noimage-760x460.png", + width: 70, + height: 70, + ), + + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + timeago.format(now.subtract(ago), locale: 'ja'), + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + Text( + article.title ?? "No title", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.clip, + maxLines: 2, + ), + ], + ), + ), + ), + // ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/news_list/news_list_page.dart b/lib/ui/news_list/news_list_page.dart index cbb97a1..2d3fc6f 100644 --- a/lib/ui/news_list/news_list_page.dart +++ b/lib/ui/news_list/news_list_page.dart @@ -1,77 +1,11 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter_news_api/ui/webview/custom_webview.dart'; -import 'package:timeago/timeago.dart' as timeago; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_news_api/model/news.dart'; +import 'package:flutter_news_api/state/articles.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_news_api/ui/components/articles_list.dart'; +import 'package:flutter_news_api/ui/components/articles_list_item.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../model/news.dart'; -import '../../provider/articles_notifier_provider.dart'; -import '../../state/articles_notifier_provider.dart'; - -class ArticleListItem extends StatelessWidget { - const ArticleListItem({ - Key? key, - required this.article, - }) : super(key: key); - - final Article article; - - @override - Widget build(BuildContext context) { - timeago.setLocaleMessages('ja', timeago.JaMessages()); - final now = DateTime.now(); - final ago = now.difference(article.publishedAt); - return GestureDetector( - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return CustomWebview(url: article.url); - })); - }, - child: Container( - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(width: 0.3, color: Colors.grey))), - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - height: 100, - child: Row( - children: [ - Image.network( - article.urlToImage ?? - "https://www.shoshinsha-design.com/wp-content/uploads/2020/05/noimage-760x460.png", - width: 70, - height: 70, - ), - - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - timeago.format(now.subtract(ago), locale: 'ja'), - style: const TextStyle( - color: Colors.grey, - fontWeight: FontWeight.bold, - ), - ), - Text( - article.title ?? "No title", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.clip, - maxLines: 2, - ), - ], - ), - ), - ), - // ) - ], - ), - ), - ); - } -} class NewsListPage extends HookConsumerWidget { const NewsListPage({ @@ -81,22 +15,16 @@ class NewsListPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final List
articles = ref.watch(articlesNotifierProvider); - ref.read(articlesNotifierProvider.notifier).fetchHeadlines(); - return RefreshIndicator( - onRefresh: () async { - await Future.delayed(const Duration(seconds: 0), () { - ref.read(articlesNotifierProvider.notifier).fetchHeadlines(); - }); - }, - child: Center( - child: ListView.builder( - padding: const EdgeInsets.only(top: 5), - itemCount: articles.length, - itemBuilder: (context, index) { - return ArticleListItem(article: articles[index]); - }), - ), + useMemoized(() => { + ref.read(articlesNotifierProvider.notifier).fetch() + }); + + return ArticlesList( + articles: articles, + onRefresh: () { + ref.read(articlesNotifierProvider.notifier).fetch(); + }, ); } } diff --git a/lib/ui/news_search/news_search_page.dart b/lib/ui/news_search/news_search_page.dart index 527f3f5..02f7906 100644 --- a/lib/ui/news_search/news_search_page.dart +++ b/lib/ui/news_search/news_search_page.dart @@ -1,4 +1,8 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_api/state/news_search_text_controller.dart'; +import 'package:flutter_news_api/state/search_articles.dart'; +import 'package:flutter_news_api/ui/components/articles_list.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class NewsSearchPage extends HookConsumerWidget { @@ -8,11 +12,35 @@ class NewsSearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return const Center( - child: Text( - "search", - style: TextStyle(fontSize: 50), - ) + final searchTextController = ref.watch(newsSearchTextProvider); + final articles = ref.watch(searchArticlesNotifierProvider); + + return Center( + child: Container( + padding: const EdgeInsets.only(left: 10, top: 5, right: 10), + child: Column( + children: [ + TextFormField( + controller: searchTextController, + onFieldSubmitted: (text) { + if(text.isNotEmpty) { + ref.read(searchArticlesNotifierProvider.notifier).search( + query: text); + } + }, + ), + Flexible( + child: ArticlesList( + articles: articles, + onRefresh: () { + ref.read(searchArticlesNotifierProvider.notifier).search( + query: searchTextController.text); + }, + ), + ), + ], + ), + ), ); } -} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index fe43c3a..0bcb264 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 dio: ^4.0.6 + flutter_hooks: ^0.18.0 hooks_riverpod: 1.0.4 timeago: 3.2.2 webview_flutter: 3.0.4