From 3d40ef99a894c8d4a7ecdd79fd65610912bc756a Mon Sep 17 00:00:00 2001 From: Ashish Date: Thu, 21 Sep 2023 09:33:27 +0530 Subject: [PATCH 1/2] Code Restructured with Clean Architecture --- lib/data/api_repository.dart | 69 ++++ lib/list_of_country.dart | 32 -- lib/main.dart | 371 +----------------- lib/model/article_model.dart | 91 +++++ lib/model/category_model.dart | 8 + lib/presentation/app.dart | 34 ++ lib/presentation/home/screen/home_screen.dart | 150 +++++++ .../home/widgets/news_article_tile.dart | 82 ++++ .../news/screen/article_news.dart} | 27 +- lib/{ => shared}/constants.dart | 0 lib/shared/drawer_list.dart | 37 ++ lib/shared/extension.dart | 35 ++ lib/shared/functions.dart | 9 + lib/shared/widgets/drop_down_list.dart | 18 + lib/shared/widgets/side_drawer.dart | 94 +++++ 15 files changed, 645 insertions(+), 412 deletions(-) create mode 100644 lib/data/api_repository.dart delete mode 100644 lib/list_of_country.dart create mode 100644 lib/model/article_model.dart create mode 100644 lib/model/category_model.dart create mode 100644 lib/presentation/app.dart create mode 100644 lib/presentation/home/screen/home_screen.dart create mode 100644 lib/presentation/home/widgets/news_article_tile.dart rename lib/{artical_news.dart => presentation/news/screen/article_news.dart} (63%) rename lib/{ => shared}/constants.dart (100%) create mode 100644 lib/shared/drawer_list.dart create mode 100644 lib/shared/extension.dart create mode 100644 lib/shared/functions.dart create mode 100644 lib/shared/widgets/drop_down_list.dart create mode 100644 lib/shared/widgets/side_drawer.dart diff --git a/lib/data/api_repository.dart b/lib/data/api_repository.dart new file mode 100644 index 0000000..ea47179 --- /dev/null +++ b/lib/data/api_repository.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +class ApiRepository { + ApiRepository({required String apiKey}) : _apiKey = apiKey; + + final String _baseApi = 'https://newsapi.org/v2/'; + final String _apiKey; + + String topHeadlines(int pageSize, String country, String category, int page, {String? channel = '', String? searchKey = ''}) => + '${_baseApi}top-headlines?pageSize=$pageSize&page=$page&country=$country&category=$category&sources=$channel&apiKey=$_apiKey'; + + Future requestData(String url) async { + print(url); + try { + final response = await get(Uri.parse(url)); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data; + } else { + if (kDebugMode) { + debugPrint('Error: ${response.statusCode}'); + } + } + } catch (e) { + debugPrint(e.toString()); + } + } + +// Future getNews({ +// String? channel, +// String? searchKey, +// bool reload = false, +// }) async { +// setState(() => notFound = false); +// +// if (!reload && !isLoading) { +// toggleDrawer(); +// } else { +// country = null; +// category = null; +// } +// if (isLoading) { +// pageNum++; +// } else { +// setState(() => news = []); +// pageNum = 1; +// } +// baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&'; +// +// baseApi += country == null ? 'country=in&' : 'country=$country&'; +// baseApi += category == null ? '' : 'category=$category&'; +// baseApi += 'apiKey=$apiKey'; +// if (channel != null) { +// country = null; +// category = null; +// baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&sources=$channel&apiKey=58b98b48d2c74d9c94dd5dc296ccf7b6'; +// } +// if (searchKey != null) { +// country = null; +// category = null; +// baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&q=$searchKey&apiKey=58b98b48d2c74d9c94dd5dc296ccf7b6'; +// } +// //print(baseApi); +// getDataFromApi(baseApi); +// } +} diff --git a/lib/list_of_country.dart b/lib/list_of_country.dart deleted file mode 100644 index a2837ed..0000000 --- a/lib/list_of_country.dart +++ /dev/null @@ -1,32 +0,0 @@ -const List> listOfCountry = [ - {'name': 'INDIA', 'code': 'in'}, - {'name': 'USA', 'code': 'us'}, - {'name': 'MEXICO', 'code': 'mx'}, - {'name': 'United Arab Emirates', 'code': 'ae'}, - {'name': 'New Zealand', 'code': 'nz'}, - {'name': 'Israel', 'code': 'il'}, - {'name': 'Indonesia', 'code': 'id'}, -]; - -const List> listOfCategory = [ - {'name': 'science', 'code': 'science'}, - {'name': 'business', 'code': 'business'}, - {'name': 'technology', 'code': 'technology'}, - {'name': 'sports', 'code': 'sports'}, - {'name': 'health', 'code': 'health'}, - {'name': 'general', 'code': 'general'}, - {'name': 'entertainment', 'code': 'entertainment'}, - {'name': 'ALL', 'code': null}, -]; -const List> listOfNewsChannel = [ - {'name': 'BBC News', 'code': 'bbc-news'}, - {'name': 'The Times of India', 'code': 'the-times-of-india'}, - {'code': 'politico', 'name': 'politico'}, - {'code': 'the-washington-post', 'name': 'The Washington Post'}, - {'code': 'reuters', 'name': 'reuters'}, - {'code': 'cnn', 'name': 'cnn'}, - {'code': 'nbc-news', 'name': 'nbc news'}, - {'code': 'the-hill', 'name': 'The Hill'}, - {'code': 'fox-news', 'name': 'Fox News'}, - {'code': 'fox-news', 'name': 'Fox News'}, -]; diff --git a/lib/main.dart b/lib/main.dart index 75d2817..da76bbd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,371 +1,4 @@ -import 'dart:convert'; - -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:http/http.dart' as http; -import 'artical_news.dart'; -import 'constants.dart'; -import 'list_of_country.dart'; - -void main() => runApp(const MyApp()); - -GlobalKey _scaffoldKey = GlobalKey(); - -void toggleDrawer() { - if (_scaffoldKey.currentState?.isDrawerOpen ?? false) { - _scaffoldKey.currentState?.openEndDrawer(); - } else { - _scaffoldKey.currentState?.openDrawer(); - } -} - -class DropDownList extends StatelessWidget { - const DropDownList({super.key, required this.name, required this.call}); - final String name; - final Function call; - - @override - Widget build(BuildContext context) { - return GestureDetector( - child: ListTile(title: Text(name)), - onTap: () => call(), - ); - } -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - dynamic cName; - dynamic country; - dynamic category; - dynamic findNews; - int pageNum = 1; - bool isPageLoading = false; - late ScrollController controller; - int pageSize = 10; - bool isSwitched = false; - List news = []; - bool notFound = false; - List data = []; - bool isLoading = false; - String baseApi = 'https://newsapi.org/v2/top-headlines?'; - - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'News', - theme: isSwitched - ? ThemeData( - fontFamily: GoogleFonts.poppins().fontFamily, - brightness: Brightness.light, - ) - : ThemeData( - fontFamily: GoogleFonts.poppins().fontFamily, - brightness: Brightness.dark, - ), - home: Scaffold( - key: _scaffoldKey, - drawer: Drawer( - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 32), - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (country != null) - Text('Country = $cName') - else - Container(), - const SizedBox(height: 10), - if (category != null) - Text('Category = $category') - else - Container(), - const SizedBox(height: 20), - ], - ), - ListTile( - title: TextFormField( - decoration: const InputDecoration(hintText: 'Find Keyword'), - scrollPadding: const EdgeInsets.all(5), - onChanged: (String val) => setState(() => findNews = val), - ), - trailing: IconButton( - onPressed: () async => getNews(searchKey: findNews as String), - icon: const Icon(Icons.search), - ), - ), - ExpansionTile( - title: const Text('Country'), - children: [ - for (int i = 0; i < listOfCountry.length; i++) - DropDownList( - call: () { - country = listOfCountry[i]['code']; - cName = listOfCountry[i]['name']!.toUpperCase(); - getNews(); - }, - name: listOfCountry[i]['name']!.toUpperCase(), - ), - ], - ), - ExpansionTile( - title: const Text('Category'), - children: [ - for (int i = 0; i < listOfCategory.length; i++) - DropDownList( - call: () { - category = listOfCategory[i]['code']; - getNews(); - }, - name: listOfCategory[i]['name']!.toUpperCase(), - ) - ], - ), - ExpansionTile( - title: const Text('Channel'), - children: [ - for (int i = 0; i < listOfNewsChannel.length; i++) - DropDownList( - call: () => - getNews(channel: listOfNewsChannel[i]['code']), - name: listOfNewsChannel[i]['name']!.toUpperCase(), - ), - ], - ), - //ListTile(title: Text("Exit"), onTap: () => exit(0)), - ], - ), - ), - appBar: AppBar( - centerTitle: true, - title: const Text('News'), - actions: [ - IconButton( - onPressed: () { - country = null; - category = null; - findNews = null; - cName = null; - getNews(reload: true); - }, - icon: const Icon(Icons.refresh), - ), - Switch( - value: isSwitched, - onChanged: (bool value) => setState(() => isSwitched = value), - activeTrackColor: Colors.white, - activeColor: Colors.white, - ), - ], - ), - body: notFound - ? const Center( - child: Text('Not Found', style: TextStyle(fontSize: 30)), - ) - : news.isEmpty - ? const Center( - child: CircularProgressIndicator( - backgroundColor: Colors.yellow, - ), - ) - : ListView.builder( - controller: controller, - itemBuilder: (BuildContext context, int index) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(5), - child: Card( - elevation: 5, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: GestureDetector( - onTap: () async { - Navigator.push( - context, - MaterialPageRoute( - fullscreenDialog: true, - builder: (BuildContext context) => - ArticalNews( - newsUrl: news[index]['url'] as String, - ), - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 15, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - ), - child: Column( - children: [ - Stack( - children: [ - if (news[index]['urlToImage'] == null) - Container() - else - ClipRRect( - borderRadius: - BorderRadius.circular(20), - child: CachedNetworkImage( - placeholder: - (BuildContext context, - String url) => - Container(), - errorWidget: - (BuildContext context, - String url, - error) => - const SizedBox(), - imageUrl: news[index] - ['urlToImage'] as String, - ), - ), - Positioned( - bottom: 8, - right: 8, - child: Card( - elevation: 0, - color: Theme.of(context) - .primaryColor - .withOpacity(0.8), - child: Padding( - padding: - const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - child: Text( - "${news[index]['source']['name']}", - style: Theme.of(context) - .textTheme - .subtitle2, - ), - ), - ), - ), - ], - ), - const Divider(), - Text( - "${news[index]['title']}", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ) - ], - ), - ), - ), - ), - ), - if (index == news.length - 1 && isLoading) - const Center( - child: CircularProgressIndicator( - backgroundColor: Colors.yellow, - ), - ) - else - const SizedBox(), - ], - ); - }, - itemCount: news.length, - ), - ), - ); - } - - Future getDataFromApi(String url) async { - final http.Response res = await http.get(Uri.parse(url)); - if (res.statusCode == 200) { - if (jsonDecode(res.body)['totalResults'] == 0) { - notFound = !isLoading; - setState(() => isLoading = false); - } else { - if (isLoading) { - final newData = jsonDecode(res.body)['articles'] as List; - for (final e in newData) { - news.add(e); - } - } else { - news = jsonDecode(res.body)['articles'] as List; - } - setState(() { - notFound = false; - isLoading = false; - }); - } - } else { - setState(() => notFound = true); - } - } - - Future getNews({ - String? channel, - String? searchKey, - bool reload = false, - }) async { - setState(() => notFound = false); - - if (!reload && !isLoading) { - toggleDrawer(); - } else { - country = null; - category = null; - } - if (isLoading) { - pageNum++; - } else { - setState(() => news = []); - pageNum = 1; - } - baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&'; - - baseApi += country == null ? 'country=in&' : 'country=$country&'; - baseApi += category == null ? '' : 'category=$category&'; - baseApi += 'apiKey=$apiKey'; - if (channel != null) { - country = null; - category = null; - baseApi = - 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&sources=$channel&apiKey=58b98b48d2c74d9c94dd5dc296ccf7b6'; - } - if (searchKey != null) { - country = null; - category = null; - baseApi = - 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&q=$searchKey&apiKey=58b98b48d2c74d9c94dd5dc296ccf7b6'; - } - //print(baseApi); - getDataFromApi(baseApi); - } - - @override - void initState() { - controller = ScrollController()..addListener(_scrollListener); - getNews(); - super.initState(); - } +import 'presentation/app.dart'; - void _scrollListener() { - if (controller.position.pixels == controller.position.maxScrollExtent) { - setState(() => isLoading = true); - getNews(); - } - } -} +void main() => runApp(const NewsApp()); diff --git a/lib/model/article_model.dart b/lib/model/article_model.dart new file mode 100644 index 0000000..dff211d --- /dev/null +++ b/lib/model/article_model.dart @@ -0,0 +1,91 @@ +class Article { + Article({ + this.source, + this.author, + this.title, + this.description, + this.url, + this.urlToImage, + this.publishedAt, + this.content, + }); + + factory Article.fromJson(Map json) => Article( + source: json['source'] == null ? null : Source.fromJson(json['source'] as Map), + author: json['author'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + url: json['url'] as String?, + urlToImage: json['urlToImage'] as String?, + publishedAt: json['publishedAt'] == null ? null : DateTime.parse(json['publishedAt'] as String), + content: json['content'] as String?, + ); + Source? source; + String? author; + String? title; + String? description; + String? url; + String? urlToImage; + DateTime? publishedAt; + String? content; + + Article copyWith({ + Source? source, + String? author, + String? title, + String? description, + String? url, + String? urlToImage, + DateTime? publishedAt, + String? content, + }) => + Article( + source: source ?? this.source, + author: author ?? this.author, + title: title ?? this.title, + description: description ?? this.description, + url: url ?? this.url, + urlToImage: urlToImage ?? this.urlToImage, + publishedAt: publishedAt ?? this.publishedAt, + content: content ?? this.content, + ); + + Map toJson() => { + 'source': source?.toJson(), + 'author': author, + 'title': title, + 'description': description, + 'url': url, + 'urlToImage': urlToImage, + 'publishedAt': publishedAt?.toIso8601String(), + 'content': content, + }; +} + +class Source { + Source({ + this.id, + this.name, + }); + + factory Source.fromJson(Map json) => Source( + id: json['id'], + name: json['name'] as String?, + ); + dynamic id; + String? name; + + Source copyWith({ + dynamic id, + String? name, + }) => + Source( + id: id ?? this.id, + name: name ?? this.name, + ); + + Map toJson() => { + 'id': id, + 'name': name, + }; +} diff --git a/lib/model/category_model.dart b/lib/model/category_model.dart new file mode 100644 index 0000000..cbb3aa1 --- /dev/null +++ b/lib/model/category_model.dart @@ -0,0 +1,8 @@ +import '../shared/drawer_list.dart'; + +class SelectableItem{ + const SelectableItem({this.name = '', this.code = '', this.type}); + final String name; + final String code; + final ListType? type; +} diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart new file mode 100644 index 0000000..0ad8502 --- /dev/null +++ b/lib/presentation/app.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'home/screen/home_screen.dart'; + +class NewsApp extends StatefulWidget { + const NewsApp({super.key}); + + @override + State createState() => _NewsAppState(); +} + +class _NewsAppState extends State { + bool isLightTheme = true; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'News', + theme: ThemeData( + fontFamily: GoogleFonts.poppins().fontFamily, + brightness: isLightTheme ? Brightness.light : Brightness.dark, + ), + home: HomeScreen( + onThemeChanged: (bool value) { + setState(() { + isLightTheme = value; + }); + }, + ), + ); + } +} diff --git a/lib/presentation/home/screen/home_screen.dart b/lib/presentation/home/screen/home_screen.dart new file mode 100644 index 0000000..2377d82 --- /dev/null +++ b/lib/presentation/home/screen/home_screen.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; + +import '../../../data/api_repository.dart'; +import '../../../model/article_model.dart'; +import '../../../model/category_model.dart'; +import '../../../shared/constants.dart'; +import '../../../shared/drawer_list.dart'; +import '../../../shared/functions.dart'; +import '../../../shared/widgets/side_drawer.dart'; +import '../widgets/news_article_tile.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key, required this.onThemeChanged}); + + final ValueChanged onThemeChanged; + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + late ScrollController controller; + bool isThemeLight = true; + ApiRepository api = ApiRepository(apiKey: apiKey); + + List
news = []; + bool _isLoading = true; + + final int pageSize = 10; + int currentPage = 0; + + String searchKey = ''; + SelectableItem selectedCountry = const SelectableItem(type: ListType.country); + SelectableItem selectedCategory = const SelectableItem(type: ListType.category); + SelectableItem selectedChannel = const SelectableItem(type: ListType.channel); + + @override + void initState() { + controller = ScrollController()..addListener(_scrollListener); + // getNews(); + super.initState(); + } + + void _scrollListener() { + if (controller.position.pixels == controller.position.maxScrollExtent) { + setState(() => _isLoading = true); + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey); + } + } + + void onRefresh() { + selectedCategory = const SelectableItem(type: ListType.category); + selectedCountry = const SelectableItem(type: ListType.country); + selectedChannel = const SelectableItem(type: ListType.channel); + news.clear(); + + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey, isReload: true); + } + + Future getNews(String country, String category, {String? channel, String? searchKey, bool isReload = false}) async { + setState(() => _isLoading = true); + + if (isReload) { + currentPage = 0; + } + + await api.requestData(api.topHeadlines(pageSize, country, category, currentPage++, channel: channel, searchKey: searchKey)).then((response) {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + centerTitle: true, + title: const Text('News'), + actions: [ + IconButton( + onPressed: () => onRefresh(), + icon: const Icon(Icons.refresh), + ), + Switch( + value: isThemeLight, + onChanged: (bool value) => setState(() { + isThemeLight = value; + widget.onThemeChanged(value); + }), + activeTrackColor: Colors.white, + activeColor: Colors.white, + ), + ], + ), + drawer: SideDrawer( + onItemSelected: (SelectableItem item) { + toggleDrawer(_scaffoldKey); + + if (item.type == ListType.channel) { + selectedChannel = item; + } else if (item.type == ListType.country) { + selectedCountry = item; + } else if (item.type == ListType.category) { + selectedCategory = item; + } + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey, isReload: true); + }, + onSearchChanged: (String searchKey) { + this.searchKey = searchKey; + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey, isReload: true); + }, + ), + body: !_isLoading + ? news.isNotEmpty + ? _buildList + : const Center( + child: CircularProgressIndicator( + backgroundColor: Colors.yellow, + ), + ) + : const Center( + child: CircularProgressIndicator( + backgroundColor: Colors.yellow, + ), + ), + ); + } + + Widget get _buildList => ListView.builder( + controller: controller, + itemBuilder: (BuildContext context, int index) { + return Column( + key: ValueKey(index), + mainAxisSize: MainAxisSize.min, + children: [ + NewsArticleTile( + key: ValueKey
(news[index]), + article: news[index], + ), + if (index == news.length - 1 && _isLoading) + const Center( + child: CircularProgressIndicator( + backgroundColor: Colors.yellow, + ), + ) + ], + ); + }, + itemCount: news.length, + ); +} diff --git a/lib/presentation/home/widgets/news_article_tile.dart b/lib/presentation/home/widgets/news_article_tile.dart new file mode 100644 index 0000000..b90034f --- /dev/null +++ b/lib/presentation/home/widgets/news_article_tile.dart @@ -0,0 +1,82 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import '../../news/screen/article_news.dart'; + +import '../../../model/article_model.dart'; +import '../../../shared/extension.dart'; + +class NewsArticleTile extends StatelessWidget { + const NewsArticleTile({super.key, required this.article}); + final Article article; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(5), + child: Card( + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: GestureDetector( + onTap: () => context.navigateTo(ArticleNews(newsUrl: article.url ?? '')), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 15, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + ), + child: Column( + children: [ + Stack( + children: [ + if ((article.urlToImage ?? '').isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + placeholder: (BuildContext context, String url) => Container(), + errorWidget: (BuildContext context, String url, error) => const SizedBox(), + imageUrl: article.urlToImage ?? '', + ), + ), + Positioned( + bottom: 8, + right: 8, + child: Card( + elevation: 0, + color: Theme.of(context).primaryColor.withOpacity(0.8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + child: Text( + article.source?.name ?? '', + style: context.theme.titleSmall, + ), + ), + ), + ), + ], + ), + const Divider(), + Text( + article.title ?? '', + style: context.theme.titleSmall!.copyWith( + fontWeight: FontWeight.bold, + ), + ) + ], + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/artical_news.dart b/lib/presentation/news/screen/article_news.dart similarity index 63% rename from lib/artical_news.dart rename to lib/presentation/news/screen/article_news.dart index e44f0f2..940b47b 100644 --- a/lib/artical_news.dart +++ b/lib/presentation/news/screen/article_news.dart @@ -3,18 +3,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; -class ArticalNews extends StatefulWidget { - const ArticalNews({super.key, required this.newsUrl}); +class ArticleNews extends StatefulWidget { + const ArticleNews({super.key, required this.newsUrl}); + final String newsUrl; + @override - _ArticalNewsState createState() => _ArticalNewsState(); + _ArticleNewsState createState() => _ArticleNewsState(); } -class _ArticalNewsState extends State { - final Completer _completer = - Completer(); +class _ArticleNewsState extends State { + final Completer _completer = Completer(); late bool _isLoadingPage; - + @override void initState() { super.initState(); @@ -24,7 +25,12 @@ class _ArticalNewsState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(centerTitle: true, title: const Text('News',),), + appBar: AppBar( + centerTitle: true, + title: const Text( + 'News', + ), + ), body: Stack( children: [ WebView( @@ -33,8 +39,7 @@ class _ArticalNewsState extends State { onWebViewCreated: (WebViewController controller) { _completer.complete(controller); }, - onPageFinished: (String finish) => - setState(() => _isLoadingPage = false), + onPageFinished: (String finish) => setState(() => _isLoadingPage = false), ), if (_isLoadingPage) Container( @@ -44,7 +49,7 @@ class _ArticalNewsState extends State { ), ) else - SizedBox.shrink() + const SizedBox.shrink() ], ), ); diff --git a/lib/constants.dart b/lib/shared/constants.dart similarity index 100% rename from lib/constants.dart rename to lib/shared/constants.dart diff --git a/lib/shared/drawer_list.dart b/lib/shared/drawer_list.dart new file mode 100644 index 0000000..50bbf89 --- /dev/null +++ b/lib/shared/drawer_list.dart @@ -0,0 +1,37 @@ +import '../model/category_model.dart'; + +enum ListType { country, category, channel } + +const List countries = [ + SelectableItem(name: 'INDIA', code: 'in', type: ListType.country), + SelectableItem(name: 'USA', code: 'us', type: ListType.country), + SelectableItem(name: 'MEXICO', code: 'mx', type: ListType.country), + SelectableItem(name: 'United Arab Emirates', code: 'ae', type: ListType.country), + SelectableItem(name: 'New Zealand', code: 'nz', type: ListType.country), + SelectableItem(name: 'Israel', code: 'il', type: ListType.country), + SelectableItem(name: 'Indonesia', code: 'id', type: ListType.country), +]; + +const List categories = [ + SelectableItem(name: 'science', code: 'science', type: ListType.category), + SelectableItem(name: 'business', code: 'business', type: ListType.category), + SelectableItem(name: 'technology', code: 'technology', type: ListType.category), + SelectableItem(name: 'sports', code: 'sports', type: ListType.category), + SelectableItem(name: 'health', code: 'health', type: ListType.category), + SelectableItem(name: 'general', code: 'general', type: ListType.category), + SelectableItem(name: 'entertainment', code: 'entertainment', type: ListType.category), + SelectableItem(name: 'ALL', code: 'all', type: ListType.category), +]; + +const List newsChannels = [ + SelectableItem(name: 'BBC News', code: 'bbc-news', type: ListType.channel), + SelectableItem(name: 'The Times of India', code: 'the-times-of-india', type: ListType.channel), + SelectableItem(name: 'politico', code: 'politico', type: ListType.channel), + SelectableItem(name: 'The Washington Post', code: 'the-washington-post', type: ListType.channel), + SelectableItem(name: 'reuters', code: 'reuters', type: ListType.channel), + SelectableItem(name: 'cnn', code: 'cnn', type: ListType.channel), + SelectableItem(name: 'nbc news', code: 'nbc-news', type: ListType.channel), + SelectableItem(name: 'The Hill', code: 'the-hill', type: ListType.channel), + SelectableItem(name: 'Fox News', code: 'fox-news', type: ListType.channel), + SelectableItem(name: 'Fox News', code: 'fox-news', type: ListType.channel), +]; diff --git a/lib/shared/extension.dart b/lib/shared/extension.dart new file mode 100644 index 0000000..1dbb6c2 --- /dev/null +++ b/lib/shared/extension.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +extension NavigationExtension on BuildContext { + void navigateTo(Widget widget) { + Navigator.push( + this, + MaterialPageRoute( + builder: (context) => widget, + ), + ); + } + + void navigateToReplacement(Widget widget) { + Navigator.pushReplacement( + this, + MaterialPageRoute( + builder: (context) => widget, + ), + ); + } + + void navigateToAndFinish(Widget widget) { + Navigator.pushAndRemoveUntil( + this, + MaterialPageRoute( + builder: (context) => widget, + ), + (route) => false, + ); + } +} + +extension ThemeExtension on BuildContext { + TextTheme get theme => Theme.of(this).textTheme; +} \ No newline at end of file diff --git a/lib/shared/functions.dart b/lib/shared/functions.dart new file mode 100644 index 0000000..e24cd10 --- /dev/null +++ b/lib/shared/functions.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +void toggleDrawer(GlobalKey scaffoldKey) { + if (scaffoldKey.currentState?.isDrawerOpen ?? false) { + scaffoldKey.currentState?.openEndDrawer(); + } else { + scaffoldKey.currentState?.openDrawer(); + } +} \ No newline at end of file diff --git a/lib/shared/widgets/drop_down_list.dart b/lib/shared/widgets/drop_down_list.dart new file mode 100644 index 0000000..202256a --- /dev/null +++ b/lib/shared/widgets/drop_down_list.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import '../../model/category_model.dart'; + +class DropDownList extends StatelessWidget { + const DropDownList({super.key, required this.item, required this.onItemSelected}); + + final SelectableItem item; + final ValueChanged onItemSelected; + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: ListTile(title: Text(item.name)), + onTap: () => onItemSelected(item), + ); + } +} diff --git a/lib/shared/widgets/side_drawer.dart b/lib/shared/widgets/side_drawer.dart new file mode 100644 index 0000000..89b10bf --- /dev/null +++ b/lib/shared/widgets/side_drawer.dart @@ -0,0 +1,94 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../../model/category_model.dart'; +import '../drawer_list.dart'; +import 'drop_down_list.dart'; + +class SideDrawer extends StatefulWidget { + const SideDrawer({super.key, this.onSearchChanged, this.onItemSelected}); + + final ValueChanged? onSearchChanged; + final ValueChanged? onItemSelected; + + @override + State createState() => _SideDrawerState(); +} + +class _SideDrawerState extends State { + Timer? _debounce; + final Duration _debounceDuration = const Duration(milliseconds: 500); + + @override + void dispose() { + _debounce?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 32), + children: [ + // Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // if (country != null) Text('Country = $cName') else Container(), + // const SizedBox(height: 10), + // if (category != null) Text('Category = $category') else Container(), + // const SizedBox(height: 20), + // ], + // ), + ListTile( + title: TextFormField( + decoration: const InputDecoration(hintText: 'Find Keyword'), + scrollPadding: const EdgeInsets.all(5), + onChanged: (String val) { + if (_debounce?.isActive ?? false) { + _debounce!.cancel(); + } + + _debounce = Timer(_debounceDuration, () { + widget.onSearchChanged!(val); + }); + }, + ), + ), + ExpansionTile( + title: const Text('Country'), + children: [ + ...countries.map((item) => + DropDownList( + onItemSelected: (SelectableItem item) => widget.onItemSelected!(item), + item: item, + )).toList(), + ], + ), + ExpansionTile( + title: const Text('Category'), + children: [ + ...categories.map((item) => + DropDownList( + onItemSelected: (SelectableItem item) => widget.onItemSelected!(item), + item: item, + )).toList(), + ], + ), + ExpansionTile( + title: const Text('Channel'), + children: [ + ...newsChannels.map((item) => + DropDownList( + onItemSelected: (SelectableItem item) => widget.onItemSelected!(item), + item: item, + )).toList(), + ], + ), + //ListTile(title: Text("Exit"), onTap: () => exit(0)), + ], + ), + ); + } +} From 7e134572849ec4f3a7556f37a30ccac0b2b86b02 Mon Sep 17 00:00:00 2001 From: Ashish Date: Thu, 21 Sep 2023 10:02:21 +0530 Subject: [PATCH 2/2] News API Restructured --- lib/data/api_repository.dart | 40 +----------- lib/presentation/home/screen/home_screen.dart | 39 +++++------ .../home/widgets/news_article_tile.dart | 64 ++++++++++--------- 3 files changed, 57 insertions(+), 86 deletions(-) diff --git a/lib/data/api_repository.dart b/lib/data/api_repository.dart index ea47179..592e331 100644 --- a/lib/data/api_repository.dart +++ b/lib/data/api_repository.dart @@ -12,7 +12,7 @@ class ApiRepository { String topHeadlines(int pageSize, String country, String category, int page, {String? channel = '', String? searchKey = ''}) => '${_baseApi}top-headlines?pageSize=$pageSize&page=$page&country=$country&category=$category&sources=$channel&apiKey=$_apiKey'; - Future requestData(String url) async { + Future requestData(String url) async { print(url); try { final response = await get(Uri.parse(url)); @@ -28,42 +28,4 @@ class ApiRepository { debugPrint(e.toString()); } } - -// Future getNews({ -// String? channel, -// String? searchKey, -// bool reload = false, -// }) async { -// setState(() => notFound = false); -// -// if (!reload && !isLoading) { -// toggleDrawer(); -// } else { -// country = null; -// category = null; -// } -// if (isLoading) { -// pageNum++; -// } else { -// setState(() => news = []); -// pageNum = 1; -// } -// baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&'; -// -// baseApi += country == null ? 'country=in&' : 'country=$country&'; -// baseApi += category == null ? '' : 'category=$category&'; -// baseApi += 'apiKey=$apiKey'; -// if (channel != null) { -// country = null; -// category = null; -// baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&sources=$channel&apiKey=58b98b48d2c74d9c94dd5dc296ccf7b6'; -// } -// if (searchKey != null) { -// country = null; -// category = null; -// baseApi = 'https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&q=$searchKey&apiKey=58b98b48d2c74d9c94dd5dc296ccf7b6'; -// } -// //print(baseApi); -// getDataFromApi(baseApi); -// } } diff --git a/lib/presentation/home/screen/home_screen.dart b/lib/presentation/home/screen/home_screen.dart index 2377d82..592b679 100644 --- a/lib/presentation/home/screen/home_screen.dart +++ b/lib/presentation/home/screen/home_screen.dart @@ -31,15 +31,15 @@ class _HomeScreenState extends State { int currentPage = 0; String searchKey = ''; - SelectableItem selectedCountry = const SelectableItem(type: ListType.country); + SelectableItem selectedCountry = countries.first; SelectableItem selectedCategory = const SelectableItem(type: ListType.category); SelectableItem selectedChannel = const SelectableItem(type: ListType.channel); @override void initState() { controller = ScrollController()..addListener(_scrollListener); - // getNews(); super.initState(); + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey); } void _scrollListener() { @@ -50,22 +50,24 @@ class _HomeScreenState extends State { } void onRefresh() { - selectedCategory = const SelectableItem(type: ListType.category); - selectedCountry = const SelectableItem(type: ListType.country); + selectedCountry = countries.first; + selectedCategory = const SelectableItem(type: ListType.country); selectedChannel = const SelectableItem(type: ListType.channel); + currentPage = 0; news.clear(); - getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey, isReload: true); + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey); } - Future getNews(String country, String category, {String? channel, String? searchKey, bool isReload = false}) async { + Future getNews(String country, String category, {String? channel, String? searchKey}) async { setState(() => _isLoading = true); - if (isReload) { - currentPage = 0; - } + currentPage++; - await api.requestData(api.topHeadlines(pageSize, country, category, currentPage++, channel: channel, searchKey: searchKey)).then((response) {}); + await api.requestData(api.topHeadlines(pageSize, country, category, currentPage, channel: channel, searchKey: searchKey)).then((response) { + news.addAll((response['articles'] as List).map
((article) => Article.fromJson(article as Map)).toList()); + setState(() => _isLoading = false); + }); } @override @@ -102,14 +104,14 @@ class _HomeScreenState extends State { } else if (item.type == ListType.category) { selectedCategory = item; } - getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey, isReload: true); + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey); }, onSearchChanged: (String searchKey) { this.searchKey = searchKey; - getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey, isReload: true); + getNews(selectedCountry.code, selectedCategory.code, channel: selectedChannel.code, searchKey: searchKey); }, ), - body: !_isLoading + body: news.isNotEmpty || !_isLoading ? news.isNotEmpty ? _buildList : const Center( @@ -129,17 +131,18 @@ class _HomeScreenState extends State { controller: controller, itemBuilder: (BuildContext context, int index) { return Column( - key: ValueKey(index), mainAxisSize: MainAxisSize.min, children: [ NewsArticleTile( - key: ValueKey
(news[index]), article: news[index], ), if (index == news.length - 1 && _isLoading) - const Center( - child: CircularProgressIndicator( - backgroundColor: Colors.yellow, + SizedBox( + height: 100, + child: const Center( + child: CircularProgressIndicator( + backgroundColor: Colors.yellow, + ), ), ) ], diff --git a/lib/presentation/home/widgets/news_article_tile.dart b/lib/presentation/home/widgets/news_article_tile.dart index b90034f..43ad2f3 100644 --- a/lib/presentation/home/widgets/news_article_tile.dart +++ b/lib/presentation/home/widgets/news_article_tile.dart @@ -1,12 +1,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import '../../news/screen/article_news.dart'; import '../../../model/article_model.dart'; import '../../../shared/extension.dart'; +import '../../news/screen/article_news.dart'; class NewsArticleTile extends StatelessWidget { const NewsArticleTile({super.key, required this.article}); + final Article article; @override @@ -31,44 +32,49 @@ class NewsArticleTile extends StatelessWidget { borderRadius: BorderRadius.circular(30), ), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Stack( - children: [ - if ((article.urlToImage ?? '').isNotEmpty) - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: CachedNetworkImage( - placeholder: (BuildContext context, String url) => Container(), - errorWidget: (BuildContext context, String url, error) => const SizedBox(), - imageUrl: article.urlToImage ?? '', - ), - ), - Positioned( - bottom: 8, - right: 8, - child: Card( - elevation: 0, - color: Theme.of(context).primaryColor.withOpacity(0.8), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, + SizedBox( + height: 240, + child: Stack( + children: [ + if ((article.urlToImage ?? '').isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + placeholder: (BuildContext context, String url) => const SizedBox(), + errorWidget: (BuildContext context, String url, error) => const SizedBox(), + imageUrl: article.urlToImage ?? '', + fit: BoxFit.cover, ), - child: Text( - article.source?.name ?? '', - style: context.theme.titleSmall, + ), + Positioned( + bottom: 8, + right: 8, + child: Card( + elevation: 0, + color: Theme.of(context).primaryColor.withOpacity(0.8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + child: Text( + article.source!.name ?? '', + style: context.theme.titleSmall, + ), ), ), ), - ), - ], + ], + ), ), const Divider(), Text( article.title ?? '', style: context.theme.titleSmall!.copyWith( - fontWeight: FontWeight.bold, - ), + fontWeight: FontWeight.bold, + ), ) ], ),