Flutter实现天气查询App

该项目是一个Flutter项目,适合新手,运用了很多的常见组件和布局。

项目特点就是简洁,好理解,运用第三方API实现天气的查询。
适用范围:
1.用于完成学校Flutter作业的朋友。
2.需要一个Flutter项目来学习技术的朋友。
3.想要写一个天气查询软件但是自己不想写界面的朋友,可以再此之上继续自己添加内容。
4.觉得Dart语言难用不想写界面的朋友。
仓库地址
这个app相当简单只有五个界面。
首先是欢迎界面
在这里插入图片描述

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return _MyApp();
  }
}

class _MyApp extends State<MyApp>{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: "天气app",
       //theme: ThemeData.dark(),
        home: WelcomePage()
    );
  }

}
class WelcomePage extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _WelcomePage();
  }
}

class _WelcomePage extends State<WelcomePage>{
  @override
  Widget build(BuildContext context) {
    void getLocationData() async {
      var weatherData = await WeatherModel().getLocationWeather();
      Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (context){
        return AppHome(
          locationWeather: weatherData,
        );
      }), (route) => false);
    }

    // TODO: implement build
    Future.delayed(Duration(seconds: 2),(){
      getLocationData();
    });
    return Scaffold(
      body: Container(
          alignment: Alignment.center,
          child: Column(
            children: <Widget>[
              Expanded(
                  flex: 1,
                  child: Text("")),
              Expanded(
                  flex: 1,
                  child: Column(
                    children: [
                      Image(image: AssetImage("assets/images/welcome.png")),
                      Text("Welcome To Weather App",style: TextStyle(fontSize: 26,color: Colors.blue,fontStyle: FontStyle.italic))
                    ],
                  )),
            ],
          )
      ),
    );
  }

}

加载欢迎页面两秒后,调用聚合数据的api请求天气数据。

请求网络之前自定义一个工具类

class NetworkHelper{
  NetworkHelper(this.url);
  final String url;
  Future getData() async{
    try{
      http.Response response = await http.get(url);
      if(response.statusCode==200){
        String data = response.body;
        return jsonDecode(data);
      }else{
        print(response.statusCode);
        return;
      }
    } catch(e){
      return "empty";
    }
  }
}

接口类

// const apiKey = 'a1229a6169b9ca8fa751980e7917fae5';
const openWeatherMapURL = 'http://v.juhe.cn/weather/geo';
const openCityWeatherMapURL = 'http://v.juhe.cn/weather/index';
class WeatherModel {
  //http://v.juhe.cn/weather/index?format=2&cityname=%E8%8B%8F%E5%B7%9E&key=您申请的KEY
  Future<dynamic> getCityWeather(String cityName) async{
    NetworkHelper networkHelper = NetworkHelper('$openCityWeatherMapURL?format=1&key=$apiKey&cityname=$cityName&dtype=json');
    var weatherData =await networkHelper.getData();
    return weatherData;
  }

  Future<dynamic> getLocationWeather() async{
    Location location = Location();
    await location.getCurrentLocation();
    NetworkHelper networkHelper = NetworkHelper(
        '$openWeatherMapURL?format=2&key=$apiKey&dtype=json&lat=${location.latitude}&lon=${location.longitude}');
    var weatherData = await networkHelper.getData();
    return weatherData;
  }

  String getMessage(int temp) {
    if (temp > 25) {
      return '好热,现在适合吃冰淇淋!';
    } else if (temp > 20) {
      return '适合穿短袖T恤 ';
    } else if (temp <= 10) {
      return '好冷,戴上围巾和手套吧';
    } else {
      return '温度宜人,开心玩耍吧';
    }
  }
}

getMessage方法是设置之后界面的一些文本丰富界面。
这里说到两种请求聚合api的方式,一种是通过所处地理位置的经纬度。
获取经纬度的方式。

