使用Flutter编写一个简单的天气查询App

    xiaoxiao2023-10-09  145

    使用Flutter编写一个简单的天气查询App

    Flutter项目目录分析入口函数home:主页面 编写天气应用网络请求数据解析 布局编写Flutter里基础的Widget上中下

    Flutter项目目录分析

    文件夹作用android安卓工程相关代码build项目编译产生的目录iosios工程相关代码libflutter相关代码test用于放置测试代码pubspec.yaml配置文件,依赖等

    我们首先看lib目录 lib目录下只有一个main.dart文件。

    入口函数

    里面有一个main方法我们称作为入口函数吧,其中void main() => runApp(MyApp()); 这是一种Dart语言特有的速写语法。

    void main() => runApp(MyApp()); //等同于 void main() { return runApp(MyApp()); }

    我们来看一下MyApp类:

    class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } }

    在Flutter框架中,很多内容都为Widget(控件),包括对齐(alignment)、填充(padding)和布局(layout)等,控件类又细分为有状态控件类(继承StatefulWidget抽象类)和无状态控件类(继承StatelessWidget抽象类),两者的差别在于是否有状态。

    StatelessWidget 是不可变的,这意味着它们的属性不能改变–所有的值都是最终的。

    StatefulWidget持有的状态可能在widget生命周期中发生变化。

    实现一个StatefulWidget至少需要两个类:

    一个StatefulWidget类。一个State类。StatefulWidget类本身是不可变的,但是 State 类在widget生命周期中始终存在。

    MyApp类继承于StatelessWidget,既无状态控件。 重载build方法返回一个MaterialApp,MaterialApp继承自StatefulWidget既有状态控件,我们可以在上面定义主题 标题 主页 颜色等很多内容,它拥有以下参数

    this.navigatorKey, // 导航的key this.home, // 主页 this.routes = const <String, WidgetBuilder>{},// 路由 this.initialRoute,//初始路由 this.onGenerateRoute,//生成路由 this.onUnknownRoute,//位置路由 this.navigatorObservers = const <NavigatorObserver>[],//导航的观察者 this.builder,//widget的构建 this.title = '',//设备用于识别用户的应用程序的单行描述。在Android上,标题显示在任务管理器的应用程序快照上方,当用户按下“最近的应用程序”按钮时会显示这些快照。 在iOS上,无法使用此值。 来自应用程序的`Info.plist`的`CFBundleDisplayName`在任何时候都会被引用,否则就会引用`CFBundleName`。要提供初始化的标题,可以用 onGenerateTitle。 this.onGenerateTitle,//每次在WidgetsApp构建时都会重新生成 this.color,//背景颜色 this.theme,//主题,用ThemeData this.locale,//app语言支持 this.localizationsDelegates,//多语言代理 this.localeResolutionCallback,// this.supportedLocales = const <Locale>[Locale('en', 'US')],//支持的多语言 this.debugShowMaterialGrid = false,//显示网格 this.showPerformanceOverlay = false,//打开性能监控,覆盖在屏幕最上面 this.checkerboardRasterCacheImages = false, this.checkerboardOffscreenLayers = false, this.showSemanticsDebugger = false,//打开一个覆盖图,显示框架报告的可访问性信息 显示边框 this.debugShowCheckedModeBanner = true,//右上角显示一个debug的图标

    home:主页面

    进入程序后显示的第一个页面,传入的是一个Widget,但实际上这个Widget需要包裹一个Scaffold以显示该程序使用Material Design风格

    我们看一下MyHomePage类,该类继承自StatefulWiget 并且重载了createState()方法

    class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); }

    _MyHomePageState 类,该应用程序的大部分逻辑和状态管理代码都在该类中,在该实例中这个类保存了屏幕上用户点击的次数,随着用户的点击,次数不断增加,且展示在界面上,代码如下:

    class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } }

    编写天气应用

    接下来我们动手更改并编写天气应用Demo。 Demo效果图如下: 接口:https://www.tianqiapi.com/api/?version=v1 新建weather.dart文件创建Weather类

    class Weather{ Weather(); String cityName;//城市名字 String updateTime;//更新时间 List<DataBean> dataBean = new List();//天气数据 } class DataBean{ DataBean(); String day;//日期 String week;//周期 String wea;//天气 int air;//空气纯净指数 String airLevel;//空气纯净度 String airTips; String tem;//当前温度 String tem1;//最高温度 String tem2;//最低温度 }

    修改pubspec.yaml内容添加http请求依赖:

    http: ^0.11.0

    Terminal输入flutter packages get 执行获取packages操作

    网络请求

    将网络请求库import到main.dart文件中,因为需要操作json所以也要import 'dart:convert'

    import 'package:http/http.dart' as http; import 'dart:convert';

    在_MyHomePageState类的initState方法中执行网络请求 initState()这个方法在生命周期中只调用一次。这里可以做一些初始化工作,比如初始化State的变量等。

    //这里面的cityid我填写的是南京的,如过未填写cityid那么将通过IP获取地理位置来返回该IP地址的天气信息 String url = "https://www.tianqiapi.com/api/?version=v1&cityid=101190101"; @override void initState() { super.initState(); getData(); } void getData() async { http.get(url).then((http.Response response) { var jsonData = json.decode(response.body); print(jsonData); }); }

    getData()方法中使用了 async 关键字,用于告诉 Dart 它是异步方法,http.get() 前面的 await 关键字则表明这是阻塞调用。

    打包运行,查看控制台输出的信息。

    数据解析

    void getData() async { http.get(url).then((http.Response response) { var jsonData = json.decode(response.body); print(jsonData["city"]); }); }

    打包运行,查看控制台输出的信息。 在mian.dart文件中import我们新建的weather.dart文件

    import 'weather.dart';

    编写相应的取值和赋值的代码。

    void getData() async { http.get(url).then((http.Response response) { var jsonData = json.decode(response.body); weather.cityName = jsonData["city"]; weather.updateTime = jsonData["update_time"]; List data = new List(); data = jsonData["data"]; for (int i = 0; i < data.length; i++) { DataBean dataBean = new DataBean(); dataBean.air = data[i]["air"]; dataBean.airLevel = data[i]["air_level"]; dataBean.airTips = data[i]["air_tips"]; dataBean.day = data[i]["day"]; dataBean.week = data[i]["week"]; dataBean.tem = data[i]["tem"]; dataBean.tem1 = data[i]["tem1"]; dataBean.tem2 = data[i]["tem2"]; dataBean.wea = data[i]["wea"]; weather.dataBean.add(dataBean); } setState(() { }); }); }

    布局编写

    在编写应用前,我们了解一下Flutter里基础的Widget

    Flutter里基础的Widget

    Flutter有一套丰富、强大的基础widget,其中以下是很常用并且本次实例有用到的widget: Text:该 widget 可让创建一个带格式的文本类似于TextView。 Row、Column: 这些具有弹性空间的布局类Widget可让我们在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。暂时可以把这两当作LinearLayout 里的horizontal与vertical。 Stack: 叠层布局(类似于安卓里面的帧布局)。 Container: Container 可让我们创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 背景、边框、或者阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。 Image: 该 widget 可让创建一个图片控件,类似于安卓里面的ImageView,可以设置本地资源图片、网络图片链接等。

    大概了解完毕后,我们分析一下布局: 此页面布局我们主要分为三个部分,上 中 下 显示数据的父布局用Stack(帧布局),要用Image实现背景图片。 我们还要定义一个在数据获取状态时显示的布局,我们写一个_loading方法返回widget,在Scaffold的body中通过三元表达式判断并设置body,在数据为空时用_loading()的布局,在有数据时用Stack, 由于该页面数据需要滑动所以用滑动控件SingleChildScrollView主要我们数据显示的控件放在此控件的子控件(child)之中。

    @override Widget build(BuildContext context) { return Scaffold( body: weather.dataBean.length == 0 ? _loading() : Stack( children: <Widget>[ Image.network( "http://pic.netbian.com/uploads/allimg/190510/221228-15574975489aa1.jpg", fit: BoxFit.fill, height: double.infinity, ), SingleChildScrollView( child: body(), ) ], ), ); } Widget _loading() { return new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new CircularProgressIndicator( strokeWidth: 5.0, ), new Container( margin: EdgeInsets.only(top: 10.0), child: new Text( "正在加载..", style: TextStyle(fontWeight: FontWeight.bold), ), ), ]), ); } Widget body() { return Container( width: double.infinity,//宽度占满 child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ AppBar( centerTitle: true,//标题居中显示 elevation: 0,//阴影为0 title: Text("${weather.cityName}"),//获取城市名字 backgroundColor: Colors.transparent,//背景颜色透明 actions: <Widget>[ Container( padding: EdgeInsets.only(right: 10),//右边padding为10 alignment: Alignment.center, child: Text( DateTime.now().hour.toString() + ":" + DateTime.now().minute.toString(),//获取时间并显示 textAlign: TextAlign.center, ), ) ], ), topView(),//上 centerView(),//中 bottomView(),//下 Padding( padding: EdgeInsets.only(bottom: 20, top: 10), child: Text( "API来源为:https://www.tianqiapi.com", style: TextStyle(color: textColor),//文字颜色白色 ), ) ], ), ); }

    一个Container和一横向布局Row 我们写一个topView方法并且返回Widget Container中我们可以设置背景颜色,padding和margin

    Widget topView() { return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), color: containerColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( weather.dataBean[0].tem, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, )), Text( weather.dataBean[0].wea, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, ), ) ], ), ); }

    Container添加子布局Column ,Column子布局树添加Row和TextView。 我们写一个centerView方法并且返回Widget

    Widget centerView() { TextStyle textStyle = TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, ); return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), color: containerColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text("空气质量", style: textStyle ), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ Text("${weather.dataBean[0].air}", style: textStyle ), Text( "${weather.dataBean[0].airLevel}", style: textStyle, ) ], ), Text( "${weather.dataBean[0].airTips}", style: TextStyle(color: textColor), ) ], ), ); }

    Container添加子布局Column ,Column子布局树添加Row。 我们写一个bottomView方法并且返回Widget,并且写一个_listView方法返回Widget树。 listView需要item,在这里我们把Row当作list的item,写一个itemView方法

    Widget bottomView() { return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), color: containerColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text("预报", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, )), Column( children: _listView(), ) ], ), ); } List<Widget> _listView() { List<Widget> widgets = new List(); for (int i = 0; i < weather.dataBean.length; i++) { widgets.add(itemView(weather.dataBean[i])); } return widgets; } List<Widget> _listView() { List<Widget> widgets = new List(); for (int i = 0; i < weather.dataBean.length; i++) { widgets.add(itemView(weather.dataBean[i])); } return widgets; } Widget itemView(DataBean data) { return Container( padding: EdgeInsets.only(top: 10, bottom: 10), width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text("${data.day}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, )), Text( "${data.wea}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), Row( children: <Widget>[ Text( "${data.tem1}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), Text( "/", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), Text( "${data.tem2}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), ], ) ], ), ); }

    运行

    全部代码:

    import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'weather.dart'; void main() { return runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: '天气', theme: ThemeData( primarySwatch: Colors.blue, ), debugShowCheckedModeBanner: false, home: MyHomePage(title: '天气'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Weather weather = new Weather(); Color textColor = Colors.white; Color containerColor = Colors.black38; String url = "https://tianqiapi.com/api?version=v1&appid=45325487&appsecret=u8L2WsAb"; @override void initState() { super.initState(); getData(); } void getData() async { http.get(url).then((http.Response response) { var jsonData = json.decode(response.body); weather.cityName = jsonData["city"]; weather.updateTime = jsonData["update_time"]; List data = new List(); data = jsonData["data"]; for (int i = 0; i < data.length; i++) { DataBean dataBean = new DataBean(); dataBean.air = data[i]["air"]; dataBean.airLevel = data[i]["air_level"]; dataBean.airTips = data[i]["air_tips"]; dataBean.day = data[i]["day"]; dataBean.week = data[i]["week"]; dataBean.tem = data[i]["tem"]; dataBean.tem1 = data[i]["tem1"]; dataBean.tem2 = data[i]["tem2"]; dataBean.wea = data[i]["wea"]; weather.dataBean.add(dataBean); } setState(() {}); }); } @override Widget build(BuildContext context) { return Scaffold( body: weather.dataBean.length == 0 ? _loading(context) : Stack( children: <Widget>[ Image.network( "http://pic.netbian.com/uploads/allimg/190510/221228-15574975489aa1.jpg", fit: BoxFit.fill, height: double.infinity, ), SingleChildScrollView( child: body(), ) ], ), ); } Widget body() { var date = DateTime.now(); return Container( width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ AppBar( centerTitle: true, elevation: 0, title: Text("${weather.cityName}"), backgroundColor: Colors.transparent, actions: <Widget>[ Container( padding: EdgeInsets.only(right: 10), alignment: Alignment.center, child: Text( date.hour.toString() + ":" + date.minute.toString(), textAlign: TextAlign.center, ), ) ], ), topView(), centerView(), bottomView(), Padding( padding: EdgeInsets.only(bottom: 20, top: 10), child: Text( "API来源为:https://www.tianqiapi.com", style: TextStyle(color: textColor), ), ) ], ), ); } Widget topView() { return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), color: containerColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text(weather.dataBean[0].tem, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, )), Text( weather.dataBean[0].wea, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, ), ) ], ), ); } Widget centerView() { TextStyle textStyle = TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, ); return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), color: containerColor, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text("空气质量", style: textStyle), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ Text("${weather.dataBean[0].air}", style: textStyle), Text( "${weather.dataBean[0].airLevel}", style: textStyle, ) ], ), Text( "${weather.dataBean[0].airTips}", style: TextStyle(color: textColor), ) ], ), ); } Widget bottomView() { return Container( padding: EdgeInsets.all(10), margin: EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), color: containerColor, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text("预报", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: textColor, )), Column( children: _listView(), ) ], ), ); } List<Widget> _listView() { List<Widget> widgets = new List(); for (int i = 0; i < weather.dataBean.length; i++) { widgets.add(itemView(weather.dataBean[i])); } return widgets; } Widget itemView(DataBean data) { return Container( padding: EdgeInsets.only(top: 10, bottom: 10), width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text("${data.day}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, )), Text( "${data.wea}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), Row( children: <Widget>[ Text( "${data.tem1}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), Text( "/", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), Text( "${data.tem2}", style: TextStyle( fontWeight: FontWeight.bold, color: textColor, ), ), ], ) ], ), ); } Widget _loading(context) { return new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new CircularProgressIndicator( strokeWidth: 5.0, ), new Container( margin: EdgeInsets.only(top: 10.0), child: new Text( "正在加载..", style: TextStyle(fontWeight: FontWeight.bold), ), ), ]), ); } }
    最新回复(0)