使用 Flutter 构建新闻应用

您是否喜欢坐在早餐桌旁喝茶或咖啡时阅读早报?好吧,我是那些喜欢在早上第一件事就是阅读新闻以保持更新的人之一。

为什么不构建一个应用程序来为我们自己保持最新的精选新闻?让我们应用我们对 Flutter 的知识来创建我们自己的新闻应用程序。以下是我们将采取的步骤来创建应用程序并提高我们的 Flutter 技能:

    1. 演示应用程序介绍

    2. 设置依赖项

    3. 为 Android 和 iOS 配置 WebView

    4. 获取新闻 API 密钥

    5. 编写模型类

    6. 使用 GetX Controller 类获取数据

    7. 创建 NewsCard 小部件

    8. 添加搜索栏小部件

    9. 添加轮播小部件

    10. 添加侧抽屉小部件

    11. 完成我们的主屏幕

请继续阅读以了解我们的最终产品会是什么样子。

演示应用程序介绍

我们正在构建一个在屏幕顶部有一个搜索栏的单屏应用;一个轮播小部件,用于显示来自世界各地的头条新闻;以及当用户从侧边抽屉中选择时基于Country、Category或Channel的新闻列表。

运行应用程序时,头条新闻的默认Country设置为India,默认Category设置为Business

我们将使用来自NewsAPI.org的 API 密钥。您还可以使用MediaStack和NewsData的 API,因为我们可以在开发人员模式下查询有限数量的新闻文章。新闻 API 每天允许大约 100 个查询。相反,MediaStack 每月允许 500 次查询,而 NewsData 每天允许 200 次查询。您可以随时注册不同的帐户来测试您的应用程序。除了唯一的 API 密钥外,一切都将保持不变。

考虑到这一点,让我们开始构建。

设置依赖项

在您的pubspec.yaml文件中,请添加以下依赖项:

  • http: ^0.13.4 — 一个可组合的、基于 Future 的库,用于发出 HTTP 请求。使用这个包,我们将从NewsApi.org查询新闻文章

  • webview_flutter:^3.0.4 — 一个 Flutter 插件,在 Android 和 iOS 上提供 WebView 小部件。有了这个包,用户可以阅读整篇新闻文章

  • carousel_slider:^4.1.1 — 支持无限滚动和自定义子小部件的轮播滑块小部件。我们正在使用这个包来显示将自动水平滚动的最新标题

  • 获取:^4.6.5 — 我们的状态管理解决方案。我已经谈到了使用GetX 作为Flutter 的状态管理解决方案的优势;它快速且易于实现,尤其是在开发人员进行原型设计时

为 Android 和 iOS 配置 WebView

由于我们使用 WebView 包来显示整篇新闻文章,因此我们必须对文件夹内的 Android app/build.gradle文件和 iOS文件进行一些更改。info.plist``Runner

安卓版

您必须将 更改minSdkVersion为至少19. 此外,添加multiDex对 Android 依赖项的支持。请看下面的图片以供参考:

对于 iOS

在该Runner文件夹中,您必须添加此行以支持 Flutter 在 iOS 设备上运行时的嵌入式视图:

io.flutter.embedded_views_preview
   YES

请看下面的图片以供参考:

我们的依赖项是为新闻应用程序设置的。现在,让我们在 NewsApi.org 上注册并获取我们唯一的 API 密钥。

获取新闻 API 密钥

访问 NewsAPI.org 并使用您的电子邮件 ID 和密码进行注册。一旦您注册,它将为您自己生成一个唯一的 API 密钥,我们将使用它来请求新闻文章。将该键保存为 Flutter 项目中的常量。

端点

要使用此 API,我们需要了解端点是什么。端点是 API 允许软件或程序相互通信的不同点。

重要的是要注意端点和 API 不是一回事。端点是 API 的一个组件,而 API 是一组规则,允许两个软件共享资源以相互通信。端点是这些资源的位置,API 使用 URL 来检索请求的响应。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


新闻 API 的主要端点

  1. https://newsapi.org/v2/everything?(这会搜索超过 80,000 个不同来源发表的每篇文章)

  2. https://newsapi.org/v2/top-headlines?(这会根据国家和类别返回突发新闻头条)

  3. 还有一个次要端点,主要返回来自特定发布者(例如:BBC、ABC)的新闻https://newsapi.org/v2/top-headlines/sources?

对于上述端点,我们必须提供用于处理身份验证的 API 密钥。如果 API 密钥没有附加在 URL 的末尾,我们一定会收到一个401 - Unauthorized HTTP error.

