Murtaza Sulaihi I am a school professor and I also develop Android applications and Flutter applications.

Building a news app with Flutter

14 min read 3962

Flutter Logo

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:

    1. Introduction to the demo application
    2. Setting up dependencies
    3. Configuring WebView for Android and iOS
    4. Acquiring the News API key
    5. Writing the model classes
    6. Fetching data with the GetX Controller class
    7. Creating a NewsCard widget
    8. Adding a search bar widget
    9. Adding a carousel widget
    10. Adding a side drawer widget
    11. Completing our home screen

Read on to see what our final product will look like.

Introduction to the demo application

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.

Finished News App

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.

Setting up dependencies

In your pubspec.yaml file, please add the below dependencies:

  • http: ^0.13.4 — a composable, Future-based library for making HTTP requests. Using this package we will query news articles from NewsApi.org
  • webview_flutter: ^3.0.4 — a Flutter plugin that provides a WebView widget on Android and iOS. With this package, a user can read the whole news article
  • carousel_slider: ^4.1.1 — a carousel slider widget that supports infinite scroll and a custom child widget. We are using this package to show the latest headlines that will automatically scroll horizontally
  • get: ^4.6.5 — our state management solution. I have spoken about the advantages of using GetX as a state management solution for Flutter; it is fast and easy to implement, especially when a developer is prototyping

Configuring WebView for Android and iOS

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.



For Android

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:

MinSDKversion

For iOS

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:

Embedded Views

Our dependencies are set up for the news app. Now, let’s sign up on NewsApi.org and get our unique API key.

Acquiring the News 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.

Endpoints

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.

The News API’s main endpoints

  1. https://newsapi.org/v2/everything? (this searches for every article published by over 80,000 different sources)
  2. https://newsapi.org/v2/top-headlines? (this returns breaking news headlines according to country and category)
  3. There is also a minor endpoint that mainly returns news from specific publishers (eg: BBC, ABC) https://newsapi.org/v2/top-headlines/sources?

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:


More great articles from LogRocket:


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.

Writing the model classes

We have 3 model classes.

  1. 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'],
         );
    }
  2. 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(),
         );
    }
  3. 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.

Fetching data with the GetX Controller class

Here we are going to define all our variables, methods, and functions to retrieve three types of news articles that are:

  1. Top headlines
  2. News according to country, category, and channel
  3. Searched news

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();
}

Creating a custom NewsCard widget

Next, 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.

Newscard

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.

Adding a search bar widget

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.

Search Bar News

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.

News Carousel

Adding a side drawer widget

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.

Sidedrawer

Sidedrawer

Completing our home screen

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.

Flash News Default View

Adding the WebView screen

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);
       });
     },
   ));
 }
}

News Hub

Making a splash screen

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.

Flash News Splash Screen

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.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Murtaza Sulaihi I am a school professor and I also develop Android applications and Flutter applications.

2 Replies to “Building a news app with Flutter”

    1. 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.

Leave a Reply