class Location{
  double latitude;
  double longitude;
  Future<void> getCurrentLocation() async{
    try{
      Position position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.low);
      latitude = position.latitude.abs();
      longitude = position.longitude.abs();
    }catch(e){
      print(e);
    }
  }
}

还有一种就是通过城市的名称。

请求的返回结果有多种情况:
1.手机没有网络的情况,会抛出一个没有网络异常,自定义返回一个字符串,方便之后的判断。
2.有网络,请求失败。
3.有网络请求成功。

最后无论是通过聚合接口还是我们自己自定义的,请求网络之后都会有一个返回值,通过不同的返回值来处理相关的逻辑。

拿到返回值后,就把返回值(无论成功与否)通过欢迎界面,传递给主界面。
主界面导航

class AppHome extends StatefulWidget {
  AppHome({this.locationWeather});
  final locationWeather;
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _HomePageState();
  }
}

class _HomePageState extends State<AppHome>{

  int _currentIndex=0;
  List<Widget> _widgets=List();
  @override
  void initState() {
    super.initState();
    _widgets.add(LocationScreen(locationWeather: widget.locationWeather,));
    _widgets.add(NewsPage());
    _widgets.add(MyPage());
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _widgets,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.wb_sunny),title: Text("今日天气")
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.library_books),title: Text("今日目标")
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.person),title: Text("关于我的")
          )
        ],
        currentIndex: _currentIndex,
        onTap: _itemTapped,
      ),
    );
  }
  void _itemTapped (int index){
    setState(() {
      _currentIndex=index;
    });
  }
}

一些简单的写法,不必多言。

在主界面添加子页面的时候,在把从欢迎页面请求的数据,通过主页面传递给天气页面。

在这里插入图片描述

class LocationScreen extends StatefulWidget {

  LocationScreen({this.locationWeather});
  final locationWeather;
  @override
  _LocationScreenState createState() => _LocationScreenState();

}
class _LocationScreenState extends State<LocationScreen> {

  WeatherModel weather = WeatherModel();
  String temperature;
  String condition;
  String cityName;
  String imgId="assets/images/init.JPG";
  String weatherMessage;

  @override
  void initState() {
    super.initState();
    updateUI(widget.locationWeather);
  }