所以 URL 看起来像这样:

https://newsapi.org/v2/everything?q=keyword&apiKey=APIKEY

上面的 URL 将返回一个 JSON 响应,如下所示:

{
"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]"
},

了解上述内容后,我们现在开始编写应用程序,从模型类开始,然后是 GetXController类。

编写模型类

我们有 3 个模型类。

  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 toJson() {
       return {
         'author': author,
         'description': description,
         'urlToImage': urlToImage,
         'content': content,
         'title': title,
         'url': url,
         'publishedAt': publishedAt,
         'source': source,
       };
     }
    ​
     factory ArticleModel.fromJson(Map json) => ArticleModel(
           SourceModel.fromJson(json['source'] as Map),
           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 articles;
    ​
     Map toJson() {
       return {
         'status': status,
         'totalResults': totalResults,
         'articles': articles,
       };
     }
    ​
     factory NewsModel.fromJson(Map json) => NewsModel(
           json['status'],
           json['totalResults'],
           (json['articles'] as List)
               .map((e) => ArticleModel.fromJson(e as Map))
               .toList(),
         );
    }
  3. SourceModel
    class SourceModel {
     SourceModel({this.id = '', required this.name});
    ​
     String? id, name;
    ​
     Map toJson() {
       return {
         'id': id,
         'name': name,
       };
     }
    ​
     factory SourceModel.fromJson(Map json) {
       return SourceModel(
         id: json['id'],
         name: json['name'],
       );
     }
    }

如果您查看上面的示例 JSON 响应,模型类是基于它的。模型类中的变量名称应与 JSON 响应中的字段匹配。

使用 GetXController类获取数据

在这里,我们将定义我们所有的变量、方法和函数来检索三种类型的新闻文章:

  1. 头条新闻

  2. 按国家、类别和频道分类的新闻

  3. 搜索新闻

首先定义和初始化变量:

// for list view
List allNews = [];
// for carousel
List breakingNews = [];
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

下一步是一个 API 函数,用于从 News API 检索所有新闻文章的 JSON 对象。使用 HTTP 响应方法,我们从 URL 获取数据并将 JSON 对象解码为可读格式。然后我们正在检查响应状态。

如果响应码为200,则表示状态正常。如果响应有一些数据,它将被加载到列表中,最终将显示在 UI 中。这是检索所有新闻的函数:

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

这是一个检索突发新闻的函数:

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

接下来,我们添加函数来与我们之前讨论的端点进行通信,并从 API 接收自定义响应。我们需要将一个 URL 字符串传递给上述函数,我们将在下面的函数中调用它时执行此操作。

根据搜索关键字获取所有新闻和新闻:

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

根据用户选择的国家获取突发新闻:

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

最后,重写该onInit方法并调用上述两个函数:

@override
void onInit() {
 scrollController = ScrollController()..addListener(_scrollListener);
 getAllNews();
 getBreakingNews();
 super.onInit();
}

创建自定义NewsCard小部件

接下来,我们将创建一个自定义小部件,用于显示我们将从 API 获取的新闻文章的图像、标题、描述和 URL。此小部件将在主屏幕上的 ListView 构建器中调用:

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

这就是我们的newsCard意愿。


来自 LogRocket 的更多精彩文章:

  • 不要错过来自 LogRocket 的精选时事通讯The Replay

  • 了解LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题

  • 使用 React 的 useEffect优化应用程序的性能

  • 在多个 Node 版本之间切换

  • 了解如何使用 AnimXYZ 为您的 React 应用程序制作动画

  • 探索 Tauri,一个用于构建二进制文件的新框架

  • 比较NestJS 与 Express.js


您可能会注意到代码中的常量值。我有在所有 Flutter 项目中创建常量文件的习惯,用于定义颜色、大小、文本字段装饰等。我没有将这些文件添加到本文中,但您可以在 GitHub 存储库中找到这些文件。

添加搜索栏小部件

现在我们开始构建我们的主屏幕。在屏幕顶部,我们有搜索文本字段。当用户输入任何关键字时,API 将搜索来自不同来源的数千篇文章,并借助 NewsCard 小部件将其显示在屏幕上:

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)),
       ),
     ],
   ),
 ),
),

这就是我们的搜索栏的外观。

添加轮播小部件

当用户从侧面抽屉中选择一个国家时,轮播小部件将显示来自不同国家的头条新闻或突发新闻。这个小部件被包裹起来,GetBuilder因此每次选择一个新的国家并且需要更新突发新闻时都会重建它。

