Flutter實現一個簡單的天氣App
參考了香港胖仔的部落格,自己加入了一些簡單的東西。作為本學期的flutter大作業。
這個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,
),
);
完工。