  Future<void> updateUI(dynamic weatherData) async {
    SharedPreferences prefs=await SharedPreferences.getInstance();
    prefs.setString('temperature', "∅");
    prefs.setString('condition', "未知");
    prefs.setString('weatherMessage', "没有查到天气");
    prefs.setString('cityName', '绵阳');
    prefs.setString('imgId', 'assets/images/init.JPG');
    setState(()  {
      if(weatherData=="empty"||weatherData['result']==null){
        temperature = prefs.get('temperature');
        condition = prefs.get('condition');
        weatherMessage = prefs.get('weatherMessage');
        cityName = prefs.get('cityName');
        imgId=prefs.get('imgId');
      }
      else {
        var result = weatherData['result'];
        var sk = result['sk'];
        var today = result['today'];
        temperature = sk['temp'];
        cityName = weatherData['result']['today']['city'];
        condition = today['weather'];
        weatherMessage = weather.getMessage(int.parse(temperature));
        if(condition.contains("雨")){
          imgId="assets/images/rain.jpg";
        }else if(condition.contains("晴")){
          imgId="assets/images/qing.png";
        } else if(condition.contains("多云")){
          imgId="assets/images/duoyun.png";
        }
      }

    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: Icon(Icons.wb_sunny,color: Colors.white,),
        title: Text("今日天气"),
        backgroundColor: Color(0xff343434),
      ),
      body: Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: AssetImage(imgId==null?'assets/images/init.JPG':imgId),
            fit: BoxFit.cover,
          ),
        ),
        //constraints: BoxConstraints.expand(),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                FlatButton(
                  onPressed: () async {
                    var weatherData = await weather.getLocationWeather();
                    updateUI(weatherData);
                  },
                  child: Icon(
                    Icons.near_me,
                    color: Colors.white,
                    size: 50.0,
                  ),
                ),
                FlatButton(
                  onPressed: () async{
                    var typedName =await Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) {
                          return CityScreen();
                        },
                      ),
                    );
                    if(typedName!=null){
                      var weatherData = await weather.getCityWeather(typedName);
                      updateUI(weatherData);
                    }
                  },
                  child: Icon(
                    Icons.location_city,
                    color: Colors.white,
                    size: 50.0,
                  ),
                ),
              ],
            ),
            Padding(
              padding: EdgeInsets.only(left: 15.0),
              child: Row(
                children: <Widget>[
                  Text(
                    '$temperature°',
                    style: kTempTextStyle,
                  ),
                  Text(
                    condition,
                    style: kConditionTextStyle,
                  ),
                ],
              ),
            ),
            Padding(
              padding: EdgeInsets.only(right: 15.0),
              child: Text(
                '$weatherMessage in $cityName',
                textAlign: TextAlign.right,
                style: kMessageTextStyle,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

再说回之前请求的情况,如果是没有网络则捕获异常返回“empty”,如果有网络但请求失败,返回的数据中的result==null(试出来的)
通过以上代码,可以看出来,我把这两种情况放在一起,当条件满足时,加载SharedPreferences 存储好的数据(其实没必要用,我用是为了完成老师的打分点)。
在这里插入图片描述

然后就是请求成功的情况,解析相应的json串,更新ui。通过返回的不同的天气状况,温度,设置不同的背景图片,通过getMessage()提示不同的语句。

右上角的按钮是进入城市选择界面

class CityScreen extends StatefulWidget {
  @override
  _CityScreenState createState() => _CityScreenState();
}

class _CityScreenState extends State<CityScreen> {
  String cityName;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(title: Text("选择城市"), backgroundColor: Color(0xff343434),),
      body: Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: AssetImage("assets/images/city_bac.jpg"),
            fit: BoxFit.cover,
          ),
        ),
        constraints: BoxConstraints.expand(),
          child: Column(
            children: <Widget>[
              Container(
                padding: EdgeInsets.all(20.0),
                child: TextField(
                  style: TextStyle(
                    color: Colors.black,
                  ), //TextStyle
                  decoration: kTextFieldInputDecoration,
                  onChanged: (value){
                    cityName = value;
                  },
                ),
              ),
              FlatButton(
                onPressed: () {
                  Navigator.pop(context,cityName);
                },
                child: Text(
                  'Get Weather',
                  style: kButtonTextStyle,
                ),
              ),
            ],
          ),
      ),
    );
  }
}

在这里插入图片描述
输入城市就可以查到相应城市的天气
左上角的按钮则是定位到当前位置,获取当前位置的天气。

为了完成老师的考核点,设置第二个界面设定目标界面,其实很简单。就是添加了一个文本框,点击按钮,将文本框的内容添加到下方的列表视图中,并保存到数据库中。

class NewsPage extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _NewsPage();
  }
}
class MyListView extends StatelessWidget {
  String title;
  MyListView(this.title);
  @override
  Widget build(BuildContext context) {
    return Container(
        child: Column(
          children: <Widget>[
            Container(
              height: 53,
              child: _mineItem(title),
            ),
            Container(
              color: Color(0xffeaeaea),
              constraints: BoxConstraints.expand(height: 1.0),
            ),
          ],
        )
    );
  }

  Widget _mineItem(String title) {
    return InkWell(
      onTap: (){

      },
      child: Row(
        children: <Widget>[
          Expanded(
            flex: 1,
            child: Container(
                padding: EdgeInsets.only(left: 16),
                child: Icon(Icons.access_time)
            ),
          ),
          Expanded(
            flex: 6,
            child: Container(
              padding: EdgeInsets.only(left: 10),
              child: Text(
                title,
                style: TextStyle(fontSize: 16),
              ),
            ),
          ),
          Expanded(
            flex: 1,
            child: Container(
              child: Icon(
                Icons.brightness_5,
                size: 20,
                color: Colors.grey,
              ),
            ),
          )
        ],
      ),
    );
  }
}
class _NewsPage extends State<NewsPage> {
   String goal;
   List widgets=[];