我已将轮播选项设置为自动播放滑块。它会自动水平滚动,无需用户滚动。Stack 小部件显示新闻的图像,并在其顶部显示新闻的标题。

我还在右上角添加了一个横幅,上面写着Top Headlines,它类似于调试横幅。Stack 小部件再次用 包裹起来InkWell,里面是一个onTap函数。Emotn电视端专用浏览器,大屏冲浪开启全新视界,省去电视端VIP会员的费用!当用户点击任何新闻条目时,它会将用户带到 WebView 屏幕,整个新闻文章将显示给读者:

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

这就是我们的旋转木马的外观。

添加侧抽屉小部件

Drawer 小部件具有三个下拉菜单,用于选择Country、Category或Channel。所有这些主要转化为我们已经讨论过的来源。它是 New API 提供的一个小端点,用于自定义文章的检索。

当您在下拉列表中选择以上任何一项时,用户的选择将显示在侧边抽屉中,国家名称将显示在NewsCard列表项上方。此功能是专门为原型设计添加的,因此作为开发人员,我们知道 API 正在根据代码返回响应:

Drawer sideDrawer(NewsController newsController) {
 return Drawer(
   backgroundColor: AppColors.lightGrey,
   child: ListView(
     children: [
       GetBuilder(
         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: [
           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()),
     ],
   ),
 );
}

这就是我们的sideDrawer意愿。

完成我们的主屏幕

接下来,我们NewsCard在轮播小部件下方添加我们之前创建的小部件,该小部件根据用户从侧边抽屉中的选择显示所有其他新闻。如果用户在搜索文本字段中输入搜索关键字,则会在此处显示新闻文章。

请注意,轮播小部件仅显示来自所选国家/地区的头条新闻和突发新闻;它没有根据类别或频道进行过滤。如果用户选择了类别或频道,轮播小部件将不会更新;只有NewsCard小部件会得到更新。但是当用户选择一个新的国家时,轮播小部件将与NewsCard小部件一起更新。

同样,该NewsCard小部件由 GetXBuilder以及该小部件包装InkWell:

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

SingleChildScrollView是主屏幕的父小部件,微软配音助手,短视频文字转语音必备神器,支持海量与真人无异的配音!作为Scaffold. 有appBar一个刷新按钮,它清除所有过滤器并将应用程序默认为其原始状态。

添加 WebView 屏幕

WebView 屏幕是一个有状态的小部件,当用户单击轮播或新闻卡中的任何新闻项目时,它会显示整篇文章。

在这里,我们必须WebViewController用一个Completer类来初始化 a。类Completer是一种生成Future对象并在以后使用值或错误完成它们的方法。Scaffold正文具有WebView直接传递的类。此屏幕上没有appBar,以免妨碍读者阅读整篇文章:

class WebViewNews extends StatefulWidget {
 final String newsUrl;
 WebViewNews({Key? key, required this.newsUrl}) : super(key: key);

 @override
 State createState() => _WebViewNewsState();
}

class _WebViewNewsState extends State {
 NewsController newsController = NewsController();

 final Completer controller =
     Completer();

 @override
 Widget build(BuildContext context) {
   return Scaffold(
       body: WebView(
     initialUrl: widget.newsUrl,
     javascriptMode: JavascriptMode.unrestricted,
     onWebViewCreated: (WebViewController webViewController) {
       setState(() {
         controller.complete(webViewController);
       });
     },
   ));
 }
}

制作启动画面

我已将我的应用程序命名为 Flash News,并在 Canva 中设计了我的初始屏幕图像。启动页面弹出三秒钟,然后用户被转移到主主屏幕。启动画面很容易实现,我建议所有应用程序都应该有一个简短的介绍。

class SplashScreen extends StatefulWidget {
 const SplashScreen({Key? key}) : super(key: key);
​
 @override
 State createState() => _SplashScreenState();
}
​
class _SplashScreenState extends State {
 @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')),
   );
 }
}

就这些!我们已经完成了我们的申请。正如我之前提到的,还有一些其他的 Dart 文件,您可以在下面的 GitHub 链接中找到它们。

我试图让 UI 保持整洁。瓜子TV盒子App,内置独家线路无需切换源,支持1080P画质非常稳定!整个重点是易于用户查找和阅读的新闻文章。API 一次返回一百多篇文章;如果您仔细查看代码,我们只会显示其中的几页。同样,我们允许有限数量的查询和几篇文章一次加载更快。

希望这能让您大致了解如何实现 JSON 端点之间的交互、从 API 获取数据以及在屏幕上显示该数据。

你可能感兴趣的:(flutter,android,webview)