Are you someone who likes to read the morning newspaper while sitting at your breakfast table and sipping on your tea or coffee? Well, I am one of those people that loves to read the news first thing in the morning to stay updated.
Why not build an app to stay updated with the latest curated news for ourselves? Let’s apply our knowledge of Flutter to create our own news app. Here are the steps we’ll take to create the app and improve our Flutter skills:
Read on to see what our final product will look like.
We’re building a single-screen app that has a search bar at the top of the screen; a Carousel widget to display top headlines from around the world; and a list of the news based on Country, Category, or Channel when the user selects from the side drawer.
When you run the application, the default Country for top headlines is set to India, and the default Category is set to Business.
We are going to use the API key from NewsAPI.org. You can also use the API from MediaStack and NewsData since we can query a limited amount of news articles in developer mode. The News API allows around 100 queries a day. Conversely, MediaStack allows 500 queries in a month and NewsData allows 200 queries a day. You can always sign up with different accounts to test your application. Everything will remain the same except the unique API key.
With that in mind, let us start building.
In your pubspec.yaml
file, please add the below dependencies:
Since we are using the WebView package to display the entire news article, we have to make a few changes to the Android app/build.gradle
file and iOS info.plist
file inside the Runner
folder.
You have to change the minSdkVersion
to at least 19
. Also, add multiDex
support to Android dependencies. Please look at the image below for reference:
Inside the Runner
folder, you have to add this line to support embedded views for Flutter when running on iOS devices:
<key>io.flutter.embedded_views_preview</key> <string>YES</string>
Please look at the image below for reference:
Our dependencies are set up for the news app. Now, let’s sign up on NewsApi.org and get our unique API key.
Go to NewsAPI.org and sign up with your email ID and password. As soon as you sign up, it will generate a unique API key for yourself that we will use to request news articles. Save that key as a constant in the Flutter project.
To use this API, we need to understand what an endpoint is. An endpoint is a distinct point at which an API allows software or programs to communicate with each other.
It is important to note that endpoints and APIs are not the same things. An endpoint is a component of an API, whereas an API is a set of rules that allows two pieces of software to share a resource to communicate with each other. Endpoints are the locations of those resources, and an API uses URLs to retrieve the requested responses.
With the above endpoints, we have to provide the API key through which authentication is handled. If the API key is not appended at the end of the URL, we are bound to receive a 401 - Unauthorized HTTP error
.
So the URL will look something like this:
https://newsapi.org/v2/everything?q=keyword&apiKey=APIKEY
The above URL will return a JSON response that will look something like this:
{ "status": "ok", "totalResults": 9364, - "articles": [ - { - "source": { "id": "the-verge", "name": "The Verge" }, "author": "Justine Calma", "title": "Texas heatwave and energy crunch curtails Bitcoin mining", "description": "Bitcoin miners in Texas powered down to respond to an energy crunch triggered by a punishing heatwave. Energy demand from cryptomining is growing in the state.", "url": "https://www.theverge.com/2022/7/12/23205066/texas-heat-curtails-bitcoin-mining-energy-demand-electricity-grid", "urlToImage": "https://cdn.vox-cdn.com/thumbor/sP9sPjh-2PfK76HRsOfHNYNQWAo=/0x285:4048x2404/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/23761862/1235927096.jpg", "publishedAt": "2022-07-12T15:50:17Z", "content": "Miners voluntarily powered down as energy demand and prices spiked \r\nAn aerial view of the Whinstone US Bitcoin mining facility in Rockdale, Texas, on October 9th, 2021. The long sheds at North Ameri... [+3770 chars]" },
After understanding the above, we will now start programming our application, starting with the model classes and then our GetX Controller
class.
We have 3 model classes.
ArticleModel
class ArticleModel { ArticleModel(this.source, this.author, this.title, this.description, this.url, this.urlToImage, this.publishedAt, this.content); String? author, description, urlToImage, content; String title, url, publishedAt; SourceModel source; Map<String, dynamic> toJson() { return { 'author': author, 'description': description, 'urlToImage': urlToImage, 'content': content, 'title': title, 'url': url, 'publishedAt': publishedAt, 'source': source, }; } factory ArticleModel.fromJson(Map<String, dynamic> json) => ArticleModel( SourceModel.fromJson(json['source'] as Map<String, dynamic>), json['author'], json['title'], json['description'], json['url'], json['urlToImage'], json['publishedAt'], json['content'], ); }
NewsModel
class NewsModel { NewsModel(this.status, this.totalResults, this.articles); String status; int totalResults; List<ArticleModel> articles; Map<String, dynamic> toJson() { return { 'status': status, 'totalResults': totalResults, 'articles': articles, }; } factory NewsModel.fromJson(Map<String, dynamic> json) => NewsModel( json['status'], json['totalResults'], (json['articles'] as List<dynamic>) .map((e) => ArticleModel.fromJson(e as Map<String, dynamic>)) .toList(), ); }
SourceModel
class SourceModel { SourceModel({this.id = '', required this.name}); String? id, name; Map<String, dynamic> toJson() { return { 'id': id, 'name': name, }; } factory SourceModel.fromJson(Map<String, dynamic> json) { return SourceModel( id: json['id'], name: json['name'], ); } }
If you look at the example JSON response above, the model classes are based on it. Variable names in the model classes should match the fields in the JSON response.
Controller
classHere we are going to define all our variables, methods, and functions to retrieve three types of news articles that are:
Start by defining and initializing the variables:
// for list view List<ArticleModel> allNews = <ArticleModel>[]; // for carousel List<ArticleModel> breakingNews = <ArticleModel>[]; ScrollController scrollController = ScrollController(); RxBool articleNotFound = false.obs; RxBool isLoading = false.obs; RxString cName = ''.obs; RxString country = ''.obs; RxString category = ''.obs; RxString channel = ''.obs; RxString searchNews = ''.obs; RxInt pageNum = 1.obs; RxInt pageSize = 10.obs; String baseUrl = "https://newsapi.org/v2/top-headlines?"; // ENDPOINT
The next step is an API function to retrieve a JSON object for all news articles from the News API. Using the HTTP response method, we are getting data from the URL and decoding the JSON object into a readable format. Then we are checking for the response status.
If the response code is 200
, it means the status is okay. If the response has some data, it will be loaded to the list, which will eventually be shown in the UI. This is the function to retrieve all the news:
// function to retrieve a JSON response for all news from newsApi.org getAllNewsFromApi(url) async { //Creates a new Uri object by parsing a URI string. http.Response res = await http.get(Uri.parse(url)); if (res.statusCode == 200) { //Parses the string and returns the resulting Json object. NewsModel newsData = NewsModel.fromJson(jsonDecode(res.body)); if (newsData.articles.isEmpty && newsData.totalResults == 0) { articleNotFound.value = isLoading.value == true ? false : true; isLoading.value = false; update(); } else { if (isLoading.value == true) { // combining two list instances with spread operator allNews = [...allNews, ...newsData.articles]; update(); } else { if (newsData.articles.isNotEmpty) { allNews = newsData.articles; // list scrolls back to the start of the screen if (scrollController.hasClients) scrollController.jumpTo(0.0); update(); } } articleNotFound.value = false; isLoading.value = false; update(); } } else { articleNotFound.value = true; update(); } }
And here’s a function to retrieve breaking news:
// function to retrieve a JSON response for breaking news from newsApi.org getBreakingNewsFromApi(url) async { http.Response res = await http.get(Uri.parse(url)); if (res.statusCode == 200) { NewsModel newsData = NewsModel.fromJson(jsonDecode(res.body)); if (newsData.articles.isEmpty && newsData.totalResults == 0) { articleNotFound.value = isLoading.value == true ? false : true; isLoading.value = false; update(); } else { if (isLoading.value == true) { // combining two list instances with spread operator breakingNews = [...breakingNews, ...newsData.articles]; update(); } else { if (newsData.articles.isNotEmpty) { breakingNews = newsData.articles; if (scrollController.hasClients) scrollController.jumpTo(0.0); update(); } } articleNotFound.value = false; isLoading.value = false; update(); } } else { articleNotFound.value = true; update(); } }
Next, we add functions to communicate if the endpoints we discussed previously and to receive customized responses from the API. We need to pass a URL string to the above functions, which we will do when we call it in the ones below.
To get all news and news according to a search keyword:
// function to load and display all news and searched news on to UI getAllNews({channel = '', searchKey = '', reload = false}) async { articleNotFound.value = false; if (!reload && isLoading.value == false) { } else { country.value = ''; category.value = ''; } if (isLoading.value == true) { pageNum++; } else { allNews = []; pageNum.value = 2; } // ENDPOINT baseUrl = "https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&"; // default country is set to India baseUrl += country.isEmpty ? 'country=in&' : 'country=$country&'; // default category is set to Business baseUrl += category.isEmpty ? 'category=business&' : 'category=$category&'; baseUrl += 'apiKey=${NewsApiConstants.newsApiKey}'; // when a user selects a channel the country and category will become null if (channel != '') { country.value = ''; category.value = ''; baseUrl = "https://newsapi.org/v2/top-headlines?sources=$channel&apiKey=${NewsApiConstants.newsApiKey}"; } // when a enters any keyword the country and category will become null if (searchKey != '') { country.value = ''; category.value = ''; baseUrl = "https://newsapi.org/v2/everything?q=$searchKey&from=2022-07-01&sortBy=popularity&pageSize=10&apiKey=${NewsApiConstants.newsApiKey}"; } print(baseUrl); // calling the API function and passing the URL here getAllNewsFromApi(baseUrl); }
To get breaking news according to the country selected by the user:
// function to load and display breaking news on to UI getBreakingNews({reload = false}) async { articleNotFound.value = false; if (!reload && isLoading.value == false) { } else { country.value = ''; } if (isLoading.value == true) { pageNum++; } else { breakingNews = []; pageNum.value = 2; } // default language is set to English /// ENDPOINT baseUrl = "https://newsapi.org/v2/top-headlines?pageSize=10&page=$pageNum&languages=en&"; // default country is set to US baseUrl += country.isEmpty ? 'country=us&' : 'country=$country&'; //baseApi += category.isEmpty ? '' : 'category=$category&'; baseUrl += 'apiKey=${NewsApiConstants.newsApiKey}'; print([baseUrl]); // calling the API function and passing the URL here getBreakingNewsFromApi(baseUrl); }
Lastly, override the onInit
method and call the above two functions:
@override void onInit() { scrollController = ScrollController()..addListener(_scrollListener); getAllNews(); getBreakingNews(); super.onInit(); }
NewsCard
widgetNext, we are creating a custom widget that will be used to display the image, title, description, and URL of the news article that we will be getting from the API. This widget will be called in the ListView builder on the main screen:
class NewsCard extends StatelessWidget { final String imgUrl, title, desc, content, postUrl; const NewsCard( {Key? key, required this.imgUrl, required this.desc, required this.title, required this.content, required this.postUrl}); @override Widget build(BuildContext context) { return Card( elevation: Sizes.dimen_4, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(Sizes.dimen_10))), margin: const EdgeInsets.fromLTRB( Sizes.dimen_16, 0, Sizes.dimen_16, Sizes.dimen_16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[ ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(Sizes.dimen_10), topRight: Radius.circular(Sizes.dimen_10)), child: Image.network( imgUrl, height: 200, width: MediaQuery.of(context).size.width, fit: BoxFit.fill, // if the image is null errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Sizes.dimen_10)), child: const SizedBox( height: 200, width: double.infinity, child: Icon(Icons.broken_image_outlined), ), ); }, )), vertical15, Padding( padding: const EdgeInsets.all(Sizes.dimen_6), child: Text( title, maxLines: 2, style: const TextStyle( color: Colors.black87, fontSize: Sizes.dimen_20, fontWeight: FontWeight.w500), ), ), Padding( padding: const EdgeInsets.all(Sizes.dimen_6), child: Text( desc, maxLines: 2, style: const TextStyle(color: Colors.black54, fontSize: Sizes.dimen_14), ), ) ], ), ); } }
This is how our newsCard
will look.
You might notice constant values in the code. I have a habit of creating constant files in all my Flutter projects, for defining color, sizes, textfield decorations, etc. I am not adding those files to the article here, but you will find those in the GitHub repository.
Now we are starting to build our home screen. At the top of the screen, we have our search text field. When a user enters any keyword, the API will search through thousands of articles from different sources and display it on the screen with the help of the NewsCard widget:
Flexible( child: Container( padding: const EdgeInsets.symmetric(horizontal: Sizes.dimen_8), margin: const EdgeInsets.symmetric( horizontal: Sizes.dimen_18, vertical: Sizes.dimen_16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(Sizes.dimen_8)), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( fit: FlexFit.tight, flex: 4, child: Padding( padding: const EdgeInsets.only(left: Sizes.dimen_16), child: TextField( controller: searchController, textInputAction: TextInputAction.search, decoration: const InputDecoration( border: InputBorder.none, hintText: "Search News"), onChanged: (val) { newsController.searchNews.value = val; newsController.update(); }, onSubmitted: (value) async { newsController.searchNews.value = value; newsController.getAllNews( searchKey: newsController.searchNews.value); searchController.clear(); }, ), ), ), Flexible( flex: 1, fit: FlexFit.tight, child: IconButton( padding: EdgeInsets.zero, color: AppColors.burgundy, onPressed: () async { newsController.getAllNews( searchKey: newsController.searchNews.value); searchController.clear(); }, icon: const Icon(Icons.search_sharp)), ), ], ), ), ),
This is how our search bar will look.
The carousel widget will display the top headlines or breaking news from different countries when a user selects a country from the side drawer. This widget is wrapped with GetBuilder
so that it gets rebuilt every time a new country is selected and breaking news needs to be updated.
I have set the carousel option to autoplay the slider. It will scroll horizontally automatically without the need for the user to scroll it. The Stack widget displays the image of the news and on top of it the title of the news.
I have also added a banner at the top right corner that says Top Headlines, which is something similar to the debug banner. The Stack widget is again wrapped with InkWell
, and inside it is an onTap
function. When a user clicks on any news item, it will take the user to the WebView screen where the whole news article will be displayed to the reader:
GetBuilder<NewsController>( init: NewsController(), builder: (controller) { return CarouselSlider( options: CarouselOptions( height: 200, autoPlay: true, enlargeCenterPage: true), items: controller.breakingNews.map((instance) { return controller.articleNotFound.value ? const Center( child: Text("Not Found", style: TextStyle(fontSize: 30))) : controller.breakingNews.isEmpty ? const Center(child: CircularProgressIndicator()) : Builder(builder: (BuildContext context) { try { return Banner( location: BannerLocation.topStart, message: 'Top Headlines', child: InkWell( onTap: () => Get.to(() => WebViewNews(newsUrl: instance.url)), child: Stack(children: [ ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.network( instance.urlToImage ?? " ", fit: BoxFit.fill, height: double.infinity, width: double.infinity, // if the image is null errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( 10)), child: const SizedBox( height: 200, width: double.infinity, child: Icon(Icons .broken_image_outlined), ), ); }, ), ), Positioned( left: 0, right: 0, bottom: 0, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular( 10), gradient: LinearGradient( colors: [ Colors.black12 .withOpacity(0), Colors.black ], begin: Alignment.topCenter, end: Alignment .bottomCenter)), child: Container( padding: const EdgeInsets .symmetric( horizontal: 5, vertical: 10), child: Container( margin: const EdgeInsets .symmetric( horizontal: 10), child: Text( instance.title, style: const TextStyle( fontSize: Sizes .dimen_16, color: Colors.white, fontWeight: FontWeight .bold), ))), )), ]), ), ); } catch (e) { if (kDebugMode) { print(e); } return Container(); } }); }).toList(), ); }),
This is how our carousel will look.
The Drawer widget has three dropdowns for selecting the Country, Category, or Channel. All these mainly translate into sources that we have discussed already. It is a minor endpoint provided by New API to customize the retrieval of the articles.
When you select any of the above in dropdowns, the user’s selection will be displayed in the side drawer and the country name will be displayed above the NewsCard
list items. This feature is added specially for prototyping so that as developers we know that the API is returning a response according to the code:
Drawer sideDrawer(NewsController newsController) { return Drawer( backgroundColor: AppColors.lightGrey, child: ListView( children: <Widget>[ GetBuilder<NewsController>( builder: (controller) { return Container( decoration: const BoxDecoration( color: AppColors.burgundy, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(Sizes.dimen_10), bottomRight: Radius.circular(Sizes.dimen_10), )), padding: const EdgeInsets.symmetric( horizontal: Sizes.dimen_18, vertical: Sizes.dimen_18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ controller.cName.isNotEmpty ? Text( "Country: ${controller.cName.value.capitalizeFirst}", style: const TextStyle( color: AppColors.white, fontSize: Sizes.dimen_18), ) : const SizedBox.shrink(), vertical15, controller.category.isNotEmpty ? Text( "Category: ${controller.category.value.capitalizeFirst}", style: const TextStyle( color: AppColors.white, fontSize: Sizes.dimen_18), ) : const SizedBox.shrink(), vertical15, controller.channel.isNotEmpty ? Text( "Category: ${controller.channel.value.capitalizeFirst}", style: const TextStyle( color: AppColors.white, fontSize: Sizes.dimen_18), ) : const SizedBox.shrink(), ], ), ); }, init: NewsController(), ), /// For Selecting the Country ExpansionTile( collapsedTextColor: AppColors.burgundy, collapsedIconColor: AppColors.burgundy, iconColor: AppColors.burgundy, textColor: AppColors.burgundy, title: const Text("Select Country"), children: <Widget>[ for (int i = 0; i < listOfCountry.length; i++) drawerDropDown( onCalled: () { newsController.country.value = listOfCountry[i]['code']!; newsController.cName.value = listOfCountry[i]['name']!.toUpperCase(); newsController.getAllNews(); newsController.getBreakingNews(); }, name: listOfCountry[i]['name']!.toUpperCase(), ), ], ), /// For Selecting the Category ExpansionTile( collapsedTextColor: AppColors.burgundy, collapsedIconColor: AppColors.burgundy, iconColor: AppColors.burgundy, textColor: AppColors.burgundy, title: const Text("Select Category"), children: [ for (int i = 0; i < listOfCategory.length; i++) drawerDropDown( onCalled: () { newsController.category.value = listOfCategory[i]['code']!; newsController.getAllNews(); }, name: listOfCategory[i]['name']!.toUpperCase()) ], ), /// For Selecting the Channel ExpansionTile( collapsedTextColor: AppColors.burgundy, collapsedIconColor: AppColors.burgundy, iconColor: AppColors.burgundy, textColor: AppColors.burgundy, title: const Text("Select Channel"), children: [ for (int i = 0; i < listOfNewsChannel.length; i++) drawerDropDown( onCalled: () { newsController.channel.value = listOfNewsChannel[i]['code']!; newsController.getAllNews( channel: listOfNewsChannel[i]['code']); }, name: listOfNewsChannel[i]['name']!.toUpperCase(), ), ], ), const Divider(), ListTile( trailing: const Icon( Icons.done_sharp, size: Sizes.dimen_28, color: Colors.black, ), title: const Text( "Done", style: TextStyle(fontSize: Sizes.dimen_16, color: Colors.black), ), onTap: () => Get.back()), ], ), ); }
This is how our sideDrawer
will look.
Next, we are adding the NewsCard
widget that we had created earlier below the carousel widget, which displays all the other news according to the user selection from the side drawer. If a user enters a search keyword in the search text field, the news articles will be displayed here.
Please note that the carousel widget only displays top headlines and breaking news from the selected country; it is not filtered according to the category or channel. If a user selects a category or a channel, the carousel widget will not get updated; only the NewsCard
widget will get updated. But when a user selects a new country, the carousel widget will get updated along with the NewsCard
widget.
Again, the NewsCard
widget is wrapped with GetX Builder
and as well the InkWell
widget:
GetBuilder<NewsController>( init: NewsController(), builder: (controller) { return controller.articleNotFound.value ? const Center( child: Text('Nothing Found'), ) : controller.allNews.isEmpty ? const Center(child: CircularProgressIndicator()) : ListView.builder( controller: controller.scrollController, physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: controller.allNews.length, itemBuilder: (context, index) { index == controller.allNews.length - 1 && controller.isLoading.isTrue ? const Center( child: CircularProgressIndicator(), ) : const SizedBox(); return InkWell( onTap: () => Get.to(() => WebViewNews( newsUrl: controller.allNews[index].url)), child: NewsCard( imgUrl: controller .allNews[index].urlToImage ?? '', desc: controller .allNews[index].description ?? '', title: controller.allNews[index].title, content: controller.allNews[index].content ?? '', postUrl: controller.allNews[index].url), ); }); }),
The SingleChildScrollView
is the parent widget for the home screen as the body of the Scaffold
. The appBar
has a refresh button, which clears all the filters and defaults the application to its original state.
The WebView screen is a stateful widget that displays the whole article when a user clicks on any of the news items either from the carousel or the NewsCard.
Here, we have to initialize a WebViewController
with a Completer
class. A Completer
class is a way to produce Future
objects and complete them later with a value or an error. The Scaffold
body has the WebView
class passed directly. There is no appBar
on this screen so that it does not obstruct the reader from reading the whole article:
class WebViewNews extends StatefulWidget { final String newsUrl; WebViewNews({Key? key, required this.newsUrl}) : super(key: key); @override State<WebViewNews> createState() => _WebViewNewsState(); } class _WebViewNewsState extends State<WebViewNews> { NewsController newsController = NewsController(); final Completer<WebViewController> controller = Completer<WebViewController>(); @override Widget build(BuildContext context) { return Scaffold( body: WebView( initialUrl: widget.newsUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController webViewController) { setState(() { controller.complete(webViewController); }); }, )); } }
I have named my app Flash⚡News and designed my splash screen image in Canva. The splash page pops up for three seconds and then the user is diverted to the main home screen. Splash screens are very easy to implement and I recommend all apps should have one for a short intro.
class SplashScreen extends StatefulWidget { const SplashScreen({Key? key}) : super(key: key); @override State<SplashScreen> createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { @override void initState() { super.initState(); Timer(const Duration(seconds: 3), () { //navigate to home screen replacing the view Get.offAndToNamed('/homePage'); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.burgundy, body: Center(child: Image.asset('assets/flashNews.jpg')), ); } }
That is all! We have completed our application. There are a few other Dart files as I mentioned earlier that you will find on my GitHub link down below.
I have tried to keep the UI pretty neat. The whole focus is on news articles that are easy for the users to find and read. The API returns more than a hundred articles at a time; if you look at the code closely, we are only displaying a few pages from it. Again, we are allowed a limited amount of queries and a few articles at a time load more quickly.
Hope this gives you a gist of how to implement the interaction between JSON endpoints, fetch data from an API, and display that data on the screen.
Thank you!
Link to the Github repository: Flash News.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowconsole.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.
See how to implement a single and multilevel dropdown menu in your React project to make your nav bars more dynamic and user-friendly.
NAPI-RS is a great module-building tool for image resizing, cryptography, and more. Learn how to use it with Rust and Node.js.
2 Replies to "Building a news app with Flutter"
Too many nested if/else statement. You need to refactor your code.
Hi Vicecarloans, could you show me an example how you would do it. I will get to learn from it and accordingly refactor the code as well if necessary.