This article has authorized the public account “code egg”, please indicate the source of reprint
After talking about common components and network requests, it is almost time to enter into the overall practice. Here we will write a familiar project, Cool Weather of Guo Shen. The project will use Fluro for route management, DIO for network request, Rxdart for BLoC for state management and logical separation, and file, shared_preferences, and SQflite for local data persistence. The address of the project: Flutter_weather, and the effect picture of the final implementation:
I’ve talked about everything except fluro, so let’s talk about fluro before we get into the actual combat
Fluro
Fluro is an encapsulation of Navigator to facilitate better management of route jump. Of course, there are still some defects, such as only supporting the transmission of string, not Chinese, etc., but these problems are not big problems.
The use of fluro is very simple, roughly divided into the following steps:
-
Define a Router instance globally
final router = Router(); Copy the code
-
Use the Router instance to define the path and its corresponding Handler object
For example, define a CityPage path and Handler Handler cityHandler = Handler(handlerFunc: (_, params) { Params is a Map
> type parameter ,> String cityId = params['city_id']? .first;return BlocProvider(child: WeatherPage(city: cityId), bloc: WeatherBloc()); }); // Define the route path and parameters Note that the path of the first page must be "/", other pages can be "/" + arbitrary concatenation router.define('/city', handler: cityHandler); // Or another way provided by the authorities router.define('/city/:city_id', handler: cityHandler); Copy the code -
Register the Router with the onGenerateRoute of the MaterialApp
MaterialApp(onGenerateRoute: router); Copy the code
-
Finally, jump through the Router instance, and if any parameters are passed, they will be received on the new page
router.navigateTo(context, '/city? city_id=CN13579'); // Or the official way router.navigateTo(context, '/city/CN13579'); Copy the code
Various routing animations are provided in Fluro, including fadeIn, inFromRight, and so on. Finished use, into the actual combat.
# # # # flutter_weather of actual combat
The import plug-in
At the beginning, the implementation requirements of the overall function have been mentioned, so the plug-in to be imported and the folder to store pictures are as follows:
"> < span style =" font-size: 14px; ^0.5.1+2 sqflite: ^1.1.3 fluttertoast: ^ 3.0.3rxdart: ^0.21.0 path_provider: 0.5.0+1 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design:true
assets:
- images/
Copy the code
Implementation of a top-level static instance
There are many instances that need to be registered at the top level and then used globally, including but not limited to Fluro’s Router, HTTP, database, and so on. In this project, these three instances need to be used, which will be called globally, so they should be initialized before starting. Of course, HTTP and database can also be created when they are used, depending on personal habits, but fluro management class must be registered at the beginning. You first need to define an Application class to hold these static instances
class Application {
static HttpUtils http; // Global network
static Router router; // Global routing
static DatabaseUtils db; // Global database
}
Copy the code
HttpUtil and DatabaseUtils are described in the previous section. We will not repeat this section. We will talk about how to set up the database.
######Fluro route management class
First of all, we need to know that the interface of the project is roughly divided into the following interface (of course, only the home page can be defined first, and the rest can be used to define again. The project is relatively simple, so it is listed first) : provincial selection page, city selection page, district selection page, weather display page, setting page. So fluro’s management class can be defined as follows:
// Check the 'phones. dart' file for more information about these routers
class Routers {
/// The path of each page
static const root = '/';
static const weather = '/weather';
static const provinces = '/provinces';
static const cities = '/cities';
static const districts = '/districts';
static const settings = '/settings';
/// This method is used to put`main`Method to define all routes,
// The corresponding handler can put the same file, or another file, depending on personal preference
static configureRouters(Router router) {
router.notFoundHandler = notFoundHandler;
router.define(root, handler: rootHandler); / / home page
router.define(weather, handler: weatherHandler); // Weather display page
router.define(provinces, handler: provincesHandler); // Province list page
router.define(cities, handler: citiesHandler); // Save the city list page
router.define(districts, handler: districtsHandler); // The list page of the lower districts
router.define(settings, handler: settingsHandler); / / Settings page
}
/// generate the weather display page path, need to use the city ID
static generateWeatherRouterPath(String cityId) => '$weather? city_id=$cityId';
/// Generate the list of cities to save the corresponding path needs to use the province ID and province name
static generateProvinceRouterPath(int provinceId, String name)
=> '$cities? province_id=$provinceId&name=$name';
/// generate the corresponding path of the district list page under the city, using the city ID and city name
static generateCityRouterPath(int provinceId, int cityId, String name)
=> '$districts? province_id=$provinceId&city_id=$cityId&name=$name';
}
Copy the code
/ / / view`routers/handler.dart`file
Handler notFoundHandler = Handler(handlerFunc: (_, params) {
Logger('RouterHandler:').log('Not Found Router'); // If the route cannot be found, information is printed
});
Handler rootHandler = Handler(handlerFunc: (_, params) => SplashPage());
Handler weatherHandler = Handler(handlerFunc: (_, params) {
String cityId = params['city_id']? .first;// Get the corresponding parameters
return WeatherPage(city: cityId);
});
Handler provincesHandler = Handler(handlerFunc: (_, params) => ProvinceListPage());
Handler citiesHandler = Handler(handlerFunc: (_, params) {
String provinceId = params['province_id']? .first;String name = params['name']? .first;return CityListPage(provinceId: provinceId,
name: FluroConvertUtils.fluroCnParamsDecode(name));
});
Handler districtsHandler = Handler(handlerFunc: (_, params) {
String provinceId = params['province_id']? .first;String cityId = params['city_id']? .first;String name = params['name']? .first;return DistrictListPage(provinceId: provinceId, cityId: cityId,
name: FluroConvertUtils.fluroCnParamsDecode(name));
});
Handler settingsHandler = Handler(handlerFunc: (_, params) => SettingsPage());
Copy the code
So the route of the interface has been written here. However, as mentioned above, Fluro does not support the transmission of Chinese at present, so transcoding is required before the transmission of Chinese. Here we provide a self-written method, and friends can directly propose the issue in the project if they have a better method
/ / / view`utils/fluro_convert_util.dart`file
class FluroConvertUtils {
/// Fluro does not support Chinese transfer
static String fluroCnParamsEncode(String originalCn) {
StringBuffer sb = StringBuffer(a);var encoded = Utf8Encoder().convert(originalCn); // utf8 encoding will generate an int list
encoded.forEach((val) => sb.write('$val,')); // Convert the int list back to a string
return sb.toString().substring(0, sb.length - 1).toString();
}
/// fluro pass the parameter, parse
static String fluroCnParamsDecode(String encodedCn) {
var decoded = encodedCn.split('[').last.split('] ').first.split(', '); // Split the argument string
var list = <int> []; decoded.forEach((s) => list.add(int.parse(s.trim()))); // Go back to the int list
return Utf8Decoder().convert(list); / / decoding}}Copy the code
Database management class writing
Because database startup is a resource-intensive process, this side is singleton and extracted to the top level. In this project, the database is mainly used to store city information, because the association between cities is complicated, which would be complicated if shared_preferences or file storage is used.
/ / / view`utils/db_utils.dart`file
class DatabaseUtils {
final String _dbName = 'weather.db'; // Table name
final String _tableProvinces = 'provinces'; / / table
final String _tableCities = 'cities'; / / table
final String _tableDistricts = 'districts'; / / table
static Database _db;
static DatabaseUtils _instance;
static DatabaseUtils get instance => DatabaseUtils();
/// Put the initialization of the database into a private construct with values that are allowed to be accessed through singletons
DatabaseUtils._internal() {
getDatabasesPath().then((path) async {
_db = await openDatabase(join(path, _dbName), version: 1, onCreate: (db, version) {
db.execute('create table $_tableProvinces('
'id integer primary key autoincrement,'
'province_id integer not null unique,' // province id, id is unique
'province_name text not null' / / province
') ');
db.execute('create table $_tableCities('
'id integer primary key autoincrement,'
'city_id integer not null unique,' // city id, unique id
'city_name text not null,' / / the municipal
'province_id integer not null,' // The id of the corresponding province is associated with the provincial table as a foreign key
'foreign key(province_id) references $_tableProvinces(province_id)'
') ');
db.execute('create table $_tableDistricts('
'id integer primary key autoincrement,'
'district_id integer not null unique,' Id / / area
'district_name text not null,' / / area
'weather_id text not null unique,' // Query the weather ID, for example, CN13579826. The ID is unique
'city_id integer not null,' // The id of the corresponding city is associated with the market table as a foreign key
'foreign key(city_id) references $_tableCities(city_id)'
') ');
}, onUpgrade: (db, oldVersion, newVersion) {});
});
}
/// Build a singleton
factory DatabaseUtils() {
if (_instance == null) {
_instance = DatabaseUtils._internal();
}
return _instance;
}
// query all provinces,`ProvinceModel`Returns the model class generated by the data for the provincial and municipal interfaces
/ / / view`model/province_model.dart`file
Future<List<ProvinceModel>> queryAllProvinces() async =>
ProvinceModel.fromProvinceTableList(await _db.rawQuery('select province_id, province_name from $_tableProvinces'));
// query all cities in a province
Future<List<ProvinceModel>> queryAllCitiesInProvince(String proid) async => ProvinceModel.fromCityTableList(await _db.rawQuery(
'select city_id, city_name from $_tableCities where province_id = ? ',
[proid],
));
// Query all districts in a city,`DistrictModel`Returns the generated Model class for the zone interface
/ / / view`model/district_model.dart`file
Future<List<DistrictModel>> queryAllDistrictsInCity(String cityid) async => DistrictModel.fromDistrictTableList(await _db.rawQuery(
'select district_id, district_name, weather_id from $_tableDistricts where city_id = ? ',
[cityid],
));
// insert all provinces into the database
Future<void> insertProvinces(List<ProvinceModel> provinces) async {
var batch = _db.batch();
provinces.forEach((p) => batch.rawInsert(
'insert or ignore into $_tableProvinces (province_id, province_name) values (? ,?) ',
[p.id, p.name],
));
batch.commit();
}
/// insert all cities under the province into the database
Future<void> insertCitiesInProvince(List<ProvinceModel> cities, String proid) async {
var batch = _db.batch();
cities.forEach((c) => batch.rawInsert(
'insert or ignore into $_tableCities (city_id, city_name, province_id) values (? ,? ,?) ',
[c.id, c.name, proid],
));
batch.commit();
}
// insert all the districts under the municipality into the database
Future<void> insertDistrictsInCity(List<DistrictModel> districts, String cityid) async {
var batch = _db.batch();
districts.forEach((d) => batch.rawInsert(
'insert or ignore into $_tableDistricts (district_id, district_name, weather_id, city_id) values (? ,? ,? ,?) ', [d.id, d.name, d.weatherId, cityid], )); batch.commit(); }}Copy the code
Define the methods to be used completely, and you can initialize them in the main function
/ / / view`main.dart`file
void main() {
// Initialize the Fluro router
Router router = Router();
Routers.configureRouters(router);
Application.router = router;
// Initialize HTTP
Application.http = HttpUtils(baseUrl: WeatherApi.WEATHER_HOST);
// Initialize db
Application.db = DatabaseUtils.instance;
// Force portrait, because the portrait is set to 'Future' method, to prevent invalid Settings can wait for the return value before starting App
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitDown, DeviceOrientation.portraitUp]).then((_) {
runApp(WeatherApp()); // The App class can be stored in the same file
if(Platform.isAndroid) { SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent)); }}); }Copy the code
class WeatherApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Weather App',
onGenerateRoute: Application.router.generator, // Register fluro routes
debugShowCheckedModeBanner: false,); }}Copy the code
After initialization, you can write the page.
Home page to write
The home page is mainly for a general display of the App, or some advertising display, but also to provide time for some data initialization, when the user enters the better experience effect. We will make an icon display here (the icon can be found in the images folder of the project) and jump to the next page after 5s delay.
/ / / view`splash_page.dart`file
class SplashPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Since rxdart has been introduced, the timer is used for the countdown
/// You can also use futuer.delayed to count down
/// 5s timing, if the city has been selected, jump to the weather interface, otherwise enter the city selection
Observable.timer(0.Duration(milliseconds: 5000)).listen((_) {
PreferenceUtils.instance.getString(PreferencesKey.WEATHER_CITY_ID)
.then((city) {
// If the city is not selected, enter the city selection page; otherwise, skip to the weather details page
/ / replace: true is the Navigator. PushReplacement method
Application.router.navigateTo(context, city.isEmpty
? Routers.provinces
: Routers.generateWeatherRouterPath(city),
replace: true);
});
});
return Scaffold(
body: Container(
alignment: Alignment.center,
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Display ICONS
Image.asset(Resource.pngSplash, width: 200.0, height: 200.0),
// Display text reminders, set area size with SizedBox
SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
child: Text(
'All weather data is simulated and used for learning purposes only. Do not use it as real weather forecasting software',
textAlign: TextAlign.center,
softWrap: true,
style: TextStyle(color: Colors.red[700], fontSize: 16.0() [() [() (() () (() () () ( }}Copy the code
City Selection page
When entering for the first time, the user must not choose city, so first select list page, write the city because of the whole project using BLoC separation of business logic and the page, so write data management class, first put data request and change the business logic in this, the realization of the BLoC told it in the front, side will not repeat. Check out the article: State Management and BLoC
/ / / view`provinces_bloc.dart`file
class ProvincesBloc extends BaseBloc {
final _logger = Logger('ProvincesBloc');
List<ProvinceModel> _provinces = []; / / provinces throughout the country
List<ProvinceModel> _cities = []; / / the city in the province
List<DistrictModel> _districts = []; / / the city area
List<ProvinceModel> get provinces => _provinces;
List<ProvinceModel> get cities => _cities;
List<DistrictModel> get districts => _districts;
BehaviorSubject<List<ProvinceModel>> _provinceController = BehaviorSubject();
BehaviorSubject<List<ProvinceModel>> _citiesController = BehaviorSubject();
BehaviorSubject<List<DistrictModel>> _districtController = BehaviorSubject();
/// stream, the stream argument for StreamBuilder
Observable<List<ProvinceModel>> get provinceStream
=> Observable(_provinceController.stream);
Observable<List<ProvinceModel>> get cityStream => Observable(_citiesController.stream);
Observable<List<DistrictModel>> get districtStream
=> Observable(_districtController.stream);
/// Refresh the list of provinces
changeProvinces(List<ProvinceModel> provinces) {
_provinces.clear();
_provinces.addAll(provinces);
_provinceController.add(_provinces);
}
/// refresh the list of cities
changeCities(List<ProvinceModel> cities) {
_cities.clear();
_cities.addAll(cities);
_citiesController.add(_cities);
}
/// Notify the list of refresh areas
changeDistricts(List<DistrictModel> districts) {
_districts.clear();
_districts.addAll(districts);
_districtController.add(_districts);
}
/// request all provinces
Future<List<ProvinceModel>> requestAllProvinces() async {
var resp = await Application.http.getRequest(WeatherApi.WEATHER_PROVINCE,
error: (msg) => _logger.log(msg, 'province'));
return resp == null || resp.data == null ? [] : ProvinceModel.fromMapList(resp.data);
}
/// request cities in the province
Future<List<ProvinceModel>> requestAllCitiesInProvince(String proid) async {
var resp = await Application.http
.getRequest('${WeatherApi.WEATHER_PROVINCE}/$proid',
error: (msg) => _logger.log(msg, 'city'));
return resp == null || resp.data == null ? [] : ProvinceModel.fromMapList(resp.data);
}
/// request a district within the city
Future<List<DistrictModel>> requestAllDistricts(String proid, String cityid) async {
var resp = await Application.http
.getRequest('${WeatherApi.WEATHER_PROVINCE}/$proid/$cityid',
error: (msg) => _logger.log(msg, 'district'));
return resp == null || resp.data == null ? [] : DistrictModel.fromMapList(resp.data);
}
@override
void dispose() { // Destroy in time_provinceController? .close(); _citiesController? .close(); _districtController? .close(); }}Copy the code
BLoC needs to be registered after it is written. Cities are selected relatively frequently, so it can be placed on the top floor for registration
return BlocProvider(
bloc: ProvincesBloc(), // City switch BLoC
child: MaterialApp(
title: 'Weather App',
onGenerateRoute: Application.router.generator,
debugShowCheckedModeBanner: false,),);Copy the code
City selection is a list, which can be generated directly through the ListView. As mentioned in the ListView earlier, fixing the height of the item as much as possible will improve the efficiency of drawing
/ / / view`provinces_page.dart`file
class ProvinceListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var _bloc = BlocProvider.of<ProvincesBloc>(context);
// Enter the database to fill the data interface
Application.db.queryAllProvinces().then((ps) => _bloc.changeProvinces(ps));
// Update the network data list and refresh the database data
_bloc.requestAllProvinces().then((provinces) {
_bloc.changeProvinces(provinces);
Application.db.insertProvinces(provinces);
});
return Scaffold(
appBar: AppBar(
title: Text('Please select province'),
),
body: Container(
color: Colors.black12,
alignment: Alignment.center,
// Provincial list selection
child: StreamBuilder(
stream: _bloc.provinceStream,
initialData: _bloc.provinces,
builder: (_, AsyncSnapshot<List<ProvinceModel>> snapshot) => ! snapshot.hasData || snapshot.data.isEmpty// If the current data is not loaded, give a load, otherwise show the list loaded
? CupertinoActivityIndicator(radius: 12.0)
: ListView.builder(
physics: BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
itemBuilder: (_, index) => InkWell(
child: Container(
alignment: Alignment.centerLeft,
child: Text(snapshot.data[index].name, style: TextStyle(fontSize: 18.0, color: Colors.black)),
),
onTap: () => Application.router.navigateTo(
context,
// Select a city in the lower level of the province, and pass in the current province ID and name
Routers.
generateProvinceRouterPath(snapshot.data[index].id, FluroConvertUtils.fluroCnParamsEncode(snapshot.data[index].name)),
transition: TransitionType.fadeIn),
),
itemExtent: 50.0, itemCount: snapshot.data.length), ), ), ); }}Copy the code
It’s the same thing for cities and districts, except that there’s a little bit of difference in the final click and the layout of the page is pretty much the same, so I’m just going to mention the click event
/ / / view`cities_page.dart`file
Application.router.navigateTo(
context,
// Jump to lower level city selection
Routers.generateProvinceRouterPath(
snapshot.data[index].id, FluroConvertUtils.fluroCnParamsEncode(snapshot.data[index].name)),
transition: TransitionType.fadeIn),
)
Copy the code
// Set it to the current zone, clean up the routing stack, and set the weather interface to the top layer
onTap: () {
PreferenceUtils.instance
.saveString(PreferencesKey.WEATHER_CITY_ID, snapshot.data[index].weatherId);
Application.router.navigateTo(context, Routers.generateWeatherRouterPath(snapshot.data[index].weatherId),
transition: TransitionType.inFromRight, clearStack: true);
})
Copy the code
Weather Details Page
Weather details page will more relative parts, in order to look comfortable, split into multiple parts to write here, or write data before the management class, because the weather details interface data returned by the nesting level is more, complicated relationship, weak database for persistence, so using file persistent way here. Of course, some friends will ask why not use shared_preferences for storage, theoretically there should be no big problem, but I suggest relatively complex data using file storage is relatively better, must say why, I also can’t tell.
/ / / view`weather_bloc.dart`file
class WeatherBloc extends BaseBloc {
final _logger = Logger('WeatherBloc');
WeatherModel _weather; // Weather conditions
String _background = WeatherApi.DEFAULT_BACKGROUND; / / the background
WeatherModel get weather => _weather;
String get background => _background;
BehaviorSubject<WeatherModel> _weatherController = BehaviorSubject();
BehaviorSubject<String> _backgroundController = BehaviorSubject();
Observable<WeatherModel> get weatherStream => Observable(_weatherController.stream);
Observable<String> get backgroundStream => Observable(_backgroundController.stream);
/// Weather update
updateWeather(WeatherModel weather) {
_weather = weather;
_weatherController.add(_weather);
}
/// Update the weather background
updateBackground(String background) {
_background = background;
_backgroundController.add(_background);
}
// Request weather conditions
Future<WeatherModel> requestWeather(String id) async {
var resp = await Application.http
.getRequest(WeatherApi.WEATHER_STATUS,
params: {'cityid': id, 'key': WeatherApi.WEATHER_KEY},
error: (msg) => _logger.log(msg, 'weather'));
// If the request succeeds, the data is written to the file
if(resp ! =null&& resp.data ! =null) {
_writeIntoFile(json.encode(resp.data));
}
return WeatherModel.fromMap(resp.data);
}
Future<String> requestBackground() async {
var resp = await Application.http
.getRequest<String>(WeatherApi.WEATHER_BACKGROUND,
error: (msg) => _logger.log(msg, 'background'));
return resp == null || resp.data == null ? WeatherApi.DEFAULT_BACKGROUND : resp.data;
}
// Get the file path
Future<String> _getPath() async= >'${(await getApplicationDocumentsDirectory()).path}/weather.txt';
// Write to file
_writeIntoFile(String contents) async {
File file = File(await _getPath());
if (await file.exists()) file.deleteSync();
file.createSync();
file.writeAsString(contents);
}
// File reads storage information, returns an empty string '' if no file exists. Null is not recommended
Future<String> readWeatherFromFile() async {
File file = File(await _getPath());
return (await file.exists()) ? file.readAsString() : ' ';
}
@override
voiddispose() { _weatherController? .close(); _backgroundController? .close(); }}Copy the code
The refresh of weather details is only a page, so the BLoC registration value needs to be registered on the route and added to the registration in the corresponding handler of Fluro
Handler weatherHandler = Handler(handlerFunc: (_, params) {
String cityId = params['city_id']? .first;// This id can be obtained by BLoC or not
return BlocProvider(child: WeatherPage(city: cityId), bloc: WeatherBloc());
});
Copy the code
Then you can write the interface, implement the outermost background changes first
/ / / view`weather_page.dart`file
class WeatherPage extends StatelessWidget {
final String city;
WeatherPage({Key key, this.city}) : super(key: key);
@override
Widget build(BuildContext context) {
var _bloc = BlocProvider.of<WeatherBloc>(context);
// Request background and update
_bloc.requestBackground().then((b) => _bloc.updateBackground(b));
// First read the local file cache for page population
_bloc.readWeatherFromFile().then((s) {
if(s.isNotEmpty) { _bloc.updateWeather(WeatherModel.fromMap(json.decode(s))); }});// Request network to update data
_bloc.requestWeather(city).then((w) => _bloc.updateWeather(w));
return Scaffold(
body: StreamBuilder(
stream: _bloc.backgroundStream,
initialData: _bloc.background,
builder: (_, AsyncSnapshot<String> themeSnapshot) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black12,
image: DecorationImage(
image: NetworkImage(themeSnapshot.data), fit: BoxFit.cover),
),
child: // The specific internal layout is achieved by splitting widgets))); }}Copy the code
The top of the page is to display two buttons, a jump city selection, a jump setting page, display the current city
class FollowedHeader extends StatelessWidget {
final AsyncSnapshot<WeatherModel> snapshot; // snapshot is passed in from the upper level
FollowedHeader({Key key, this.snapshot}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
// City select page jump button
IconButton(
icon: Icon(Icons.home, color: Colors.white, size: 32.0),
onPressed: () => Application.router.
navigateTo(context, Routers.provinces,
transition: TransitionType.inFromLeft)),
// The current city
Text('${snapshot.data.heWeather[0].basic.location}',
style: TextStyle(fontSize: 28.0, color: Colors.white)),
// Set the page jump button
IconButton(
icon: Icon(Icons.settings, color: Colors.white, size: 32.0), onPressed: () => Application.router .navigateTo(context, Routers.settings, transition: TransitionType.inFromRight)) ], ); }}Copy the code
Next comes the current weather details section
class CurrentWeatherState extends StatelessWidget {
final AsyncSnapshot<WeatherModel> snapshot;
CurrentWeatherState({Key key, this.snapshot}) : super(key: key);
@override
Widget build(BuildContext context) {
var _now = snapshot.data.heWeather[0].now;
var _update = snapshot.data.heWeather[0].update.loc.split(' ').last;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
// Current temperature
Text('${_now.tmp}℃ ', style: TextStyle(fontSize: 50.0, color: Colors.white)),
// The current weather conditions
Text('${_now.condTxt}', style: TextStyle(fontSize: 24.0, color: Colors.white)),
Row( // Refresh time
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.refresh, size: 16.0, color: Colors.white),
Padding(padding: const EdgeInsets.only(left: 4.0)),
Text(_update, style: TextStyle(fontSize: 12.0, color: Colors.white)) ], ) ], ); }}Copy the code
Cloumn can be used as a list, but the glue of the list is —- CustomScrollView, so the overall connection will be made through the CustomScrollView. Then you can safely add the CustomScrollView to the child property of the topmost container. Then we implement this prediction module
class WeatherForecast extends StatelessWidget {
final AsyncSnapshot<WeatherModel> snapshot;
WeatherForecast({Key key, this.snapshot}) : super(key: key);
@override
Widget build(BuildContext context) {
var _forecastList = snapshot.data.heWeather[0].dailyForecasts; // Get the weather forecast
return SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(_, index) => Container(
color: Colors.black54, // Set the background color in the outer layer to prevent the text from being obscured by the outermost image background
padding: const EdgeInsets.all(12.0),
alignment: Alignment.centerLeft,
child: index == 0 // Display 'forecast' when first item case
? Text('prediction', style: TextStyle(fontSize: 24.0, color: Colors.white))
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(_forecastList[index - 1].date, // Forecast date
style: TextStyle(fontSize: 16.0, color: Colors.white)),
Expanded( // Weather information is displayed in the center through expanded
child: Center(child: Text(_forecastList[index - 1].cond.txtD,
style: TextStyle(fontSize: 16.0, color: Colors.white))),
flex: 2),
Expanded(
child: Row( // Maximum temperature, minimum temperature
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(_forecastList[index - 1].tmp.max,
style: TextStyle(fontSize: 16.0,
color: Colors.white)),
Text(_forecastList[index - 1].tmp.min,
style: TextStyle(fontSize: 16.0,
color: Colors.white)),
],
),
flex: 1)
],
)),
childCount: _forecastList.length + 1.// This quantity needs +1 because a heading needs a quantity
),
itemExtent: 50.0); }}Copy the code
Then there is the air quality report, a title, which is divided equally between two layouts
class AirQuality extends StatelessWidget {
final AsyncSnapshot<WeatherModel> snapshot;
AirQuality({Key key, this.snapshot}) : super(key: key);
@override
Widget build(BuildContext context) {
var quality = snapshot.data.heWeather[0].aqi.city;
return Container(
padding: const EdgeInsets.all(12.0),
color: Colors.black54,
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
/ / title
Padding(padding: const EdgeInsets.only(bottom: 20.0), child:
Text('Air Quality', style: TextStyle(fontSize: 24.0,
color: Colors.white))),
Row(
children: <Widget>[
// Bisect the horizontal distance with expanded
Expanded(
child: Center(
// Inner center display
child: Column(
children: <Widget>[
Text('${quality.aqi}', style:
TextStyle(fontSize: 40.0, color: Colors.white)),
Text('AQI index', style:
TextStyle(fontSize: 20.0, color: Colors.white)),
],
),
)),
Expanded(
child: Center(
child: Column(
children: <Widget>[
Text('${quality.pm25}', style:
TextStyle(fontSize: 40.0, color: Colors.white)),
Text('PM2.5 index', style:
TextStyle(fontSize: 20.0, color: Colors.white)), ], ), )), ], ) ], )); }}Copy the code
Next comes the quality of life module, which also looks like a list, but instead of a list, the backend returns different quality indices based on different fields. Since the layout is similar, it can be wrapped and called as a whole
class LifeSuggestions extends StatelessWidget { final AsyncSnapshot<WeatherModel> snapshot; LifeSuggestions({Key key, this.snapshot}) : super(key: key); // suggestionWidget _suggestionWidget(String content) => Padding(Padding: const edgeinsets.only (top: 20.0), child: suggestionWidget(String content) => Padding(Padding: const edgeinsets.only (top: 20.0), child: Text(content, style: TextStyle(color: color.white, fontSize: 16.0)); @override Widget build(BuildContext context) { var _suggestion = snapshot.data.heWeather[0].suggestion; Return Container(padding: const EdgeInsets. All (12.0), color: color.black54, alignment: alignment. CenterLeft, child: The Column (crossAxisAlignment: crossAxisAlignment. Start, children: < widgets > [Text (' life advice, style: TextStyle (fontSize: 24.0 color: Colors.white), _suggestionWidget(' comfort: ${_suggestion.comf.brf}\n${_suggestion.comf.txt}'), _suggestionWidget(' carwash index: ${_suggestion.cw.brf}\n${_suggestion.cw.txt}'), _suggestionWidget(' Motion index: ${_suggestion.sport.brf}\n${_suggestion.sport.txt}'), ], ), ); }}Copy the code
All the sub-modules have been written, and all that remains is to assemble them with adhesive
child: StreamBuilder( initialData: _bloc.weather, stream: _bloc.weatherStream, builder: (_, AsyncSnapshot<WeatherModel> snapshot) => ! snapshot.hasData ? CupertinoActivityIndicator(radius:12.0)
: SafeArea(
child: RefreshIndicator(
child: CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(child: FollowedHeader(snapshot: snapshot)),
// Real-time weather
SliverPadding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
sliver: SliverToBoxAdapter(
child: CurrentWeatherState(snapshot: snapshot, city: city),
),
),
// Weather forecast
WeatherForecast(snapshot: snapshot),
// Air quality
SliverPadding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
sliver: SliverToBoxAdapter(child: AirQuality(snapshot: snapshot)),
),
// Life advice
SliverToBoxAdapter(child: LifeSuggestions(snapshot: snapshot))
],
),
onRefresh: () async {
_bloc.requestWeather(city).then((w) => _bloc.updateWeather(w));
return null; }))),Copy the code
All that is left is to set the page’s global theme switch
Set the page global theme switch
Since data switching is mentioned, BLoC is definitely involved, and the management class is still written as usual
/ / / view`setting_bloc.dart`file
class SettingBloc extends BaseBloc {
/// List of all theme colors
static const themeColors = [Colors.blue, Colors.red, Colors.green,
Colors.deepOrange, Colors.pink, Colors.purple];
Color _color = themeColors[0];
Color get color => _color;
BehaviorSubject<Color> _colorController = BehaviorSubject();
Observable<Color> get colorStream => Observable(_colorController.stream);
/// toggle theme notification refresh
switchTheme(int themeIndex) {
_color = themeColors[themeIndex];
_colorController.add(_color);
}
@override
void dispose() {
_colorController?.close();
}
}
Copy the code
Since it is a global switch, this BLoC must be registered at the top layer, so there is no code attached here, which is the same as ProvinceBloc. And then you write the interface, you set up the interface because you have a GridView and other widgets, so you also have to use CustomScrollView as the glue, and of course you can use Wrap instead of GridView to implement the grid, so you don’t have to use CustomScrollView, Use Column.
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var _bloc = BlocProvider.of<SettingBloc>(context);
return StreamBuilder(
stream: _bloc.colorStream,
initialData: _bloc.color,
// Theme is a component that contains a Theme that can be set to multiple colors.
// Change the theme color by receiving the change of color
builder: (_, AsyncSnapshot<Color> snapshot) => Theme(
// IconThemeData sets the theme color of the button
data: ThemeData(primarySwatch: snapshot.data, iconTheme: IconThemeData(color: snapshot.data)),
child: Scaffold(
appBar: AppBar(
title: Text('set'),
),
body: Container(
color: Colors.black12,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
child: CustomScrollView(
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.only(right: 12.0),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text('Current theme color:', style: TextStyle(fontSize: 16.0,
color: snapshot.data)),
Container(width: 20.0, height: 20.0, color: snapshot.data)
],
)),
),
SliverPadding(padding: const EdgeInsets.symmetric(vertical: 15.0)),
SliverGrid(
delegate: SliverChildBuilderDelegate(
(_, index) => InkWell(
child: Container(color: SettingBloc.themeColors[index]),
onTap: () {
// Save the selection and use the theme color directly when entering the next time
// Change the theme color at the same time
_bloc.switchTheme(index);
PreferenceUtils.instance.saveInteger(PreferencesKey.THEME_COLOR_INDEX, index);
},
),
childCount: SettingBloc.themeColors.length),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, mainAxisSpacing: 20.0, crossAxisSpacing: 20.0() [() [() [() [() [() [() }}Copy the code
Finally, global topic switching is also implemented.
This is how to pack apK, IPA and flutter under Android because there is no MAC so you can understand.
Apk file packaging
-
Create a JKS file, which can be ignored if it already exists, starting with step 2. Open terminal and enter
Keytool -genkey -v -keystore [path of your signature file]. JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Then enter the password and some basic information to create a successful
-
Create a key.properties file in the Android directory of your project and do the following
storePassword=<password from previous step> keyPassword=<password from previous step> keyAlias=key StoreFile =<[path of your signature file].jks>Copy the code
-
Make the following changes in build.gradle under Android /app
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" // Add the following code def keystorePropertiesFile = rootProject.file("key.properties") def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { // ... defaultConfigs{ // ... } // Add the following code signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] } } buildTypes{ // ...}}Copy the code
-
Opening the terminal again and running the Flutter Build APk will automatically generate an APK file. The file path is
[your project address] build, app, outputs, apk, release
-
Formal packages can be run on the phone using Flutter Install
conclusion
2019.03.09-2019.04.08. It took a whole month to finish the writing, which can also be regarded as an account for the friends who have been paying attention to. To write a series, to tell the truth really suffering, because early need to be the thinking of the overall structure, needs simple to complex, in-depth step by step, if the overall train of thought did not build good, it will head into a point of no return, either crustily skin of head, or return to start from scratch, 2 it is to write this article series, basically all abandoned the private time, You have to read it several times before Posting it. However, I have learned a lot. During this period, I have read a lot of source code of FLUTTER, and my understanding of flutter components will be deepened. I will also learn the code writing specifications, which will be of great help to my future work. Finally, I hope this series can introduce Flutter to more people. See you next time