   @override
  void initState() {
     super.initState();
     DatabaseHelper.instance.queryAllRows().then((value) {
       setState(() {
         value.forEach((element) {
           widgets.add(element['goalText']);
         });
       });
     }).catchError((onError){
       print(onError);
     });
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Color(0xff343434),
        leading: Icon(Icons.library_books,color: Colors.white,),
        title: Text("今日目标"),),
      body:Column(
        children: [
          Container(
            padding: EdgeInsets.all(20.0),
            child: new TextField(
              style: TextStyle(
                color: Colors.black,
              ), //TextStyle
              decoration: InputDecoration(
                filled: true,
                fillColor: Colors.white,
                icon: Icon(
                  Icons.location_city,
                  color: Colors.black,
                ), //Icon
                hintText: '输入今天的目标吧!',
                hintStyle: TextStyle(
                  color: Colors.grey,
                ), //TextStyle
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(
                    Radius.circular(10.0),
                  ),
                  borderSide: BorderSide.none,
                ),
              ),
              onChanged: (value){
                 goal=value;
              },
            ),
          ),
          FlatButton(
             color: Colors.black,
            onPressed: () {
              setState(() {
                if(goal!=""){
                  widgets.add(goal);
                  DatabaseHelper.instance.insert(new Goal(goalText: goal));
                }
              });
            },
            child: Text(
              '设定目标!',
              style: TextStyle(
                fontSize: 15,
                  fontStyle: FontStyle.italic,
                color: Colors.white
              ),
              //style: kButtonTextStyle,
            ),
          ),
          Expanded(
                child:new ListView.builder(
                  itemCount: widgets.length,
                  itemBuilder:(context,index){
                    // return ListTile(
                    //   leading: new Icon(Icons.access_time),
                    //   title: Text('${widgets[index]}'),
                    // );
                    return new MyListView(widgets[index]);
                  },
                ),
              )
        ],
      ),
    );
  }
}

自定义了列表项,没什么用,就是丰富一下加个图标。
在这里插入图片描述
数据库部分也很简单直接贴代码就ok了。

class Goal {
  int id;
  String goalText;
  Goal({ this .id,this .goalText});
  Map<String, dynamic> toMap() {
    return { 'id':id,'goalText': goalText};
  }
}
  class DatabaseHelper {
   static final _databaseName = "myDB.db" ;
   static final _databaseVersion = 1 ;
   static final table = 'goal' ;
   static final columnId = 'id' ;
   static final columnTitle = 'goalText' ;
   DatabaseHelper.init();
   static final DatabaseHelper instance = DatabaseHelper.init();
   static Database _database;
   Future<Database> get database async {
   if (_database != null ) return _database;
   _database = await _initDatabase();
   return _database;
   }
   _initDatabase() async {
   String path = join(await getDatabasesPath(), _databaseName);
   return await openDatabase(path,
   version: _databaseVersion, onCreate: _onCreate);
   }
   Future _onCreate(Database db, int version) async {
     await db.execute('' 'CREATE TABLE $table ($columnId INTEGER PRIMARY KEY AUTOINCREMENT,$columnTitle TEXT NOT NULL)'' ');
}
   Future<int> insert(Goal goal) async {
      Database db = await instance.database;
      var res = await db.insert(table,goal.toMap());
      String str=goal.goalText;
      print("add $str");
      return res;
   }
      Future<List<Map<String, dynamic>>> queryAllRows() async {
      Database db = await instance.database;
      var res = await db.query(table);
      return res;
   }
}

最后就是关于页面,实在不知道些什么就,很简单写了一些简单的介绍,就是一些文本内容不作过多介绍。
在这里插入图片描述
为了使界面代码清晰,将一些格式封装了起来。

const kTempTextStyle = TextStyle(
  color: Colors.white,
  fontSize: 100.0,
);

const kMessageTextStyle = TextStyle(
  color: Colors.white,
  fontSize: 30.0,
);

const kButtonTextStyle = TextStyle(
  fontSize: 30.0,
  color: Colors.white,
);

const kConditionTextStyle = TextStyle(
  fontSize: 30.0,
  color: Colors.white,
);

const kTextFieldInputDecoration = InputDecoration(
  filled: true,
  fillColor: Colors.white,
  icon: Icon(
    Icons.location_city,
    color: Colors.white,
  ), //Icon
  hintText: 'Enter City Name',
  hintStyle: TextStyle(
    color: Colors.grey,
  ), //TextStyle
  border: OutlineInputBorder(
    borderRadius: BorderRadius.all(
      Radius.circular(10.0),
    ),
    borderSide: BorderSide.none,
  ),
);

完工。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flutter 是一个跨平台的移动应用开发框架,可以使用 Dart 语言进行开发。要实现天气预报功能,你可以按照以下步骤进行操作: 1. 导入相关依赖:在 pubspec.yaml 文件中添加 http 和 json_serializable 依赖。http 用于发送网络请求,json_serializable 用于序列化和反序列化 JSON 数据。 ```yaml dependencies: flutter: sdk: flutter http: ^0.13.3 json_serializable: ^4.4.0 ``` 2. 创建数据模型:根据天气 API 返回的 JSON 数据,创建相应的数据模型。例如,可以创建 Weather 类来表示天气信息。 ```dart import 'package:json_annotation/json_annotation.dart'; part 'weather.g.dart'; @JsonSerializable() class Weather { final String city; final double temperature; final String description; Weather({required this.city, required this.temperature, required this.description}); factory Weather.fromJson(Map<String, dynamic> json) => _$WeatherFromJson(json); Map<String, dynamic> toJson() => _$WeatherToJson(this); } ``` 3. 生成序列化代码:运行以下命令生成序列化代码: ```bash flutter pub run build_runner build ``` 4. 创建网络请求服务:可以创建一个 WeatherService 类,封装获取天气数据的网络请求。 ```dart import 'package:http/http.dart' as http; class WeatherService { static const String apiKey = 'YOUR_API_KEY'; static Future<Weather> fetchWeather(String city) async { final url = Uri.parse('https://api.weatherapi.com/v1/current.json?key=$apiKey&q=$city'); final response = await http.get(url); if (response.statusCode == 200) { final json = jsonDecode(response.body); return Weather.fromJson(json['current']); } else { throw Exception('Failed to fetch weather'); } } } ``` 5. 创建天气预报界面:使用 Flutter 的 Widget 构建界面,可以显示天气信息。 ```dart import 'package:flutter/material.dart'; class WeatherPage extends StatefulWidget { final String city; const WeatherPage({required this.city}); @override _WeatherPageState createState() => _WeatherPageState(); } class _WeatherPageState extends State<WeatherPage> { Weather? _weather; @override void initState() { super.initState(); _fetchWeather(); } void _fetchWeather() async { try { final weather = await WeatherService.fetchWeather(widget.city); setState(() { _weather = weather; }); } catch (e) { print('Failed to fetch weather: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Weather Forecast'), ), body: Center( child: _weather != null ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('City: ${_weather!.city}'), Text('Temperature: ${_weather!.temperature}'), Text('Description: ${_weather!.description}'), ], ) : CircularProgressIndicator(), ), ); } } ``` 以上是一个简单的示例,通过 WeatherService 类发送网络请求获取天气数据,并在 WeatherPage 中显示天气信息。你需要替换 apiKey 为你自己的天气 API 密钥,并根据具体的天气 API 接口文档进行调整。希望对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值